From fe6a4a4741038f834e62d099c555dc040462b345 Mon Sep 17 00:00:00 2001 From: wanabe Date: Thu, 20 Aug 2020 21:30:08 +0900 Subject: [PATCH 001/304] Skip to set default value unless `meets_dependency?` (#2097) --- CHANGELOG.md | 1 + lib/grape/validations/validators/default.rb | 1 + .../validations/validators/default_spec.rb | 49 +++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5054c1dac..eb4988696 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ * [#2083](https://github.com/ruby-grape/grape/pull/2083): 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). ### 1.4.0 (2020/07/10) diff --git a/lib/grape/validations/validators/default.rb b/lib/grape/validations/validators/default.rb index 79d6951f3..dbf754ed8 100644 --- a/lib/grape/validations/validators/default.rb +++ b/lib/grape/validations/validators/default.rb @@ -21,6 +21,7 @@ def validate_param!(attr_name, params) def validate!(params) attrs = SingleAttributeIterator.new(self, @scope, params) attrs.each do |resource_params, attr_name| + next unless @scope.meets_dependency?(resource_params, params) validate_param!(attr_name, resource_params) if resource_params.is_a?(Hash) && resource_params[attr_name].nil? end end diff --git a/spec/grape/validations/validators/default_spec.rb b/spec/grape/validations/validators/default_spec.rb index 10220eb58..ae16445eb 100644 --- a/spec/grape/validations/validators/default_spec.rb +++ b/spec/grape/validations/validators/default_spec.rb @@ -419,4 +419,53 @@ def app 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 From 192a2a2bbe59da0dac4ea435916ea5576d866155 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gw=C3=A9na=C3=ABl=20Rault?= Date: Fri, 21 Aug 2020 15:10:44 +0200 Subject: [PATCH 002/304] Fixes redundant dependency check and adds a vrp benchmark (#2096) --- CHANGELOG.md | 1 + benchmark/large_model.rb | 245 ++++++++++++++++++++ benchmark/resource/vrp_example.json | 1 + lib/grape/validations/params_scope.rb | 3 +- spec/grape/validations/params_scope_spec.rb | 26 +++ 5 files changed, 275 insertions(+), 1 deletion(-) create mode 100644 benchmark/large_model.rb create mode 100644 benchmark/resource/vrp_example.json diff --git a/CHANGELOG.md b/CHANGELOG.md index eb4988696..445342c0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ * [#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). ### 1.4.0 (2020/07/10) diff --git a/benchmark/large_model.rb b/benchmark/large_model.rb new file mode 100644 index 000000000..523e8887b --- /dev/null +++ b/benchmark/large_model.rb @@ -0,0 +1,245 @@ +# 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]]) + + 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 + 'hello' + 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 + API.call env +end +puts Time.now - start +printer = RubyProf::FlatPrinter.new(result) +File.open('test_prof.out', 'w+') { |f| printer.print(f, {}) } diff --git a/benchmark/resource/vrp_example.json b/benchmark/resource/vrp_example.json new file mode 100644 index 000000000..be3d734a3 --- /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","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","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}}}} \ No newline at end of file diff --git a/lib/grape/validations/params_scope.rb b/lib/grape/validations/params_scope.rb index 84e72f8bc..a51be6e1e 100644 --- a/lib/grape/validations/params_scope.rb +++ b/lib/grape/validations/params_scope.rb @@ -54,11 +54,12 @@ def 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) return false unless params.respond_to?(:with_indifferent_access) params = params.with_indifferent_access diff --git a/spec/grape/validations/params_scope_spec.rb b/spec/grape/validations/params_scope_spec.rb index 984524c8f..9ef14af9f 100644 --- a/spec/grape/validations/params_scope_spec.rb +++ b/spec/grape/validations/params_scope_spec.rb @@ -633,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 From 7e95ac8ce98f7b049ab216d6c449a211150edc19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gw=C3=A9na=C3=ABl=20Rault?= Date: Thu, 27 Aug 2020 15:07:17 +0200 Subject: [PATCH 003/304] Add nested array coercion spec (#2098) --- CHANGELOG.md | 1 + benchmark/large_model.rb | 10 +++++--- benchmark/resource/vrp_example.json | 2 +- .../validations/types/custom_type_coercer.rb | 14 ++++++++++- .../validations/validators/coerce_spec.rb | 24 +++++++++++++++++++ 5 files changed, 46 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 445342c0d..609e4235a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ * [#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). ### 1.4.0 (2020/07/10) diff --git a/benchmark/large_model.rb b/benchmark/large_model.rb index 523e8887b..48827528d 100644 --- a/benchmark/large_model.rb +++ b/benchmark/large_model.rb @@ -79,7 +79,7 @@ def self.vrp_request_vehicle(this) this.optional(:cost_time_multiplier, type: Float) this.optional :router_dimension, type: String, values: %w[time distance] - this.optional(:skills, type: Array[Array[String]]) + 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]) @@ -224,7 +224,10 @@ def self.vrp_request_schedule(this) end end post '/' do - 'hello' + { + skills_v1: params[:vrp][:vehicles].first[:skills], + skills_v2: params[:vrp][:vehicles].last[:skills] + } end end puts Grape::VERSION @@ -238,7 +241,8 @@ def self.vrp_request_schedule(this) start = Time.now result = RubyProf.profile do - API.call env + response = API.call env + puts response.last end puts Time.now - start printer = RubyProf::FlatPrinter.new(result) diff --git a/benchmark/resource/vrp_example.json b/benchmark/resource/vrp_example.json index be3d734a3..d9c9cd5bc 100644 --- a/benchmark/resource/vrp_example.json +++ b/benchmark/resource/vrp_example.json @@ -1 +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","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","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}}}} \ No newline at end of file +{"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/lib/grape/validations/types/custom_type_coercer.rb b/lib/grape/validations/types/custom_type_coercer.rb index f6a4e0cf9..a11403e1c 100644 --- a/lib/grape/validations/types/custom_type_coercer.rb +++ b/lib/grape/validations/types/custom_type_coercer.rb @@ -103,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 diff --git a/spec/grape/validations/validators/coerce_spec.rb b/spec/grape/validations/validators/coerce_spec.rb index 41acb5f0d..e157748e9 100644 --- a/spec/grape/validations/validators/coerce_spec.rb +++ b/spec/grape/validations/validators/coerce_spec.rb @@ -620,6 +620,30 @@ def self.parsed?(value) 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 subject.params do requires :values, type: Array[Integer], coerce_with: ->(val) { val.split(/\s+/).map(&:to_i) } From d40d6977d651a0eb2f64795fdbab3a10353cbc1d Mon Sep 17 00:00:00 2001 From: Igor Victor Date: Tue, 1 Sep 2020 22:15:57 +0200 Subject: [PATCH 004/304] Add Truffleruby head to CI (#2099) --- .travis.yml | 6 ++++-- CHANGELOG.md | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 134679cfe..48632523d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,8 @@ sudo: false # see https://docs.travis-ci.com/user/customizing-the-build/#matching-jobs-with-allow_failures gemfile: +script: bundle exec rake spec + matrix: include: - rvm: 2.7.1 @@ -32,12 +34,10 @@ matrix: - rvm: 2.7.1 gemfile: gemfiles/multi_json.gemfile script: - - bundle exec rake - bundle exec rspec spec/integration/multi_json - rvm: 2.7.1 gemfile: gemfiles/multi_xml.gemfile script: - - bundle exec rake - bundle exec rspec spec/integration/multi_xml - rvm: 2.6.6 gemfile: Gemfile @@ -54,9 +54,11 @@ matrix: - rvm: ruby-head - rvm: jruby-head - rvm: rbx-3 + - rvm: truffleruby-head allow_failures: - rvm: ruby-head - rvm: jruby-head - rvm: rbx-3 + - rvm: truffleruby-head bundler_args: --without development diff --git a/CHANGELOG.md b/CHANGELOG.md index 609e4235a..0ec4232b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ #### Fixes * Your contribution here. +* [#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). * [#2083](https://github.com/ruby-grape/grape/pull/2083): 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). From 416a7e15bdfda29cd4f9b335a911bb59a416be60 Mon Sep 17 00:00:00 2001 From: James Lamont Date: Mon, 14 Sep 2020 10:46:28 +1000 Subject: [PATCH 005/304] Fix retaining setup blocks when remounting APIs --- CHANGELOG.md | 1 + Gemfile | 1 + benchmark/remounting.rb | 47 +++++++++++++++++++++++++++++++++++++++++ lib/grape/api.rb | 2 +- spec/grape/api_spec.rb | 5 +++++ 5 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 benchmark/remounting.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ec4232b7..08995a1a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ * [#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) diff --git a/Gemfile b/Gemfile index 48a26eb17..7e70dbcfe 100644 --- a/Gemfile +++ b/Gemfile @@ -17,6 +17,7 @@ end group :development do gem 'appraisal' gem 'benchmark-ips' + gem 'benchmark-memory' gem 'guard' gem 'guard-rspec' gem 'guard-rubocop' diff --git a/benchmark/remounting.rb b/benchmark/remounting.rb new file mode 100644 index 000000000..a585c1d2e --- /dev/null +++ b/benchmark/remounting.rb @@ -0,0 +1,47 @@ +$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/lib/grape/api.rb b/lib/grape/api.rb index 45dd76c74..dd8ae8d89 100644 --- a/lib/grape/api.rb +++ b/lib/grape/api.rb @@ -30,7 +30,7 @@ def inherited(api, base_instance_parent = Grape::API::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 = [] + @setup = Set.new @base_parent = base_instance_parent @base_instance = mount_instance end diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index 4d9325671..0d722a4b1 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -1600,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 From e79cf04277f60f3e21a71e3d3c02451087220590 Mon Sep 17 00:00:00 2001 From: Tim Connor Date: Tue, 29 Sep 2020 10:18:13 +1300 Subject: [PATCH 006/304] Lock rubocop-ast to < 0.7 The latest versions of rubocop-ast (>= 0.7) are incompatible with older versions of rubocop. Since grape currently use rubocop 0.84 we need to lock rubocop-ast to a compatible version --- Gemfile | 1 + gemfiles/multi_json.gemfile | 1 + gemfiles/multi_xml.gemfile | 1 + gemfiles/rack1.gemfile | 1 + gemfiles/rack2.gemfile | 1 + gemfiles/rack_edge.gemfile | 1 + gemfiles/rails_5.gemfile | 1 + gemfiles/rails_6.gemfile | 1 + gemfiles/rails_edge.gemfile | 1 + 9 files changed, 9 insertions(+) diff --git a/Gemfile b/Gemfile index 7e70dbcfe..6c8f23291 100644 --- a/Gemfile +++ b/Gemfile @@ -11,6 +11,7 @@ group :development, :test do gem 'hashie' gem 'rake' gem 'rubocop', '0.84.0' + gem 'rubocop-ast', '< 0.7' gem 'rubocop-performance', require: false end diff --git a/gemfiles/multi_json.gemfile b/gemfiles/multi_json.gemfile index 0cb8517c1..279eabba5 100644 --- a/gemfiles/multi_json.gemfile +++ b/gemfiles/multi_json.gemfile @@ -11,6 +11,7 @@ group :development, :test do gem 'hashie' gem 'rake' gem 'rubocop', '0.84.0' + gem 'rubocop-ast', '< 0.7' gem 'rubocop-performance', require: false end diff --git a/gemfiles/multi_xml.gemfile b/gemfiles/multi_xml.gemfile index d352caf98..ba20f9415 100644 --- a/gemfiles/multi_xml.gemfile +++ b/gemfiles/multi_xml.gemfile @@ -11,6 +11,7 @@ group :development, :test do gem 'hashie' gem 'rake' gem 'rubocop', '0.84.0' + gem 'rubocop-ast', '< 0.7' gem 'rubocop-performance', require: false end diff --git a/gemfiles/rack1.gemfile b/gemfiles/rack1.gemfile index 836f9c902..05e84c399 100644 --- a/gemfiles/rack1.gemfile +++ b/gemfiles/rack1.gemfile @@ -11,6 +11,7 @@ group :development, :test do gem 'hashie' gem 'rake' gem 'rubocop', '0.84.0' + gem 'rubocop-ast', '< 0.7' gem 'rubocop-performance', require: false end diff --git a/gemfiles/rack2.gemfile b/gemfiles/rack2.gemfile index 9f92d32ff..accc673b3 100644 --- a/gemfiles/rack2.gemfile +++ b/gemfiles/rack2.gemfile @@ -11,6 +11,7 @@ group :development, :test do gem 'hashie' gem 'rake' gem 'rubocop', '0.84.0' + gem 'rubocop-ast', '< 0.7' gem 'rubocop-performance', require: false end diff --git a/gemfiles/rack_edge.gemfile b/gemfiles/rack_edge.gemfile index 185c39391..d4271ef76 100644 --- a/gemfiles/rack_edge.gemfile +++ b/gemfiles/rack_edge.gemfile @@ -11,6 +11,7 @@ group :development, :test do gem 'hashie' gem 'rake' gem 'rubocop', '0.84.0' + gem 'rubocop-ast', '< 0.7' gem 'rubocop-performance', require: false end diff --git a/gemfiles/rails_5.gemfile b/gemfiles/rails_5.gemfile index 894d2bd50..b32c7d3ac 100644 --- a/gemfiles/rails_5.gemfile +++ b/gemfiles/rails_5.gemfile @@ -11,6 +11,7 @@ group :development, :test do gem 'hashie' gem 'rake' gem 'rubocop', '0.84.0' + gem 'rubocop-ast', '< 0.7' gem 'rubocop-performance', require: false end diff --git a/gemfiles/rails_6.gemfile b/gemfiles/rails_6.gemfile index c7b9671f2..85e5f69fc 100644 --- a/gemfiles/rails_6.gemfile +++ b/gemfiles/rails_6.gemfile @@ -11,6 +11,7 @@ group :development, :test do gem 'hashie' gem 'rake' gem 'rubocop', '0.84.0' + gem 'rubocop-ast', '< 0.7' gem 'rubocop-performance', require: false end diff --git a/gemfiles/rails_edge.gemfile b/gemfiles/rails_edge.gemfile index 24b5c5a8f..bfb0d3f72 100644 --- a/gemfiles/rails_edge.gemfile +++ b/gemfiles/rails_edge.gemfile @@ -11,6 +11,7 @@ group :development, :test do gem 'hashie' gem 'rake' gem 'rubocop', '0.84.0' + gem 'rubocop-ast', '< 0.7' gem 'rubocop-performance', require: false end From 4913534bcdb1f4f5bd521f84c30ac5ce6772ca46 Mon Sep 17 00:00:00 2001 From: Tim Connor Date: Fri, 18 Sep 2020 13:08:49 +1200 Subject: [PATCH 007/304] Move Grape::Endpoint#declared specs into own spec file --- spec/grape/endpoint/declared_spec.rb | 545 +++++++++++++++++++++++++++ spec/grape/endpoint_spec.rb | 534 -------------------------- 2 files changed, 545 insertions(+), 534 deletions(-) create mode 100644 spec/grape/endpoint/declared_spec.rb diff --git a/spec/grape/endpoint/declared_spec.rb b/spec/grape/endpoint/declared_spec.rb new file mode 100644 index 000000000..73d145a8e --- /dev/null +++ b/spec/grape/endpoint/declared_spec.rb @@ -0,0 +1,545 @@ +# 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 :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 + end + optional :arr, type: Array do + optional :nineth + 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 4 + 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 eq({}) + 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)['arr']).to be_a(Array) + end + + it 'to be an array when nested and include_missing is true' do + subject.get '/declared' do + declared(params, include_missing: true) + end + + get '/declared?first=present&nested[fourth]=1' + expect(last_response.status).to eq(200) + expect(JSON.parse(last_response.body)['nested']['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 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 a45abe36d..4bbeb070c 100644 --- a/spec/grape/endpoint_spec.rb +++ b/spec/grape/endpoint_spec.rb @@ -280,540 +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 - optional :nested_arr, type: Array do - optional :eighth - end - end - optional :arr, type: Array do - optional :nineth - 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 4 - 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 eq({}) - 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)['arr']).to be_a(Array) - end - - it 'to be an array when nested and include_missing is true' do - subject.get '/declared' do - declared(params, include_missing: true) - end - - get '/declared?first=present&nested[fourth]=1' - expect(last_response.status).to eq(200) - expect(JSON.parse(last_response.body)['nested']['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 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 - describe '#params' do it 'is available to the caller' do subject.get('/hey') do From 678cd1366462c1a679a65833d9fb6f1fc0c87c72 Mon Sep 17 00:00:00 2001 From: Tim Connor Date: Fri, 18 Sep 2020 15:21:46 +1200 Subject: [PATCH 008/304] Ensure complete declared params structure is present --- CHANGELOG.md | 3 +- README.md | 55 ++++++++++++++++--- UPGRADING.md | 47 ++++++++++++++-- lib/grape/dsl/inside_route.rb | 59 ++++++++------------ lib/grape/version.rb | 2 +- spec/grape/endpoint/declared_spec.rb | 81 +++++++++++++++++++--------- 6 files changed, 174 insertions(+), 73 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08995a1a4..eaf05a116 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -### 1.4.1 (Next) +### 1.5.0 (Next) #### Features @@ -7,6 +7,7 @@ #### Fixes * Your contribution here. +* [#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). * [#2083](https://github.com/ruby-grape/grape/pull/2083): Set `Cache-Control` header only for streamed responses - [@stanhu](https://github.com/stanhu). diff --git a/README.md b/README.md index c3a5ce73b..43f35cbf0 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,7 @@ content negotiation, versioning and much more. ## Stable Release -You're reading the documentation for the next release of Grape, which should be **1.4.1**. +You're reading the documentation for the next release of Grape, which should be **1.5.0**. Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. The current stable release is [1.4.0](https://github.com/ruby-grape/grape/blob/v1.4.0/README.md). @@ -353,7 +353,7 @@ use Rack::Session::Cookie run Rack::Cascade.new [Web, API] ``` -Note that order of loading apps using `Rack::Cascade` matters. The grape application must be last if you want to raise custom 404 errors from grape (such as `error!('Not Found',404)`). If the grape application is not last and returns 404 or 405 response, [cascade utilizes that as a signal to try the next app](https://www.rubydoc.info/gems/rack/Rack/Cascade). This may lead to undesirable behavior showing the [wrong 404 page from the wrong app](https://github.com/ruby-grape/grape/issues/1515). +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 @@ -787,7 +787,12 @@ Available parameter builders are `Grape::Extensions::Hash::ParamBuilder`, `Grape ### 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 @@ -820,9 +825,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 @@ -850,6 +855,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. diff --git a/UPGRADING.md b/UPGRADING.md index 591fa51ad..9d63392b9 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,6 +1,45 @@ Upgrading Grape =============== +### 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 @@ -28,17 +67,17 @@ class API < Grape::API end ``` -Or use `stream` to stream other kinds of content. In the following example a streamer class +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 +class MyObject attr_accessor :result def initialize(query) @result = query end - + def each yield '[' # Do paginated DB fetches and return each page formatted @@ -47,7 +86,7 @@ class MyObject yield process_records(records, first) first = false end - yield ']' + yield ']' end def process_records(records, first) diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index dc8f9f05c..1eb0c3084 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -58,7 +58,7 @@ def declared_hash(passed_params, options, declared_params, params_nested_path) 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(passed_children_params, params_nested_path_dup) do + 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 @@ -70,57 +70,44 @@ def declared_hash(passed_params, options, declared_params, params_nested_path) next unless options[:include_missing] || passed_params.key?(declared_param) || (param_renaming && passed_params.key?(param_renaming)) - if param_renaming - memo[optioned_param_key(param_renaming, options)] = passed_params[param_renaming] - else - memo[optioned_param_key(declared_param, options)] = passed_params[declared_param] + memo_key = optioned_param_key(param_renaming || declared_param, options) + passed_param = passed_params[param_renaming || declared_param] + + params_nested_path_dup = params_nested_path.dup + params_nested_path_dup << declared_param.to_s + + memo[memo_key] = handle_passed_param(params_nested_path_dup) do + passed_param end end end end - def handle_passed_param(passed_children_params, params_nested_path, &_block) - if should_be_empty_hash?(passed_children_params, params_nested_path) + def handle_passed_param(params_nested_path, has_passed_children = false, &_block) + return yield if has_passed_children + + key = params_nested_path[0] + key += '[' + params_nested_path[1..-1].join('][') + ']' if params_nested_path.size > 1 + + 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) } + + if type == 'Hash' && !has_children {} - elsif should_be_empty_array?(passed_children_params, params_nested_path) + elsif type == 'Array' || type&.start_with?('[') [] + elsif type == 'Set' || type&.start_with?('# 1 - key - end - def optioned_declared_params(**options) declared_params = if options[:include_parent_namespaces] # Declared params including parent namespaces diff --git a/lib/grape/version.rb b/lib/grape/version.rb index 615c251ab..05aefeb27 100644 --- a/lib/grape/version.rb +++ b/lib/grape/version.rb @@ -2,5 +2,5 @@ module Grape # The current version of Grape. - VERSION = '1.4.1' + VERSION = '1.5.0' end diff --git a/spec/grape/endpoint/declared_spec.rb b/spec/grape/endpoint/declared_spec.rb index 73d145a8e..cf332933b 100644 --- a/spec/grape/endpoint/declared_spec.rb +++ b/spec/grape/endpoint/declared_spec.rb @@ -28,10 +28,20 @@ def app 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 @@ -103,7 +113,7 @@ def app end get '/declared?first=present' expect(last_response.status).to eq(200) - expect(JSON.parse(last_response.body).keys.size).to eq(5) + expect(JSON.parse(last_response.body).keys.size).to eq(10) end it 'has a optional param with default value all the time' do @@ -122,7 +132,7 @@ def app 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 4 + expect(JSON.parse(last_response.body)['nested'].keys.size).to eq 9 end it 'builds nested params when given array' do @@ -145,45 +155,66 @@ def app 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 + 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 eq({}) + expect(JSON.parse(last_response.body)['nested']).to be_nil end + end - it 'to be an array when include_missing is true' do - subject.get '/declared' do - declared(params, include_missing: true) - 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) - expect(JSON.parse(last_response.body)['arr']).to be_a(Array) - end - it 'to be an array when nested and include_missing is true' do - subject.get '/declared' do - declared(params, include_missing: true) - end + 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 - get '/declared?first=present&nested[fourth]=1' + it 'sets objects with type=Set to be a set' do + get '/declared?first=present' expect(last_response.status).to eq(200) - expect(JSON.parse(last_response.body)['nested']['nested_arr']).to be_a(Array) + + 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 'to be nil when include_missing is false' do - subject.get '/declared' do - declared(params, include_missing: false) - 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) - expect(JSON.parse(last_response.body)['nested']).to be_nil + + 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 From 9b678f44ad167f3192ee2c89c7da81b44cc3fd09 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Wed, 30 Sep 2020 08:46:52 -0700 Subject: [PATCH 009/304] Fix Ruby 2.7 deprecation warning This fixes the warning: ``` ruby/2.7.0/gems/grape-1.4.0/lib/grape/dsl/inside_route.rb: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call ``` More details: https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/ --- CHANGELOG.md | 1 + lib/grape/dsl/inside_route.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eaf05a116..e50ff9355 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ #### Fixes * Your contribution here. +* [#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). diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index 1eb0c3084..35046e803 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -422,7 +422,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 From 6d937c9f89a29d5c3e4725a63d9d0d26b73735fc Mon Sep 17 00:00:00 2001 From: dblock Date: Wed, 30 Sep 2020 12:55:25 -0400 Subject: [PATCH 010/304] Enable new cops for RuboCop. --- .rubocop.yml | 7 +-- .rubocop_todo.yml | 49 +++++-------------- Gemfile | 2 +- Rakefile | 16 +++--- benchmark/remounting.rb | 2 + lib/grape/dsl/helpers.rb | 1 + lib/grape/middleware/base.rb | 1 + lib/grape/util/lazy_value.rb | 1 + spec/grape/entity_spec.rb | 6 +++ spec/grape/middleware/stack_spec.rb | 1 + .../validators/except_values_spec.rb | 1 + 11 files changed, 37 insertions(+), 50 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index f2d32fbfd..e03c5ca66 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,12 +1,13 @@ -require: - - rubocop-performance - AllCops: + NewCops: enable TargetRubyVersion: 2.4 Exclude: - vendor/**/* - bin/**/* +require: + - rubocop-performance + inherit_from: .rubocop_todo.yml Style/Documentation: diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 79cfdde38..84b9fc45b 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2020-05-26 08:28:37 -0400 using RuboCop version 0.84.0. +# on 2020-09-30 12:54:06 -0400 using RuboCop version 0.84.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -13,7 +13,7 @@ Layout/ClosingHeredocIndentation: - 'spec/grape/api_spec.rb' - 'spec/grape/entity_spec.rb' -# Offense count: 71 +# Offense count: 73 # Cop supports --auto-correct. Layout/EmptyLineAfterGuardClause: Enabled: false @@ -53,22 +53,16 @@ Lint/NonDeterministicRequireOrder: Exclude: - 'spec/spec_helper.rb' -# Offense count: 1 -# Cop supports --auto-correct. -Lint/RedundantCopDisableDirective: - Exclude: - - 'lib/grape/router/attribute_translator.rb' - # Offense count: 2 # Cop supports --auto-correct. Lint/ToJSON: Exclude: - 'spec/grape/middleware/formatter_spec.rb' -# Offense count: 47 +# Offense count: 50 # Configuration parameters: IgnoredMethods. Metrics/AbcSize: - Max: 44 + Max: 43 # Offense count: 6 # Configuration parameters: CountComments, ExcludedMethods. @@ -76,20 +70,20 @@ Metrics/AbcSize: Metrics/BlockLength: Max: 182 -# Offense count: 10 +# Offense count: 11 # Configuration parameters: CountComments. Metrics/ClassLength: - Max: 305 + Max: 304 # Offense count: 30 # Configuration parameters: IgnoredMethods. Metrics/CyclomaticComplexity: Max: 14 -# Offense count: 61 +# Offense count: 69 # Configuration parameters: CountComments, ExcludedMethods. Metrics/MethodLength: - Max: 36 + Max: 32 # Offense count: 12 # Configuration parameters: CountComments. @@ -120,13 +114,6 @@ Naming/MethodParameterName: - 'lib/grape/middleware/stack.rb' - 'spec/grape/api_spec.rb' -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: PreferredName. -Naming/RescuedExceptionsVariableName: - Exclude: - - 'lib/grape/middleware/error.rb' - # Offense count: 3 # Cop supports --auto-correct. Performance/InefficientHashSearch: @@ -157,25 +144,20 @@ Style/ExpandPathArguments: Style/FormatStringToken: EnforcedStyle: template -# Offense count: 23 +# 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/routing.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/error.rb' - 'lib/grape/middleware/formatter.rb' - 'lib/grape/middleware/versioner/accept_version_header.rb' - 'lib/grape/validations/params_scope.rb' - - 'lib/grape/validations/validators/base.rb' - - 'lib/grape/validations/validators/default.rb' - - 'spec/support/versioned_helpers.rb' # Offense count: 1 Style/MethodMissingSuper: @@ -191,7 +173,7 @@ Style/NumericPredicate: - 'spec/**/*' - 'lib/grape/middleware/formatter.rb' -# Offense count: 11 +# Offense count: 10 # Cop supports --auto-correct. # Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods. # AllowedMethods: present?, blank?, presence, try, try! @@ -202,18 +184,9 @@ Style/SafeNavigation: - 'lib/grape/dsl/inside_route.rb' - 'lib/grape/dsl/request_response.rb' - 'lib/grape/endpoint.rb' - - 'lib/grape/middleware/error.rb' - 'lib/grape/middleware/versioner/accept_version_header.rb' - 'lib/grape/middleware/versioner/header.rb' -# Offense count: 2 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyleForMultiline. -# SupportedStylesForMultiline: comma, consistent_comma, no_comma -Style/TrailingCommaInHashLiteral: - Exclude: - - 'lib/grape/middleware/error.rb' - # Offense count: 10 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, MinSize, WordRegex. @@ -223,7 +196,7 @@ Style/WordArray: - 'spec/grape/validations/validators/except_values_spec.rb' - 'spec/grape/validations/validators/values_spec.rb' -# Offense count: 125 +# Offense count: 131 # Cop supports --auto-correct. # Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. # URISchemes: http, https diff --git a/Gemfile b/Gemfile index 6c8f23291..99de12cfb 100644 --- a/Gemfile +++ b/Gemfile @@ -2,7 +2,7 @@ # when changing this file, run appraisal install ; rubocop -a gemfiles/*.gemfile -source 'https://rubygems.org' +source('https://rubygems.org') gemspec diff --git a/Rakefile b/Rakefile index 58788aa82..2d79797f7 100644 --- a/Rakefile +++ b/Rakefile @@ -1,12 +1,12 @@ # frozen_string_literal: true -require 'rubygems' -require 'bundler' -Bundler.setup :default, :test, :development +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' @@ -17,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/benchmark/remounting.rb b/benchmark/remounting.rb index a585c1d2e..5c565b1d3 100644 --- a/benchmark/remounting.rb +++ b/benchmark/remounting.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) require 'grape' require 'benchmark/memory' diff --git a/lib/grape/dsl/helpers.rb b/lib/grape/dsl/helpers.rb index bbd2ed3ba..d461b34e3 100644 --- a/lib/grape/dsl/helpers.rb +++ b/lib/grape/dsl/helpers.rb @@ -81,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 diff --git a/lib/grape/middleware/base.rb b/lib/grape/middleware/base.rb index e21a94e9e..0e0f1729c 100644 --- a/lib/grape/middleware/base.rb +++ b/lib/grape/middleware/base.rb @@ -8,6 +8,7 @@ class Base include Helpers attr_reader :app, :env, :options + TEXT_HTML = 'text/html' include Grape::DSL::Headers diff --git a/lib/grape/util/lazy_value.rb b/lib/grape/util/lazy_value.rb index b11364538..d757ad3e2 100644 --- a/lib/grape/util/lazy_value.rb +++ b/lib/grape/util/lazy_value.rb @@ -4,6 +4,7 @@ module Grape module Util class LazyValue attr_reader :access_keys + def initialize(value, access_keys = []) @value = value @access_keys = access_keys diff --git a/spec/grape/entity_spec.rb b/spec/grape/entity_spec.rb index 08c378ef2..add8461be 100644 --- a/spec/grape/entity_spec.rb +++ b/spec/grape/entity_spec.rb @@ -181,6 +181,7 @@ def first subject.get '/example' do c = Class.new do attr_reader :id + def initialize(id) @id = id end @@ -202,6 +203,7 @@ def initialize(id) subject.get '/examples' do c = Class.new do attr_reader :id + def initialize(id) @id = id end @@ -226,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 @@ -255,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 @@ -284,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 @@ -302,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/middleware/stack_spec.rb b/spec/grape/middleware/stack_spec.rb index 3833337e4..64f9bf382 100644 --- a/spec/grape/middleware/stack_spec.rb +++ b/spec/grape/middleware/stack_spec.rb @@ -8,6 +8,7 @@ class FooMiddleware; end class BarMiddleware; end class BlockMiddleware attr_reader :block + def initialize(&block) @block = block end diff --git a/spec/grape/validations/validators/except_values_spec.rb b/spec/grape/validations/validators/except_values_spec.rb index 1bdbfc805..4757cff8a 100644 --- a/spec/grape/validations/validators/except_values_spec.rb +++ b/spec/grape/validations/validators/except_values_spec.rb @@ -8,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 From b8ebd82f418e8b370aed84b5a856ea8875d687f8 Mon Sep 17 00:00:00 2001 From: dblock Date: Wed, 30 Sep 2020 14:03:04 -0400 Subject: [PATCH 011/304] Move read_chunks into a support helper file. --- spec/spec_helper.rb | 10 ---------- spec/support/chunks.rb | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 10 deletions(-) create mode 100644 spec/support/chunks.rb diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index db3cf8b7b..d0bb66554 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -20,17 +20,7 @@ # so it should be set to true here as well to reflect that. I18n.enforce_available_locales = true -module Chunks - def read_chunks(body) - buffer = [] - body.each { |chunk| buffer << chunk } - - buffer - end -end - RSpec.configure do |config| - config.include Chunks config.include Rack::Test::Methods config.include Spec::Support::Helpers config.raise_errors_for_deprecations! 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 From 028c10e9d4908d301667a18658f83c28f891ee5d Mon Sep 17 00:00:00 2001 From: Tim Connor Date: Thu, 1 Oct 2020 18:37:53 +1300 Subject: [PATCH 012/304] Fix bug with handling array params. Fixes an issue introduced in 7e432153f08d4985b0df625d11aef28a38beb4fe that results in Array leaf params being ignored. For example, given the following API and Request: ``` class Api < Grape::API params do optional :array, type: Array end post 'example' do declared(params, include_missing: true) end end POST /example { "array": ["value"] } ``` Expected Response Body: ``` { "array": ["value"] } ``` Observed Response Body: ``` { "array": [] } ``` --- lib/grape/dsl/inside_route.rb | 3 +-- spec/grape/endpoint/declared_spec.rb | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index 1eb0c3084..828414bd7 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -75,8 +75,7 @@ def declared_hash(passed_params, options, declared_params, params_nested_path) params_nested_path_dup = params_nested_path.dup params_nested_path_dup << declared_param.to_s - - memo[memo_key] = handle_passed_param(params_nested_path_dup) do + memo[memo_key] = passed_param || handle_passed_param(params_nested_path_dup) do passed_param end end diff --git a/spec/grape/endpoint/declared_spec.rb b/spec/grape/endpoint/declared_spec.rb index cf332933b..860f1cce4 100644 --- a/spec/grape/endpoint/declared_spec.rb +++ b/spec/grape/endpoint/declared_spec.rb @@ -135,6 +135,20 @@ def app 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 From b82040fb177dc28e0b8761b474c58209bf5bbe11 Mon Sep 17 00:00:00 2001 From: dblock Date: Mon, 5 Oct 2020 08:24:43 -0400 Subject: [PATCH 013/304] Preparing for release, 1.5.0. --- CHANGELOG.md | 7 +------ README.md | 3 +-- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e50ff9355..dbaae37a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,7 @@ -### 1.5.0 (Next) - -#### Features - -* Your contribution here. +### 1.5.0 (2020/10/05) #### Fixes -* Your contribution here. * [#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). diff --git a/README.md b/README.md index 43f35cbf0..4d1f25e42 100644 --- a/README.md +++ b/README.md @@ -156,9 +156,8 @@ content negotiation, versioning and much more. ## Stable Release -You're reading the documentation for the next release of Grape, which should be **1.5.0**. +You're reading the documentation for the stable release of Grape, 1.5.0. Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. -The current stable release is [1.4.0](https://github.com/ruby-grape/grape/blob/v1.4.0/README.md). ## Project Resources From 02d7113d09eb9fcb4264c841d1fdd305e3e8adb5 Mon Sep 17 00:00:00 2001 From: dblock Date: Mon, 5 Oct 2020 08:26:01 -0400 Subject: [PATCH 014/304] Preparing for next developer iteration, 1.5.1. --- CHANGELOG.md | 10 ++++++++++ README.md | 3 ++- lib/grape/version.rb | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbaae37a6..71a7cde38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +### 1.5.1 (Next) + +#### Features + +* Your contribution here. + +#### Fixes + +* Your contribution here. + ### 1.5.0 (2020/10/05) #### Fixes diff --git a/README.md b/README.md index 4d1f25e42..f68d58bcb 100644 --- a/README.md +++ b/README.md @@ -156,8 +156,9 @@ content negotiation, versioning and much more. ## Stable Release -You're reading the documentation for the stable release of Grape, 1.5.0. +You're reading the documentation for the next release of Grape, which should be **1.5.1**. Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. +The current stable release is [1.5.0](https://github.com/ruby-grape/grape/blob/v1.5.0/README.md). ## Project Resources diff --git a/lib/grape/version.rb b/lib/grape/version.rb index 05aefeb27..5fd4319c1 100644 --- a/lib/grape/version.rb +++ b/lib/grape/version.rb @@ -2,5 +2,5 @@ module Grape # The current version of Grape. - VERSION = '1.5.0' + VERSION = '1.5.1' end From 44217ddef12ca53d930f46b80601ae40dd5cb82f Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 9 Oct 2020 09:52:52 -0700 Subject: [PATCH 015/304] Fix declared_params regression with multiple allowed types Prior to Grape v1.5.0 and https://github.com/ruby-grape/grape/pull/2103, the following would return `nil`: ``` params do optional :status_code, types: [Integer, String] end get '/' do declared_params end ``` However, now it turns an empty `Array`. We restore the previous behavior by not returning an empty `Array` if multiple types are used. Closes https://github.com/ruby-grape/grape/issues/2115 --- CHANGELOG.md | 1 + lib/grape/dsl/inside_route.rb | 2 +- spec/grape/endpoint/declared_spec.rb | 13 ++++++++++++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71a7cde38..ebddceccb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ #### Fixes * Your contribution here. +* [#2115](https://github.com/ruby-grape/grape/pull/2115): Fix declared_params regression with multiple allowed types - [@stanhu](https://github.com/stanhu). ### 1.5.0 (2020/10/05) diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index 6f4becfea..134f1ea24 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -94,7 +94,7 @@ def handle_passed_param(params_nested_path, has_passed_children = false, &_block if type == 'Hash' && !has_children {} - elsif type == 'Array' || type&.start_with?('[') + elsif type == 'Array' || type&.start_with?('[') && !type&.include?(',') [] elsif type == 'Set' || type&.start_with?('# Date: Fri, 16 Oct 2020 12:41:51 +0200 Subject: [PATCH 016/304] Update README.md Fixed missing 's' --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f68d58bcb..3ad038cf4 100644 --- a/README.md +++ b/README.md @@ -2040,10 +2040,10 @@ end # is NOT the same as -get ':status' do # this makes param[:status] available +get ':status' do # this makes params[:status] available end -# This will make both param[:status_id] and param[:id] available +# This will make both params[:status_id] and params[:id] available get 'statuses/:status_id/reviews/:id' do end From 552e01c059e6a751d6e620a936a85e6d51ddf2d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Edstr=C3=B6m?= <108799+Legogris@users.noreply.github.com> Date: Sat, 17 Oct 2020 08:51:16 +0900 Subject: [PATCH 017/304] Fix 2.7 deprecation warning in middleware/stack (#2123) --- CHANGELOG.md | 1 + lib/grape/middleware/stack.rb | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebddceccb..76c167eef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * Your contribution here. * [#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) diff --git a/lib/grape/middleware/stack.rb b/lib/grape/middleware/stack.rb index b725e8787..a11869da8 100644 --- a/lib/grape/middleware/stack.rb +++ b/lib/grape/middleware/stack.rb @@ -70,9 +70,9 @@ def [](i) middlewares[i] end - def insert(index, *args, &block) + def insert(index, *args, **kwargs, &block) index = assert_index(index, :before) - middleware = self.class::Middleware.new(*args, &block) + middleware = self.class::Middleware.new(*args, **kwargs, &block) middlewares.insert(index, middleware) end @@ -83,8 +83,8 @@ def insert_after(index, *args, &block) insert(index + 1, *args, &block) end - def use(*args, &block) - middleware = self.class::Middleware.new(*args, &block) + def use(*args, **kwargs, &block) + middleware = self.class::Middleware.new(*args, **kwargs, &block) middlewares.push(middleware) end From 4753cb3f0015823a9a4ccacb1abdbb29bbd0b256 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Edstr=C3=B6m?= <108799+Legogris@users.noreply.github.com> Date: Sat, 17 Oct 2020 23:39:43 +0900 Subject: [PATCH 018/304] Fix 2.7 deprecation warning in validator_factory (#2121) --- CHANGELOG.md | 1 + lib/grape/validations/validator_factory.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76c167eef..3d43e98f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ #### Fixes * Your contribution here. +* [#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). diff --git a/lib/grape/validations/validator_factory.rb b/lib/grape/validations/validator_factory.rb index f23655f10..444fa0421 100644 --- a/lib/grape/validations/validator_factory.rb +++ b/lib/grape/validations/validator_factory.rb @@ -8,7 +8,7 @@ def self.create_validator(**options) options[:options], options[:required], options[:params_scope], - options[:opts]) + **options[:opts]) end end end From 5d20ed89d22377aa16fc26f9520ebaf70a1fdd4c Mon Sep 17 00:00:00 2001 From: Sami Samhuri Date: Sun, 25 Oct 2020 13:33:38 -0700 Subject: [PATCH 019/304] Remove redundant attributes on AttributeTranslator This fixes warnings about redefined attribute readers for #requirements and #request_method. --- CHANGELOG.md | 1 + lib/grape/router/attribute_translator.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d43e98f4..75f599fb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ #### Fixes * Your contribution here. +* [#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). diff --git a/lib/grape/router/attribute_translator.rb b/lib/grape/router/attribute_translator.rb index 88003887c..93ba4bdcd 100644 --- a/lib/grape/router/attribute_translator.rb +++ b/lib/grape/router/attribute_translator.rb @@ -4,7 +4,7 @@ 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 - attr_reader :attributes, :request_method, :requirements + attr_reader :attributes ROUTE_ATTRIBUTES = %i[ prefix From affd474311fe9cb4dbbcb958742b73cb27037ffb Mon Sep 17 00:00:00 2001 From: Dmitriy Nesteryuk Date: Tue, 27 Oct 2020 08:48:00 +0200 Subject: [PATCH 020/304] fix a performance issue with dependent params MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes https://github.com/ruby-grape/grape/issues/2100 The reason was in `ActiveSupport::HashWithIndifferentAccess`, it is super expensive. When users use a `Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder` or `Grape::Extensions::Hashie::Mash::ParamBuilder` parameter builder there is no change. However, users who use `Grape::Extensions::Hash::ParamBuilder` must make sure a parameter to be dependent on must be a symbol. given :matrix do # block here end Benchmark after this fix: Warming up -------------------------------------- Given 1.000 i/100ms Simple 1.000 i/100ms Calculating ------------------------------------- Given 0.804 (± 0.0%) i/s - 49.000 in 61.186831s Simple 0.855 (± 0.0%) i/s - 52.000 in 60.926097s Comparison: Simple: 0.9 i/s Given: 0.8 i/s - 1.06x slower --- CHANGELOG.md | 1 + UPGRADING.md | 22 ++++++++++++++++++++++ lib/grape/validations/params_scope.rb | 5 +++-- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75f599fb3..4aefdb27d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ #### Fixes * Your contribution here. +* [#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). diff --git a/UPGRADING.md b/UPGRADING.md index 9d63392b9..e5b6947fa 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,6 +1,28 @@ Upgrading Grape =============== +### 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 `{}`. diff --git a/lib/grape/validations/params_scope.rb b/lib/grape/validations/params_scope.rb index a51be6e1e..792762b06 100644 --- a/lib/grape/validations/params_scope.rb +++ b/lib/grape/validations/params_scope.rb @@ -61,8 +61,9 @@ def meets_dependency?(params, request_params) end return params.any? { |param| meets_dependency?(param, request_params) } if params.is_a?(Array) - return false unless params.respond_to?(:with_indifferent_access) - 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) From 7637b2724f69c1ec24c974d4993054e9bdcc5f7e Mon Sep 17 00:00:00 2001 From: David Henry Date: Wed, 4 Nov 2020 18:20:01 +0000 Subject: [PATCH 021/304] Fix validation error for nested array when `requires` => `optional` => `requires` This bug is due to the params parsing code return `{}` when the value is not found. This is fine in most cases, however in the instance that the value was optional and has nested required values. I was not able to find a way to confind the changes to just the parameter parsing as this resulted in the indexing being incorrect if any of the other array objects had an error. I have used a class to indicate that an Optional Value was missing as this avoid issues with the value actually being in the response. --- CHANGELOG.md | 1 + lib/grape/dsl/parameters.rb | 10 +++++-- .../validations/single_attribute_iterator.rb | 12 +++++++- lib/grape/validations/validators/base.rb | 3 +- .../single_attribute_iterator_spec.rb | 23 +++++++++++---- spec/grape/validations_spec.rb | 28 +++++++++++++++++++ 6 files changed, 66 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4aefdb27d..90ae03275 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ #### Fixes * Your contribution here. +* [#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). diff --git a/lib/grape/dsl/parameters.rb b/lib/grape/dsl/parameters.rb index 9d393fd93..b448ca52a 100644 --- a/lib/grape/dsl/parameters.rb +++ b/lib/grape/dsl/parameters.rb @@ -227,13 +227,17 @@ def declared_param?(param) 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 diff --git a/lib/grape/validations/single_attribute_iterator.rb b/lib/grape/validations/single_attribute_iterator.rb index f28159896..bbfad45ac 100644 --- a/lib/grape/validations/single_attribute_iterator.rb +++ b/lib/grape/validations/single_attribute_iterator.rb @@ -7,10 +7,20 @@ class SingleAttributeIterator < AttributesIterator def yield_attributes(val, attrs) attrs.each do |attr_name| - yield val, attr_name, empty?(val) + yield val, attr_name, empty?(val), skip?(val) end 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) + # return false + val == Grape::DSL::Parameters::EmptyOptionalValue + end + # Primitives like Integers and Booleans don't respond to +empty?+. # It could be possible to use +blank?+ instead, but # diff --git a/lib/grape/validations/validators/base.rb b/lib/grape/validations/validators/base.rb index 4799f4923..af7030f14 100644 --- a/lib/grape/validations/validators/base.rb +++ b/lib/grape/validations/validators/base.rb @@ -43,7 +43,8 @@ def validate!(params) # there may be more than one error per field array_errors = [] - attributes.each do |val, attr_name, empty_val| + 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 diff --git a/spec/grape/validations/single_attribute_iterator_spec.rb b/spec/grape/validations/single_attribute_iterator_spec.rb index 2b3edbf06..31a51dc47 100644 --- a/spec/grape/validations/single_attribute_iterator_spec.rb +++ b/spec/grape/validations/single_attribute_iterator_spec.rb @@ -15,7 +15,7 @@ it 'yields params and every single attribute from the list' do expect { |b| iterator.each(&b) } - .to yield_successive_args([params, :first, false], [params, :second, false]) + .to yield_successive_args([params, :first, false, false], [params, :second, false, false]) end end @@ -26,8 +26,8 @@ it 'yields every single attribute from the list for each of the array elements' do expect { |b| iterator.each(&b) }.to yield_successive_args( - [params[0], :first, false], [params[0], :second, false], - [params[1], :first, false], [params[1], :second, false] + [params[0], :first, false, false], [params[0], :second, false, false], + [params[1], :first, false, false], [params[1], :second, false, false] ) end @@ -36,9 +36,20 @@ it 'marks params with empty values' do expect { |b| iterator.each(&b) }.to yield_successive_args( - [params[0], :first, true], [params[0], :second, true], - [params[1], :first, true], [params[1], :second, true], - [params[2], :first, false], [params[2], :second, false] + [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 diff --git a/spec/grape/validations_spec.rb b/spec/grape/validations_spec.rb index 232c23534..a9d2cd18e 100644 --- a/spec/grape/validations_spec.rb +++ b/spec/grape/validations_spec.rb @@ -883,6 +883,34 @@ def validate_param!(attr_name, params) end expect(declared_params).to eq([items: [:key, { optional_subitems: [:value] }, { required_subitems: [:value] }]]) end + + it "does not report errors when required array inside missing optional array" 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 end context 'multiple validation errors' do From 2647e2cf76383a750c11d95b006499a378d6ba2f Mon Sep 17 00:00:00 2001 From: David Henry Date: Thu, 5 Nov 2020 16:33:32 +0000 Subject: [PATCH 022/304] Increase test coverage around nexted arrays/hashes with optional components This adds a number of tests around edge cases and different structures that could result in the previous validation incorrectly reporting missing data as a result of an optional element not being present. --- .../validations/single_attribute_iterator.rb | 1 - spec/grape/validations_spec.rb | 190 ++++++++++++++++-- 2 files changed, 173 insertions(+), 18 deletions(-) diff --git a/lib/grape/validations/single_attribute_iterator.rb b/lib/grape/validations/single_attribute_iterator.rb index bbfad45ac..639b03957 100644 --- a/lib/grape/validations/single_attribute_iterator.rb +++ b/lib/grape/validations/single_attribute_iterator.rb @@ -17,7 +17,6 @@ def yield_attributes(val, attrs) # are the parameter parsing stage as they are required to ensure # the correct indexing is maintained def skip?(val) - # return false val == Grape::DSL::Parameters::EmptyOptionalValue end diff --git a/spec/grape/validations_spec.rb b/spec/grape/validations_spec.rb index a9d2cd18e..5f71a330e 100644 --- a/spec/grape/validations_spec.rb +++ b/spec/grape/validations_spec.rb @@ -884,32 +884,188 @@ def validate_param!(attr_name, params) expect(declared_params).to eq([items: [:key, { optional_subitems: [:value] }, { required_subitems: [:value] }]]) end - it "does not report errors when required array inside missing optional array" do - subject.params do - requires :orders, type: Array do - requires :id, type: Integer - optional :drugs, type: Array do - requires :batches, type: Array do + 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 Arry -> 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 Arry -> 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 - subject.get '/validate_required_arrays_under_optional_arrays' do - 'validate_required_arrays_under_optional_arrays works!' + 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 - data = { - orders: [ - { id: 77, drugs: [{batches: [{batch_no: "A1234567"}]}]}, - { id: 70 } - ] - } + 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 - 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) + subject.get '/multi_level' do + 'multi_level works!' + end + end + + it "with valid data" do + data_without_errors = { + 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_without_errors + 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 end From 413a935140d9218afda28a71c1699daf22960d49 Mon Sep 17 00:00:00 2001 From: David Henry Date: Tue, 10 Nov 2020 18:28:11 +0000 Subject: [PATCH 023/304] Fix incorrect optional validation issues for MultipleParamsBase Theese have previously been fixed for the Base class --- CHANGELOG.md | 1 + lib/grape/validations/attributes_iterator.rb | 8 ++ .../multiple_attributes_iterator.rb | 2 +- .../validations/single_attribute_iterator.rb | 9 -- .../validators/multiple_params_base.rb | 3 +- .../multiple_attributes_iterator_spec.rb | 16 +++- spec/grape/validations_spec.rb | 88 ++++++++++++++++++- 7 files changed, 111 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90ae03275..2ca271902 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ #### Fixes * Your contribution here. +* [#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). diff --git a/lib/grape/validations/attributes_iterator.rb b/lib/grape/validations/attributes_iterator.rb index 6c53d469a..c16d44f3f 100644 --- a/lib/grape/validations/attributes_iterator.rb +++ b/lib/grape/validations/attributes_iterator.rb @@ -48,6 +48,14 @@ def do_each(params_to_process, parent_indicies = [], &block) 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 index 49c7c2bc6..d9ef7264b 100644 --- a/lib/grape/validations/multiple_attributes_iterator.rb +++ b/lib/grape/validations/multiple_attributes_iterator.rb @@ -6,7 +6,7 @@ class MultipleAttributesIterator < AttributesIterator private def yield_attributes(resource_params, _attrs) - yield resource_params + yield resource_params, skip?(resource_params) end end end diff --git a/lib/grape/validations/single_attribute_iterator.rb b/lib/grape/validations/single_attribute_iterator.rb index 639b03957..7fd3c3f47 100644 --- a/lib/grape/validations/single_attribute_iterator.rb +++ b/lib/grape/validations/single_attribute_iterator.rb @@ -11,15 +11,6 @@ def yield_attributes(val, attrs) end 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 - # Primitives like Integers and Booleans don't respond to +empty?+. # It could be possible to use +blank?+ instead, but # diff --git a/lib/grape/validations/validators/multiple_params_base.rb b/lib/grape/validations/validators/multiple_params_base.rb index 013386b59..03867ff1a 100644 --- a/lib/grape/validations/validators/multiple_params_base.rb +++ b/lib/grape/validations/validators/multiple_params_base.rb @@ -7,7 +7,8 @@ def validate!(params) attributes = MultipleAttributesIterator.new(self, @scope, params) array_errors = [] - attributes.each do |resource_params| + attributes.each do |resource_params, skip_value| + next if skip_value begin validate_params!(resource_params) rescue Grape::Exceptions::Validation => e diff --git a/spec/grape/validations/multiple_attributes_iterator_spec.rb b/spec/grape/validations/multiple_attributes_iterator_spec.rb index 85f848207..1508a76ed 100644 --- a/spec/grape/validations/multiple_attributes_iterator_spec.rb +++ b/spec/grape/validations/multiple_attributes_iterator_spec.rb @@ -13,8 +13,8 @@ { first: 'string', second: 'string' } end - it 'yields the whole params hash without the list of attrs' do - expect { |b| iterator.each(&b) }.to yield_with_args(params) + 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 @@ -24,7 +24,17 @@ end it 'yields each element of the array without the list of attrs' do - expect { |b| iterator.each(&b) }.to yield_successive_args(params[0], params[1]) + 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 diff --git a/spec/grape/validations_spec.rb b/spec/grape/validations_spec.rb index 5f71a330e..7629b2bc1 100644 --- a/spec/grape/validations_spec.rb +++ b/spec/grape/validations_spec.rb @@ -1023,7 +1023,7 @@ def validate_param!(attr_name, params) end it "with valid data" do - data_without_errors = { + data = { top: [ { top_id: 1, middle_1: [ {middle_1_id: 11}, {middle_1_id: 12, middle_2: [ @@ -1037,7 +1037,7 @@ def validate_param!(attr_name, params) ] } - get '/multi_level', data_without_errors + get '/multi_level', data expect(last_response.body).to eq("multi_level works!") expect(last_response.status).to eq(200) end @@ -1067,6 +1067,90 @@ def validate_param!(attr_name, params) end end end + + it "exactly_one_of" do + subject.params do + requires :orders, type: Array do + requires :id, type: Integer + optional :drugs, type: Hash do + optional :batch_no, type: String + optional :batch_id, type: String + exactly_one_of :batch_no, :batch_id + end + end + end + + subject.get '/exactly_one_of' do + 'exactly_one_of works!' + end + + data = { + orders: [ + { id: 77, drugs: {batch_no: "A1234567"}}, + { id: 70 } + ] + } + + get '/exactly_one_of', data + expect(last_response.body).to eq("exactly_one_of works!") + expect(last_response.status).to eq(200) + end + + it "at_least_one_of" do + subject.params do + requires :orders, type: Array do + requires :id, type: Integer + optional :drugs, type: Hash do + optional :batch_no, type: String + optional :batch_id, type: String + at_least_one_of :batch_no, :batch_id + end + end + end + + subject.get '/at_least_one_of' do + 'at_least_one_of works!' + end + + data = { + orders: [ + { id: 77, drugs: {batch_no: "A1234567"}}, + { id: 70 } + ] + } + + get '/at_least_one_of', data + expect(last_response.body).to eq("at_least_one_of works!") + expect(last_response.status).to eq(200) + end + + it "all_or_none_of" do + subject.params do + requires :orders, type: Array do + requires :id, type: Integer + optional :drugs, type: Hash do + optional :batch_no, type: String + optional :batch_id, type: String + all_or_none_of :batch_no, :batch_id + end + end + end + + subject.get '/all_or_none_of' do + 'all_or_none_of works!' + end + + data = { + orders: [ + { id: 77, drugs: {batch_no: "A1234567", batch_id: "12"}}, + { id: 70 } + ] + } + + get '/all_or_none_of', data + expect(last_response.body).to eq("all_or_none_of works!") + expect(last_response.status).to eq(200) + end end context 'multiple validation errors' do From d00534851fac6ddd6b9b871149843a977b28dd3a Mon Sep 17 00:00:00 2001 From: Dmitriy Nesteryuk Date: Sun, 15 Nov 2020 15:27:42 +0200 Subject: [PATCH 024/304] Preparing for release, 1.5.1 --- CHANGELOG.md | 6 +----- README.md | 4 +--- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ca271902..ba17932d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,4 @@ -### 1.5.1 (Next) - -#### Features - -* Your contribution here. +### 1.5.1 (2020/11/15) #### Fixes diff --git a/README.md b/README.md index 3ad038cf4..5c17c3c76 100644 --- a/README.md +++ b/README.md @@ -156,9 +156,7 @@ content negotiation, versioning and much more. ## Stable Release -You're reading the documentation for the next release of Grape, which should be **1.5.1**. -Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. -The current stable release is [1.5.0](https://github.com/ruby-grape/grape/blob/v1.5.0/README.md). +You're reading the documentation for the stable release of Grape, 1.5.1. ## Project Resources From 88b6484a8ae692242133701dc6ce0e12fcb1886b Mon Sep 17 00:00:00 2001 From: Dmitriy Nesteryuk Date: Sun, 15 Nov 2020 15:30:15 +0200 Subject: [PATCH 025/304] Preparing for next development iteration, 1.5.2 --- CHANGELOG.md | 10 ++++++++++ README.md | 4 +++- lib/grape/version.rb | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba17932d9..699c16b3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +### 1.5.2 (Next) + +#### Features + +* Your contribution here. + +#### Fixes + +* Your contribution here. + ### 1.5.1 (2020/11/15) #### Fixes diff --git a/README.md b/README.md index 5c17c3c76..0261cbc53 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,9 @@ content negotiation, versioning and much more. ## Stable Release -You're reading the documentation for the stable release of Grape, 1.5.1. +You're reading the documentation for the next release of Grape, which should be **1.5.2**. +Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. +The current stable release is [1.5.1](https://github.com/ruby-grape/grape/blob/v1.5.1/README.md). ## Project Resources diff --git a/lib/grape/version.rb b/lib/grape/version.rb index 5fd4319c1..b62f63d74 100644 --- a/lib/grape/version.rb +++ b/lib/grape/version.rb @@ -2,5 +2,5 @@ module Grape # The current version of Grape. - VERSION = '1.5.1' + VERSION = '1.5.2' end From 42489c243c78761238093c40f5af2b2bb9a05580 Mon Sep 17 00:00:00 2001 From: Dmitriy Nesteryuk Date: Sun, 15 Nov 2020 15:54:42 +0200 Subject: [PATCH 026/304] remove the placeholder from CHAGELOG.md --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 699c16b3f..ac5e2a5cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,6 @@ #### Fixes -* Your contribution here. * [#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). From 94c9d6ff88999e9f5476f25e32ff7c117db1317d Mon Sep 17 00:00:00 2001 From: K0H205 Date: Wed, 18 Nov 2020 09:43:13 +0900 Subject: [PATCH 027/304] Fix Ruby 2.7 keyword deprecation warning in validators/coerce (#2131) --- CHANGELOG.md | 1 + lib/grape/validations/validators/coerce.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac5e2a5cc..1462d08d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ #### Fixes * Your contribution here. +* [#2131](https://github.com/ruby-grape/grape/pull/2131): Fix Ruby 2.7 keyword deprecation warning in validators/coerce - [@K0H205](https://github.com/K0H205). ### 1.5.1 (2020/11/15) diff --git a/lib/grape/validations/validators/coerce.rb b/lib/grape/validations/validators/coerce.rb index 5b6f960a6..1b15c069d 100644 --- a/lib/grape/validations/validators/coerce.rb +++ b/lib/grape/validations/validators/coerce.rb @@ -17,7 +17,7 @@ class Instance module Validations class CoerceValidator < Base - def initialize(*_args) + def initialize(attrs, options, required, scope, **opts) super @converter = if type.is_a?(Grape::Validations::Types::VariantCollectionCoercer) From a2f6725d5dbe4f76cc9ed252f0897b4b6cb4f8b3 Mon Sep 17 00:00:00 2001 From: Edward Rudd Date: Thu, 19 Nov 2020 16:16:55 -0500 Subject: [PATCH 028/304] fix PR reference in changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1462d08d8..12d67e88d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,7 @@ * [#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). -* [#2083](https://github.com/ruby-grape/grape/pull/2083): Set `Cache-Control` header only for streamed responses - [@stanhu](https://github.com/stanhu). +* [#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). From 8cd284b6111f9b51ec5da1bde5f32e924dd9a074 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Thu, 19 Nov 2020 20:47:44 +0100 Subject: [PATCH 029/304] Use #ruby2_keywords for correct delegation on Ruby <= 2.6, 2.7 and 3 * See https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/#a-compatible-delegation --- CHANGELOG.md | 1 + lib/grape/middleware/stack.rb | 30 ++++++++++++----------------- spec/grape/middleware/stack_spec.rb | 3 +-- 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1462d08d8..7377d3d64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * Your contribution here. * [#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). ### 1.5.1 (2020/11/15) diff --git a/lib/grape/middleware/stack.rb b/lib/grape/middleware/stack.rb index a11869da8..dab755fe6 100644 --- a/lib/grape/middleware/stack.rb +++ b/lib/grape/middleware/stack.rb @@ -6,12 +6,11 @@ module Middleware # It allows to insert and insert after class Stack class Middleware - attr_reader :args, :opts, :block, :klass + attr_reader :args, :block, :klass - def initialize(klass, *args, **opts, &block) + def initialize(klass, *args, &block) @klass = klass - @args = args - @opts = opts + @args = args @block = block end @@ -32,16 +31,8 @@ def inspect klass.to_s end - if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.7') - def use_in(builder) - block ? builder.use(klass, *args, **opts, &block) : builder.use(klass, *args, **opts) - end - else - def use_in(builder) - args = self.args - args += [opts] unless opts.empty? - block ? builder.use(klass, *args, &block) : builder.use(klass, *args) - end + def use_in(builder) + builder.use(@klass, *@args, &@block) end end @@ -70,11 +61,12 @@ def [](i) middlewares[i] end - def insert(index, *args, **kwargs, &block) + def insert(index, *args, &block) index = assert_index(index, :before) - middleware = self.class::Middleware.new(*args, **kwargs, &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 @@ -82,11 +74,13 @@ 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, **kwargs, &block) - middleware = self.class::Middleware.new(*args, **kwargs, &block) + 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| diff --git a/spec/grape/middleware/stack_spec.rb b/spec/grape/middleware/stack_spec.rb index 64f9bf382..b7ac1b149 100644 --- a/spec/grape/middleware/stack_spec.rb +++ b/spec/grape/middleware/stack_spec.rb @@ -35,8 +35,7 @@ def initialize(&block) expect { subject.use StackSpec::BarMiddleware, false, my_arg: 42 } .to change { subject.size }.by(1) expect(subject.last).to eq(StackSpec::BarMiddleware) - expect(subject.last.args).to eq([false]) - expect(subject.last.opts).to eq(my_arg: 42) + expect(subject.last.args).to eq([false, { my_arg: 42 }]) end it 'pushes a middleware class with block arguments onto the stack' do From 6ce287027183b2a9edc9d2d5187b68d22dcdb321 Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Sun, 22 Nov 2020 21:50:04 +0200 Subject: [PATCH 030/304] Removed obsolete coercer benchmark --- benchmark/simple_with_type_coercer.rb | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 benchmark/simple_with_type_coercer.rb diff --git a/benchmark/simple_with_type_coercer.rb b/benchmark/simple_with_type_coercer.rb deleted file mode 100644 index 1dc0e7be4..000000000 --- a/benchmark/simple_with_type_coercer.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -$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 From 552af82c508ac16368bfd40c6962281312b95b25 Mon Sep 17 00:00:00 2001 From: Miyake J Takuma Date: Tue, 1 Dec 2020 16:55:20 +0900 Subject: [PATCH 031/304] Fix typos --- CHANGELOG.md | 1 + spec/grape/validations_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99aba0612..5cef5172f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ #### Fixes * Your contribution here. +* [#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). diff --git a/spec/grape/validations_spec.rb b/spec/grape/validations_spec.rb index 7629b2bc1..d325634f6 100644 --- a/spec/grape/validations_spec.rb +++ b/spec/grape/validations_spec.rb @@ -921,7 +921,7 @@ def validate_param!(attr_name, params) expect(last_response.status).to eq(200) end - it "simplest example using Arry -> Array -> Hash -> String" do + it "simplest example using Array -> Array -> Hash -> String" do subject.params do requires :orders, type: Array do requires :id, type: Integer @@ -947,7 +947,7 @@ def validate_param!(attr_name, params) expect(last_response.status).to eq(200) end - it "simplest example using Arry -> Hash -> String" do + it "simplest example using Array -> Hash -> String" do subject.params do requires :orders, type: Array do requires :id, type: Integer From e65bdc10c67a3775997ef04e718e22d781f95a11 Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Mon, 28 Dec 2020 19:15:54 +0200 Subject: [PATCH 032/304] Explicitly require active_support/core_ext/array/conversions for the Array#to_xml method --- CHANGELOG.md | 1 + lib/grape.rb | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cef5172f..c652b0844 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ #### Fixes * Your contribution here. +* [#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). diff --git a/lib/grape.rb b/lib/grape.rb index e2a0b9808..c1f313c2f 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -12,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' From 9ede39ab0166c4d2aac7ce193246d07aa2085f85 Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Sat, 26 Dec 2020 22:45:59 +0200 Subject: [PATCH 033/304] Enable GitHub Actions with updated Rubocop and Danger --- .github/workflows/danger.yml | 19 +++ .github/workflows/test.yml | 80 +++++++++ .rubocop.yml | 1 + .rubocop_todo.yml | 305 ++++++++++++++++++++++++++++++++--- .travis.yml | 64 -------- CHANGELOG.md | 2 + Dangerfile | 2 +- Gemfile | 9 +- README.md | 2 +- 9 files changed, 387 insertions(+), 97 deletions(-) create mode 100644 .github/workflows/danger.yml create mode 100644 .github/workflows/test.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml new file mode 100644 index 000000000..681f52b46 --- /dev/null +++ b/.github/workflows/danger.yml @@ -0,0 +1,19 @@ +--- +name: Danger +on: + pull_request: + ypes: [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: bundle exec danger + env: + DANGER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..8782c8247 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,80 @@ +--- +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 + gemfile: + - Gemfile + - gemfiles/rack1.gemfile + - gemfiles/rack2.gemfile + - gemfiles/rack_edge.gemfile + - gemfiles/rails_edge.gemfile + - gemfiles/rails_5.gemfile + - gemfiles/rails_6.gemfile + experimental: [false] + include: + - ruby: 2.7 + gemfile: 'gemfiles/multi_json.gemfile' + experimental: false + - ruby: 2.7 + gemfile: 'gemfiles/multi_xml.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/.rubocop.yml b/.rubocop.yml index e03c5ca66..ee4a91c64 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,6 +1,7 @@ AllCops: NewCops: enable TargetRubyVersion: 2.4 + SuggestExtensions: false Exclude: - vendor/**/* - bin/**/* diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 84b9fc45b..4320b3937 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,12 +1,12 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2020-09-30 12:54:06 -0400 using RuboCop version 0.84.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: 6 +# Offense count: 5 # Cop supports --auto-correct. Layout/ClosingHeredocIndentation: Exclude: @@ -18,6 +18,22 @@ Layout/ClosingHeredocIndentation: 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. @@ -34,19 +50,90 @@ Layout/HashAlignment: # Offense count: 7 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: squiggly, active_support, powerpack, unindent 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: - '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 # Cop supports --auto-correct. Lint/NonDeterministicRequireOrder: @@ -59,54 +146,68 @@ Lint/ToJSON: Exclude: - 'spec/grape/middleware/formatter_spec.rb' -# Offense count: 50 -# Configuration parameters: IgnoredMethods. +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: AllowComments. +Lint/UselessMethodDefinition: + Exclude: + - 'lib/grape/validations/validators/coerce.rb' + +# Offense count: 42 +# Configuration parameters: IgnoredMethods, CountRepeatedAttributes. Metrics/AbcSize: - Max: 43 + Max: 47 # Offense count: 6 -# Configuration parameters: CountComments, ExcludedMethods. -# ExcludedMethods: refine +# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. +# IgnoredMethods: refine Metrics/BlockLength: Max: 182 # Offense count: 11 -# Configuration parameters: CountComments. +# Configuration parameters: CountComments, CountAsOne. Metrics/ClassLength: Max: 304 # Offense count: 30 # Configuration parameters: IgnoredMethods. Metrics/CyclomaticComplexity: - Max: 14 + Max: 17 -# Offense count: 69 -# Configuration parameters: CountComments, ExcludedMethods. +# Offense count: 71 +# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. Metrics/MethodLength: Max: 32 # Offense count: 12 -# Configuration parameters: CountComments. +# Configuration parameters: CountComments, CountAsOne. Metrics/ModuleLength: Max: 220 -# Offense count: 25 +# 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: 3 +# Offense count: 4 # Configuration parameters: EnforcedStyleForLeadingUnderscores. # SupportedStylesForLeadingUnderscores: disallowed, required, optional Naming/MemoizedInstanceVariableName: Exclude: - 'lib/grape/api/instance.rb' + - 'lib/grape/config.rb' - 'lib/grape/middleware/base.rb' - 'spec/grape/integration/rack_spec.rb' # Offense count: 5 # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. -# AllowedNames: io, id, to, by, on, in, at, ip, db, os, pp +# AllowedNames: at, by, db, id, in, io, ip, of, on, os, pp, to Naming/MethodParameterName: Exclude: - 'lib/grape/endpoint.rb' @@ -114,12 +215,95 @@ Naming/MethodParameterName: - '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. +Performance/BigDecimalWithNumericArgument: + Exclude: + - 'spec/grape/validations/types/primitive_coercer_spec.rb' + +# Offense count: 21 +# Cop supports --auto-correct. +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 +Performance/MethodObjectAsBlock: + Exclude: + - '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 +# 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: @@ -138,12 +322,24 @@ Style/ExpandPathArguments: - '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: . +# Configuration parameters: MaxUnannotatedPlaceholdersAllowed. # SupportedStyles: annotated, template, unannotated Style/FormatStringToken: EnforcedStyle: template +# Offense count: 1 +# Cop supports --auto-correct. +Style/GlobalStdStream: + Exclude: + - 'benchmark/remounting.rb' + # Offense count: 19 # Cop supports --auto-correct. Style/IfUnlessModifier: @@ -159,20 +355,41 @@ Style/IfUnlessModifier: - 'lib/grape/middleware/versioner/accept_version_header.rb' - 'lib/grape/validations/params_scope.rb' -# Offense count: 1 -Style/MethodMissingSuper: - Exclude: - - 'lib/grape/router/attribute_translator.rb' - # Offense count: 1 # Cop supports --auto-correct. -# Configuration parameters: AutoCorrect, EnforcedStyle, IgnoredMethods. +# 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. @@ -187,6 +404,42 @@ Style/SafeNavigation: - '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/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 +# 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. @@ -196,7 +449,7 @@ Style/WordArray: - 'spec/grape/validations/validators/except_values_spec.rb' - 'spec/grape/validations/validators/values_spec.rb' -# Offense count: 131 +# Offense count: 132 # Cop supports --auto-correct. # Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. # URISchemes: http, https diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 48632523d..000000000 --- a/.travis.yml +++ /dev/null @@ -1,64 +0,0 @@ -language: ruby - -sudo: false - -# "gemfile" is required for "allow_failures" option, -# see https://docs.travis-ci.com/user/customizing-the-build/#matching-jobs-with-allow_failures -gemfile: - -script: bundle exec rake spec - -matrix: - include: - - rvm: 2.7.1 - script: - - bundle exec danger - - rvm: 2.7.1 - gemfile: Gemfile - - rvm: 2.7.1 - gemfile: gemfiles/rack1.gemfile - - rvm: 2.7.1 - gemfile: gemfiles/rack2.gemfile - - rvm: 2.7.1 - gemfile: gemfiles/rack_edge.gemfile - - rvm: 2.7.1 - gemfile: gemfiles/rails_edge.gemfile - - rvm: 2.7.1 - gemfile: gemfiles/rails_5.gemfile - - rvm: 2.7.1 - gemfile: gemfiles/rails_6.gemfile - - rvm: 2.7.1 - gemfile: Gemfile - script: - - bundle exec rspec spec/integration/eager_load - - rvm: 2.7.1 - gemfile: gemfiles/multi_json.gemfile - script: - - bundle exec rspec spec/integration/multi_json - - rvm: 2.7.1 - gemfile: gemfiles/multi_xml.gemfile - script: - - bundle exec rspec spec/integration/multi_xml - - rvm: 2.6.6 - gemfile: Gemfile - - rvm: 2.6.6 - gemfile: gemfiles/rails_5.gemfile - - rvm: 2.6.6 - gemfile: gemfiles/rails_6.gemfile - - rvm: 2.5.8 - gemfile: Gemfile - - rvm: 2.5.8 - gemfile: gemfiles/rails_5.gemfile - - rvm: 2.5.8 - gemfile: gemfiles/rails_6.gemfile - - rvm: ruby-head - - rvm: jruby-head - - rvm: rbx-3 - - rvm: truffleruby-head - allow_failures: - - rvm: ruby-head - - rvm: jruby-head - - rvm: rbx-3 - - rvm: truffleruby-head - -bundler_args: --without development diff --git a/CHANGELOG.md b/CHANGELOG.md index c652b0844..9197d989d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ #### Features * Your contribution here. +* [#2143](https://github.com/ruby-grape/grape/pull/2143): Enable GitHub Actions with updated RuboCop and Danger - [@anakinj](https://github.com/anakinj). + #### Fixes diff --git a/Dangerfile b/Dangerfile index 2f1200640..527dbb8b7 100644 --- a/Dangerfile +++ b/Dangerfile @@ -1,4 +1,4 @@ # frozen_string_literal: true danger.import_dangerfile(gem: 'ruby-grape-danger') -toc.check +toc.check! diff --git a/Gemfile b/Gemfile index 99de12cfb..6c4124a52 100644 --- a/Gemfile +++ b/Gemfile @@ -10,9 +10,9 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '0.84.0' - gem 'rubocop-ast', '< 0.7' - gem 'rubocop-performance', require: false + gem 'rubocop', '1.7.0' + gem 'rubocop-ast', '1.3.0' + gem 'rubocop-performance', '1.9.1', require: false end group :development do @@ -27,12 +27,11 @@ end group :test do gem 'cookiejar' gem 'coveralls_reborn' - gem 'danger-toc', '~> 0.1.2' 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.1.0', require: false + gem 'ruby-grape-danger', '~> 0.2.0', require: false end diff --git a/README.md b/README.md index 0261cbc53..9e3a1dd6c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ![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) +[![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](https://inch-ci.org/github/ruby-grape/grape.svg)](https://inch-ci.org/github/ruby-grape/grape) From 7f4f72c5e3750ab3b903b24ea940882bdbb1f111 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Mon, 28 Dec 2020 18:46:37 +0100 Subject: [PATCH 034/304] Add ruby 3.0.0 travis Add rails_6_1.gemfile Add rails_6_1.gemfile Transform **options into *options since Rack::Builder is calling middleware with .new(klass, *args, &block) Add ** to versioned_path Explicit ** for macro options Replace ** by * since its anonymous Add explicit { } Transform **opts to *opts since its called with with (*args, &block) Add explicit ** Transform **opts to *opts since rspec-mock is calling .new(*args) Add 6_1 gemfile Add CHANGELOG Explicitly require active_support/core_ext/array/conversions for the Array#to_xml method Enable GitHub Actions with updated Rubocop and Danger Merge master Rubocop gemfiles Explicitly require active_support/core_ext/array/conversions for the Array#to_xml method --- .github/workflows/danger.yml | 19 ++ .github/workflows/test.yml | 80 ++++++ .rubocop.yml | 1 + .rubocop_todo.yml | 305 ++++++++++++++++++++-- .travis.yml | 64 ----- Appraisals | 4 + CHANGELOG.md | 3 + Dangerfile | 2 +- Gemfile | 9 +- README.md | 2 +- gemfiles/multi_json.gemfile | 10 +- gemfiles/multi_xml.gemfile | 10 +- gemfiles/rack1.gemfile | 10 +- gemfiles/rack2.gemfile | 10 +- gemfiles/rack_edge.gemfile | 10 +- gemfiles/rails_5.gemfile | 10 +- gemfiles/rails_6.gemfile | 10 +- gemfiles/rails_6_1.gemfile | 39 +++ gemfiles/rails_edge.gemfile | 10 +- lib/grape.rb | 1 + lib/grape/dsl/routing.rb | 5 +- lib/grape/exceptions/validation_errors.rb | 2 +- lib/grape/middleware/auth/base.rb | 6 +- lib/grape/middleware/base.rb | 4 +- lib/grape/middleware/error.rb | 2 +- lib/grape/validations/validators/base.rb | 10 +- spec/grape/request_spec.rb | 2 +- spec/shared/versioning_examples.rb | 40 +-- spec/support/versioned_helpers.rb | 2 +- 29 files changed, 510 insertions(+), 172 deletions(-) create mode 100644 .github/workflows/danger.yml create mode 100644 .github/workflows/test.yml delete mode 100644 .travis.yml create mode 100644 gemfiles/rails_6_1.gemfile diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml new file mode 100644 index 000000000..681f52b46 --- /dev/null +++ b/.github/workflows/danger.yml @@ -0,0 +1,19 @@ +--- +name: Danger +on: + pull_request: + ypes: [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: bundle exec danger + env: + DANGER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..8782c8247 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,80 @@ +--- +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 + gemfile: + - Gemfile + - gemfiles/rack1.gemfile + - gemfiles/rack2.gemfile + - gemfiles/rack_edge.gemfile + - gemfiles/rails_edge.gemfile + - gemfiles/rails_5.gemfile + - gemfiles/rails_6.gemfile + experimental: [false] + include: + - ruby: 2.7 + gemfile: 'gemfiles/multi_json.gemfile' + experimental: false + - ruby: 2.7 + gemfile: 'gemfiles/multi_xml.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/.rubocop.yml b/.rubocop.yml index e03c5ca66..ee4a91c64 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,6 +1,7 @@ AllCops: NewCops: enable TargetRubyVersion: 2.4 + SuggestExtensions: false Exclude: - vendor/**/* - bin/**/* diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 84b9fc45b..4320b3937 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,12 +1,12 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2020-09-30 12:54:06 -0400 using RuboCop version 0.84.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: 6 +# Offense count: 5 # Cop supports --auto-correct. Layout/ClosingHeredocIndentation: Exclude: @@ -18,6 +18,22 @@ Layout/ClosingHeredocIndentation: 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. @@ -34,19 +50,90 @@ Layout/HashAlignment: # Offense count: 7 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: squiggly, active_support, powerpack, unindent 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: - '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 # Cop supports --auto-correct. Lint/NonDeterministicRequireOrder: @@ -59,54 +146,68 @@ Lint/ToJSON: Exclude: - 'spec/grape/middleware/formatter_spec.rb' -# Offense count: 50 -# Configuration parameters: IgnoredMethods. +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: AllowComments. +Lint/UselessMethodDefinition: + Exclude: + - 'lib/grape/validations/validators/coerce.rb' + +# Offense count: 42 +# Configuration parameters: IgnoredMethods, CountRepeatedAttributes. Metrics/AbcSize: - Max: 43 + Max: 47 # Offense count: 6 -# Configuration parameters: CountComments, ExcludedMethods. -# ExcludedMethods: refine +# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. +# IgnoredMethods: refine Metrics/BlockLength: Max: 182 # Offense count: 11 -# Configuration parameters: CountComments. +# Configuration parameters: CountComments, CountAsOne. Metrics/ClassLength: Max: 304 # Offense count: 30 # Configuration parameters: IgnoredMethods. Metrics/CyclomaticComplexity: - Max: 14 + Max: 17 -# Offense count: 69 -# Configuration parameters: CountComments, ExcludedMethods. +# Offense count: 71 +# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. Metrics/MethodLength: Max: 32 # Offense count: 12 -# Configuration parameters: CountComments. +# Configuration parameters: CountComments, CountAsOne. Metrics/ModuleLength: Max: 220 -# Offense count: 25 +# 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: 3 +# Offense count: 4 # Configuration parameters: EnforcedStyleForLeadingUnderscores. # SupportedStylesForLeadingUnderscores: disallowed, required, optional Naming/MemoizedInstanceVariableName: Exclude: - 'lib/grape/api/instance.rb' + - 'lib/grape/config.rb' - 'lib/grape/middleware/base.rb' - 'spec/grape/integration/rack_spec.rb' # Offense count: 5 # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. -# AllowedNames: io, id, to, by, on, in, at, ip, db, os, pp +# AllowedNames: at, by, db, id, in, io, ip, of, on, os, pp, to Naming/MethodParameterName: Exclude: - 'lib/grape/endpoint.rb' @@ -114,12 +215,95 @@ Naming/MethodParameterName: - '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. +Performance/BigDecimalWithNumericArgument: + Exclude: + - 'spec/grape/validations/types/primitive_coercer_spec.rb' + +# Offense count: 21 +# Cop supports --auto-correct. +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 +Performance/MethodObjectAsBlock: + Exclude: + - '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 +# 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: @@ -138,12 +322,24 @@ Style/ExpandPathArguments: - '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: . +# Configuration parameters: MaxUnannotatedPlaceholdersAllowed. # SupportedStyles: annotated, template, unannotated Style/FormatStringToken: EnforcedStyle: template +# Offense count: 1 +# Cop supports --auto-correct. +Style/GlobalStdStream: + Exclude: + - 'benchmark/remounting.rb' + # Offense count: 19 # Cop supports --auto-correct. Style/IfUnlessModifier: @@ -159,20 +355,41 @@ Style/IfUnlessModifier: - 'lib/grape/middleware/versioner/accept_version_header.rb' - 'lib/grape/validations/params_scope.rb' -# Offense count: 1 -Style/MethodMissingSuper: - Exclude: - - 'lib/grape/router/attribute_translator.rb' - # Offense count: 1 # Cop supports --auto-correct. -# Configuration parameters: AutoCorrect, EnforcedStyle, IgnoredMethods. +# 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. @@ -187,6 +404,42 @@ Style/SafeNavigation: - '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/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 +# 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. @@ -196,7 +449,7 @@ Style/WordArray: - 'spec/grape/validations/validators/except_values_spec.rb' - 'spec/grape/validations/validators/values_spec.rb' -# Offense count: 131 +# Offense count: 132 # Cop supports --auto-correct. # Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. # URISchemes: http, https diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 48632523d..000000000 --- a/.travis.yml +++ /dev/null @@ -1,64 +0,0 @@ -language: ruby - -sudo: false - -# "gemfile" is required for "allow_failures" option, -# see https://docs.travis-ci.com/user/customizing-the-build/#matching-jobs-with-allow_failures -gemfile: - -script: bundle exec rake spec - -matrix: - include: - - rvm: 2.7.1 - script: - - bundle exec danger - - rvm: 2.7.1 - gemfile: Gemfile - - rvm: 2.7.1 - gemfile: gemfiles/rack1.gemfile - - rvm: 2.7.1 - gemfile: gemfiles/rack2.gemfile - - rvm: 2.7.1 - gemfile: gemfiles/rack_edge.gemfile - - rvm: 2.7.1 - gemfile: gemfiles/rails_edge.gemfile - - rvm: 2.7.1 - gemfile: gemfiles/rails_5.gemfile - - rvm: 2.7.1 - gemfile: gemfiles/rails_6.gemfile - - rvm: 2.7.1 - gemfile: Gemfile - script: - - bundle exec rspec spec/integration/eager_load - - rvm: 2.7.1 - gemfile: gemfiles/multi_json.gemfile - script: - - bundle exec rspec spec/integration/multi_json - - rvm: 2.7.1 - gemfile: gemfiles/multi_xml.gemfile - script: - - bundle exec rspec spec/integration/multi_xml - - rvm: 2.6.6 - gemfile: Gemfile - - rvm: 2.6.6 - gemfile: gemfiles/rails_5.gemfile - - rvm: 2.6.6 - gemfile: gemfiles/rails_6.gemfile - - rvm: 2.5.8 - gemfile: Gemfile - - rvm: 2.5.8 - gemfile: gemfiles/rails_5.gemfile - - rvm: 2.5.8 - gemfile: gemfiles/rails_6.gemfile - - rvm: ruby-head - - rvm: jruby-head - - rvm: rbx-3 - - rvm: truffleruby-head - allow_failures: - - rvm: ruby-head - - rvm: jruby-head - - rvm: rbx-3 - - rvm: truffleruby-head - -bundler_args: --without development diff --git a/Appraisals b/Appraisals index 7e17deba5..1613cfa03 100644 --- a/Appraisals +++ b/Appraisals @@ -8,6 +8,10 @@ appraise 'rails-6' do gem 'rails', '~> 6.0' end +appraise 'rails-6-1' do + gem 'rails', '~> 6.1' +end + appraise 'rails-edge' do gem 'rails', github: 'rails/rails' end diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cef5172f..7d89d9352 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,13 @@ #### Features * Your contribution here. +* [#2143](https://github.com/ruby-grape/grape/pull/2143): Enable GitHub Actions with updated RuboCop and Danger - [@anakinj](https://github.com/anakinj). +* [#2145](https://github.com/ruby-grape/grape/pull/2145): Ruby 3.0 compatibility - [@ericproulx](https://github.com/ericproulx). #### Fixes * Your contribution here. +* [#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). diff --git a/Dangerfile b/Dangerfile index 2f1200640..527dbb8b7 100644 --- a/Dangerfile +++ b/Dangerfile @@ -1,4 +1,4 @@ # frozen_string_literal: true danger.import_dangerfile(gem: 'ruby-grape-danger') -toc.check +toc.check! diff --git a/Gemfile b/Gemfile index 99de12cfb..6c4124a52 100644 --- a/Gemfile +++ b/Gemfile @@ -10,9 +10,9 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '0.84.0' - gem 'rubocop-ast', '< 0.7' - gem 'rubocop-performance', require: false + gem 'rubocop', '1.7.0' + gem 'rubocop-ast', '1.3.0' + gem 'rubocop-performance', '1.9.1', require: false end group :development do @@ -27,12 +27,11 @@ end group :test do gem 'cookiejar' gem 'coveralls_reborn' - gem 'danger-toc', '~> 0.1.2' 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.1.0', require: false + gem 'ruby-grape-danger', '~> 0.2.0', require: false end diff --git a/README.md b/README.md index 0261cbc53..9e3a1dd6c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ![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) +[![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](https://inch-ci.org/github/ruby-grape/grape.svg)](https://inch-ci.org/github/ruby-grape/grape) diff --git a/gemfiles/multi_json.gemfile b/gemfiles/multi_json.gemfile index 279eabba5..726c8be19 100644 --- a/gemfiles/multi_json.gemfile +++ b/gemfiles/multi_json.gemfile @@ -10,14 +10,15 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '0.84.0' - gem 'rubocop-ast', '< 0.7' - gem 'rubocop-performance', require: false + 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' @@ -26,14 +27,13 @@ end group :test do gem 'cookiejar' gem 'coveralls_reborn' - gem 'danger-toc', '~> 0.1.2' 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.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 ba20f9415..72d4e3f30 100644 --- a/gemfiles/multi_xml.gemfile +++ b/gemfiles/multi_xml.gemfile @@ -10,14 +10,15 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '0.84.0' - gem 'rubocop-ast', '< 0.7' - gem 'rubocop-performance', require: false + 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' @@ -26,14 +27,13 @@ end group :test do gem 'cookiejar' gem 'coveralls_reborn' - gem 'danger-toc', '~> 0.1.2' 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.1.0', require: false + gem 'ruby-grape-danger', '~> 0.2.0', require: false end gemspec path: '../' diff --git a/gemfiles/rack1.gemfile b/gemfiles/rack1.gemfile index 05e84c399..bb72144a0 100644 --- a/gemfiles/rack1.gemfile +++ b/gemfiles/rack1.gemfile @@ -10,14 +10,15 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '0.84.0' - gem 'rubocop-ast', '< 0.7' - gem 'rubocop-performance', require: false + 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' @@ -26,14 +27,13 @@ end group :test do gem 'cookiejar' gem 'coveralls_reborn' - gem 'danger-toc', '~> 0.1.2' 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.1.0', require: false + gem 'ruby-grape-danger', '~> 0.2.0', require: false end gemspec path: '../' diff --git a/gemfiles/rack2.gemfile b/gemfiles/rack2.gemfile index accc673b3..6522140d9 100644 --- a/gemfiles/rack2.gemfile +++ b/gemfiles/rack2.gemfile @@ -10,14 +10,15 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '0.84.0' - gem 'rubocop-ast', '< 0.7' - gem 'rubocop-performance', require: false + 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' @@ -26,14 +27,13 @@ end group :test do gem 'cookiejar' gem 'coveralls_reborn' - gem 'danger-toc', '~> 0.1.2' 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.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 d4271ef76..25b275b6c 100644 --- a/gemfiles/rack_edge.gemfile +++ b/gemfiles/rack_edge.gemfile @@ -10,14 +10,15 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '0.84.0' - gem 'rubocop-ast', '< 0.7' - gem 'rubocop-performance', require: false + 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' @@ -26,14 +27,13 @@ end group :test do gem 'cookiejar' gem 'coveralls_reborn' - gem 'danger-toc', '~> 0.1.2' 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.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 b32c7d3ac..7dc94e695 100644 --- a/gemfiles/rails_5.gemfile +++ b/gemfiles/rails_5.gemfile @@ -10,14 +10,15 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '0.84.0' - gem 'rubocop-ast', '< 0.7' - gem 'rubocop-performance', require: false + 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' @@ -26,14 +27,13 @@ end group :test do gem 'cookiejar' gem 'coveralls_reborn' - gem 'danger-toc', '~> 0.1.2' 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.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 index 85e5f69fc..5db9b049d 100644 --- a/gemfiles/rails_6.gemfile +++ b/gemfiles/rails_6.gemfile @@ -10,14 +10,15 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '0.84.0' - gem 'rubocop-ast', '< 0.7' - gem 'rubocop-performance', require: false + 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' @@ -26,14 +27,13 @@ end group :test do gem 'cookiejar' gem 'coveralls_reborn' - gem 'danger-toc', '~> 0.1.2' 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.1.0', require: false + 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 bfb0d3f72..5d179f078 100644 --- a/gemfiles/rails_edge.gemfile +++ b/gemfiles/rails_edge.gemfile @@ -10,14 +10,15 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '0.84.0' - gem 'rubocop-ast', '< 0.7' - gem 'rubocop-performance', require: false + 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' @@ -26,14 +27,13 @@ end group :test do gem 'cookiejar' gem 'coveralls_reborn' - gem 'danger-toc', '~> 0.1.2' 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.1.0', require: false + gem 'ruby-grape-danger', '~> 0.2.0', require: false end gemspec path: '../' diff --git a/lib/grape.rb b/lib/grape.rb index e2a0b9808..c1f313c2f 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -12,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' diff --git a/lib/grape/dsl/routing.rb b/lib/grape/dsl/routing.rb index 76a22a79f..61bf1f21a 100644 --- a/lib/grape/dsl/routing.rb +++ b/lib/grape/dsl/routing.rb @@ -79,11 +79,12 @@ def do_not_route_options! namespace_inheritable(:do_not_route_options, true) end - def mount(mounts, **opts) + def mount(mounts, *opts) mounts = { mounts => '/' } unless mounts.respond_to?(:each_pair) mounts.each_pair do |app, path| if app.respond_to?(:mount_instance) - mount(app.mount_instance(configuration: opts[:with] || {}) => path) + opts_with = opts.any? ? opts.shift[:with] : {} + mount({ app.mount_instance(configuration: opts_with) => path }) next end in_setting = inheritable_setting diff --git a/lib/grape/exceptions/validation_errors.rb b/lib/grape/exceptions/validation_errors.rb index f8bf32e31..20f7dd2bf 100644 --- a/lib/grape/exceptions/validation_errors.rb +++ b/lib/grape/exceptions/validation_errors.rb @@ -39,7 +39,7 @@ def as_json(**_opts) end end - def to_json(**_opts) + def to_json(*_opts) as_json.to_json end diff --git a/lib/grape/middleware/auth/base.rb b/lib/grape/middleware/auth/base.rb index 61a8cc588..081613c92 100644 --- a/lib/grape/middleware/auth/base.rb +++ b/lib/grape/middleware/auth/base.rb @@ -10,9 +10,9 @@ class Base attr_accessor :options, :app, :env - def initialize(app, **options) + def initialize(app, *options) @app = app - @options = options + @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/base.rb b/lib/grape/middleware/base.rb index 0e0f1729c..730a6cb3c 100644 --- a/lib/grape/middleware/base.rb +++ b/lib/grape/middleware/base.rb @@ -15,9 +15,9 @@ class Base # @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 diff --git a/lib/grape/middleware/error.rb b/lib/grape/middleware/error.rb index 54bded3c8..20f6a89f3 100644 --- a/lib/grape/middleware/error.rb +++ b/lib/grape/middleware/error.rb @@ -27,7 +27,7 @@ def default_options } end - def initialize(app, **options) + def initialize(app, *options) super self.class.send(:include, @options[:helpers]) if @options[:helpers] end diff --git a/lib/grape/validations/validators/base.rb b/lib/grape/validations/validators/base.rb index af7030f14..f45a254ca 100644 --- a/lib/grape/validations/validators/base.rb +++ b/lib/grape/validations/validators/base.rb @@ -12,14 +12,16 @@ 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 - @allow_blank = opts[:allow_blank] || false + @opts = opts + opts = opts.any? ? opts.shift : {} + @fail_fast = opts.fetch(:fail_fast, false) + @allow_blank = opts.fetch(:allow_blank, false) end # Validates a given request. diff --git a/spec/grape/request_spec.rb b/spec/grape/request_spec.rb index ee5a43643..1bfce035e 100644 --- a/spec/grape/request_spec.rb +++ b/spec/grape/request_spec.rb @@ -75,7 +75,7 @@ module Grape Grape.config.reset end - subject(:request_params) { Grape::Request.new(env, opts).params } + subject(:request_params) { Grape::Request.new(env, **opts).params } context 'when the API does not include a specific param builder' do let(:opts) { {} } diff --git a/spec/shared/versioning_examples.rb b/spec/shared/versioning_examples.rb index 8e8552070..ebc0742d4 100644 --- a/spec/shared/versioning_examples.rb +++ b/spec/shared/versioning_examples.rb @@ -7,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 @@ -18,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 @@ -34,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 @@ -51,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 @@ -74,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 @@ -98,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 @@ -131,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 @@ -148,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 @@ -183,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 @@ -197,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/support/versioned_helpers.rb b/spec/support/versioned_helpers.rb index 8d7c07f18..ea78013d4 100644 --- a/spec/support/versioned_helpers.rb +++ b/spec/support/versioned_helpers.rb @@ -44,7 +44,7 @@ def versioned_headers(**options) end def versioned_get(path, version_name, **version_options) - path = versioned_path(version_options.merge(version: version_name, path: path)) + path = versioned_path(**version_options.merge(version: version_name, path: path)) headers = versioned_headers(**version_options.merge(version: version_name)) params = {} params = { version_options[:parameter] => version_name } if version_options[:using] == :param From da1fe0218b054f4b3f03a217ccbfe0dd489ae606 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Tue, 29 Dec 2020 17:58:42 +0100 Subject: [PATCH 035/304] Add ruby 3.0 in GH Actions --- .github/workflows/test.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8782c8247..95b941260 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,6 +28,7 @@ jobs: - 2.5 - 2.6 - 2.7 + - 3.0 gemfile: - Gemfile - gemfiles/rack1.gemfile @@ -36,8 +37,15 @@ jobs: - gemfiles/rails_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 From a09efb0be8d77df13dde1296613da03bf9a039d8 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Tue, 29 Dec 2020 18:06:02 +0100 Subject: [PATCH 036/304] Remove @opts --- lib/grape/validations/validators/base.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/grape/validations/validators/base.rb b/lib/grape/validations/validators/base.rb index f45a254ca..f8a2a1bcf 100644 --- a/lib/grape/validations/validators/base.rb +++ b/lib/grape/validations/validators/base.rb @@ -18,7 +18,6 @@ def initialize(attrs, options, required, scope, *opts) @option = options @required = required @scope = scope - @opts = opts opts = opts.any? ? opts.shift : {} @fail_fast = opts.fetch(:fail_fast, false) @allow_blank = opts.fetch(:allow_blank, false) From 501a76ee3d2c51a53066995b5ae63c8b6aaf2da1 Mon Sep 17 00:00:00 2001 From: yanpz Date: Thu, 14 Jan 2021 16:28:32 +0800 Subject: [PATCH 037/304] Update doc ConnectionManagement without rails --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9e3a1dd6c..2f9a0bae6 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,8 @@ - [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) @@ -317,13 +319,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) From fef726931e64e3938e013c34914b1f3fcbbfd57d Mon Sep 17 00:00:00 2001 From: dblock Date: Tue, 19 Jan 2021 10:18:07 -0500 Subject: [PATCH 038/304] Use raw DANGER_GITHUB_API_TOKEN. --- .github/workflows/danger.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml index 681f52b46..040beda21 100644 --- a/.github/workflows/danger.yml +++ b/.github/workflows/danger.yml @@ -1,8 +1,8 @@ --- -name: Danger +name: danger on: pull_request: - ypes: [opened, reopened, edited, synchronize] + types: [opened, reopened, edited, synchronize] jobs: danger: runs-on: ubuntu-20.04 @@ -14,6 +14,8 @@ jobs: ruby-version: 2.6 bundler-cache: true - name: Run Danger - run: bundle exec danger - env: - DANGER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # the token is public, this is ok + TOKEN='b8b19daa0ade737762c' + TOKEN+='f35edcb328642d371ce86' + DANGER_GITHUB_API_TOKEN=$TOKEN bundle exec danger --verbose From e7f771e63359e54e2cb0feb7ff52b9746fe13e05 Mon Sep 17 00:00:00 2001 From: Fernando Sainz <167863+fsainz@users.noreply.github.com> Date: Tue, 26 Jan 2021 15:18:16 +0100 Subject: [PATCH 039/304] fixes configuration inside namespaced params scope (#2152) --- CHANGELOG.md | 1 + lib/grape/validations/params_scope.rb | 2 +- spec/grape/api_remount_spec.rb | 13 +++++++++---- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad0462bf9..ad32abbc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ * [#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) diff --git a/lib/grape/validations/params_scope.rb b/lib/grape/validations/params_scope.rb index 792762b06..a39383d5d 100644 --- a/lib/grape/validations/params_scope.rb +++ b/lib/grape/validations/params_scope.rb @@ -39,7 +39,7 @@ def initialize(opts, &block) end def configuration - @api.configuration.evaluate + @api.configuration.respond_to?(:evaluate) ? @api.configuration.evaluate : @api.configuration end # @return [Boolean] whether or not this entire scope needs to be diff --git a/spec/grape/api_remount_spec.rb b/spec/grape/api_remount_spec.rb index 2f7501765..ff764293c 100644 --- a/spec/grape/api_remount_spec.rb +++ b/spec/grape/api_remount_spec.rb @@ -340,19 +340,24 @@ def app 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' } - root_api.mount a_remounted_api, with: { path: 'scores' } + 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' + get 'api/votes', param_key: 'a' expect(last_response.body).to eql '10 votes' - get 'api/scores' + 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 From 54b2e116574a3fd105088e3dc1db06abf40c383c Mon Sep 17 00:00:00 2001 From: Dmitriy Nesteryuk Date: Wed, 27 Jan 2021 19:37:15 +0200 Subject: [PATCH 040/304] custom types can set a message to be used in the response when invalid Just return an instance of `Grape::Types::InvalidValue` with the message: class Color def self.parse(value) return value if %w[blue red green].include?(value) Grape::Types::InvalidValue.new('Invalid color') end end Any raised exception will be treated as an invalid value as it was before. --- CHANGELOG.md | 1 + Gemfile | 4 ++ README.md | 8 +-- lib/grape/validations/types.rb | 5 +- .../validations/types/custom_type_coercer.rb | 2 + lib/grape/validations/types/invalid_value.rb | 24 +++++++++ lib/grape/validations/validators/coerce.rb | 9 ++-- .../validations/validators/coerce_spec.rb | 52 ++++++++++++++----- 8 files changed, 83 insertions(+), 22 deletions(-) create mode 100644 lib/grape/validations/types/invalid_value.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index ad32abbc9..6ca67c53a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ #### Features * Your contribution here. +* [#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). diff --git a/Gemfile b/Gemfile index 6c4124a52..8e32048c4 100644 --- a/Gemfile +++ b/Gemfile @@ -35,3 +35,7 @@ group :test do gem 'rspec', '~> 3.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false end + +platforms :jruby do + gem 'racc' +end diff --git a/README.md b/README.md index 2f9a0bae6..224711058 100644 --- a/README.md +++ b/README.md @@ -1183,7 +1183,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 @@ -1193,8 +1194,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 diff --git a/lib/grape/validations/types.rb b/lib/grape/validations/types.rb index a682286a5..2240a7fa7 100644 --- a/lib/grape/validations/types.rb +++ b/lib/grape/validations/types.rb @@ -7,6 +7,7 @@ require_relative 'types/variant_collection_coercer' require_relative 'types/json' require_relative 'types/file' +require_relative 'types/invalid_value' module Grape module Validations @@ -21,10 +22,6 @@ 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. PRIMITIVES = [ # Numerical diff --git a/lib/grape/validations/types/custom_type_coercer.rb b/lib/grape/validations/types/custom_type_coercer.rb index a11403e1c..f1fd4a80c 100644 --- a/lib/grape/validations/types/custom_type_coercer.rb +++ b/lib/grape/validations/types/custom_type_coercer.rb @@ -55,6 +55,8 @@ def call(val) return if val.nil? coerced_val = @method.call(val) + + return coerced_val if coerced_val.is_a?(InvalidValue) return InvalidValue.new unless coerced?(coerced_val) coerced_val 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/validators/coerce.rb b/lib/grape/validations/validators/coerce.rb index 1b15c069d..f79b4fa6e 100644 --- a/lib/grape/validations/validators/coerce.rb +++ b/lib/grape/validations/validators/coerce.rb @@ -36,7 +36,7 @@ def validate_param!(attr_name, params) new_value = coerce_value(params[attr_name]) - raise validation_exception(attr_name) 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 @@ -80,8 +80,11 @@ def type @option[:type].is_a?(Hash) ? @option[:type][:value] : @option[:type] end - def validation_exception(attr_name) - Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:coerce)) + 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/spec/grape/validations/validators/coerce_spec.rb b/spec/grape/validations/validators/coerce_spec.rb index e157748e9..a9d3123a9 100644 --- a/spec/grape/validations/validators/coerce_spec.rb +++ b/spec/grape/validations/validators/coerce_spec.rb @@ -227,23 +227,51 @@ def self.parsed?(value) expect(last_response.body).to eq('NilClass') end - it 'is a custom type' do - subject.params do - requires :uri, coerce: SecureURIOnly - end - subject.get '/secure_uri' do - params[:uri].class + 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 - get 'secure_uri', uri: 'https://www.example.com' + 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 - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('URI::HTTPS') + 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 'secure_uri', uri: 'http://www.example.com' + get 'whatever', name: 'Bob' - expect(last_response.status).to eq(400) - expect(last_response.body).to eq('uri is invalid') + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('name must be unique') + end + end end context 'Array' do From 6cb8eaf65c352fb675720fd6a8ff1c96fea7aca8 Mon Sep 17 00:00:00 2001 From: Matias Asis Date: Tue, 2 Feb 2021 00:10:25 -0300 Subject: [PATCH 041/304] spelling corrections --- CHANGELOG.md | 2 +- lib/grape/api.rb | 2 +- lib/grape/dsl/callbacks.rb | 2 +- lib/grape/dsl/inside_route.rb | 2 +- lib/grape/dsl/parameters.rb | 2 +- lib/grape/dsl/routing.rb | 4 ++-- lib/grape/exceptions/validation.rb | 2 +- spec/grape/entity_spec.rb | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ca67c53a..cd1a2ca6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -639,7 +639,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). diff --git a/lib/grape/api.rb b/lib/grape/api.rb index dd8ae8d89..219538292 100644 --- a/lib/grape/api.rb +++ b/lib/grape/api.rb @@ -87,7 +87,7 @@ def const_missing(*args) end # The remountable class can have a configuration hash to provide some dynamic class-level variables. - # For instance, a descripcion could be done using: `desc configuration[:description]` if it may vary + # 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) diff --git a/lib/grape/dsl/callbacks.rb b/lib/grape/dsl/callbacks.rb index ae6049aa2..03827684d 100644 --- a/lib/grape/dsl/callbacks.rb +++ b/lib/grape/dsl/callbacks.rb @@ -59,7 +59,7 @@ def after(&block) # end # end # - # This will make sure that the ApiLogger is opened and close around every + # 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) diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index 134f1ea24..d12107199 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -399,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 diff --git a/lib/grape/dsl/parameters.rb b/lib/grape/dsl/parameters.rb index b448ca52a..84a0fa574 100644 --- a/lib/grape/dsl/parameters.rb +++ b/lib/grape/dsl/parameters.rb @@ -72,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 diff --git a/lib/grape/dsl/routing.rb b/lib/grape/dsl/routing.rb index 61bf1f21a..7f1e78c08 100644 --- a/lib/grape/dsl/routing.rb +++ b/lib/grape/dsl/routing.rb @@ -152,7 +152,7 @@ def route(methods, paths = ['/'], route_options = {}, &block) 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. # @@ -200,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/exceptions/validation.rb b/lib/grape/exceptions/validation.rb index bfd1dee2d..6b98112ab 100644 --- a/lib/grape/exceptions/validation.rb +++ b/lib/grape/exceptions/validation.rb @@ -17,7 +17,7 @@ def initialize(params:, message: nil, **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/spec/grape/entity_spec.rb b/spec/grape/entity_spec.rb index add8461be..beb304061 100644 --- a/spec/grape/entity_spec.rb +++ b/spec/grape/entity_spec.rb @@ -116,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 = [] From 381c00e14d29b45edd61c4f1d3a5f68fe5fbbf36 Mon Sep 17 00:00:00 2001 From: Dmitriy Nesteryuk Date: Sat, 6 Feb 2021 12:56:54 +0200 Subject: [PATCH 042/304] Preparing for release, 1.5.2 --- CHANGELOG.md | 4 +--- README.md | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd1a2ca6c..d75cccf98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,13 @@ -### 1.5.2 (Next) +### 1.5.2 (2021/02/06) #### Features -* Your contribution here. * [#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 -* Your contribution here. * [#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). diff --git a/README.md b/README.md index 224711058..0f967f65b 100644 --- a/README.md +++ b/README.md @@ -158,9 +158,7 @@ content negotiation, versioning and much more. ## Stable Release -You're reading the documentation for the next release of Grape, which should be **1.5.2**. -Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. -The current stable release is [1.5.1](https://github.com/ruby-grape/grape/blob/v1.5.1/README.md). +You're reading the documentation for the stable release of Grape, 1.5.2. ## Project Resources From 0e0ac104c51ce5e70e204ce51965d231f1a6d216 Mon Sep 17 00:00:00 2001 From: Dmitriy Nesteryuk Date: Sat, 6 Feb 2021 12:59:08 +0200 Subject: [PATCH 043/304] Preparing for next development iteration, 1.5.3 --- CHANGELOG.md | 10 ++++++++++ README.md | 4 +++- lib/grape/version.rb | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d75cccf98..279349688 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +### 1.5.3 (Next) + +#### Features + +* Your contribution here. + +#### Fixes + +* Your contribution here. + ### 1.5.2 (2021/02/06) #### Features diff --git a/README.md b/README.md index 0f967f65b..373d53885 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,9 @@ content negotiation, versioning and much more. ## Stable Release -You're reading the documentation for the stable release of Grape, 1.5.2. +You're reading the documentation for the next release of Grape, which should be **1.5.3**. +Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. +The current stable release is [1.5.2](https://github.com/ruby-grape/grape/blob/v1.5.2/README.md). ## Project Resources diff --git a/lib/grape/version.rb b/lib/grape/version.rb index b62f63d74..9c0689139 100644 --- a/lib/grape/version.rb +++ b/lib/grape/version.rb @@ -2,5 +2,5 @@ module Grape # The current version of Grape. - VERSION = '1.5.2' + VERSION = '1.5.3' end From 7c524c3e8d8bb864dc12e8539b75a5958c97418c Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Tue, 9 Feb 2021 12:06:07 +0200 Subject: [PATCH 044/304] Fix testing to be compatible with Rails 7 dependencies --- .github/workflows/test.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 95b941260..188e01c6d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,7 +34,6 @@ jobs: - gemfiles/rack1.gemfile - gemfiles/rack2.gemfile - gemfiles/rack_edge.gemfile - - gemfiles/rails_edge.gemfile - gemfiles/rails_5.gemfile - gemfiles/rails_6.gemfile - gemfiles/rails_6_1.gemfile @@ -52,6 +51,9 @@ jobs: - 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" From 6a21f8080056593cfb5cc3eebc2a5985d7618fec Mon Sep 17 00:00:00 2001 From: Ben Schmeckpeper Date: Wed, 10 Feb 2021 10:41:48 -0600 Subject: [PATCH 045/304] Handle EOFError raised by Rack (#2161) --- .github/workflows/test.yml | 5 +-- CHANGELOG.md | 2 ++ gemfiles/rack2_2.gemfile | 39 ++++++++++++++++++++++ lib/grape.rb | 1 + lib/grape/exceptions/empty_message_body.rb | 11 ++++++ lib/grape/locale/en.yml | 2 +- lib/grape/request.rb | 2 ++ spec/grape/endpoint_spec.rb | 13 ++++++++ 8 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 gemfiles/rack2_2.gemfile create mode 100644 lib/grape/exceptions/empty_message_body.rb diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 188e01c6d..44a7541f9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,6 +33,7 @@ jobs: - Gemfile - gemfiles/rack1.gemfile - gemfiles/rack2.gemfile + - gemfiles/rack2_2.gemfile - gemfiles/rack_edge.gemfile - gemfiles/rails_5.gemfile - gemfiles/rails_6.gemfile @@ -76,7 +77,7 @@ jobs: - 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 @@ -84,7 +85,7 @@ jobs: - 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/CHANGELOG.md b/CHANGELOG.md index 279349688..b610254e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ * Your contribution here. +* [#2161](https://github.com/ruby-grape/grape/pull/2157): Handle EOFError from Rack when given an empty multipart body - [@bschmeck](https://github.com/bschmeck). + ### 1.5.2 (2021/02/06) #### Features diff --git a/gemfiles/rack2_2.gemfile b/gemfiles/rack2_2.gemfile new file mode 100644 index 000000000..88cfa240d --- /dev/null +++ b/gemfiles/rack2_2.gemfile @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +# This file was generated by Appraisal + +source 'https://rubygems.org' + +gem 'rack', '~> 2.2' + +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/lib/grape.rb b/lib/grape.rb index c1f313c2f..818c8f6bc 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -76,6 +76,7 @@ module Exceptions autoload :InvalidVersionHeader autoload :MethodNotAllowed autoload :InvalidResponse + autoload :EmptyMessageBody 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/locale/en.yml b/lib/grape/locale/en.yml index 10fa381f7..036eb93b8 100644 --- a/lib/grape/locale/en.yml +++ b/lib/grape/locale/en.yml @@ -44,6 +44,7 @@ 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}' @@ -51,4 +52,3 @@ en: problem: 'Invalid version header' resolution: '%{message}' invalid_response: 'Invalid response' - diff --git a/lib/grape/request.rb b/lib/grape/request.rb index b54779748..0357477d5 100644 --- a/lib/grape/request.rb +++ b/lib/grape/request.rb @@ -15,6 +15,8 @@ def initialize(env, **options) def params @params ||= build_params + rescue EOFError + raise Grape::Exceptions::EmptyMessageBody, content_type end def headers diff --git a/spec/grape/endpoint_spec.rb b/spec/grape/endpoint_spec.rb index 4bbeb070c..487fbb0d3 100644 --- a/spec/grape/endpoint_spec.rb +++ b/spec/grape/endpoint_spec.rb @@ -420,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 From c859eeee2f7b207c973e0156c938e62931a8a12e Mon Sep 17 00:00:00 2001 From: Hermann Mayer Date: Fri, 19 Feb 2021 20:19:36 +0100 Subject: [PATCH 046/304] Corrected a hash modification while iterating issue. Signed-off-by: Hermann Mayer --- CHANGELOG.md | 1 + lib/grape/api.rb | 2 +- spec/grape/api_spec.rb | 77 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b610254e7..760e2a123 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * Your contribution here. * [#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). ### 1.5.2 (2021/02/06) diff --git a/lib/grape/api.rb b/lib/grape/api.rb index 219538292..11bcb6a44 100644 --- a/lib/grape/api.rb +++ b/lib/grape/api.rb @@ -141,7 +141,7 @@ def instance_for_rack # 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 + @setup += [setup_step] last_response = nil @instances.each do |instance| last_response = replay_step_on(instance, setup_step) diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index 0d722a4b1..0cb7e1251 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -4056,4 +4056,81 @@ def before 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 From e0f412ca73f6188c233ac9c35661027d28f38a54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gw=C3=A9na=C3=ABl=20Rault?= Date: Fri, 5 Mar 2021 15:58:34 +0100 Subject: [PATCH 047/304] coerce_with should be called for params with nil value (#2164) --- CHANGELOG.md | 1 + README.md | 1 + UPGRADING.md | 27 +++++++ .../validations/types/custom_type_coercer.rb | 2 - .../validations/validators/coerce_spec.rb | 76 +++++++++++++++++++ 5 files changed, 105 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 760e2a123..480c27f3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ * [#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) diff --git a/README.md b/README.md index 373d53885..e7f9a14b1 100644 --- a/README.md +++ b/README.md @@ -1228,6 +1228,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`. diff --git a/UPGRADING.md b/UPGRADING.md index e5b6947fa..c8813e32f 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,6 +1,33 @@ 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 diff --git a/lib/grape/validations/types/custom_type_coercer.rb b/lib/grape/validations/types/custom_type_coercer.rb index f1fd4a80c..be72aff56 100644 --- a/lib/grape/validations/types/custom_type_coercer.rb +++ b/lib/grape/validations/types/custom_type_coercer.rb @@ -52,8 +52,6 @@ def initialize(type, method = nil) # this should always be a string. # @return [Object] the coerced result def call(val) - return if val.nil? - coerced_val = @method.call(val) return coerced_val if coerced_val.is_a?(InvalidValue) diff --git a/spec/grape/validations/validators/coerce_spec.rb b/spec/grape/validations/validators/coerce_spec.rb index a9d3123a9..3f8046f8a 100644 --- a/spec/grape/validations/validators/coerce_spec.rb +++ b/spec/grape/validations/validators/coerce_spec.rb @@ -706,6 +706,44 @@ def self.parse(_val) 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 @@ -754,6 +792,44 @@ def self.parse(_val) 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 From 4e8abf0e95ba86cb033c98041a288c7ea494de62 Mon Sep 17 00:00:00 2001 From: Brandon Fish Date: Fri, 5 Mar 2021 16:34:39 -0600 Subject: [PATCH 048/304] Initialize error data using constructors directly --- lib/grape/endpoint.rb | 2 +- lib/grape/parser/json.rb | 2 +- lib/grape/parser/xml.rb | 2 +- lib/grape/request.rb | 2 +- lib/grape/validations/validators/base.rb | 2 +- lib/grape/validations/validators/multiple_params_base.rb | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/grape/endpoint.rb b/lib/grape/endpoint.rb index 7a8439692..068e1ca0a 100644 --- a/lib/grape/endpoint.rb +++ b/lib/grape/endpoint.rb @@ -255,7 +255,7 @@ def run run_filters befores, :before if (allowed_methods = env[Grape::Env::GRAPE_ALLOWED_METHODS]) - raise Grape::Exceptions::MethodNotAllowed, header.merge('Allow' => allowed_methods) unless options? + raise Grape::Exceptions::MethodNotAllowed.new(header.merge('Allow' => allowed_methods)) unless options? header 'Allow', allowed_methods response_object = '' status 204 diff --git a/lib/grape/parser/json.rb b/lib/grape/parser/json.rb index 7f72c9a94..4e665a1ec 100644 --- a/lib/grape/parser/json.rb +++ b/lib/grape/parser/json.rb @@ -8,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 20cde6e27..930c57f13 100644 --- a/lib/grape/parser/xml.rb +++ b/lib/grape/parser/xml.rb @@ -8,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/request.rb b/lib/grape/request.rb index 0357477d5..ed97f3194 100644 --- a/lib/grape/request.rb +++ b/lib/grape/request.rb @@ -16,7 +16,7 @@ def initialize(env, **options) def params @params ||= build_params rescue EOFError - raise Grape::Exceptions::EmptyMessageBody, content_type + raise Grape::Exceptions::EmptyMessageBody.new(content_type) end def headers diff --git a/lib/grape/validations/validators/base.rb b/lib/grape/validations/validators/base.rb index f8a2a1bcf..84584e0b3 100644 --- a/lib/grape/validations/validators/base.rb +++ b/lib/grape/validations/validators/base.rb @@ -55,7 +55,7 @@ def validate!(params) 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) diff --git a/lib/grape/validations/validators/multiple_params_base.rb b/lib/grape/validations/validators/multiple_params_base.rb index 03867ff1a..9ed0b6b96 100644 --- a/lib/grape/validations/validators/multiple_params_base.rb +++ b/lib/grape/validations/validators/multiple_params_base.rb @@ -16,7 +16,7 @@ def validate!(params) end end - raise Grape::Exceptions::ValidationArrayErrors, array_errors if array_errors.any? + raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors.any? end private From e83e81fe95d71134f2156aa6f31c22fcf954227f Mon Sep 17 00:00:00 2001 From: dblock Date: Sun, 7 Mar 2021 16:52:00 -0500 Subject: [PATCH 049/304] Preparing for release, 1.5.3. --- CHANGELOG.md | 8 +------- README.md | 3 +-- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 480c27f3c..489ef8aa2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,7 @@ -### 1.5.3 (Next) - -#### Features - -* Your contribution here. +### 1.5.3 (2021/03/07) #### Fixes -* Your contribution here. - * [#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). diff --git a/README.md b/README.md index e7f9a14b1..f74f17f74 100644 --- a/README.md +++ b/README.md @@ -158,9 +158,8 @@ content negotiation, versioning and much more. ## Stable Release -You're reading the documentation for the next release of Grape, which should be **1.5.3**. +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. -The current stable release is [1.5.2](https://github.com/ruby-grape/grape/blob/v1.5.2/README.md). ## Project Resources From 7741f022b5928f0ddcee27de08555d95ea4556f8 Mon Sep 17 00:00:00 2001 From: dblock Date: Sun, 7 Mar 2021 16:53:17 -0500 Subject: [PATCH 050/304] Preparing for next developer iteration, 1.5.4. --- CHANGELOG.md | 10 ++++++++++ README.md | 3 ++- lib/grape/version.rb | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 489ef8aa2..e79fbc693 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +### 1.5.4 (Next) + +#### Features + +* Your contribution here. + +#### Fixes + +* Your contribution here. + ### 1.5.3 (2021/03/07) #### Fixes diff --git a/README.md b/README.md index f74f17f74..25c5a3820 100644 --- a/README.md +++ b/README.md @@ -158,8 +158,9 @@ content negotiation, versioning and much more. ## Stable Release -You're reading the documentation for the stable release of Grape, **v1.5.3**. +You're reading the documentation for the next release of Grape, which should be **1.5.4**. Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. +The current stable release is [1.5.3](https://github.com/ruby-grape/grape/blob/v1.5.3/README.md). ## Project Resources diff --git a/lib/grape/version.rb b/lib/grape/version.rb index 9c0689139..6eb5000f6 100644 --- a/lib/grape/version.rb +++ b/lib/grape/version.rb @@ -2,5 +2,5 @@ module Grape # The current version of Grape. - VERSION = '1.5.3' + VERSION = '1.5.4' end From 69435b0b14fb9b5937a334ba384b7df4c5e37bbc Mon Sep 17 00:00:00 2001 From: Nicolas Klein Date: Fri, 30 Apr 2021 23:35:25 +0100 Subject: [PATCH 051/304] Fixes ISSUE-2175 - Options call failing if matching all routes (#2176) --- CHANGELOG.md | 2 +- lib/grape/api/instance.rb | 5 +++-- spec/grape/api_spec.rb | 41 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e79fbc693..a03ade6ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,8 @@ #### Fixes +* [#2176](https://github.com/ruby-grape/grape/pull/2176): Fixes issue-2175 - options call failing if matching all routes - [@myxoh](https://github.com/myxoh). * Your contribution here. - ### 1.5.3 (2021/03/07) #### Fixes diff --git a/lib/grape/api/instance.rb b/lib/grape/api/instance.rb index e8f8d85fb..7335fdbc5 100644 --- a/lib/grape/api/instance.rb +++ b/lib/grape/api/instance.rb @@ -200,6 +200,8 @@ def add_head_not_allowed_methods_and_options_methods without_root_prefix do without_versioning do versioned_route_configs.each do |config| + next if config[:options][:matching_wildchar] + allowed_methods = config[:methods].dup unless self.class.namespace_inheritable(:do_not_route_head) @@ -228,7 +230,7 @@ def collect_route_config_per_pattern last_route = routes.last # Most of the configuration is taken from the last endpoint matching_wildchar = routes.any? { |route| route.request_method == '*' } { - options: {}, + options: { matching_wildchar: matching_wildchar }, pattern: last_route.pattern, requirements: last_route.requirements, path: last_route.origin, @@ -248,7 +250,6 @@ def generate_not_allowed_method(pattern, allowed_methods: [], **attributes) 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 diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index 0cb7e1251..126b816f2 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -749,6 +749,47 @@ class DummyFormatClass end end + describe 'when a resource routes by POST, GET, PATCH, PUT, and DELETE' do + before do + subject.namespace :example do + get do + 'example' + end + + patch do + 'example' + end + + post do + 'example' + end + + delete do + 'example' + end + + put do + 'example' + end + end + options '/example' + end + + describe 'it adds an OPTIONS route for namespaced endpoints that' do + it 'returns a 204' do + expect(last_response.status).to eql 204 + end + + it 'has an empty body' do + expect(last_response.body).to be_blank + end + + it 'has an Allow header' do + expect(last_response.headers['Allow']).to eql 'OPTIONS, GET, PATCH, POST, DELETE, PUT, HEAD' + end + end + end + describe 'adds an OPTIONS route for namespaced endpoints that' do before do subject.before { header 'X-Custom-Header', 'foo' } From 613f9f1c98811e78a247f70db74eea5e00f31624 Mon Sep 17 00:00:00 2001 From: Max Kerp Date: Wed, 5 May 2021 15:42:23 +0200 Subject: [PATCH 052/304] Mention rack 2.1.0 support in UPGRADING.md (#2130) --- UPGRADING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/UPGRADING.md b/UPGRADING.md index c8813e32f..67c8b20ca 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -216,6 +216,8 @@ end ### Upgrading to >= 1.3.0 +You will need to upgrade to this version if you depend on `rack >= 2.1.0`. + #### Ruby After adding dry-types, Ruby 2.4 or newer is required. From fe0f78bf9e07879085dfaeb72e4f6b4eaf13388a Mon Sep 17 00:00:00 2001 From: Lewis Reid Date: Wed, 5 May 2021 23:50:29 +1000 Subject: [PATCH 053/304] Fix ordering issues between as: and default: validations (#2177) --- CHANGELOG.md | 3 ++- lib/grape/validations/params_scope.rb | 28 ++++++++++++++++----- spec/grape/validations/params_scope_spec.rb | 22 ++++++++++++++++ 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a03ade6ff..d1a554be5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ #### Fixes -* [#2176](https://github.com/ruby-grape/grape/pull/2176): Fixes issue-2175 - options call failing if matching all routes - [@myxoh](https://github.com/myxoh). +* [#2176](https://github.com/ruby-grape/grape/pull/2176): Fix: OPTIONS fails if matching all routes - [@myxoh](https://github.com/myxoh). +* [#2177](https://github.com/ruby-grape/grape/pull/2177): Fix: `default` validator fails if preceded by `as` validator - [@Catsuko](https://github.com/Catsuko). * Your contribution here. ### 1.5.3 (2021/03/07) diff --git a/lib/grape/validations/params_scope.rb b/lib/grape/validations/params_scope.rb index a39383d5d..3f3325ed2 100644 --- a/lib/grape/validations/params_scope.rb +++ b/lib/grape/validations/params_scope.rb @@ -283,16 +283,15 @@ def validates(attrs, validations) doc_attrs[:documentation] = validations.delete(:documentation) if validations.key?(:documentation) - full_attrs = attrs.collect { |name| { name: name, full_name: full_name(name) } } - @api.document_attribute(full_attrs, doc_attrs) + document_attribute(attrs, doc_attrs) opts = derive_validator_options(validations) + order_specific_validations = Set[:as] + # Validate for presence before any other validators - if validations.key?(:presence) && validations[:presence] - validate('presence', validations[:presence], attrs, doc_attrs, opts) - validations.delete(:presence) - validations.delete(:message) if validations.key?(:message) + validates_presence(validations, attrs, doc_attrs, opts) do |validation_type| + order_specific_validations << validation_type end # Before we run the rest of the validators, let's handle @@ -301,8 +300,13 @@ def validates(attrs, validations) coerce_type validations, attrs, doc_attrs, opts validations.each do |type, options| + next if order_specific_validations.include?(type) validate(type, options, attrs, doc_attrs, opts) end + + # Apply as validator last so other validations are applied to + # renamed param + validate(:as, validations[:as], attrs, doc_attrs, opts) if validations.key?(:as) end # Validate and comprehend the +:type+, +:types+, and +:coerce_with+ @@ -464,6 +468,18 @@ def derive_validator_options(validations) fail_fast: validations.delete(:fail_fast) || false } end + + def validates_presence(validations, attrs, doc_attrs, opts) + return unless validations.key?(:presence) && validations[:presence] + validate(:presence, validations[:presence], attrs, doc_attrs, opts) + yield :presence + yield :message if validations.key?(:message) + end + + def document_attribute(attrs, doc_attrs) + full_attrs = attrs.collect { |name| { name: name, full_name: full_name(name) } } + @api.document_attribute(full_attrs, doc_attrs) + end end end end diff --git a/spec/grape/validations/params_scope_spec.rb b/spec/grape/validations/params_scope_spec.rb index 9ef14af9f..e0ed39169 100644 --- a/spec/grape/validations/params_scope_spec.rb +++ b/spec/grape/validations/params_scope_spec.rb @@ -180,6 +180,28 @@ def initialize(value) expect(last_response.status).to eq(200) expect(last_response.body).to eq('{"baz":{"qux":"any"}}') end + + it 'renaming can be defined before default' do + subject.params do + optional :foo, as: :bar, default: 'before' + end + subject.get('/rename-before-default') { params[:bar] } + get '/rename-before-default' + + expect(last_response.status).to eq(200) + expect(last_response.body).to eq('before') + end + + it 'renaming can be defined after default' do + subject.params do + optional :foo, default: 'after', as: :bar + end + subject.get('/rename-after-default') { params[:bar] } + get '/rename-after-default' + + expect(last_response.status).to eq(200) + expect(last_response.body).to eq('after') + end end context 'array without coerce type explicitly given' do From 885f76f24bac40007cf5347aa9af77cd017d4e1e Mon Sep 17 00:00:00 2001 From: Olle Jonsson Date: Thu, 27 May 2021 09:17:17 +0200 Subject: [PATCH 054/304] CI: Avoid YAML float 3.0 => "3" This is a very small change, but which avoids a Float-to-String loss of characters. --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 44a7541f9..ea8cdf787 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,7 +28,7 @@ jobs: - 2.5 - 2.6 - 2.7 - - 3.0 + - "3.0" gemfile: - Gemfile - gemfiles/rack1.gemfile @@ -40,10 +40,10 @@ jobs: - gemfiles/rails_6_1.gemfile experimental: [false] include: - - ruby: 3.0 + - ruby: "3.0" gemfile: 'gemfiles/multi_json.gemfile' experimental: false - - ruby: 3.0 + - ruby: "3.0" gemfile: 'gemfiles/multi_xml.gemfile' experimental: false - ruby: 2.7 From 1587f0790851fb3310364e90d12a8ac8c08263e1 Mon Sep 17 00:00:00 2001 From: Yogesh Khater Date: Thu, 8 Jul 2021 12:07:27 +0530 Subject: [PATCH 055/304] Call `super` in `API.inherited` `API.make_inheritable` was overriding `.inherited` hook on given instance itself. But it was not calling `super` which is required to bubble up inheritance chain. This commit removes `.make_inheritable` and calls `super` instead. Fixes PR with reproducible spec #2179. --- CHANGELOG.md | 1 + lib/grape/api.rb | 16 ++++------------ spec/grape/api_spec.rb | 43 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1a554be5..21ac88953 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * [#2176](https://github.com/ruby-grape/grape/pull/2176): Fix: OPTIONS fails if matching all routes - [@myxoh](https://github.com/myxoh). * [#2177](https://github.com/ruby-grape/grape/pull/2177): Fix: `default` validator fails if preceded by `as` validator - [@Catsuko](https://github.com/Catsuko). +* [#2180](https://github.com/ruby-grape/grape/pull/2180): Call `super` in `API.inherited` - [@yogeshjain999](https://github.com/yogeshjain999). * Your contribution here. ### 1.5.3 (2021/03/07) diff --git a/lib/grape/api.rb b/lib/grape/api.rb index 11bcb6a44..9d81bf95b 100644 --- a/lib/grape/api.rb +++ b/lib/grape/api.rb @@ -20,10 +20,11 @@ def new(*args, &block) # 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) + def inherited(api) + super + + api.initial_setup(Grape::API == self ? Grape::API::Instance : @base_instance) api.override_all_methods! - make_inheritable(api) end # Initialize the instance variables on the remountable class, and the base_instance @@ -68,15 +69,6 @@ def call(*args, &block) instance_for_rack.call(*args, &block) end - # 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 - # Alleviates problems with autoloading by tring to search for the constant def const_missing(*args) if base_instance.const_defined?(*args) diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index 126b816f2..af2419ceb 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -4081,6 +4081,49 @@ def before end end + describe '.inherited' do + context 'overriding within class' do + let(:root_api) do + Class.new(Grape::API) do + @bar = 'Hello, world' + + def self.inherited(child_api) + super + child_api.instance_variable_set(:@foo, @bar.dup) + end + end + end + + let(:child_api) { Class.new(root_api) } + + it 'allows overriding the hook' do + expect(child_api.instance_variable_get(:@foo)).to eq('Hello, world') + end + end + + context 'overriding via composition' do + module Inherited + def inherited(api) + super + api.instance_variable_set(:@foo, @bar.dup) + end + end + + let(:root_api) do + Class.new(Grape::API) do + @bar = 'Hello, world' + extend Inherited + end + end + + let(:child_api) { Class.new(root_api) } + + it 'allows overriding the hook' do + expect(child_api.instance_variable_get(:@foo)).to eq('Hello, world') + end + end + end + describe 'const_missing' do subject(:grape_api) { Class.new(Grape::API) } let(:mounted) do From c5f0515997bfda417fb7ccea26345c10febedec0 Mon Sep 17 00:00:00 2001 From: Onur Kucukkece Date: Sun, 1 Aug 2021 16:31:50 +0200 Subject: [PATCH 056/304] Update readme about registering middleware in custom Rails applications --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 25c5a3820..fc66498fd 100644 --- a/README.md +++ b/README.md @@ -3642,6 +3642,14 @@ You can access the controller params, headers, and helpers through the context w Note that when you're using Grape mounted on Rails you don't have to use Rails middleware because it's already included into your middleware stack. You only have to implement the helpers to access the specific `env` variable. +If you are using a custom application that is inherited from `Rails::Application` and need to insert a new middleware among the ones initiated via Rails, you will need to register it manually in your custom application class. + +```ruby +class Company::Application < Rails::Application + config.middleware.insert_before(Rack::Attack, Middleware::ApiLogger) +end +``` + ### Remote IP By default you can access remote IP with `request.ip`. This is the remote IP address implemented by Rack. Sometimes it is desirable to get the remote IP [Rails-style](http://stackoverflow.com/questions/10997005/whats-the-difference-between-request-remote-ip-and-request-ip-in-rails) with `ActionDispatch::RemoteIp`. From 189043a41b15a43d158bdff612c5bb41a70d010e Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Wed, 4 Aug 2021 12:05:01 +0100 Subject: [PATCH 057/304] Add ddtrace gem to monitoring products list --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index fc66498fd..dd88ce559 100644 --- a/README.md +++ b/README.md @@ -3954,6 +3954,7 @@ Grape integrates with following third-party tools: * **[Skylight](https://www.skylight.io/)** - [skylight](https://github.com/skylightio/skylight-ruby) gem, [documentation](https://docs.skylight.io/grape/) * **[AppSignal](https://www.appsignal.com)** - [appsignal-ruby](https://github.com/appsignal/appsignal-ruby) gem, [documentation](http://docs.appsignal.com/getting-started/supported-frameworks.html#grape) * **[ElasticAPM](https://www.elastic.co/products/apm)** - [elastic-apm](https://github.com/elastic/apm-agent-ruby) gem, [documentation](https://www.elastic.co/guide/en/apm/agent/ruby/3.x/getting-started-rack.html#getting-started-grape) +* **[Datadog APM](https://docs.datadoghq.com/tracing/)** - [ddtrace](https://github.com/datadog/dd-trace-rb) gem, [documentation](https://docs.datadoghq.com/tracing/setup_overview/setup/ruby/#grape) ## Contributing to Grape From 5b90b2cb1371e8b2c623ae555949ba230c5ac3df Mon Sep 17 00:00:00 2001 From: Maciej Pankanin Date: Thu, 12 Aug 2021 17:45:01 +0200 Subject: [PATCH 058/304] Update README.md about values validator used with allow_blank --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index dd88ce559..84c6813fb 100644 --- a/README.md +++ b/README.md @@ -1530,6 +1530,14 @@ end While Procs are convenient for single cases, consider using [Custom Validators](#custom-validators) in cases where a validation is used more than once. +Note that [allow_blank](#allow_blank) validator applies while using `:values`. In the following example the absence of `:allow_blank` does not prevent `:state` from receiving blank values because `:allow_blank` defaults to `true`. + +```ruby +params do + requires :state, type: Symbol, values: [:active, :inactive] +end +``` + #### `except_values` Parameters can be restricted from having a specific set of values with the `:except_values` option. From 5b4a2dd51ce998a13c368218b228d78665097539 Mon Sep 17 00:00:00 2001 From: Hermann Mayer Date: Wed, 15 Sep 2021 18:20:35 +0200 Subject: [PATCH 059/304] Corrected the endpoint parameter name generation. (#2189) * Corrected the endpoint parameter name generation. Signed-off-by: Hermann Mayer * Reworked the parameter renaming to be a declared-only feature. Signed-off-by: Hermann Mayer * Corrected already existing, but now broken specs. Signed-off-by: Hermann Mayer * PR fixes and docs. Signed-off-by: Hermann Mayer * Version bump and rephrasing. Signed-off-by: Hermann Mayer --- .rubocop_todo.yml | 2 +- CHANGELOG.md | 4 +- README.md | 7 +- UPGRADING.md | 42 +++- lib/grape/dsl/inside_route.rb | 17 +- lib/grape/endpoint.rb | 10 +- lib/grape/validations/params_scope.rb | 61 +++-- lib/grape/validations/validators/as.rb | 12 +- spec/grape/endpoint/declared_spec.rb | 247 ++++++++++++++++++++ spec/grape/validations/params_scope_spec.rb | 21 +- 10 files changed, 369 insertions(+), 54 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 4320b3937..14a99edc1 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -167,7 +167,7 @@ Metrics/BlockLength: # Offense count: 11 # Configuration parameters: CountComments, CountAsOne. Metrics/ClassLength: - Max: 304 + Max: 310 # Offense count: 30 # Configuration parameters: IgnoredMethods. diff --git a/CHANGELOG.md b/CHANGELOG.md index 21ac88953..b1b0d9e86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -### 1.5.4 (Next) +### 1.6.0 (Next) #### Features @@ -9,7 +9,9 @@ * [#2176](https://github.com/ruby-grape/grape/pull/2176): Fix: OPTIONS fails if matching all routes - [@myxoh](https://github.com/myxoh). * [#2177](https://github.com/ruby-grape/grape/pull/2177): Fix: `default` validator fails if preceded by `as` validator - [@Catsuko](https://github.com/Catsuko). * [#2180](https://github.com/ruby-grape/grape/pull/2180): Call `super` in `API.inherited` - [@yogeshjain999](https://github.com/yogeshjain999). +* [#2189](https://github.com/ruby-grape/grape/pull/2189): Fix: rename parameters when using `:as` (behaviour and grape-swagger documentation) - [@Jack12816](https://github.com/Jack12816). * Your contribution here. + ### 1.5.3 (2021/03/07) #### Fixes diff --git a/README.md b/README.md index 84c6813fb..9133f1ce1 100644 --- a/README.md +++ b/README.md @@ -801,6 +801,7 @@ Grape allows you to access only the parameters that have been declared by your ` * Filter out the params that have been passed, but are not allowed. * Include any optional params that are declared but not passed. + * Perform any parameter renaming on the resulting hash. Consider the following API endpoint: @@ -995,8 +996,10 @@ curl -X POST -H "Content-Type: application/json" localhost:9292/users/signup -d ````json { "declared_params": { - "first_name": "first name", - "last_name": null + "user": { + "first_name": "first name", + "last_name": null + } } } ```` diff --git a/UPGRADING.md b/UPGRADING.md index 67c8b20ca..aceed0b51 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,10 +1,50 @@ Upgrading Grape =============== +### Upgrading to >= 1.6.0 + +#### Parameter renaming with :as + +Prior to 1.6.0 the [parameter renaming](https://github.com/ruby-grape/grape#renaming) with `:as` was directly touching the request payload ([`#params`](https://github.com/ruby-grape/grape#parameters)) while duplicating the old and the new key to be both available in the hash. This allowed clients to bypass any validation in case they knew the internal name of the parameter. Unfortunately, in combination with [grape-swagger](https://github.com/ruby-grape/grape-swagger) the internal name (name set with `:as`) of the parameters were documented. + +This behavior was fixed. Parameter renaming is now done when using the [`#declared(params)`](https://github.com/ruby-grape/grape#declared) parameters helper. This stops confusing validation/coercion behavior. + +Here comes an illustration of the old and new behaviour as code: + +```ruby +# (1) Rename a to b, while client sends +a+ +optional :a, type: Integer, as: :b +params = { a: 1 } +declared(params, include_missing: false) +# expected => { b: 1 } +# actual => { b: 1 } + +# (2) Rename a to b, while client sends +b+ +optional :a, type: Integer, as: :b, values: [1, 2, 3] +params = { b: '5' } +declared(params, include_missing: false) +# expected => { } (>= 1.6.0) +# actual => { b: '5' } (uncasted, unvalidated, <= 1.5.3) +``` + +Another implication of this change is the dependent parameter resolution. Prior to 1.6.0 the following code produced an `Grape::Exceptions::UnknownParameter` because `:a` was replace by `:b`: + +```ruby +params do + optional :a, as: :b + given :a do # (<= 1.5.3 you had to reference +:b+ here to make it work) + requires :c + end +end +``` + +This code now works without any errors, as the renaming is just an internal behaviour of the `#declared(params)` parameter helper. + +See [#2189](https://github.com/ruby-grape/grape/pull/2189) for more information. ### Upgrading to >= 1.5.3 -### Nil value and coercion +#### 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: diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index d12107199..9038fc60b 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -48,6 +48,8 @@ def declared_array(passed_params, options, declared_params, params_nested_path) end def declared_hash(passed_params, options, declared_params, params_nested_path) + renamed_params = route_setting(:renamed_params) || {} + 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| @@ -55,8 +57,11 @@ def declared_hash(passed_params, options, declared_params, params_nested_path) params_nested_path_dup << declared_parent_param.to_s next unless options[:include_missing] || passed_params.key?(declared_parent_param) + rename_path = params_nested_path + [declared_parent_param.to_s] + renamed_param_name = renamed_params[rename_path] + + memo_key = optioned_param_key(renamed_param_name || declared_parent_param, options) 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(params_nested_path_dup, passed_children_params.any?) do declared(passed_children_params, options, declared_children_params, params_nested_path_dup) @@ -65,13 +70,13 @@ def declared_hash(passed_params, options, declared_params, params_nested_path) else # If it is not a Hash then it does not have children. # Find its value or set it to nil. - 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) - next unless options[:include_missing] || passed_params.key?(declared_param) || (param_renaming && passed_params.key?(param_renaming)) + rename_path = params_nested_path + [declared_param.to_s] + renamed_param_name = renamed_params[rename_path] - memo_key = optioned_param_key(param_renaming || declared_param, options) - passed_param = passed_params[param_renaming || declared_param] + memo_key = optioned_param_key(renamed_param_name || declared_param, options) + passed_param = passed_params[declared_param] params_nested_path_dup = params_nested_path.dup params_nested_path_dup << declared_param.to_s diff --git a/lib/grape/endpoint.rb b/lib/grape/endpoint.rb index 068e1ca0a..c28ab6138 100644 --- a/lib/grape/endpoint.rb +++ b/lib/grape/endpoint.rb @@ -262,7 +262,6 @@ def run else run_filters before_validations, :before_validation run_validators validations, request - remove_renamed_params run_filters after_validations, :after_validation response_object = execute end @@ -328,14 +327,7 @@ def build_helpers Module.new { helpers.each { |mod_to_include| include mod_to_include } } end - 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 + private :build_stack, :build_helpers def execute @block ? @block.call(self) : nil diff --git a/lib/grape/validations/params_scope.rb b/lib/grape/validations/params_scope.rb index 3f3325ed2..8a6bee152 100644 --- a/lib/grape/validations/params_scope.rb +++ b/lib/grape/validations/params_scope.rb @@ -13,6 +13,8 @@ class ParamsScope # @param opts [Hash] options for this scope # @option opts :element [Symbol] the element that contains this scope; for # this to be relevant, @parent must be set + # @option opts :element_renamed [Symbol, nil] whenever this scope should + # be renamed and to what, given +nil+ no renaming is done # @option opts :parent [ParamsScope] the scope containing this scope # @option opts :api [API] the API endpoint to modify # @option opts :optional [Boolean] whether or not this scope needs to have @@ -23,13 +25,14 @@ class ParamsScope # validate if this param is present in the parent scope # @yield the instance context, open for parameter definitions def initialize(opts, &block) - @element = opts[:element] - @parent = opts[:parent] - @api = opts[:api] - @optional = opts[:optional] || false - @type = opts[:type] - @group = opts[:group] || {} - @dependent_on = opts[:dependent_on] + @element = opts[:element] + @element_renamed = opts[:element_renamed] + @parent = opts[:parent] + @api = opts[:api] + @optional = opts[:optional] || false + @type = opts[:type] + @group = opts[:group] || {} + @dependent_on = opts[:dependent_on] @declared_params = [] @index = nil @@ -129,18 +132,35 @@ def push_declared_params(attrs, **opts) if lateral? @parent.push_declared_params(attrs, **opts) else - if opts && 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 + push_renamed_param(full_path + [attrs.first], opts[:as]) \ + if opts && opts[:as] @declared_params.concat attrs end end + # Get the full path of the parameter scope in the hierarchy. + # + # @return [Array] the nesting/path of the current parameter scope + def full_path + nested? ? @parent.full_path + [@element] : [] + end + private + # Add a new parameter which should be renamed when using the +#declared+ + # method. + # + # @param path [Array] the full path of the parameter + # (including the parameter name as last array element) + # @param new_name [String, Symbol] the new name of the parameter (the + # renamed name, with the +as: ...+ semantic) + def push_renamed_param(path, new_name) + base = @api.route_setting(:renamed_params) || {} + base[Array(path).map(&:to_s)] = new_name.to_s + @api.route_setting(:renamed_params, base) + end + def require_required_and_optional_fields(context, opts) if context == :all optional_fields = Array(opts[:except]) @@ -191,11 +211,12 @@ def new_scope(attrs, optional = false, &block) end self.class.new( - api: @api, - element: attrs[1][:as] || attrs.first, - parent: self, - optional: optional, - type: type || Array, + api: @api, + element: attrs.first, + element_renamed: attrs[1][:as], + parent: self, + optional: optional, + type: type || Array, &block ) end @@ -235,6 +256,8 @@ def new_group_scope(attrs, &block) # Pushes declared params to parent or settings def configure_declared_params + push_renamed_param(full_path, @element_renamed) if @element_renamed + if nested? @parent.push_declared_params [element => @declared_params] else @@ -303,10 +326,6 @@ def validates(attrs, validations) next if order_specific_validations.include?(type) validate(type, options, attrs, doc_attrs, opts) end - - # Apply as validator last so other validations are applied to - # renamed param - validate(:as, validations[:as], attrs, doc_attrs, opts) if validations.key?(:as) end # Validate and comprehend the +:type+, +:types+, and +:coerce_with+ diff --git a/lib/grape/validations/validators/as.rb b/lib/grape/validations/validators/as.rb index 77cef5f1c..78f8e592e 100644 --- a/lib/grape/validations/validators/as.rb +++ b/lib/grape/validations/validators/as.rb @@ -3,14 +3,10 @@ module Grape module Validations class AsValidator < Base - def initialize(attrs, options, required, scope, **opts) - @renamed_options = options - super - end - - def validate_param!(attr_name, params) - params[@renamed_options] = params[attr_name] - end + # We use a validator for renaming parameters. This is just a marker for + # the parameter scope to handle the renaming. No actual validation + # happens here. + def validate_param!(*); end end end end diff --git a/spec/grape/endpoint/declared_spec.rb b/spec/grape/endpoint/declared_spec.rb index 6d85dd19b..e50f45bb8 100644 --- a/spec/grape/endpoint/declared_spec.rb +++ b/spec/grape/endpoint/declared_spec.rb @@ -598,4 +598,251 @@ def app expect(json.key?(:artist_id)).not_to be_truthy end end + + describe 'parameter renaming' do + context 'with a deeply nested parameter structure' do + let(:params) do + { + i_a: 'a', + i_b: { + i_c: 'c', + i_d: { + i_e: { + i_f: 'f', + i_g: 'g', + i_h: [ + { + i_ha: 'ha1', + i_hb: { + i_hc: 'c' + } + }, + { + i_ha: 'ha2', + i_hb: { + i_hc: 'c' + } + } + ] + } + } + } + } + end + let(:declared) do + { + o_a: 'a', + o_b: { + o_c: 'c', + o_d: { + o_e: { + o_f: 'f', + o_g: 'g', + o_h: [ + { + o_ha: 'ha1', + o_hb: { + o_hc: 'c' + } + }, + { + o_ha: 'ha2', + o_hb: { + o_hc: 'c' + } + } + ] + } + } + } + } + end + let(:params_keys) do + [ + 'i_a', + 'i_b', + 'i_b[i_c]', + 'i_b[i_d]', + 'i_b[i_d][i_e]', + 'i_b[i_d][i_e][i_f]', + 'i_b[i_d][i_e][i_g]', + 'i_b[i_d][i_e][i_h]', + 'i_b[i_d][i_e][i_h][i_ha]', + 'i_b[i_d][i_e][i_h][i_hb]', + 'i_b[i_d][i_e][i_h][i_hb][i_hc]' + ] + end + + before do + subject.format :json + subject.params do + optional :i_a, type: String, as: :o_a + optional :i_b, type: Hash, as: :o_b do + optional :i_c, type: String, as: :o_c + optional :i_d, type: Hash, as: :o_d do + optional :i_e, type: Hash, as: :o_e do + optional :i_f, type: String, as: :o_f + optional :i_g, type: String, as: :o_g + optional :i_h, type: Array, as: :o_h do + optional :i_ha, type: String, as: :o_ha + optional :i_hb, type: Hash, as: :o_hb do + optional :i_hc, type: String, as: :o_hc + end + end + end + end + end + end + subject.post '/test' do + declared(params, include_missing: false) + end + subject.post '/test/no-mod' do + before = params.to_h + declared(params, include_missing: false) + after = params.to_h + { before: before, after: after } + end + end + + it 'generates the correct parameter names for documentation' do + expect(subject.routes.first.params.keys).to match(params_keys) + end + + it 'maps the renamed parameter correctly' do + post '/test', **params + expect(JSON.parse(last_response.body, symbolize_names: true)).to \ + match(declared) + end + + it 'maps no parameters when none are given' do + post '/test' + expect(JSON.parse(last_response.body)).to match({}) + end + + it 'does not modify the request params' do + post '/test/no-mod', **params + result = JSON.parse(last_response.body, symbolize_names: true) + expect(result[:before]).to match(result[:after]) + end + end + + context 'with a renamed root parameter' do + before do + subject.format :json + subject.params do + optional :email_address, type: String, regexp: /.+@.+/, as: :email + end + subject.post '/test' do + declared(params, include_missing: false) + end + end + + it 'generates the correct parameter names for documentation' do + expect(subject.routes.first.params.keys).to match(%w[email_address]) + end + + it 'maps the renamed parameter correctly (original name)' do + post '/test', email_address: 'test@example.com' + expect(JSON.parse(last_response.body)).to \ + match('email' => 'test@example.com') + end + + it 'validates the renamed parameter correctly (original name)' do + post '/test', email_address: 'bad[at]example.com' + expect(JSON.parse(last_response.body)).to \ + match('error' => 'email_address is invalid') + end + + it 'ignores the renamed parameter (as name)' do + post '/test', email: 'test@example.com' + expect(JSON.parse(last_response.body)).to match({}) + end + end + + context 'with a renamed hash with nested parameters' do + before do + subject.format :json + subject.params do + optional :address, type: Hash, as: :address_attributes do + optional :street, type: String, values: ['Street 1', 'Street 2'], + default: 'Street 1' + optional :city, type: String + end + end + subject.post '/test' do + declared(params, include_missing: false) + end + end + + it 'generates the correct parameter names for documentation' do + expect(subject.routes.first.params.keys).to \ + match(%w[address address[street] address[city]]) + end + + it 'maps the renamed parameter correctly (original name)' do + post '/test', address: { city: 'Berlin', street: 'Street 2', t: 't' } + expect(JSON.parse(last_response.body)).to \ + match('address_attributes' => { 'city' => 'Berlin', + 'street' => 'Street 2' }) + end + + it 'validates the renamed parameter correctly (original name)' do + post '/test', address: { street: 'unknown' } + expect(JSON.parse(last_response.body)).to \ + match('error' => 'address[street] does not have a valid value') + end + + it 'ignores the renamed parameter (as name)' do + post '/test', address_attributes: { city: 'Berlin', unknown: '1' } + expect(JSON.parse(last_response.body)).to match({}) + end + end + + context 'with a renamed hash with nested renamed parameter' do + before do + subject.format :json + subject.params do + optional :user, type: Hash, as: :user_attributes do + optional :email_address, type: String, regexp: /.+@.+/, as: :email + end + end + subject.post '/test' do + declared(params, include_missing: false) + end + end + + it 'generates the correct parameter names for documentation' do + expect(subject.routes.first.params.keys).to \ + match(%w[user user[email_address]]) + end + + it 'maps the renamed parameter correctly (original name)' do + post '/test', user: { email_address: 'test@example.com' } + expect(JSON.parse(last_response.body)).to \ + match('user_attributes' => { 'email' => 'test@example.com' }) + end + + it 'validates the renamed parameter correctly (original name)' do + post '/test', user: { email_address: 'bad[at]example.com' } + expect(JSON.parse(last_response.body)).to \ + match('error' => 'user[email_address] is invalid') + end + + it 'ignores the renamed parameter (as name, 1)' do + post '/test', user: { email: 'test@example.com' } + expect(JSON.parse(last_response.body)).to \ + match({ 'user_attributes' => {} }) + end + + it 'ignores the renamed parameter (as name, 2)' do + post '/test', user_attributes: { email_address: 'test@example.com' } + expect(JSON.parse(last_response.body)).to match({}) + end + + it 'ignores the renamed parameter (as name, 3)' do + post '/test', user_attributes: { email: 'test@example.com' } + expect(JSON.parse(last_response.body)).to match({}) + end + end + end end diff --git a/spec/grape/validations/params_scope_spec.rb b/spec/grape/validations/params_scope_spec.rb index e0ed39169..fdc0357f8 100644 --- a/spec/grape/validations/params_scope_spec.rb +++ b/spec/grape/validations/params_scope_spec.rb @@ -144,7 +144,7 @@ def initialize(value) get '/renaming-coerced', foo: ' there we go ' expect(last_response.status).to eq(200) - expect(last_response.body).to eq('there we go-') + expect(last_response.body).to eq('-there we go') end it do @@ -185,7 +185,7 @@ def initialize(value) subject.params do optional :foo, as: :bar, default: 'before' end - subject.get('/rename-before-default') { params[:bar] } + subject.get('/rename-before-default') { declared(params)[:bar] } get '/rename-before-default' expect(last_response.status).to eq(200) @@ -196,7 +196,7 @@ def initialize(value) subject.params do optional :foo, default: 'after', as: :bar end - subject.get('/rename-after-default') { params[:bar] } + subject.get('/rename-after-default') { declared(params)[:bar] } get '/rename-after-default' expect(last_response.status).to eq(200) @@ -590,7 +590,7 @@ def initialize(value) it 'allows renaming of dependent on parameter' do subject.params do optional :a, as: :b - given b: ->(val) { val == 'x' } do + given a: ->(val) { val == 'x' } do requires :c end end @@ -604,7 +604,7 @@ def initialize(value) expect(last_response.status).to eq 200 end - it 'raises an error if the dependent parameter is not the renamed one' do + it 'does not raise if the dependent parameter is not the renamed one' do expect do subject.params do optional :a, as: :b @@ -612,6 +612,17 @@ def initialize(value) requires :c end end + end.not_to raise_error + end + + it 'raises an error if the dependent parameter is the renamed one' do + expect do + subject.params do + optional :a, as: :b + given :b do + requires :c + end + end end.to raise_error(Grape::Exceptions::UnknownParameter) end From f5d9831bac2e2dd439d0f3901797995a91139690 Mon Sep 17 00:00:00 2001 From: Dmitriy Nesteryuk Date: Mon, 27 Sep 2021 15:50:47 +0300 Subject: [PATCH 060/304] upgrade Rubocop & drop Ruby 2.4.x support (#2190) * upgrade Rubocop & drop Ruby 2.4 support To use latest Rubocop we need to drop support for Ruby 2.4.x. Also, this change fixes some offenses with auto correction. * next version will be 1.6.0 --- .rubocop.yml | 2 +- .rubocop_todo.yml | 316 +----------------- CHANGELOG.md | 1 + Gemfile | 6 +- README.md | 2 +- benchmark/large_model.rb | 4 +- benchmark/remounting.rb | 2 +- grape.gemspec | 10 +- lib/grape.rb | 2 +- lib/grape/api.rb | 1 + lib/grape/api/instance.rb | 25 +- lib/grape/cookies.rb | 2 + lib/grape/dsl/desc.rb | 8 +- lib/grape/dsl/helpers.rb | 10 +- lib/grape/dsl/inside_route.rb | 8 +- lib/grape/dsl/middleware.rb | 8 +- lib/grape/dsl/parameters.rb | 6 +- lib/grape/dsl/request_response.rb | 15 +- lib/grape/dsl/routing.rb | 4 +- lib/grape/dsl/settings.rb | 10 +- lib/grape/endpoint.rb | 45 ++- lib/grape/error_formatter/json.rb | 8 +- lib/grape/error_formatter/xml.rb | 8 +- lib/grape/exceptions/validation.rb | 3 +- lib/grape/formatter/json.rb | 1 + lib/grape/formatter/serializable_hash.rb | 3 +- lib/grape/formatter/xml.rb | 1 + lib/grape/middleware/base.rb | 2 + lib/grape/middleware/formatter.rb | 8 +- lib/grape/middleware/stack.rb | 4 +- .../versioner/accept_version_header.rb | 8 +- lib/grape/middleware/versioner/header.rb | 10 +- lib/grape/middleware/versioner/param.rb | 1 + .../versioner/parse_media_type_patch.rb | 3 +- lib/grape/middleware/versioner/path.rb | 2 + lib/grape/path.rb | 1 + lib/grape/request.rb | 1 + lib/grape/router.rb | 6 + lib/grape/router/pattern.rb | 2 +- lib/grape/router/route.rb | 4 +- lib/grape/util/inheritable_setting.rb | 4 +- lib/grape/util/lazy_value.rb | 5 +- lib/grape/validations/params_scope.rb | 74 ++-- .../validations/types/custom_type_coercer.rb | 1 + .../validations/types/dry_type_coercer.rb | 2 +- lib/grape/validations/types/json.rb | 3 +- .../validations/types/primitive_coercer.rb | 6 +- .../validations/validators/all_or_none.rb | 1 + .../validations/validators/at_least_one_of.rb | 1 + lib/grape/validations/validators/base.rb | 5 +- lib/grape/validations/validators/coerce.rb | 6 +- lib/grape/validations/validators/default.rb | 1 + .../validations/validators/exactly_one_of.rb | 1 + .../validators/multiple_params_base.rb | 2 + .../validators/mutual_exclusion.rb | 1 + lib/grape/validations/validators/presence.rb | 1 + lib/grape/validations/validators/regexp.rb | 1 + lib/grape/validations/validators/same_as.rb | 1 + lib/grape/validations/validators/values.rb | 3 + lib/grape/version.rb | 2 +- spec/grape/api/custom_validations_spec.rb | 1 + .../api/routes_with_requirements_spec.rb | 16 +- spec/grape/api_spec.rb | 79 +++-- spec/grape/dsl/callbacks_spec.rb | 2 +- spec/grape/dsl/middleware_spec.rb | 2 +- spec/grape/dsl/parameters_spec.rb | 1 + spec/grape/dsl/routing_spec.rb | 2 +- spec/grape/endpoint_spec.rb | 10 +- spec/grape/entity_spec.rb | 18 +- spec/grape/middleware/auth/dsl_spec.rb | 2 +- spec/grape/middleware/error_spec.rb | 3 +- spec/grape/middleware/formatter_spec.rb | 4 +- spec/grape/middleware/stack_spec.rb | 4 +- spec/grape/validations/params_scope_spec.rb | 1 + .../single_attribute_iterator_spec.rb | 2 +- .../types/primitive_coercer_spec.rb | 4 +- .../validations/validators/coerce_spec.rb | 23 +- .../validators/except_values_spec.rb | 4 +- .../validations/validators/values_spec.rb | 26 +- spec/grape/validations_spec.rb | 96 +++--- spec/shared/versioning_examples.rb | 4 +- spec/spec_helper.rb | 2 +- spec/support/basic_auth_encode_helpers.rb | 2 +- 83 files changed, 375 insertions(+), 617 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index ee4a91c64..16744c161 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,6 +1,6 @@ AllCops: NewCops: enable - TargetRubyVersion: 2.4 + TargetRubyVersion: 2.5 SuggestExtensions: false Exclude: - vendor/**/* diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 14a99edc1..26c7d0988 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,100 +1,18 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2020-12-26 22:10:33 UTC using RuboCop version 1.7.0. +# on 2021-09-26 08:03:47 UTC using RuboCop version 1.21.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. -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 +# Configuration parameters: IgnoredMethods. Lint/AmbiguousBlockAssociation: Exclude: - 'spec/grape/dsl/routing_spec.rb' -# Offense count: 54 +# Offense count: 56 # Configuration parameters: AllowedMethods. # AllowedMethods: enums Lint/ConstantDefinitionInBlock: @@ -107,25 +25,23 @@ Lint/DuplicateBranch: - 'lib/grape/extensions/deep_symbolize_hash.rb' - 'spec/support/versioned_helpers.rb' -# Offense count: 85 +# Offense count: 72 # Configuration parameters: AllowComments, AllowEmptyLambdas. Lint/EmptyBlock: Enabled: false -# Offense count: 6 +# Offense count: 5 # 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 +# Offense count: 7 Lint/MissingSuper: Exclude: - - 'lib/grape/api.rb' - 'lib/grape/api/instance.rb' - 'lib/grape/exceptions/base.rb' - 'lib/grape/exceptions/validation_array_errors.rb' @@ -134,29 +50,10 @@ Lint/MissingSuper: - 'lib/grape/router/pattern.rb' - 'lib/grape/validations/validators/base.rb' -# Offense count: 1 -# 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: 42 +# Offense count: 43 # Configuration parameters: IgnoredMethods, CountRepeatedAttributes. Metrics/AbcSize: - Max: 47 + Max: 43 # Offense count: 6 # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. @@ -172,7 +69,7 @@ Metrics/ClassLength: # Offense count: 30 # Configuration parameters: IgnoredMethods. Metrics/CyclomaticComplexity: - Max: 17 + Max: 15 # Offense count: 71 # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. @@ -185,15 +82,14 @@ Metrics/ModuleLength: Max: 220 # Offense count: 1 -# Configuration parameters: Max, CountKeywordArgs, MaxOptionalParameters. +# Configuration parameters: Max, CountKeywordArgs. Metrics/ParameterLists: - Exclude: - - 'lib/grape/middleware/error.rb' + MaxOptionalParameters: 4 # Offense count: 27 # Configuration parameters: IgnoredMethods. Metrics/PerceivedComplexity: - Max: 18 + Max: 15 # Offense count: 4 # Configuration parameters: EnforcedStyleForLeadingUnderscores. @@ -225,27 +121,6 @@ Naming/VariableNumber: - 'spec/grape/exceptions/validation_errors_spec.rb' - 'spec/grape/validations_spec.rb' -# Offense count: 3 -# Cop supports --auto-correct. -Performance/BigDecimalWithNumericArgument: - Exclude: - - 'spec/grape/validations/types/primitive_coercer_spec.rb' - -# Offense count: 21 -# Cop supports --auto-correct. -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: @@ -253,117 +128,22 @@ Performance/CollectionLiteralInLoop: - '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 Performance/MethodObjectAsBlock: Exclude: - '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 -# 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. +# Configuration parameters: MaxUnannotatedPlaceholdersAllowed, IgnoredMethods. # SupportedStyles: annotated, template, unannotated Style/FormatStringToken: EnforcedStyle: template -# 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 -# 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? @@ -381,77 +161,9 @@ Style/OptionalBooleanParameter: - '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/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 -# 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: - - '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. +# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. # URISchemes: http, https Layout/LineLength: Max: 215 diff --git a/CHANGELOG.md b/CHANGELOG.md index b1b0d9e86..624f486da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ #### Features +* [#2190](https://github.com/ruby-grape/grape/pull/2190): Upgrade dev deps & drop Ruby 2.4.x support - [@dnesteryuk](https://github.com/dnesteryuk). * Your contribution here. #### Fixes diff --git a/Gemfile b/Gemfile index 8e32048c4..383f71ba0 100644 --- a/Gemfile +++ b/Gemfile @@ -10,9 +10,9 @@ 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 + gem 'rubocop', '~> 1.21.0' + gem 'rubocop-ast', '~> 1.11.0' + gem 'rubocop-performance', '~> 1.11.5', require: false end group :development do diff --git a/README.md b/README.md index 9133f1ce1..e63323310 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ content negotiation, versioning and much more. ## Stable Release -You're reading the documentation for the next release of Grape, which should be **1.5.4**. +You're reading the documentation for the next release of Grape, which should be **1.6.0**. Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. The current stable release is [1.5.3](https://github.com/ruby-grape/grape/blob/v1.5.3/README.md). diff --git a/benchmark/large_model.rb b/benchmark/large_model.rb index 48827528d..272fc93d1 100644 --- a/benchmark/large_model.rb +++ b/benchmark/large_model.rb @@ -11,7 +11,7 @@ class API < Grape::API # include Grape::Extensions::Hashie::Mash::ParamBuilder rescue_from do |e| - warn "\n\n#{e.class} (#{e.message}):\n " + e.backtrace.join("\n ") + "\n\n" + warn "\n\n#{e.class} (#{e.message}):\n #{e.backtrace.join("\n ")}\n\n" end prefix :api @@ -79,7 +79,7 @@ def self.vrp_request_vehicle(this) 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(: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]) diff --git a/benchmark/remounting.rb b/benchmark/remounting.rb index 5c565b1d3..e174cda75 100644 --- a/benchmark/remounting.rb +++ b/benchmark/remounting.rb @@ -5,7 +5,7 @@ require 'benchmark/memory' class VotingApi < Grape::API - logger Logger.new(STDOUT) + logger Logger.new($stdout) helpers do def logger diff --git a/grape.gemspec b/grape.gemspec index 6ba65bb33..8eb2cf2b4 100644 --- a/grape.gemspec +++ b/grape.gemspec @@ -1,6 +1,6 @@ # frozen_string_literal: true -$LOAD_PATH.unshift File.expand_path('../lib', __FILE__) +$LOAD_PATH.unshift File.expand_path('lib', __dir__) require 'grape/version' Gem::Specification.new do |s| @@ -14,10 +14,10 @@ Gem::Specification.new do |s| 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", + '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}" + 'source_code_uri' => "https://github.com/ruby-grape/grape/tree/v#{s.version}" } s.add_runtime_dependency 'activesupport' @@ -32,5 +32,5 @@ Gem::Specification.new do |s| s.files += Dir['lib/**/*'] s.test_files = Dir['spec/**/*'] s.require_paths = ['lib'] - s.required_ruby_version = '>= 2.4.0' + s.required_ruby_version = '>= 2.5.0' end diff --git a/lib/grape.rb b/lib/grape.rb index 818c8f6bc..2fc201e6c 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -22,7 +22,7 @@ require 'active_support/notifications' require 'i18n' -I18n.load_path << File.expand_path('../grape/locale/en.yml', __FILE__) +I18n.load_path << File.expand_path('grape/locale/en.yml', __dir__) module Grape extend ::ActiveSupport::Autoload diff --git a/lib/grape/api.rb b/lib/grape/api.rb index 9d81bf95b..8e4fbbe1d 100644 --- a/lib/grape/api.rb +++ b/lib/grape/api.rb @@ -143,6 +143,7 @@ def add_setup(method, *args, &block) 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]) diff --git a/lib/grape/api/instance.rb b/lib/grape/api/instance.rb index 7335fdbc5..d0dd57734 100644 --- a/lib/grape/api/instance.rb +++ b/lib/grape/api/instance.rb @@ -10,12 +10,11 @@ class Instance include Grape::DSL::API class << self - attr_reader :instance - attr_reader :base + attr_reader :instance, :base attr_accessor :configuration def given(conditional_option, &block) - evaluate_as_instance_with_configuration(block, lazy: true) if conditional_option && block_given? + evaluate_as_instance_with_configuration(block, lazy: true) if conditional_option && block end def mounted(&block) @@ -28,7 +27,7 @@ def base=(grape_api) end def to_s - (base && base.to_s) || super + base&.to_s || super end def base_instance? @@ -82,6 +81,7 @@ def cascade(value = nil) def compile! return if instance + LOCK.synchronize { compile unless instance } end @@ -103,7 +103,7 @@ def prepare_routes def nest(*blocks, &block) blocks.reject!(&:nil?) if blocks.any? - evaluate_as_instance_with_configuration(block) if block_given? + evaluate_as_instance_with_configuration(block) if block blocks.each { |b| evaluate_as_instance_with_configuration(b) } reset_validations! else @@ -114,9 +114,7 @@ def nest(*blocks, &block) 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 + self.configuration = value_for_configuration.evaluate if value_for_configuration.respond_to?(:lazy?) && value_for_configuration.lazy? response = instance_eval(&block) self.configuration = value_for_configuration response @@ -179,7 +177,8 @@ def call(env) # 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) + return self.class.namespace_inheritable(:version_options)[:cascade] if self.class.namespace_inheritable(:version_options)&.key?(:cascade) + true end @@ -204,15 +203,11 @@ def add_head_not_allowed_methods_and_options_methods 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 + allowed_methods |= [Grape::Http::Headers::HEAD] if !self.class.namespace_inheritable(:do_not_route_head) && allowed_methods.include?(Grape::Http::Headers::GET) 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 + config[:endpoint].options[:options_route_enabled] = true unless self.class.namespace_inheritable(:do_not_route_options) || allowed_methods.include?(Grape::Http::Headers::OPTIONS) attributes = config.merge(allowed_methods: allowed_methods, allow_header: allow_header) generate_not_allowed_method(config[:pattern], **attributes) diff --git a/lib/grape/cookies.rb b/lib/grape/cookies.rb index 6e37c6d32..51d02112e 100644 --- a/lib/grape/cookies.rb +++ b/lib/grape/cookies.rb @@ -33,9 +33,11 @@ def each(&block) @cookies.each(&block) end + # rubocop:disable Layout/SpaceBeforeBrackets def delete(name, **opts) options = opts.merge(value: 'deleted', expires: Time.at(0)) self.[]=(name, options) end + # rubocop:enable Layout/SpaceBeforeBrackets end end diff --git a/lib/grape/dsl/desc.rb b/lib/grape/dsl/desc.rb index 8e94750c7..ab19bfd05 100644 --- a/lib/grape/dsl/desc.rb +++ b/lib/grape/dsl/desc.rb @@ -50,7 +50,7 @@ module Desc # end # def desc(description, options = {}, &config_block) - if block_given? + if config_block endpoint_configuration = if defined?(configuration) # When the instance is mounted - the configuration is executed on mount time if configuration.respond_to?(:evaluate) @@ -68,9 +68,7 @@ def desc(description, options = {}, &config_block) end config_class.configure(&config_block) - unless options.empty? - warn '[DEPRECATION] Passing a options hash and a block to `desc` is deprecated. Move all hash options to block.' - end + warn '[DEPRECATION] Passing a options hash and a block to `desc` is deprecated. Move all hash options to block.' unless options.empty? options = config_class.settings else options = options.merge(description: description) @@ -92,7 +90,7 @@ def description_field(field, value = nil) def unset_description_field(field) description = route_setting(:description) - description.delete(field) if description + description&.delete(field) end # Returns an object which configures itself via an instance-context DSL. diff --git a/lib/grape/dsl/helpers.rb b/lib/grape/dsl/helpers.rb index d461b34e3..c51940842 100644 --- a/lib/grape/dsl/helpers.rb +++ b/lib/grape/dsl/helpers.rb @@ -36,8 +36,8 @@ module ClassMethods # def helpers(*new_modules, &block) include_new_modules(new_modules) if new_modules.any? - include_block(block) if block_given? - include_all_in_scope if !block_given? && new_modules.empty? + include_block(block) if block + include_all_in_scope if !block && new_modules.empty? end protected @@ -67,12 +67,13 @@ def include_all_in_scope def define_boolean_in_mod(mod) return if defined? mod::Boolean + mod.const_set('Boolean', Grape::API::Boolean) end - def inject_api_helpers_to_mod(mod, &_block) + def inject_api_helpers_to_mod(mod, &block) mod.extend(BaseHelper) unless mod.is_a?(BaseHelper) - yield if block_given? + yield if block mod.api_changed(self) end end @@ -96,6 +97,7 @@ def api_changed(new_api) def process_named_params 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 9038fc60b..e2ac44888 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -91,7 +91,7 @@ def handle_passed_param(params_nested_path, has_passed_children = false, &_block return yield if has_passed_children key = params_nested_path[0] - key += '[' + params_nested_path[1..-1].join('][') + ']' if params_nested_path.size > 1 + key += "[#{params_nested_path[1..-1].join('][')}]" if params_nested_path.size > 1 route_options_params = options[:route_options][:params] || {} type = route_options_params.dig(key, :type) @@ -99,7 +99,7 @@ def handle_passed_param(params_nested_path, has_passed_children = false, &_block if type == 'Hash' && !has_children {} - elsif type == 'Array' || type&.start_with?('[') && !type&.include?(',') + elsif type == 'Array' || (type&.start_with?('[') && !type&.include?(',')) [] elsif type == 'Set' || type&.start_with?('# representation) elsif entity_class.present? && body raise ArgumentError, "Representation of type #{representation.class} cannot be merged." unless representation.respond_to?(:merge) + representation = body.merge(representation) end diff --git a/lib/grape/dsl/middleware.rb b/lib/grape/dsl/middleware.rb index f07f310b1..2a8d050db 100644 --- a/lib/grape/dsl/middleware.rb +++ b/lib/grape/dsl/middleware.rb @@ -18,28 +18,28 @@ module ClassMethods # to inject. def use(middleware_class, *args, &block) arr = [:use, middleware_class, *args] - arr << block if block_given? + arr << block if block namespace_stackable(:middleware, arr) end def insert(*args, &block) arr = [:insert, *args] - arr << block if block_given? + arr << block if block namespace_stackable(:middleware, arr) end def insert_before(*args, &block) arr = [:insert_before, *args] - arr << block if block_given? + arr << block if block namespace_stackable(:middleware, arr) end def insert_after(*args, &block) arr = [:insert_after, *args] - arr << block if block_given? + arr << block if block namespace_stackable(:middleware, arr) end diff --git a/lib/grape/dsl/parameters.rb b/lib/grape/dsl/parameters.rb index 84a0fa574..81a30b168 100644 --- a/lib/grape/dsl/parameters.rb +++ b/lib/grape/dsl/parameters.rb @@ -133,7 +133,7 @@ def requires(*attrs, &block) 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 ? new_scope(orig_attrs, &block) : push_declared_params(attrs, **opts.slice(:as)) end end @@ -149,7 +149,7 @@ def optional(*attrs, &block) opts = @group.merge(opts) if instance_variable_defined?(:@group) && @group # check type for optional parameter group - if attrs && block_given? + if attrs && block raise Grape::Exceptions::MissingGroupTypeError.new if type.nil? raise Grape::Exceptions::UnsupportedGroupTypeError.new unless Grape::Validations::Types.group?(type) end @@ -159,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 ? new_scope(orig_attrs, true, &block) : push_declared_params(attrs, **opts.slice(:as)) end end diff --git a/lib/grape/dsl/request_response.rb b/lib/grape/dsl/request_response.rb index cbb1db912..23b57b0da 100644 --- a/lib/grape/dsl/request_response.rb +++ b/lib/grape/dsl/request_response.rb @@ -26,6 +26,7 @@ def format(new_format = nil) # define a single mime type mime_type = content_types[new_format.to_sym] raise Grape::Exceptions::MissingMimeType.new(new_format) unless mime_type + namespace_stackable(:content_types, new_format.to_sym => mime_type) else namespace_inheritable(:format) @@ -102,14 +103,13 @@ def default_error_status(new_status = nil) def rescue_from(*args, &block) if args.last.is_a?(Proc) handler = args.pop - elsif block_given? + elsif block handler = block end options = args.extract_options! - if block_given? && options.key?(:with) - raise ArgumentError, 'both :with option and block cannot be passed' - end + raise ArgumentError, 'both :with option and block cannot be passed' if block && options.key?(:with) + handler ||= extract_with(options) if args.include?(:all) @@ -127,7 +127,7 @@ def rescue_from(*args, &block) :base_only_rescue_handlers end - namespace_reverse_stackable handler_type, Hash[args.map { |arg| [arg, handler] }] + namespace_reverse_stackable handler_type, args.map { |arg| [arg, handler] }.to_h end namespace_stackable(:rescue_options, options) @@ -154,7 +154,8 @@ def rescue_from(*args, &block) # @param model_class [Class] The model class that will be represented. # @option options [Class] :with The entity class that will represent the model. def represent(model_class, options) - raise Grape::Exceptions::InvalidWithOptionForRepresent.new unless options[:with] && options[:with].is_a?(Class) + raise Grape::Exceptions::InvalidWithOptionForRepresent.new unless options[:with].is_a?(Class) + namespace_stackable(:representations, model_class => options[:with]) end @@ -162,9 +163,11 @@ def represent(model_class, options) def extract_with(options) return unless options.key?(:with) + with_option = options.delete(:with) return with_option if with_option.instance_of?(Proc) return with_option.to_sym if with_option.instance_of?(Symbol) || with_option.instance_of?(String) + raise ArgumentError, "with: #{with_option.class}, expected Symbol, String or Proc" end end diff --git a/lib/grape/dsl/routing.rb b/lib/grape/dsl/routing.rb index 7f1e78c08..cbd3a2326 100644 --- a/lib/grape/dsl/routing.rb +++ b/lib/grape/dsl/routing.rb @@ -38,7 +38,7 @@ def version(*args, &block) @versions = versions | requested_versions - if block_given? + if block within_namespace do namespace_inheritable(:version, requested_versions) namespace_inheritable(:version_options, options) @@ -166,7 +166,7 @@ def route(methods, paths = ['/'], route_options = {}, &block) def namespace(space = nil, options = {}, &block) @namespace_description = nil unless instance_variable_defined?(:@namespace_description) && @namespace_description - if space || block_given? + if space || block within_namespace do previous_namespace_description = @namespace_description @namespace_description = (@namespace_description || {}).deep_merge(namespace_setting(:description) || {}) diff --git a/lib/grape/dsl/settings.rb b/lib/grape/dsl/settings.rb index 706f8adb6..899cb42f8 100644 --- a/lib/grape/dsl/settings.rb +++ b/lib/grape/dsl/settings.rb @@ -103,12 +103,14 @@ def namespace_reverse_stackable(key, value = nil) def namespace_stackable_with_hash(key) settings = get_or_set :namespace_stackable, key, nil return if settings.blank? + settings.each_with_object({}) { |value, result| result.deep_merge!(value) } end def namespace_reverse_stackable_with_hash(key) settings = get_or_set :namespace_reverse_stackable, key, nil return if settings.blank? + result = {} settings.each do |setting| setting.each do |field, value| @@ -154,10 +156,10 @@ def route_end # Execute the block within a context where our inheritable settings are forked # to a new copy (see #namespace_start). - def within_namespace(&_block) + def within_namespace(&block) namespace_start - result = yield if block_given? + result = yield if block namespace_end reset_validations! @@ -175,9 +177,7 @@ def build_top_level_setting # +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 + setting.inherit_from superclass.inheritable_setting if defined?(superclass) && superclass.respond_to?(:inheritable_setting) && superclass != Grape::API::Instance end end end diff --git a/lib/grape/endpoint.rb b/lib/grape/endpoint.rb index c28ab6138..35f5a3fc2 100644 --- a/lib/grape/endpoint.rb +++ b/lib/grape/endpoint.rb @@ -20,7 +20,8 @@ def new(*args, &block) def before_each(new_setup = false, &block) @before_each ||= [] if new_setup == false - return @before_each unless block_given? + return @before_each unless block + @before_each << block else @before_each = [new_setup] @@ -46,9 +47,7 @@ def run_before_each(endpoint) # @return [Proc] # @raise [NameError] an instance method with the same name already exists def generate_api_method(method_name, &block) - if method_defined?(method_name) - raise NameError.new("method #{method_name.inspect} already exists and cannot be used as an unbound method name") - end + raise NameError.new("method #{method_name.inspect} already exists and cannot be used as an unbound method name") if method_defined?(method_name) define_method(method_name, &block) method = instance_method(method_name) @@ -106,7 +105,7 @@ def initialize(new_settings, options = {}, &block) @body = nil @proc = nil - return unless block_given? + return unless block @source = block @block = self.class.generate_api_method(method_name, &block) @@ -118,11 +117,9 @@ def inherit_settings(namespace_stackable) inheritable_setting.route[:saved_validations] += namespace_stackable[:validations] parent_declared_params = namespace_stackable[:declared_params] - if parent_declared_params - inheritable_setting.route[:declared_params].concat(parent_declared_params.flatten) - end + inheritable_setting.route[:declared_params].concat(parent_declared_params.flatten) if parent_declared_params - endpoints && endpoints.each { |e| e.inherit_settings(namespace_stackable) } + endpoints&.each { |e| e.inherit_settings(namespace_stackable) } end def require_option(options, key) @@ -142,7 +139,7 @@ def routes end def reset_routes! - endpoints.each(&:reset_routes!) if endpoints + endpoints&.each(&:reset_routes!) @namespace = nil @routes = nil end @@ -154,13 +151,9 @@ def mount_in(router) reset_routes! routes.each do |route| methods = [route.request_method] - if !namespace_inheritable(:do_not_route_head) && route.request_method == Grape::Http::Headers::GET - methods << Grape::Http::Headers::HEAD - end + methods << Grape::Http::Headers::HEAD if !namespace_inheritable(:do_not_route_head) && route.request_method == Grape::Http::Headers::GET methods.each do |method| - unless route.request_method == method - route = Grape::Router::Route.new(method, route.origin, **route.attributes.to_h) - end + route = Grape::Router::Route.new(method, route.origin, **route.attributes.to_h) unless route.request_method == method router.append(route.apply(self)) end end @@ -200,6 +193,7 @@ def prepare_default_route_attributes def prepare_version version = namespace_inheritable(:version) || [] return if version.empty? + version.length == 1 ? version.first.to_s : version end @@ -234,7 +228,7 @@ def call!(env) # Return the collection of endpoints within this endpoint. # This is the case when an Grape::API mounts another Grape::API. def endpoints - options[:app].endpoints if options[:app] && options[:app].respond_to?(:endpoints) + options[:app].endpoints if options[:app].respond_to?(:endpoints) end def equals?(e) @@ -256,6 +250,7 @@ def run 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 @@ -357,15 +352,13 @@ def run_validators(validator_factories, request) ActiveSupport::Notifications.instrument('endpoint_run_validators.grape', endpoint: self, validators: validators, request: request) do validators.each do |validator| - begin - validator.validate(request) - rescue Grape::Exceptions::Validation => e - validation_errors << e - break if validator.fail_fast? - rescue Grape::Exceptions::ValidationArrayErrors => e - validation_errors.concat e.errors - break if validator.fail_fast? - end + validator.validate(request) + rescue Grape::Exceptions::Validation => e + validation_errors << e + break if validator.fail_fast? + rescue Grape::Exceptions::ValidationArrayErrors => e + validation_errors.concat e.errors + break if validator.fail_fast? end end diff --git a/lib/grape/error_formatter/json.rb b/lib/grape/error_formatter/json.rb index 6c160e099..770c5482d 100644 --- a/lib/grape/error_formatter/json.rb +++ b/lib/grape/error_formatter/json.rb @@ -10,12 +10,8 @@ def call(message, backtrace, options = {}, env = nil, original_exception = nil) result = wrap_message(present(message, env)) rescue_options = options[:rescue_options] || {} - if rescue_options[:backtrace] && backtrace && !backtrace.empty? - result = result.merge(backtrace: backtrace) - end - if rescue_options[:original_exception] && original_exception - result = result.merge(original_exception: original_exception.inspect) - end + result = result.merge(backtrace: backtrace) if rescue_options[:backtrace] && backtrace && !backtrace.empty? + result = result.merge(original_exception: original_exception.inspect) if rescue_options[:original_exception] && original_exception ::Grape::Json.dump(result) end diff --git a/lib/grape/error_formatter/xml.rb b/lib/grape/error_formatter/xml.rb index 39ea87387..e423c2fd9 100644 --- a/lib/grape/error_formatter/xml.rb +++ b/lib/grape/error_formatter/xml.rb @@ -11,12 +11,8 @@ def call(message, backtrace, options = {}, env = nil, original_exception = nil) result = message.is_a?(Hash) ? message : { message: message } rescue_options = options[:rescue_options] || {} - if rescue_options[:backtrace] && backtrace && !backtrace.empty? - result = result.merge(backtrace: backtrace) - end - if rescue_options[:original_exception] && original_exception - result = result.merge(original_exception: original_exception.inspect) - end + result = result.merge(backtrace: backtrace) if rescue_options[:backtrace] && backtrace && !backtrace.empty? + result = result.merge(original_exception: original_exception.inspect) if rescue_options[:original_exception] && original_exception result.respond_to?(:to_xml) ? result.to_xml(root: :error) : result.to_s end end diff --git a/lib/grape/exceptions/validation.rb b/lib/grape/exceptions/validation.rb index 6b98112ab..c2901039d 100644 --- a/lib/grape/exceptions/validation.rb +++ b/lib/grape/exceptions/validation.rb @@ -5,8 +5,7 @@ module Grape module Exceptions class Validation < Grape::Exceptions::Base - attr_accessor :params - attr_accessor :message_key + attr_accessor :params, :message_key def initialize(params:, message: nil, **args) @params = params diff --git a/lib/grape/formatter/json.rb b/lib/grape/formatter/json.rb index 753468a3c..0f0152f6c 100644 --- a/lib/grape/formatter/json.rb +++ b/lib/grape/formatter/json.rb @@ -6,6 +6,7 @@ module Json class << self def call(object, _env) return object.to_json if object.respond_to?(:to_json) + ::Grape::Json.dump(object) end end diff --git a/lib/grape/formatter/serializable_hash.rb b/lib/grape/formatter/serializable_hash.rb index 5b6256d59..d9c1dad64 100644 --- a/lib/grape/formatter/serializable_hash.rb +++ b/lib/grape/formatter/serializable_hash.rb @@ -8,13 +8,14 @@ def call(object, _env) return object if object.is_a?(String) return ::Grape::Json.dump(serialize(object)) if serializable?(object) return object.to_json if object.respond_to?(:to_json) + ::Grape::Json.dump(object) end private def serializable?(object) - object.respond_to?(:serializable_hash) || object.is_a?(Array) && object.all? { |o| o.respond_to? :serializable_hash } || object.is_a?(Hash) + object.respond_to?(:serializable_hash) || (object.is_a?(Array) && object.all? { |o| o.respond_to? :serializable_hash }) || object.is_a?(Hash) end def serialize(object) diff --git a/lib/grape/formatter/xml.rb b/lib/grape/formatter/xml.rb index 9db91d015..c170d1843 100644 --- a/lib/grape/formatter/xml.rb +++ b/lib/grape/formatter/xml.rb @@ -6,6 +6,7 @@ module Xml class << self def call(object, _env) return object.to_xml if object.respond_to?(:to_xml) + raise Grape::Exceptions::InvalidFormatter.new(object.class, 'xml') end end diff --git a/lib/grape/middleware/base.rb b/lib/grape/middleware/base.rb index 730a6cb3c..132353a67 100644 --- a/lib/grape/middleware/base.rb +++ b/lib/grape/middleware/base.rb @@ -59,6 +59,7 @@ def after; end def response return @app_response if @app_response.is_a?(Rack::Response) + Rack::Response.new(@app_response[2], @app_response[0], @app_response[1]) end @@ -84,6 +85,7 @@ def mime_types def merge_headers(response) return unless headers.is_a?(Hash) + case response when Rack::Response then response.headers.merge!(headers) when Array then response[1].merge!(headers) diff --git a/lib/grape/middleware/formatter.rb b/lib/grape/middleware/formatter.rb index bb9888c2a..e242cc0e2 100644 --- a/lib/grape/middleware/formatter.rb +++ b/lib/grape/middleware/formatter.rb @@ -22,6 +22,7 @@ def before def after return unless @app_response + status, headers, bodies = *@app_response if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status) @@ -79,7 +80,7 @@ def read_body_input (request.post? || request.put? || request.patch? || request.delete?) && (!request.form_data? || !request.media_type) && !request.parseable_data? && - (request.content_length.to_i > 0 || request.env[Grape::Http::Headers::HTTP_TRANSFER_ENCODING] == CHUNKED) + (request.content_length.to_i.positive? || request.env[Grape::Http::Headers::HTTP_TRANSFER_ENCODING] == CHUNKED) return unless (input = env[Grape::Env::RACK_INPUT]) @@ -96,9 +97,7 @@ def read_body_input def read_rack_input(body) fmt = request.media_type ? mime_types[request.media_type] : options[:default_format] - unless content_type_for(fmt) - throw :error, status: 415, message: "The provided content-type '#{request.media_type}' is not supported." - end + throw :error, status: 415, message: "The provided content-type '#{request.media_type}' is not supported." unless content_type_for(fmt) parser = Grape::Parser.parser_for fmt, **options if parser begin @@ -145,6 +144,7 @@ def format_from_params fmt = Rack::Utils.parse_nested_query(env[Grape::Http::Headers::QUERY_STRING])[Grape::Http::Headers::FORMAT] # avoid symbol memory leak on an unknown format return fmt.to_sym if content_type_for(fmt) + fmt end diff --git a/lib/grape/middleware/stack.rb b/lib/grape/middleware/stack.rb index dab755fe6..9492448a4 100644 --- a/lib/grape/middleware/stack.rb +++ b/lib/grape/middleware/stack.rb @@ -45,8 +45,8 @@ def initialize @others = [] end - def each - @middlewares.each { |x| yield x } + def each(&block) + @middlewares.each(&block) end def size diff --git a/lib/grape/middleware/versioner/accept_version_header.rb b/lib/grape/middleware/versioner/accept_version_header.rb index 953a78392..6198ae0cf 100644 --- a/lib/grape/middleware/versioner/accept_version_header.rb +++ b/lib/grape/middleware/versioner/accept_version_header.rb @@ -22,11 +22,9 @@ class AcceptVersionHeader < Base def before potential_version = (env[Grape::Http::Headers::HTTP_ACCEPT_VERSION] || '').strip - if strict? + if strict? && potential_version.empty? # If no Accept-Version header: - if potential_version.empty? - throw :error, status: 406, headers: error_headers, message: 'Accept-Version header must be set.' - end + throw :error, status: 406, headers: error_headers, message: 'Accept-Version header must be set.' end return if potential_version.empty? @@ -51,7 +49,7 @@ def strict? # of routes (see Grape::Router) for more information). To prevent # this behavior, and not add the `X-Cascade` header, one can set the `:cascade` option to `false`. def cascade? - if options[:version_options] && options[:version_options].key?(:cascade) + if options[:version_options]&.key?(:cascade) options[:version_options][:cascade] else true diff --git a/lib/grape/middleware/versioner/header.rb b/lib/grape/middleware/versioner/header.rb index bb5fc1671..caba4398c 100644 --- a/lib/grape/middleware/versioner/header.rb +++ b/lib/grape/middleware/versioner/header.rb @@ -26,10 +26,10 @@ module Versioner # route. class Header < Base VENDOR_VERSION_HEADER_REGEX = - /\Avnd\.([a-z0-9.\-_!#\$&\^]+?)(?:-([a-z0-9*.]+))?(?:\+([a-z0-9*\-.]+))?\z/.freeze + /\Avnd\.([a-z0-9.\-_!#{Regexp.last_match(0)}\^]+?)(?:-([a-z0-9*.]+))?(?:\+([a-z0-9*\-.]+))?\z/.freeze - HAS_VENDOR_REGEX = /\Avnd\.[a-z0-9.\-_!#\$&\^]+/.freeze - HAS_VERSION_REGEX = /\Avnd\.([a-z0-9.\-_!#\$&\^]+?)(?:-([a-z0-9*.]+))+/.freeze + HAS_VENDOR_REGEX = /\Avnd\.[a-z0-9.\-_!#{Regexp.last_match(0)}\^]+/.freeze + HAS_VERSION_REGEX = /\Avnd\.([a-z0-9.\-_!#{Regexp.last_match(0)}\^]+?)(?:-([a-z0-9*.]+))+/.freeze def before strict_header_checks if strict? @@ -52,12 +52,14 @@ def strict_header_checks def strict_accept_header_presence_check return unless header.qvalues.empty? + fail_with_invalid_accept_header!('Accept header must be set.') end def strict_version_vendor_accept_header_presence_check return unless versions.present? return if an_accept_header_with_version_and_vendor_is_present? + fail_with_invalid_accept_header!('API vendor or version not found.') end @@ -160,7 +162,7 @@ def version_options # information). To prevent # this behavior, and not add the `X-Cascade` # header, one can set the `:cascade` option to `false`. def cascade? - if version_options && version_options.key?(:cascade) + if version_options&.key?(:cascade) version_options[:cascade] else true diff --git a/lib/grape/middleware/versioner/param.rb b/lib/grape/middleware/versioner/param.rb index 8e7b17a4e..8e9fe36c0 100644 --- a/lib/grape/middleware/versioner/param.rb +++ b/lib/grape/middleware/versioner/param.rb @@ -32,6 +32,7 @@ def default_options def before potential_version = Rack::Utils.parse_nested_query(env[Grape::Http::Headers::QUERY_STRING])[paramkey] return if potential_version.nil? + throw :error, status: 404, message: '404 API Version Not Found', headers: { Grape::Http::Headers::X_CASCADE => 'pass' } if options[:versions] && !options[:versions].find { |v| v.to_s == potential_version } env[Grape::Env::API_VERSION] = potential_version env[Grape::Env::RACK_REQUEST_QUERY_HASH].delete(paramkey) if env.key? Grape::Env::RACK_REQUEST_QUERY_HASH diff --git a/lib/grape/middleware/versioner/parse_media_type_patch.rb b/lib/grape/middleware/versioner/parse_media_type_patch.rb index 7098b32c0..798068ff8 100644 --- a/lib/grape/middleware/versioner/parse_media_type_patch.rb +++ b/lib/grape/middleware/versioner/parse_media_type_patch.rb @@ -1,9 +1,10 @@ # frozen_string_literal: true +require 'English' module Rack module Accept module Header - ALLOWED_CHARACTERS = %r{^([a-z*]+)\/([a-z0-9*\&\^\-_#\$!.+]+)(?:;([a-z0-9=;]+))?$}.freeze + ALLOWED_CHARACTERS = %r{^([a-z*]+)/([a-z0-9*&\^\-_#{$ERROR_INFO}.+]+)(?:;([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) diff --git a/lib/grape/middleware/versioner/path.rb b/lib/grape/middleware/versioner/path.rb index b7becc749..ec15a6d7c 100644 --- a/lib/grape/middleware/versioner/path.rb +++ b/lib/grape/middleware/versioner/path.rb @@ -37,6 +37,7 @@ def before pieces = path.split('/') potential_version = pieces[1] 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 @@ -45,6 +46,7 @@ def before def mounted_path?(path) return false unless mount_path && path.start_with?(mount_path) + rest = path.slice(mount_path.length..-1) rest.start_with?('/') || rest.empty? end diff --git a/lib/grape/path.rb b/lib/grape/path.rb index e36684627..574855f3c 100644 --- a/lib/grape/path.rb +++ b/lib/grape/path.rb @@ -91,6 +91,7 @@ def parts def split_setting(key) return if settings[key].nil? + settings[key].to_s.split('/') end end diff --git a/lib/grape/request.rb b/lib/grape/request.rb index ed97f3194..e3ed4492d 100644 --- a/lib/grape/request.rb +++ b/lib/grape/request.rb @@ -37,6 +37,7 @@ def build_headers 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 diff --git a/lib/grape/router.rb b/lib/grape/router.rb index 8c5dce872..691889cd6 100644 --- a/lib/grape/router.rb +++ b/lib/grape/router.rb @@ -28,6 +28,7 @@ def initialize def compile! return if compiled + @union = Regexp.union(@neutral_regexes) @neutral_regexes = nil self.class.supported_methods.each do |method| @@ -60,6 +61,7 @@ def call(env) def recognize_path(input) any = with_optimization { greedy_match?(input) } return if any == default_response + any.endpoint end @@ -80,6 +82,7 @@ def rotation(env, exact_route = nil) map[method].each do |route| next if exact_route == route next unless route.match?(input) + response = process_route(route, env) break unless cascade?(response) end @@ -91,6 +94,7 @@ def transaction(env) response = yield(input, method) return response if response && !(cascade = cascade?(response)) + last_neighbor_route = greedy_match?(input) # If last_neighbor_route exists and request method is OPTIONS, @@ -139,12 +143,14 @@ def default_response def match?(input, method) current_regexp = @optimized_map[method] return unless current_regexp.match(input) + last_match = Regexp.last_match @map[method].detect { |route| last_match["_#{route.index}"] } end def greedy_match?(input) return unless @union.match(input) + last_match = Regexp.last_match @neutral_map.detect { |route| last_match["_#{route.index}"] } end diff --git a/lib/grape/router/pattern.rb b/lib/grape/router/pattern.rb index e8c108ad8..a23998048 100644 --- a/lib/grape/router/pattern.rb +++ b/lib/grape/router/pattern.rb @@ -41,7 +41,7 @@ def build_path(pattern, anchor: false, suffix: nil, **_options) end pattern = -pattern.split('/').tap do |parts| - parts[parts.length - 1] = '?' + parts.last + parts[parts.length - 1] = "?#{parts.last}" end.join('/') if pattern.end_with?('*path') PatternCache[[pattern, suffix]] diff --git a/lib/grape/router/route.rb b/lib/grape/router/route.rb index fa940c925..3ced9ca2a 100644 --- a/lib/grape/router/route.rb +++ b/lib/grape/router/route.rb @@ -84,8 +84,8 @@ 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 <<-WARNING -#{path}:#{line}: The route_xxx methods such as route_#{name} have been deprecated, please use #{expected}. + warn <<~WARNING + #{path}:#{line}: The route_xxx methods such as route_#{name} have been deprecated, please use #{expected}. WARNING end end diff --git a/lib/grape/util/inheritable_setting.rb b/lib/grape/util/inheritable_setting.rb index 2a6024bca..b3504fe97 100644 --- a/lib/grape/util/inheritable_setting.rb +++ b/lib/grape/util/inheritable_setting.rb @@ -5,9 +5,7 @@ module Util # A branchable, inheritable settings object which can store both stackable # and inheritable values (see InheritableValues and StackableValues). class InheritableSetting - attr_accessor :route, :api_class, :namespace - attr_accessor :namespace_inheritable, :namespace_stackable, :namespace_reverse_stackable - attr_accessor :parent, :point_in_time_copies + attr_accessor :route, :api_class, :namespace, :namespace_inheritable, :namespace_stackable, :namespace_reverse_stackable, :parent, :point_in_time_copies # Retrieve global settings. def self.global diff --git a/lib/grape/util/lazy_value.rb b/lib/grape/util/lazy_value.rb index d757ad3e2..af3017e32 100644 --- a/lib/grape/util/lazy_value.rb +++ b/lib/grape/util/lazy_value.rb @@ -49,9 +49,10 @@ def fetch(access_keys) end def []=(key, value) - @value_hash[key] = if value.is_a?(Hash) + @value_hash[key] = case value + when Hash LazyValueHash.new(value) - elsif value.is_a?(Array) + when Array LazyValueArray.new(value) else LazyValue.new(value) diff --git a/lib/grape/validations/params_scope.rb b/lib/grape/validations/params_scope.rb index 8a6bee152..a3fa9af0d 100644 --- a/lib/grape/validations/params_scope.rb +++ b/lib/grape/validations/params_scope.rb @@ -36,7 +36,7 @@ def initialize(opts, &block) @declared_params = [] @index = nil - instance_eval(&block) if block_given? + instance_eval(&block) if block configure_declared_params end @@ -53,15 +53,14 @@ def should_validate?(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 false if @parent.present? && !@parent.meets_dependency?(@parent.params(request_params), request_params) return params.any? { |param| meets_dependency?(param, request_params) } if params.is_a?(Array) @@ -172,6 +171,7 @@ def require_required_and_optional_fields(context, opts) required_fields.each do |field| field_opts = opts[:using][field] raise ArgumentError, "required field not exist: #{field}" unless field_opts + requires(field, field_opts) end optional_fields.each do |field| @@ -211,12 +211,12 @@ def new_scope(attrs, optional = false, &block) end self.class.new( - api: @api, - element: attrs.first, + api: @api, + element: attrs.first, element_renamed: attrs[1][:as], - parent: self, - optional: optional, - type: type || Array, + parent: self, + optional: optional, + type: type || Array, &block ) end @@ -230,11 +230,11 @@ def new_scope(attrs, optional = false, &block) # @yield parameter scope def new_lateral_scope(options, &block) self.class.new( - api: @api, - element: nil, - parent: self, - options: @optional, - type: type == Array ? Array : Hash, + api: @api, + element: nil, + parent: self, + options: @optional, + type: type == Array ? Array : Hash, dependent_on: options[:dependent_on], &block ) @@ -247,9 +247,9 @@ def new_lateral_scope(options, &block) # @yield parameter scope def new_group_scope(attrs, &block) self.class.new( - api: @api, - parent: self, - group: attrs.first, + api: @api, + parent: self, + group: attrs.first, &block ) end @@ -324,6 +324,7 @@ def validates(attrs, validations) validations.each do |type, options| next if order_specific_validations.include?(type) + validate(type, options, attrs, doc_attrs, opts) end end @@ -342,9 +343,7 @@ def validates(attrs, validations) # @return [class-like] type to which the parameter will be coerced # @raise [ArgumentError] if the given type options are invalid def infer_coercion(validations) - if validations.key?(:type) && validations.key?(:types) - raise ArgumentError, ':type may not be supplied with :types' - end + raise ArgumentError, ':type may not be supplied with :types' if validations.key?(:type) && validations.key?(:types) validations[:coerce] = (options_key?(:type, :value, validations) ? validations[:type][:value] : validations[:type]) if validations.key?(:type) validations[:coerce_message] = (options_key?(:type, :message, validations) ? validations[:type][:message] : nil) if validations.key?(:type) @@ -380,6 +379,7 @@ def check_coerce_with(validations) # but not special JSON types, which # already imply coercion method return unless [JSON, Array[JSON]].include? validations[:coerce] + raise ArgumentError, 'coerce_with disallowed for type: JSON' end @@ -407,6 +407,7 @@ def coerce_type(validations, attrs, doc_attrs, opts) def guess_coerce_type(coerce_type, *values_list) return coerce_type unless coerce_type == Array + values_list.each do |values| next if !values || values.is_a?(Proc) return values.first.class if values.is_a?(Range) || !values.empty? @@ -417,14 +418,11 @@ def guess_coerce_type(coerce_type, *values_list) def check_incompatible_option_values(default, values, except_values, excepts) return unless default && !default.is_a?(Proc) - if values && !values.is_a?(Proc) - raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :values, values) \ - unless Array(default).all? { |def_val| values.include?(def_val) } - end + raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :values, values) if values && !values.is_a?(Proc) && !Array(default).all? { |def_val| values.include?(def_val) } - if except_values && !except_values.is_a?(Proc) + if except_values && !except_values.is_a?(Proc) && Array(default).any? { |def_val| except_values.include?(def_val) } raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :except, except_values) \ - unless Array(default).none? { |def_val| except_values.include?(def_val) } + end return unless excepts && !excepts.is_a?(Proc) @@ -438,11 +436,11 @@ def validate(type, options, attrs, doc_attrs, opts) raise Grape::Exceptions::UnknownValidator.new(type) unless validator_class validator_options = { - attributes: attrs, - options: options, - required: doc_attrs[:required], - params_scope: self, - opts: opts, + attributes: attrs, + options: options, + required: doc_attrs[:required], + params_scope: self, + opts: opts, validator_class: validator_class } @api.namespace_stackable(:validations, validator_options) @@ -450,21 +448,20 @@ def validate(type, options, attrs, doc_attrs, opts) def validate_value_coercion(coerce_type, *values_list) return unless coerce_type + coerce_type = coerce_type.first if coerce_type.is_a?(Array) 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 == 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) - end + value_types = value_types.map { |type| Grape::API::Boolean.build(type) } if coerce_type == Grape::API::Boolean + raise Grape::Exceptions::IncompatibleOptionValues.new(:type, coerce_type, :values, values) unless value_types.all?(coerce_type) end end def extract_message_option(attrs) return nil unless attrs.is_a?(Array) + opts = attrs.last.is_a?(Hash) ? attrs.pop : {} opts.key?(:message) && !opts[:message].nil? ? opts.delete(:message) : nil end @@ -484,12 +481,13 @@ def derive_validator_options(validations) { allow_blank: allow_blank.is_a?(Hash) ? allow_blank[:value] : allow_blank, - fail_fast: validations.delete(:fail_fast) || false + fail_fast: validations.delete(:fail_fast) || false } end def validates_presence(validations, attrs, doc_attrs, opts) return unless validations.key?(:presence) && validations[:presence] + validate(:presence, validations[:presence], attrs, doc_attrs, opts) yield :presence yield :message if validations.key?(:message) diff --git a/lib/grape/validations/types/custom_type_coercer.rb b/lib/grape/validations/types/custom_type_coercer.rb index be72aff56..a761ee130 100644 --- a/lib/grape/validations/types/custom_type_coercer.rb +++ b/lib/grape/validations/types/custom_type_coercer.rb @@ -56,6 +56,7 @@ def call(val) return coerced_val if coerced_val.is_a?(InvalidValue) return InvalidValue.new unless coerced?(coerced_val) + coerced_val end diff --git a/lib/grape/validations/types/dry_type_coercer.rb b/lib/grape/validations/types/dry_type_coercer.rb index 0a682e53e..6b772378e 100644 --- a/lib/grape/validations/types/dry_type_coercer.rb +++ b/lib/grape/validations/types/dry_type_coercer.rb @@ -35,7 +35,7 @@ def collection_coercer_for(type) # 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 + return PrimitiveCoercer.new(type, strict) if type.instance_of?(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 diff --git a/lib/grape/validations/types/json.rb b/lib/grape/validations/types/json.rb index 25dded6f0..3240de27b 100644 --- a/lib/grape/validations/types/json.rb +++ b/lib/grape/validations/types/json.rb @@ -22,6 +22,7 @@ def parse(input) # Allow nulls and blank strings return if input.nil? || input.match?(/^\s*$/) + JSON.parse(input, symbolize_names: true) end @@ -41,7 +42,7 @@ def parsed?(value) # @param value [Object] result of {#parse} # @return [true,false] def coerced_collection?(value) - value.is_a?(::Array) && value.all? { |i| i.is_a? ::Hash } + value.is_a?(::Array) && value.all?(::Hash) end end end diff --git a/lib/grape/validations/types/primitive_coercer.rb b/lib/grape/validations/types/primitive_coercer.rb index 7b1e84180..368ccff64 100644 --- a/lib/grape/validations/types/primitive_coercer.rb +++ b/lib/grape/validations/types/primitive_coercer.rb @@ -11,15 +11,15 @@ module Types class PrimitiveCoercer < DryTypeCoercer MAPPING = { Grape::API::Boolean => DryTypes::Params::Bool, - BigDecimal => DryTypes::Params::Decimal, + BigDecimal => DryTypes::Params::Decimal, # unfortunately, a +Params+ scope doesn't contain String - String => DryTypes::Coercible::String + String => DryTypes::Coercible::String }.freeze STRICT_MAPPING = { Grape::API::Boolean => DryTypes::Strict::Bool, - BigDecimal => DryTypes::Strict::Decimal + BigDecimal => DryTypes::Strict::Decimal }.freeze def initialize(type, strict = false) diff --git a/lib/grape/validations/validators/all_or_none.rb b/lib/grape/validations/validators/all_or_none.rb index 186361f0d..385e8ea82 100644 --- a/lib/grape/validations/validators/all_or_none.rb +++ b/lib/grape/validations/validators/all_or_none.rb @@ -8,6 +8,7 @@ class AllOrNoneOfValidator < MultipleParamsBase def validate_params!(params) keys = keys_in_common(params) return if keys.empty? || keys.length == all_keys.length + raise Grape::Exceptions::Validation.new(params: all_keys, message: message(:all_or_none)) end end diff --git a/lib/grape/validations/validators/at_least_one_of.rb b/lib/grape/validations/validators/at_least_one_of.rb index 001c784dd..61e1d30c7 100644 --- a/lib/grape/validations/validators/at_least_one_of.rb +++ b/lib/grape/validations/validators/at_least_one_of.rb @@ -7,6 +7,7 @@ module Validations class AtLeastOneOfValidator < MultipleParamsBase def validate_params!(params) return unless keys_in_common(params).empty? + raise Grape::Exceptions::Validation.new(params: all_keys, message: message(:at_least_one)) end end diff --git a/lib/grape/validations/validators/base.rb b/lib/grape/validations/validators/base.rb index 84584e0b3..40fce8d07 100644 --- a/lib/grape/validations/validators/base.rb +++ b/lib/grape/validations/validators/base.rb @@ -30,6 +30,7 @@ def initialize(attrs, options, required, scope, *opts) # @return [void] def validate(request) return unless @scope.should_validate?(request.params) + validate!(request.params) end @@ -48,8 +49,9 @@ def validate!(params) next if skip_value next if !@scope.required? && empty_val next unless @scope.meets_dependency?(val, params) + begin - validate_param!(attr_name, val) if @required || val.respond_to?(:key?) && val.key?(attr_name) + validate_param!(attr_name, val) if @required || (val.respond_to?(:key?) && val.key?(attr_name)) rescue Grape::Exceptions::Validation => e array_errors << e end @@ -69,6 +71,7 @@ def self.convert_to_short_name(klass) def self.inherited(klass) return unless klass.name.present? + Validations.register_validator(convert_to_short_name(klass), klass) end diff --git a/lib/grape/validations/validators/coerce.rb b/lib/grape/validations/validators/coerce.rb index f79b4fa6e..edbc6a06d 100644 --- a/lib/grape/validations/validators/coerce.rb +++ b/lib/grape/validations/validators/coerce.rb @@ -27,10 +27,6 @@ def initialize(attrs, options, required, scope, **opts) end end - def validate(request) - super - end - def validate_param!(attr_name, params) raise validation_exception(attr_name) unless params.is_a? Hash @@ -47,7 +43,7 @@ def validate_param!(attr_name, params) # h[:list] = list # h # => # - return if params[attr_name].class == new_value.class && params[attr_name] == new_value + return if params[attr_name].instance_of?(new_value.class) && params[attr_name] == new_value params[attr_name] = new_value end diff --git a/lib/grape/validations/validators/default.rb b/lib/grape/validations/validators/default.rb index dbf754ed8..5f004d685 100644 --- a/lib/grape/validations/validators/default.rb +++ b/lib/grape/validations/validators/default.rb @@ -22,6 +22,7 @@ def validate!(params) attrs = SingleAttributeIterator.new(self, @scope, params) attrs.each do |resource_params, attr_name| next unless @scope.meets_dependency?(resource_params, params) + validate_param!(attr_name, resource_params) if resource_params.is_a?(Hash) && resource_params[attr_name].nil? end end diff --git a/lib/grape/validations/validators/exactly_one_of.rb b/lib/grape/validations/validators/exactly_one_of.rb index b8e4ecb9c..2a6f95614 100644 --- a/lib/grape/validations/validators/exactly_one_of.rb +++ b/lib/grape/validations/validators/exactly_one_of.rb @@ -9,6 +9,7 @@ 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 diff --git a/lib/grape/validations/validators/multiple_params_base.rb b/lib/grape/validations/validators/multiple_params_base.rb index 9ed0b6b96..1a3f44bd2 100644 --- a/lib/grape/validations/validators/multiple_params_base.rb +++ b/lib/grape/validations/validators/multiple_params_base.rb @@ -9,6 +9,7 @@ def validate!(params) attributes.each do |resource_params, skip_value| next if skip_value + begin validate_params!(resource_params) rescue Grape::Exceptions::Validation => e @@ -23,6 +24,7 @@ def validate!(params) def keys_in_common(resource_params) return [] unless resource_params.is_a?(Hash) + all_keys & resource_params.keys.map! { |attr| @scope.full_name(attr) } end diff --git a/lib/grape/validations/validators/mutual_exclusion.rb b/lib/grape/validations/validators/mutual_exclusion.rb index bcd25bcae..e2817bb1d 100644 --- a/lib/grape/validations/validators/mutual_exclusion.rb +++ b/lib/grape/validations/validators/mutual_exclusion.rb @@ -8,6 +8,7 @@ class MutualExclusionValidator < MultipleParamsBase def validate_params!(params) keys = keys_in_common(params) return if keys.length <= 1 + raise Grape::Exceptions::Validation.new(params: keys, message: message(:mutual_exclusion)) end end diff --git a/lib/grape/validations/validators/presence.rb b/lib/grape/validations/validators/presence.rb index 92ec570f4..a75d3d0d6 100644 --- a/lib/grape/validations/validators/presence.rb +++ b/lib/grape/validations/validators/presence.rb @@ -5,6 +5,7 @@ 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.new(params: [@scope.full_name(attr_name)], message: message(:presence)) end end diff --git a/lib/grape/validations/validators/regexp.rb b/lib/grape/validations/validators/regexp.rb index 23f6a29ad..e796d3d57 100644 --- a/lib/grape/validations/validators/regexp.rb +++ b/lib/grape/validations/validators/regexp.rb @@ -6,6 +6,7 @@ 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.match?((options_key?(:value) ? @option[:value] : @option)) } + raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:regexp)) end end diff --git a/lib/grape/validations/validators/same_as.rb b/lib/grape/validations/validators/same_as.rb index 087150f16..ae98617c6 100644 --- a/lib/grape/validations/validators/same_as.rb +++ b/lib/grape/validations/validators/same_as.rb @@ -6,6 +6,7 @@ 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 diff --git a/lib/grape/validations/validators/values.rb b/lib/grape/validations/validators/values.rb index f3d676d0b..b72ffd4c3 100644 --- a/lib/grape/validations/validators/values.rb +++ b/lib/grape/validations/validators/values.rb @@ -13,6 +13,7 @@ def initialize(attrs, options, required, scope, **opts) 'Use the except validator instead.' if @excepts raise ArgumentError, 'proc must be a Proc' if @proc && !@proc.is_a?(Proc) + warn '[DEPRECATION] The values validator proc option is deprecated. ' \ 'The lambda expression can now be assigned directly to values.' if @proc else @@ -51,6 +52,7 @@ def validate_param!(attr_name, params) def check_values(param_array, attr_name) values = @values.is_a?(Proc) && @values.arity.zero? ? @values.call : @values return true if values.nil? + begin return param_array.all? { |param| values.call(param) } if values.is_a? Proc rescue StandardError => e @@ -63,6 +65,7 @@ def check_values(param_array, attr_name) def check_excepts(param_array) excepts = @excepts.is_a?(Proc) ? @excepts.call : @excepts return true if excepts.nil? + param_array.none? { |param| excepts.include?(param) } end diff --git a/lib/grape/version.rb b/lib/grape/version.rb index 6eb5000f6..2fe05f947 100644 --- a/lib/grape/version.rb +++ b/lib/grape/version.rb @@ -2,5 +2,5 @@ module Grape # The current version of Grape. - VERSION = '1.5.4' + VERSION = '1.6.0' end diff --git a/spec/grape/api/custom_validations_spec.rb b/spec/grape/api/custom_validations_spec.rb index f69ec5199..8a5b1ec5c 100644 --- a/spec/grape/api/custom_validations_spec.rb +++ b/spec/grape/api/custom_validations_spec.rb @@ -10,6 +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.new(params: [@scope.full_name(attr_name)], message: "must be at the most #{@option} characters long") end end diff --git a/spec/grape/api/routes_with_requirements_spec.rb b/spec/grape/api/routes_with_requirements_spec.rb index 83c5b85ec..2ebac9609 100644 --- a/spec/grape/api/routes_with_requirements_spec.rb +++ b/spec/grape/api/routes_with_requirements_spec.rb @@ -11,7 +11,7 @@ def app context 'get' do it 'routes to a namespace param with dots' do - subject.namespace ':ns_with_dots', requirements: { ns_with_dots: %r{[^\/]+} } do + subject.namespace ':ns_with_dots', requirements: { ns_with_dots: %r{[^/]+} } do get '/' do params[:ns_with_dots] end @@ -23,8 +23,8 @@ def app 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 + 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 @@ -34,9 +34,9 @@ def app 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 + 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 @@ -47,8 +47,8 @@ def app 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 + 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 diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index af2419ceb..e38a8a148 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -610,6 +610,7 @@ class DummyFormatClass subject.namespace :example do before do raise 'before filter ran twice' if already_run + already_run = true header 'X-Custom-Header', 'foo' end @@ -650,12 +651,12 @@ class DummyFormatClass put '/example' expect(last_response.status).to eql 405 - expect(last_response.body).to eq <<-XML - - - 405 Not Allowed - -XML + expect(last_response.body).to eq <<~XML + + + 405 Not Allowed + + XML end end @@ -2147,7 +2148,9 @@ class CustomError < Grape::Exceptions::Base; end context 'custom errors' do before do class ConnectionError < RuntimeError; end + class DatabaseError < RuntimeError; end + class CommunicationError < StandardError; end end @@ -2303,6 +2306,7 @@ def rescue_all_errors module ApiSpec module APIErrors class ParentError < StandardError; end + class ChildError < ParentError; end end end @@ -3160,10 +3164,10 @@ def static subject.get 'method' expect(subject.routes.map(&:params)).to eq [{ - 'group1' => { required: true, type: 'Array' }, + 'group1' => { required: true, type: 'Array' }, 'group1[param1]' => { required: false, desc: 'group1 param1 desc' }, 'group1[param2]' => { required: true, desc: 'group1 param2 desc' }, - 'group2' => { required: true, type: 'Array' }, + 'group2' => { required: true, type: 'Array' }, 'group2[param1]' => { required: false, desc: 'group2 param1 desc' }, 'group2[param2]' => { required: true, desc: 'group2 param2 desc' } }] @@ -3376,8 +3380,8 @@ def static mount app end expect(subject.routes.size).to eq(2) - expect(subject.routes.first.path).to match(%r{\/cool\/awesome}) - expect(subject.routes.last.path).to match(%r{\/cool\/sauce}) + expect(subject.routes.first.path).to match(%r{/cool/awesome}) + expect(subject.routes.last.path).to match(%r{/cool/sauce}) end it 'mounts on a path' do @@ -3399,7 +3403,7 @@ def static APP2.get '/nice' do 'play' end - # note that the reverse won't work, mount from outside-in + # NOTE: that the reverse won't work, mount from outside-in APP3 = subject APP3.mount APP1 => '/app1' APP1.mount APP2 => '/app2' @@ -3590,6 +3594,7 @@ def static def self.included(base) base.extend(ClassMethods) end + module ClassMethods def my_method @test = true @@ -3834,12 +3839,12 @@ def serializable_hash end get '/example' expect(last_response.status).to eq(500) - expect(last_response.body).to eq <<-XML - - - cannot convert String to xml - -XML + expect(last_response.body).to eq <<~XML + + + cannot convert String to xml + + XML end it 'hash' do subject.get '/example' do @@ -3850,13 +3855,13 @@ def serializable_hash end get '/example' expect(last_response.status).to eq(200) - expect(last_response.body).to eq <<-XML - - - example1 - example2 - -XML + expect(last_response.body).to eq <<~XML + + + example1 + example2 + + XML end it 'array' do subject.get '/example' do @@ -3864,13 +3869,13 @@ def serializable_hash end get '/example' expect(last_response.status).to eq(200) - expect(last_response.body).to eq <<-XML - - - example1 - example2 - -XML + expect(last_response.body).to eq <<~XML + + + example1 + example2 + + XML end it 'raised :error from middleware' do middleware = Class.new(Grape::Middleware::Base) do @@ -3883,12 +3888,12 @@ def before end get '/' expect(last_response.status).to eq(42) - expect(last_response.body).to eq <<-XML - - - Unauthorized - -XML + expect(last_response.body).to eq <<~XML + + + Unauthorized + + XML end end end diff --git a/spec/grape/dsl/callbacks_spec.rb b/spec/grape/dsl/callbacks_spec.rb index 73dbc259e..8844b02d7 100644 --- a/spec/grape/dsl/callbacks_spec.rb +++ b/spec/grape/dsl/callbacks_spec.rb @@ -12,7 +12,7 @@ class Dummy describe Callbacks do subject { Class.new(CallbacksSpec::Dummy) } - let(:proc) { ->() {} } + let(:proc) { -> {} } describe '.before' do it 'adds a block to "before"' do diff --git a/spec/grape/dsl/middleware_spec.rb b/spec/grape/dsl/middleware_spec.rb index b116fb735..a9b11d74c 100644 --- a/spec/grape/dsl/middleware_spec.rb +++ b/spec/grape/dsl/middleware_spec.rb @@ -12,7 +12,7 @@ class Dummy describe Middleware do subject { Class.new(MiddlewareSpec::Dummy) } - let(:proc) { ->() {} } + let(:proc) { -> {} } let(:foo_middleware) { Class.new } let(:bar_middleware) { Class.new } diff --git a/spec/grape/dsl/parameters_spec.rb b/spec/grape/dsl/parameters_spec.rb index bd60195e1..51967d3ff 100644 --- a/spec/grape/dsl/parameters_spec.rb +++ b/spec/grape/dsl/parameters_spec.rb @@ -40,6 +40,7 @@ def new_group_scope(args) def extract_message_option(attrs) return nil unless attrs.is_a?(Array) + opts = attrs.last.is_a?(Hash) ? attrs.pop : {} opts.key?(:message) && !opts[:message].nil? ? opts.delete(:message) : nil end diff --git a/spec/grape/dsl/routing_spec.rb b/spec/grape/dsl/routing_spec.rb index e26d039d3..ea225eaff 100644 --- a/spec/grape/dsl/routing_spec.rb +++ b/spec/grape/dsl/routing_spec.rb @@ -12,7 +12,7 @@ class Dummy describe Routing do subject { Class.new(RoutingSpec::Dummy) } - let(:proc) { ->() {} } + let(:proc) { -> {} } let(:options) { { a: :b } } let(:path) { '/dummy' } diff --git a/spec/grape/endpoint_spec.rb b/spec/grape/endpoint_spec.rb index 487fbb0d3..08f7c3529 100644 --- a/spec/grape/endpoint_spec.rb +++ b/spec/grape/endpoint_spec.rb @@ -150,7 +150,7 @@ def app end it 'includes headers passed as symbols' do env = Rack::MockRequest.env_for('/headers') - env['HTTP_SYMBOL_HEADER'.to_sym] = 'Goliath passes symbols' + env[:HTTP_SYMBOL_HEADER] = 'Goliath passes symbols' body = read_chunks(subject.call(env)[2]).join expect(JSON.parse(body)['Symbol-Header']).to eq('Goliath passes symbols') end @@ -212,10 +212,10 @@ def app end get '/test', {}, 'HTTP_COOKIE' => 'delete_this_cookie=1; and_this=2' expect(last_response.body).to eq('3') - cookies = Hash[last_response.headers['Set-Cookie'].split("\n").map do |set_cookie| + cookies = last_response.headers['Set-Cookie'].split("\n").map do |set_cookie| cookie = CookieJar::Cookie.from_set_cookie 'http://localhost/test', set_cookie [cookie.name, cookie] - end] + end.to_h expect(cookies.size).to eq(2) %w[and_this delete_this_cookie].each do |cookie_name| cookie = cookies[cookie_name] @@ -236,10 +236,10 @@ def app end get('/test', {}, 'HTTP_COOKIE' => 'delete_this_cookie=1; and_this=2') expect(last_response.body).to eq('3') - cookies = Hash[last_response.headers['Set-Cookie'].split("\n").map do |set_cookie| + cookies = last_response.headers['Set-Cookie'].split("\n").map do |set_cookie| cookie = CookieJar::Cookie.from_set_cookie 'http://localhost/test', set_cookie [cookie.name, cookie] - end] + end.to_h expect(cookies.size).to eq(2) %w[and_this delete_this_cookie].each do |cookie_name| cookie = cookies[cookie_name] diff --git a/spec/grape/entity_spec.rb b/spec/grape/entity_spec.rb index beb304061..a099cdaa3 100644 --- a/spec/grape/entity_spec.rb +++ b/spec/grape/entity_spec.rb @@ -238,14 +238,14 @@ def initialize(args) get '/example' expect(last_response.status).to eq(200) expect(last_response.headers['Content-type']).to eq('application/xml') - expect(last_response.body).to eq <<-XML - - - - johnnyiller - - -XML + expect(last_response.body).to eq <<~XML + + + + johnnyiller + + + XML end it 'presents with json' do @@ -326,7 +326,7 @@ def initialize(args) end get '/example' expect_response_json = { - 'page' => 1, + 'page' => 1, 'user1' => { 'name' => 'user1' }, 'user2' => { 'name' => 'user2' } } diff --git a/spec/grape/middleware/auth/dsl_spec.rb b/spec/grape/middleware/auth/dsl_spec.rb index 338d17c7c..41169a3bf 100644 --- a/spec/grape/middleware/auth/dsl_spec.rb +++ b/spec/grape/middleware/auth/dsl_spec.rb @@ -5,7 +5,7 @@ describe Grape::Middleware::Auth::DSL do subject { Class.new(Grape::API) } - let(:block) { ->() {} } + let(:block) { -> {} } let(:settings) do { opaque: 'secret', diff --git a/spec/grape/middleware/error_spec.rb b/spec/grape/middleware/error_spec.rb index d586b9820..cb2befbf9 100644 --- a/spec/grape/middleware/error_spec.rb +++ b/spec/grape/middleware/error_spec.rb @@ -16,8 +16,7 @@ def static class ErrApp class << self - attr_accessor :error - attr_accessor :format + attr_accessor :error, :format def call(_env) throw :error, error diff --git a/spec/grape/middleware/formatter_spec.rb b/spec/grape/middleware/formatter_spec.rb index 9f6333cd0..8040d8e87 100644 --- a/spec/grape/middleware/formatter_spec.rb +++ b/spec/grape/middleware/formatter_spec.rb @@ -20,7 +20,7 @@ let(:body) { ['foo'] } it 'calls #to_json since default format is json' do body.instance_eval do - def to_json + def to_json(*_args) '"bar"' end end @@ -33,7 +33,7 @@ def to_json let(:body) { { 'foos' => [{ 'bar' => 'baz' }] } } it 'calls #to_json if the content type is jsonapi' do body.instance_eval do - def to_json + def to_json(*_args) '{"foos":[{"bar":"baz"}] }' end end diff --git a/spec/grape/middleware/stack_spec.rb b/spec/grape/middleware/stack_spec.rb index b7ac1b149..27af97526 100644 --- a/spec/grape/middleware/stack_spec.rb +++ b/spec/grape/middleware/stack_spec.rb @@ -5,7 +5,9 @@ describe Grape::Middleware::Stack do module StackSpec class FooMiddleware; end + class BarMiddleware; end + class BlockMiddleware attr_reader :block @@ -15,7 +17,7 @@ def initialize(&block) end end - let(:proc) { ->() {} } + let(:proc) { -> {} } let(:others) { [[:use, StackSpec::BarMiddleware], [:insert_before, StackSpec::BarMiddleware, StackSpec::BlockMiddleware, proc]] } subject { Grape::Middleware::Stack.new } diff --git a/spec/grape/validations/params_scope_spec.rb b/spec/grape/validations/params_scope_spec.rb index fdc0357f8..290d646bf 100644 --- a/spec/grape/validations/params_scope_spec.rb +++ b/spec/grape/validations/params_scope_spec.rb @@ -98,6 +98,7 @@ class CustomType def self.parse(value) raise if value == 'invalid' + new(value) end diff --git a/spec/grape/validations/single_attribute_iterator_spec.rb b/spec/grape/validations/single_attribute_iterator_spec.rb index 31a51dc47..e23b9a359 100644 --- a/spec/grape/validations/single_attribute_iterator_spec.rb +++ b/spec/grape/validations/single_attribute_iterator_spec.rb @@ -49,7 +49,7 @@ 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], + [params[1], :first, false, false], [params[1], :second, false, false] ) end end diff --git a/spec/grape/validations/types/primitive_coercer_spec.rb b/spec/grape/validations/types/primitive_coercer_spec.rb index df375ac37..88f22efc0 100644 --- a/spec/grape/validations/types/primitive_coercer_spec.rb +++ b/spec/grape/validations/types/primitive_coercer_spec.rb @@ -12,7 +12,7 @@ let(:type) { BigDecimal } it 'coerces to BigDecimal' do - expect(subject.call(5)).to eq(BigDecimal(5)) + expect(subject.call(5)).to eq(BigDecimal('5')) end it 'coerces an empty string to nil' do @@ -127,7 +127,7 @@ end it 'returns a value as it is when the given value is BigDecimal' do - expect(subject.call(BigDecimal(0))).to eq(BigDecimal(0)) + expect(subject.call(BigDecimal('0'))).to eq(BigDecimal('0')) end end end diff --git a/spec/grape/validations/validators/coerce_spec.rb b/spec/grape/validations/validators/coerce_spec.rb index 3f8046f8a..a24ca94cf 100644 --- a/spec/grape/validations/validators/coerce_spec.rb +++ b/spec/grape/validations/validators/coerce_spec.rb @@ -31,9 +31,9 @@ def self.parsed?(value) 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.load_path << File.expand_path('zh-CN.yml', __dir__) I18n.reload! - I18n.locale = 'zh-CN'.to_sym + I18n.locale = :'zh-CN' subject.params do requires :age, type: Integer end @@ -48,7 +48,7 @@ def self.parsed?(value) 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 + I18n.locale = :'pt-BR' subject.params do requires :age, type: Integer end @@ -84,9 +84,10 @@ def self.parsed?(value) before do subject.params do requires :a, types: { value: [Boolean, String], message: 'type cast is invalid' }, coerce_with: (lambda do |val| - if val == 'yup' + case val + when 'yup' true - elsif val == 'false' + when 'false' 0 else val @@ -650,7 +651,7 @@ def self.parse(_val) 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 } + 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] @@ -834,9 +835,10 @@ def self.parse(_val) before do subject.params do requires :int, type: Integer, coerce_with: (lambda do |val| - if val == '0' + case val + when '0' nil - elsif val.match?(/^-?\d+$/) + when /^-?\d+$/ val.to_i else val @@ -1201,9 +1203,10 @@ def self.parse(_val) before do subject.params do requires :a, types: [Boolean, String], coerce_with: (lambda do |val| - if val == 'yup' + case val + when 'yup' true - elsif val == 'false' + when 'false' 0 else val diff --git a/spec/grape/validations/validators/except_values_spec.rb b/spec/grape/validations/validators/except_values_spec.rb index 4757cff8a..bcc756dd5 100644 --- a/spec/grape/validations/validators/except_values_spec.rb +++ b/spec/grape/validations/validators/except_values_spec.rb @@ -5,7 +5,7 @@ describe Grape::Validations::ExceptValuesValidator do module ValidationsSpec class ExceptValuesModel - DEFAULT_EXCEPTS = ['invalid-type1', 'invalid-type2', 'invalid-type3'].freeze + DEFAULT_EXCEPTS = %w[invalid-type1 invalid-type2 invalid-type3].freeze class << self attr_accessor :excepts @@ -170,7 +170,7 @@ class API < Grape::API it 'raises IncompatibleOptionValues when type is incompatible with values array' do subject = Class.new(Grape::API) expect do - subject.params { optional :type, except_values: ['valid-type1', 'valid-type2', 'valid-type3'], type: Symbol } + subject.params { optional :type, except_values: %w[valid-type1 valid-type2 valid-type3], type: Symbol } end.to raise_error Grape::Exceptions::IncompatibleOptionValues end diff --git a/spec/grape/validations/validators/values_spec.rb b/spec/grape/validations/validators/values_spec.rb index 491b32834..73c35b390 100644 --- a/spec/grape/validations/validators/values_spec.rb +++ b/spec/grape/validations/validators/values_spec.rb @@ -5,8 +5,8 @@ describe Grape::Validations::ValuesValidator do module ValidationsSpec class ValuesModel - DEFAULT_VALUES = ['valid-type1', 'valid-type2', 'valid-type3'].freeze - DEFAULT_EXCEPTS = ['invalid-type1', 'invalid-type2', 'invalid-type3'].freeze + DEFAULT_VALUES = %w[valid-type1 valid-type2 valid-type3].freeze + DEFAULT_EXCEPTS = %w[invalid-type1 invalid-type2 invalid-type3].freeze class << self def values @values ||= [] @@ -27,6 +27,10 @@ def add_except(except) @excepts ||= [] @excepts << except end + + def include?(value) + values.include?(value) + end end end @@ -106,7 +110,7 @@ class API < Grape::API end params do - requires :type, values: ->(v) { ValuesModel.values.include? v } + requires :type, values: ->(v) { ValuesModel.include? v } end get '/lambda_val' do { type: params[:type] } @@ -214,14 +218,14 @@ class API < Grape::API put '/optional_with_array_of_string_values' params do - requires :type, values: { proc: ->(v) { ValuesModel.values.include? v } } + requires :type, values: { proc: ->(v) { ValuesModel.include? v } } end get '/proc' do { type: params[:type] } end params do - requires :type, values: { proc: ->(v) { ValuesModel.values.include? v }, message: 'failed check' } + requires :type, values: { proc: ->(v) { ValuesModel.include? v }, message: 'failed check' } end get '/proc/message' @@ -420,21 +424,21 @@ def app it 'raises IncompatibleOptionValues on an invalid default value from proc' do subject = Class.new(Grape::API) expect do - subject.params { optional :type, values: ['valid-type1', 'valid-type2', 'valid-type3'], default: ValidationsSpec::ValuesModel.values.sample + '_invalid' } + subject.params { optional :type, values: %w[valid-type1 valid-type2 valid-type3], default: "#{ValidationsSpec::ValuesModel.values.sample}_invalid" } end.to raise_error Grape::Exceptions::IncompatibleOptionValues end it 'raises IncompatibleOptionValues on an invalid default value' do subject = Class.new(Grape::API) expect do - subject.params { optional :type, values: ['valid-type1', 'valid-type2', 'valid-type3'], default: 'invalid-type' } + subject.params { optional :type, values: %w[valid-type1 valid-type2 valid-type3], default: 'invalid-type' } end.to raise_error Grape::Exceptions::IncompatibleOptionValues end it 'raises IncompatibleOptionValues when type is incompatible with values array' do subject = Class.new(Grape::API) expect do - subject.params { optional :type, values: ['valid-type1', 'valid-type2', 'valid-type3'], type: Symbol } + subject.params { optional :type, values: %w[valid-type1 valid-type2 valid-type3], type: Symbol } end.to raise_error Grape::Exceptions::IncompatibleOptionValues end @@ -648,9 +652,9 @@ def app end it 'accepts multiple valid values' do - get '/proc', type: ['valid-type1', 'valid-type3'] + get '/proc', type: %w[valid-type1 valid-type3] expect(last_response.status).to eq 200 - expect(last_response.body).to eq({ type: ['valid-type1', 'valid-type3'] }.to_json) + expect(last_response.body).to eq({ type: %w[valid-type1 valid-type3] }.to_json) end it 'rejects a single invalid value' do @@ -660,7 +664,7 @@ def app end it 'rejects an invalid value among valid ones' do - get '/proc', type: ['valid-type1', 'invalid-type1', 'valid-type3'] + get '/proc', type: %w[valid-type1 invalid-type1 valid-type3] expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end diff --git a/spec/grape/validations_spec.rb b/spec/grape/validations_spec.rb index d325634f6..5d6ee7f13 100644 --- a/spec/grape/validations_spec.rb +++ b/spec/grape/validations_spec.rb @@ -494,6 +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.new(params: [@scope.full_name(attr_name)], message: "'from' must be lower or equal to 'to'") end end @@ -887,13 +888,13 @@ def validate_param!(attr_name, params) 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 + * 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 + it 'example based off actual real world use case' do subject.params do requires :orders, type: Array do requires :id, type: Integer @@ -911,17 +912,17 @@ def validate_param!(attr_name, params) data = { orders: [ - { id: 77, drugs: [{batches: [{batch_no: "A1234567"}]}]}, + { 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.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 + it 'simplest example using Array -> Array -> Hash -> String' do subject.params do requires :orders, type: Array do requires :id, type: Integer @@ -937,17 +938,17 @@ def validate_param!(attr_name, params) data = { orders: [ - { id: 77, drugs: [{batch_no: "A1234567"}]}, + { 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.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 + it 'simplest example using Array -> Hash -> String' do subject.params do requires :orders, type: Array do requires :id, type: Integer @@ -963,17 +964,17 @@ def validate_param!(attr_name, params) data = { orders: [ - { id: 77, drugs: {batch_no: "A1234567"}}, + { 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.body).to eq('validate_required_arrays_under_optional_arrays works!') expect(last_response.status).to eq(200) end - it "correctly indexes invalida data" do + it 'correctly indexes invalida data' do subject.params do requires :orders, type: Array do requires :id, type: Integer @@ -991,16 +992,16 @@ def validate_param!(attr_name, params) data = { orders: [ { id: 70 }, - { id: 77, drugs: [{batch_no: "A1234567", quantity: 12}, {batch_no: "B222222"}]} + { 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.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 + context 'multiple levels of optional and requires settings' do before do subject.params do requires :top, type: Array do @@ -1022,53 +1023,62 @@ def validate_param!(attr_name, params) end end - it "with valid data" do + 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}]}]}]}, + { 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}]}]}, + { 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}]}, + { 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.body).to eq('multi_level works!') expect(last_response.status).to eq(200) end - it "with invalid data" do + 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}]}]}]}, + { 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}]}]}, + { 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}]}, + { 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.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 + it 'exactly_one_of' do subject.params do requires :orders, type: Array do requires :id, type: Integer @@ -1086,17 +1096,17 @@ def validate_param!(attr_name, params) data = { orders: [ - { id: 77, drugs: {batch_no: "A1234567"}}, + { 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.body).to eq('exactly_one_of works!') expect(last_response.status).to eq(200) end - it "at_least_one_of" do + it 'at_least_one_of' do subject.params do requires :orders, type: Array do requires :id, type: Integer @@ -1114,17 +1124,17 @@ def validate_param!(attr_name, params) data = { orders: [ - { id: 77, drugs: {batch_no: "A1234567"}}, + { 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.body).to eq('at_least_one_of works!') expect(last_response.status).to eq(200) end - it "all_or_none_of" do + it 'all_or_none_of' do subject.params do requires :orders, type: Array do requires :id, type: Integer @@ -1142,13 +1152,13 @@ def validate_param!(attr_name, params) data = { orders: [ - { id: 77, drugs: {batch_no: "A1234567", batch_id: "12"}}, + { 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.body).to eq('all_or_none_of works!') expect(last_response.status).to eq(200) end end @@ -1177,6 +1187,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.new(params: [@scope.full_name(attr_name)], message: 'is not custom!') end end @@ -1325,6 +1336,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.new(params: [@scope.full_name(attr_name)], message: message) end end diff --git a/spec/shared/versioning_examples.rb b/spec/shared/versioning_examples.rb index ebc0742d4..d7b61dc1a 100644 --- a/spec/shared/versioning_examples.rb +++ b/spec/shared/versioning_examples.rb @@ -70,7 +70,7 @@ subject.version 'v1', macro_options do get 'version' do - 'version ' + request.env['api.version'] + "version #{request.env['api.version']}" end end @@ -94,7 +94,7 @@ subject.version 'v1', macro_options do get 'version' do - 'version ' + request.env['api.version'] + "version #{request.env['api.version']}" end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d0bb66554..ce4e84d31 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -10,7 +10,7 @@ require 'bundler' Bundler.require :default, :test -Dir["#{File.dirname(__FILE__)}/support/*.rb"].each do |file| +Dir["#{File.dirname(__FILE__)}/support/*.rb"].sort.each do |file| require file end diff --git a/spec/support/basic_auth_encode_helpers.rb b/spec/support/basic_auth_encode_helpers.rb index c58acada6..78e21e6c8 100644 --- a/spec/support/basic_auth_encode_helpers.rb +++ b/spec/support/basic_auth_encode_helpers.rb @@ -4,7 +4,7 @@ module Spec module Support module Helpers def encode_basic_auth(username, password) - 'Basic ' + Base64.encode64("#{username}:#{password}") + "Basic #{Base64.encode64("#{username}:#{password}")}" end end end From 23365442fdb9a17e022d7fe5ce3f4c0a315015c4 Mon Sep 17 00:00:00 2001 From: dblock Date: Mon, 4 Oct 2021 07:28:20 -0400 Subject: [PATCH 061/304] Preparing for release, 1.6.0. --- CHANGELOG.md | 4 +--- README.md | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 624f486da..c7109d6b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,8 @@ -### 1.6.0 (Next) +### 1.6.0 (2021/10/04) #### Features * [#2190](https://github.com/ruby-grape/grape/pull/2190): Upgrade dev deps & drop Ruby 2.4.x support - [@dnesteryuk](https://github.com/dnesteryuk). -* Your contribution here. #### Fixes @@ -11,7 +10,6 @@ * [#2177](https://github.com/ruby-grape/grape/pull/2177): Fix: `default` validator fails if preceded by `as` validator - [@Catsuko](https://github.com/Catsuko). * [#2180](https://github.com/ruby-grape/grape/pull/2180): Call `super` in `API.inherited` - [@yogeshjain999](https://github.com/yogeshjain999). * [#2189](https://github.com/ruby-grape/grape/pull/2189): Fix: rename parameters when using `:as` (behaviour and grape-swagger documentation) - [@Jack12816](https://github.com/Jack12816). -* Your contribution here. ### 1.5.3 (2021/03/07) diff --git a/README.md b/README.md index e63323310..d1fff18e3 100644 --- a/README.md +++ b/README.md @@ -158,9 +158,8 @@ content negotiation, versioning and much more. ## Stable Release -You're reading the documentation for the next release of Grape, which should be **1.6.0**. +You're reading the documentation for the stable release of Grape, **1.6.0**. Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. -The current stable release is [1.5.3](https://github.com/ruby-grape/grape/blob/v1.5.3/README.md). ## Project Resources From 54f86a970cf0dc00bdd7b80b015d193666900d79 Mon Sep 17 00:00:00 2001 From: dblock Date: Mon, 4 Oct 2021 07:30:27 -0400 Subject: [PATCH 062/304] Preparing for next developer iteration, 1.6.1. --- CHANGELOG.md | 10 ++++++++++ README.md | 3 ++- lib/grape/version.rb | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7109d6b9..d2177cee7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +### 1.6.1 (Next) + +#### Features + +* Your contribution here. + +#### Fixes + +* Your contribution here. + ### 1.6.0 (2021/10/04) #### Features diff --git a/README.md b/README.md index d1fff18e3..a2b7f3136 100644 --- a/README.md +++ b/README.md @@ -158,8 +158,9 @@ content negotiation, versioning and much more. ## Stable Release -You're reading the documentation for the stable release of Grape, **1.6.0**. +You're reading the documentation for the next release of Grape, which should be **1.6.1**. Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. +The current stable release is [1.6.0](https://github.com/ruby-grape/grape/blob/v1.6.0/README.md). ## Project Resources diff --git a/lib/grape/version.rb b/lib/grape/version.rb index 2fe05f947..653f9e487 100644 --- a/lib/grape/version.rb +++ b/lib/grape/version.rb @@ -2,5 +2,5 @@ module Grape # The current version of Grape. - VERSION = '1.6.0' + VERSION = '1.6.1' end From 1517aac3e90b660dfc1913b1006d5960fbf2f0fa Mon Sep 17 00:00:00 2001 From: Hermann Mayer Date: Mon, 4 Oct 2021 15:42:12 +0200 Subject: [PATCH 063/304] Memoize the result of Grape::Middleware::Base#response. Signed-off-by: Hermann Mayer --- CHANGELOG.md | 1 + lib/grape/middleware/base.rb | 2 +- spec/grape/middleware/base_spec.rb | 16 ++++++++++------ 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2177cee7..9c5cd79ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ #### Fixes +* [#2192](https://github.com/ruby-grape/grape/pull/2192): Memoize the result of Grape::Middleware::Base#response - [@Jack12816](https://github.com/Jack12816). * Your contribution here. ### 1.6.0 (2021/10/04) diff --git a/lib/grape/middleware/base.rb b/lib/grape/middleware/base.rb index 132353a67..5eb998fc0 100644 --- a/lib/grape/middleware/base.rb +++ b/lib/grape/middleware/base.rb @@ -60,7 +60,7 @@ def after; end def response return @app_response if @app_response.is_a?(Rack::Response) - Rack::Response.new(@app_response[2], @app_response[0], @app_response[1]) + @app_response = Rack::Response.new(@app_response[2], @app_response[0], @app_response[1]) end def content_type_for(format) diff --git a/spec/grape/middleware/base_spec.rb b/spec/grape/middleware/base_spec.rb index 02a745e11..57cba82d1 100644 --- a/spec/grape/middleware/base_spec.rb +++ b/spec/grape/middleware/base_spec.rb @@ -77,42 +77,46 @@ describe '#response' do subject { Grape::Middleware::Base.new(response) } + before { subject.call({}) } + context Array do let(:response) { ->(_) { [204, { abc: 1 }, 'test'] } } it 'status' do - subject.call({}) expect(subject.response.status).to eq(204) end it 'body' do - subject.call({}) expect(subject.response.body).to eq(['test']) end it 'header' do - subject.call({}) expect(subject.response.header).to have_key(:abc) end + + it 'returns the memoized Rack::Response instance' do + expect(subject.response).to be(subject.response) + end end context Rack::Response do let(:response) { ->(_) { Rack::Response.new('test', 204, abc: 1) } } it 'status' do - subject.call({}) expect(subject.response.status).to eq(204) end it 'body' do - subject.call({}) expect(subject.response.body).to eq(['test']) end it 'header' do - subject.call({}) expect(subject.response.header).to have_key(:abc) end + + it 'returns the memoized Rack::Response instance' do + expect(subject.response).to be(subject.response) + end end end From 43936ac719ee524fd0f8ccd830d1eb50abd721c7 Mon Sep 17 00:00:00 2001 From: Hermann Mayer Date: Mon, 4 Oct 2021 18:35:45 +0200 Subject: [PATCH 064/304] Fixed the broken ruby-head NoMethodError spec. (#2193) Signed-off-by: Hermann Mayer Co-authored-by: Daniel Doubrovkine (dB.) --- CHANGELOG.md | 1 + spec/grape/api_spec.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c5cd79ec..b6998446b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ #### Fixes +* [#2193](https://github.com/ruby-grape/grape/pull/2193): Fixed the broken ruby-head NoMethodError spec - [@Jack12816](https://github.com/Jack12816). * [#2192](https://github.com/ruby-grape/grape/pull/2192): Memoize the result of Grape::Middleware::Base#response - [@Jack12816](https://github.com/Jack12816). * Your contribution here. diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index e38a8a148..576925e9f 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -2272,7 +2272,7 @@ def rescue_no_method_error subject.rescue_from :all, with: :not_exist_method subject.get('/rescue_method') { raise StandardError } - expect { get '/rescue_method' }.to raise_error(NoMethodError, 'undefined method `not_exist_method\'') + expect { get '/rescue_method' }.to raise_error(NoMethodError, /^undefined method `not_exist_method'/) end it 'correctly chooses exception handler if :all handler is specified' do From 68d62d5deee42dd0838c480b294aa916419b6ac8 Mon Sep 17 00:00:00 2001 From: Georgiy Melnikov Date: Mon, 1 Nov 2021 20:36:37 +0500 Subject: [PATCH 065/304] allow set passwords_hashed option for digest auth --- CHANGELOG.md | 1 + README.md | 8 ++ lib/grape/middleware/auth/dsl.rb | 8 +- spec/grape/middleware/auth/dsl_spec.rb | 19 +++-- spec/grape/middleware/auth/strategies_spec.rb | 80 ++++++++++++++----- 5 files changed, 90 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6998446b..1448c755c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ #### Features +* [#2196](https://github.com/ruby-grape/grape/pull/2196): Add support for `passwords_hashed` param for `digest_auth` - [@lHydra](https://github.com/lhydra). * Your contribution here. #### Fixes diff --git a/README.md b/README.md index a2b7f3136..471659743 100644 --- a/README.md +++ b/README.md @@ -3297,12 +3297,20 @@ http_basic do |username, password| end ``` +Digest auth supports clear-text passwords and password hashes. + ```ruby http_digest({ realm: 'Test Api', opaque: 'app secret' }) do |username| # lookup the user's password here end ``` +```ruby +http_digest(realm: { realm: 'Test Api', opaque: 'app secret', passwords_hashed: true }) do |username| + # lookup the user's password hash here +end +``` + ### Register custom middleware for authentication Grape can use custom Middleware for authentication. How to implement these diff --git a/lib/grape/middleware/auth/dsl.rb b/lib/grape/middleware/auth/dsl.rb index 1b2e8f456..d85171fc5 100644 --- a/lib/grape/middleware/auth/dsl.rb +++ b/lib/grape/middleware/auth/dsl.rb @@ -32,7 +32,13 @@ def http_basic(options = {}, &block) def http_digest(options = {}, &block) options[:realm] ||= 'API Authorization' - options[:opaque] ||= 'secret' + + if options[:realm].respond_to?(:values_at) + options[:realm][:opaque] ||= 'secret' + else + options[:opaque] ||= 'secret' + end + auth :http_digest, options, &block end end diff --git a/spec/grape/middleware/auth/dsl_spec.rb b/spec/grape/middleware/auth/dsl_spec.rb index 41169a3bf..5e694d08e 100644 --- a/spec/grape/middleware/auth/dsl_spec.rb +++ b/spec/grape/middleware/auth/dsl_spec.rb @@ -16,7 +16,7 @@ end describe '.auth' do - it 'stets auth parameters' do + it 'sets auth parameters' do expect(subject.base_instance).to receive(:use).with(Grape::Middleware::Auth::Base, settings) subject.auth :http_digest, realm: settings[:realm], opaque: settings[:opaque], &settings[:proc] @@ -38,16 +38,25 @@ end describe '.http_basic' do - it 'stets auth parameters' do + it 'sets auth parameters' do subject.http_basic realm: 'my_realm', &settings[:proc] expect(subject.auth).to eq(realm: 'my_realm', type: :http_basic, proc: block) end end describe '.http_digest' do - it 'stets auth parameters' do - subject.http_digest realm: 'my_realm', opaque: 'my_opaque', &settings[:proc] - expect(subject.auth).to eq(realm: 'my_realm', type: :http_digest, proc: block, opaque: 'my_opaque') + context 'when realm is a hash' do + it 'sets auth parameters' do + subject.http_digest realm: { realm: 'my_realm', opaque: 'my_opaque' }, &settings[:proc] + expect(subject.auth).to eq(realm: { realm: 'my_realm', opaque: 'my_opaque' }, type: :http_digest, proc: block) + end + end + + context 'when realm is not hash' do + it 'sets auth parameters' do + subject.http_digest realm: 'my_realm', opaque: 'my_opaque', &settings[:proc] + expect(subject.auth).to eq(realm: 'my_realm', type: :http_digest, proc: block, opaque: 'my_opaque') + end end end end diff --git a/spec/grape/middleware/auth/strategies_spec.rb b/spec/grape/middleware/auth/strategies_spec.rb index 954c99631..fcbe9e7bd 100644 --- a/spec/grape/middleware/auth/strategies_spec.rb +++ b/spec/grape/middleware/auth/strategies_spec.rb @@ -42,7 +42,17 @@ def app end module StrategiesSpec - class Test < Grape::API + class PasswordHashed < Grape::API + http_digest(realm: { realm: 'Test Api', opaque: 'secret', passwords_hashed: true }) do |username| + { 'foo' => Digest::MD5.hexdigest(['foo', 'Test Api', 'bar'].join(':')) }[username] + end + + get '/test' do + [{ hey: 'you' }, { there: 'bar' }, { foo: 'baz' }] + end + end + + class PasswordIsNotHashed < Grape::API http_digest(realm: 'Test Api', opaque: 'secret') do |username| { 'foo' => 'bar' }[username] end @@ -53,30 +63,60 @@ class Test < Grape::API end end - def app - StrategiesSpec::Test - end + context 'when password is hashed' do + def app + StrategiesSpec::PasswordHashed + end - it 'is a digest authentication challenge' do - get '/test' - expect(last_response).to be_challenge - end + it 'is a digest authentication challenge' do + get '/test' + expect(last_response).to be_challenge + end - it 'throws a 401 if no auth is given' do - get '/test' - expect(last_response.status).to eq(401) - end + it 'throws a 401 if no auth is given' do + get '/test' + expect(last_response.status).to eq(401) + end - it 'authenticates if given valid creds' do - digest_authorize 'foo', 'bar' - get '/test' - expect(last_response.status).to eq(200) + it 'authenticates if given valid creds' do + digest_authorize 'foo', 'bar' + get '/test' + expect(last_response.status).to eq(200) + end + + it 'throws a 401 if given invalid creds' do + digest_authorize 'bar', 'foo' + get '/test' + expect(last_response.status).to eq(401) + end end - it 'throws a 401 if given invalid creds' do - digest_authorize 'bar', 'foo' - get '/test' - expect(last_response.status).to eq(401) + context 'when password is not hashed' do + def app + StrategiesSpec::PasswordIsNotHashed + end + + it 'is a digest authentication challenge' do + get '/test' + expect(last_response).to be_challenge + end + + it 'throws a 401 if no auth is given' do + get '/test' + expect(last_response.status).to eq(401) + end + + it 'authenticates if given valid creds' do + digest_authorize 'foo', 'bar' + get '/test' + expect(last_response.status).to eq(200) + end + + it 'throws a 401 if given invalid creds' do + digest_authorize 'bar', 'foo' + get '/test' + expect(last_response.status).to eq(401) + end end end end From b0e7c2a6dc53f7b7c1a12b882701fb0ddcef5e4b Mon Sep 17 00:00:00 2001 From: Jacob Herrington Date: Tue, 23 Nov 2021 12:32:49 -0600 Subject: [PATCH 066/304] Update CONTRIBUTING.md Add a note about install appraisal to run tests --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e27368ca6..6b528c523 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,6 +35,7 @@ bundle exec rake Run tests against all supported versions of Rails. ``` +gem install appraisal appraisal install appraisal rake spec ``` From d295f4656ea3cb6b281a0c5066927207594c0cca Mon Sep 17 00:00:00 2001 From: Jacob Herrington Date: Tue, 23 Nov 2021 13:42:45 -0600 Subject: [PATCH 067/304] Expand test coverage of undocumented functionality In an interview recently, I was given the Grape repository sans the content of this method as a technical screening question. The task was to discover what had been removed from the project and re-implement the missing code using only the test suite. It was actually fairly difficult to re-implement this method because there are a couple of untested responsibilities. I think this is a fairly integral component of the framework, so it makes sense to fully test its functionality. --- lib/grape/dsl/headers.rb | 7 ++++-- spec/grape/dsl/headers_spec.rb | 44 +++++++++++++++++++++++++++------- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/lib/grape/dsl/headers.rb b/lib/grape/dsl/headers.rb index c3c7bc3e8..b84c4efe4 100644 --- a/lib/grape/dsl/headers.rb +++ b/lib/grape/dsl/headers.rb @@ -3,8 +3,11 @@ module Grape module DSL module Headers - # Set an individual header or retrieve - # all headers that have been set. + # This method has four responsibilities: + # 1. Set a specifc header value by key + # 2. Retrieve a specifc header value by key + # 3. Retrieve all headers that have been set + # 4. Delete a specifc header key-value pair def header(key = nil, val = nil) if key val ? header[key.to_s] = val : header.delete(key.to_s) diff --git a/spec/grape/dsl/headers_spec.rb b/spec/grape/dsl/headers_spec.rb index d23652d07..d96c9be0e 100644 --- a/spec/grape/dsl/headers_spec.rb +++ b/spec/grape/dsl/headers_spec.rb @@ -11,22 +11,48 @@ class Dummy end describe Headers do subject { HeadersSpec::Dummy.new } + let(:header_data) do + { 'First Key' => 'First Value', + 'Second Key' => 'Second Value' } + end - describe '#header' do - describe 'set' do + context 'when headers are set' do + describe '#header' do before do - subject.header 'Name', 'Value' + header_data.each { |k, v| subject.header(k, v) } end + describe 'get' do + it 'returns a specifc value' do + expect(subject.header['First Key']).to eq 'First Value' + expect(subject.header['Second Key']).to eq 'Second Value' + end - it 'returns value' do - expect(subject.header['Name']).to eq 'Value' - expect(subject.header('Name')).to eq 'Value' + it 'returns all set headers' do + expect(subject.header).to eq header_data + expect(subject.headers).to eq header_data + end + end + describe 'set' do + it 'returns value' do + expect(subject.header('Third Key', 'Third Value')) + expect(subject.header['Third Key']).to eq 'Third Value' + end + end + describe 'delete' do + it 'deletes a header key-value pair' do + expect(subject.header('First Key')).to eq header_data['First Key'] + expect(subject.header).not_to have_key('First Key') + end end end + end - it 'returns nil' do - expect(subject.header['Name']).to be nil - expect(subject.header('Name')).to be nil + context 'when no headers are set' do + describe '#header' do + it 'returns nil' do + expect(subject.header['First Key']).to be nil + expect(subject.header('First Key')).to be nil + end end end end From 937b2fc8a7081c5eb74ff8a4a760e30ebabbd780 Mon Sep 17 00:00:00 2001 From: eproulx Date: Sun, 5 Dec 2021 13:13:05 +0100 Subject: [PATCH 068/304] Add validators module to all validators Move Boolean to Api Class Add test_prof for let_it_be Refactor validators spec without LeakyConstantDeclaration https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/LeakyConstantDeclaration --- Gemfile | 1 + lib/grape/api.rb | 12 + lib/grape/validations.rb | 6 + .../validations/validators/all_or_none.rb | 12 +- .../validations/validators/allow_blank.rb | 16 +- lib/grape/validations/validators/as.rb | 12 +- .../validations/validators/at_least_one_of.rb | 10 +- lib/grape/validations/validators/base.rb | 144 ++++---- lib/grape/validations/validators/coerce.rb | 138 ++++--- lib/grape/validations/validators/default.rb | 70 ++-- .../validations/validators/exactly_one_of.rb | 14 +- .../validations/validators/except_values.rb | 24 +- .../validators/multiple_params_base.rb | 46 +-- .../validators/mutual_exclusion.rb | 12 +- lib/grape/validations/validators/presence.rb | 10 +- lib/grape/validations/validators/regexp.rb | 12 +- lib/grape/validations/validators/same_as.rb | 32 +- lib/grape/validations/validators/values.rb | 116 +++--- spec/grape/api/custom_validations_spec.rb | 96 +++-- .../validations/instance_behaivour_spec.rb | 2 +- .../validators/all_or_none_spec.rb | 106 +++--- .../validators/allow_blank_spec.rb | 274 +++++++------- .../validators/at_least_one_of_spec.rb | 106 +++--- .../validations/validators/coerce_spec.rb | 20 +- .../validations/validators/default_spec.rb | 150 ++++---- .../validators/exactly_one_of_spec.rb | 148 ++++---- .../validators/except_values_spec.rb | 2 +- .../validators/mutual_exclusion_spec.rb | 148 ++++---- .../validations/validators/presence_spec.rb | 3 +- .../validations/validators/regexp_spec.rb | 56 ++- .../validations/validators/same_as_spec.rb | 34 +- .../validations/validators/values_spec.rb | 343 +++++++++--------- spec/grape/validations_spec.rb | 44 ++- spec/spec_helper.rb | 9 + 34 files changed, 1137 insertions(+), 1091 deletions(-) diff --git a/Gemfile b/Gemfile index 383f71ba0..fded526d1 100644 --- a/Gemfile +++ b/Gemfile @@ -34,6 +34,7 @@ group :test do gem 'rack-test', '~> 1.1.0' gem 'rspec', '~> 3.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false + gem 'test-prof', require: false end platforms :jruby do diff --git a/lib/grape/api.rb b/lib/grape/api.rb index 8e4fbbe1d..e597f257b 100644 --- a/lib/grape/api.rb +++ b/lib/grape/api.rb @@ -10,6 +10,18 @@ class 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 Boolean + def self.build(val) + return nil if val != true && val != false + + new + end + end + + class Instance + Boolean = Grape::API::Boolean + end + class << self attr_accessor :base_instance, :instances diff --git a/lib/grape/validations.rb b/lib/grape/validations.rb index bd55c0611..c0736ef22 100644 --- a/lib/grape/validations.rb +++ b/lib/grape/validations.rb @@ -1,5 +1,11 @@ # frozen_string_literal: true +require 'grape/validations/attributes_iterator' +require 'grape/validations/single_attribute_iterator' +require 'grape/validations/multiple_attributes_iterator' +require 'grape/validations/params_scope' +require 'grape/validations/types' + module Grape # Registry to store and locate known Validators. module Validations diff --git a/lib/grape/validations/validators/all_or_none.rb b/lib/grape/validations/validators/all_or_none.rb index 385e8ea82..24dc4f8b6 100644 --- a/lib/grape/validations/validators/all_or_none.rb +++ b/lib/grape/validations/validators/all_or_none.rb @@ -4,12 +4,14 @@ module Grape module Validations - class AllOrNoneOfValidator < MultipleParamsBase - def validate_params!(params) - keys = keys_in_common(params) - return if keys.empty? || keys.length == all_keys.length + module Validators + class AllOrNoneOfValidator < MultipleParamsBase + def validate_params!(params) + keys = keys_in_common(params) + return if keys.empty? || keys.length == all_keys.length - raise Grape::Exceptions::Validation.new(params: all_keys, message: message(:all_or_none)) + raise Grape::Exceptions::Validation.new(params: all_keys, message: message(:all_or_none)) + end end end end diff --git a/lib/grape/validations/validators/allow_blank.rb b/lib/grape/validations/validators/allow_blank.rb index e212c273c..c35753ed3 100644 --- a/lib/grape/validations/validators/allow_blank.rb +++ b/lib/grape/validations/validators/allow_blank.rb @@ -2,16 +2,18 @@ module Grape module Validations - class AllowBlankValidator < Base - def validate_param!(attr_name, params) - return if (options_key?(:value) ? @option[:value] : @option) || !params.is_a?(Hash) + module Validators + class AllowBlankValidator < Base + def validate_param!(attr_name, params) + return if (options_key?(:value) ? @option[:value] : @option) || !params.is_a?(Hash) - value = params[attr_name] - value = value.strip if value.respond_to?(:strip) + value = params[attr_name] + value = value.strip if value.respond_to?(:strip) - return if value == false || value.present? + return if value == false || value.present? - raise Grape::Exceptions::Validation.new(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 end diff --git a/lib/grape/validations/validators/as.rb b/lib/grape/validations/validators/as.rb index 78f8e592e..8a3d8db16 100644 --- a/lib/grape/validations/validators/as.rb +++ b/lib/grape/validations/validators/as.rb @@ -2,11 +2,13 @@ module Grape module Validations - class AsValidator < Base - # We use a validator for renaming parameters. This is just a marker for - # the parameter scope to handle the renaming. No actual validation - # happens here. - def validate_param!(*); end + module Validators + class AsValidator < Base + # We use a validator for renaming parameters. This is just a marker for + # the parameter scope to handle the renaming. No actual validation + # happens here. + def validate_param!(*); end + end end end end diff --git a/lib/grape/validations/validators/at_least_one_of.rb b/lib/grape/validations/validators/at_least_one_of.rb index 61e1d30c7..6fedbef46 100644 --- a/lib/grape/validations/validators/at_least_one_of.rb +++ b/lib/grape/validations/validators/at_least_one_of.rb @@ -4,11 +4,13 @@ module Grape module Validations - class AtLeastOneOfValidator < MultipleParamsBase - def validate_params!(params) - return unless keys_in_common(params).empty? + module Validators + class AtLeastOneOfValidator < MultipleParamsBase + def validate_params!(params) + return unless keys_in_common(params).empty? - raise Grape::Exceptions::Validation.new(params: all_keys, message: message(:at_least_one)) + raise Grape::Exceptions::Validation.new(params: all_keys, message: message(:at_least_one)) + end end end end diff --git a/lib/grape/validations/validators/base.rb b/lib/grape/validations/validators/base.rb index 40fce8d07..aaa06dc37 100644 --- a/lib/grape/validations/validators/base.rb +++ b/lib/grape/validations/validators/base.rb @@ -2,91 +2,93 @@ module Grape module Validations - class Base - attr_reader :attrs + module Validators + class Base + attr_reader :attrs - # Creates a new Validator from options specified - # by a +requires+ or +optional+ directive during - # parameter definition. - # @param attrs [Array] names of attributes to which the Validator applies - # @param options [Object] implementation-dependent Validator options - # @param required [Boolean] attribute(s) are required or optional - # @param scope [ParamsScope] parent scope for this Validator - # @param opts [Array] additional validation options - def initialize(attrs, options, required, scope, *opts) - @attrs = Array(attrs) - @option = options - @required = required - @scope = scope - opts = opts.any? ? opts.shift : {} - @fail_fast = opts.fetch(:fail_fast, false) - @allow_blank = opts.fetch(:allow_blank, false) - end + # Creates a new Validator from options specified + # by a +requires+ or +optional+ directive during + # parameter definition. + # @param attrs [Array] names of attributes to which the Validator applies + # @param options [Object] implementation-dependent Validator options + # @param required [Boolean] attribute(s) are required or optional + # @param scope [ParamsScope] parent scope for this Validator + # @param opts [Array] additional validation options + def initialize(attrs, options, required, scope, *opts) + @attrs = Array(attrs) + @option = options + @required = required + @scope = scope + opts = opts.any? ? opts.shift : {} + @fail_fast = opts.fetch(:fail_fast, false) + @allow_blank = opts.fetch(:allow_blank, false) + end - # Validates a given request. - # @note Override #validate! unless you need to access the entire request. - # @param request [Grape::Request] the request currently being handled - # @raise [Grape::Exceptions::Validation] if validation failed - # @return [void] - def validate(request) - return unless @scope.should_validate?(request.params) + # Validates a given request. + # @note Override #validate! unless you need to access the entire request. + # @param request [Grape::Request] the request currently being handled + # @raise [Grape::Exceptions::Validation] if validation failed + # @return [void] + def validate(request) + return unless @scope.should_validate?(request.params) - validate!(request.params) - end + validate!(request.params) + end - # Validates a given parameter hash. - # @note Override #validate if you need to access the entire request. - # @param params [Hash] parameters to validate - # @raise [Grape::Exceptions::Validation] if validation failed - # @return [void] - def validate!(params) - attributes = SingleAttributeIterator.new(self, @scope, params) - # we collect errors inside array because - # there may be more than one error per field - array_errors = [] + # Validates a given parameter hash. + # @note Override #validate if you need to access the entire request. + # @param params [Hash] parameters to validate + # @raise [Grape::Exceptions::Validation] if validation failed + # @return [void] + def validate!(params) + attributes = SingleAttributeIterator.new(self, @scope, params) + # we collect errors inside array because + # there may be more than one error per field + array_errors = [] - attributes.each do |val, attr_name, empty_val, skip_value| - next if skip_value - next if !@scope.required? && empty_val - next unless @scope.meets_dependency?(val, 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, val) if @required || (val.respond_to?(:key?) && val.key?(attr_name)) - rescue Grape::Exceptions::Validation => e - array_errors << e + begin + validate_param!(attr_name, val) if @required || (val.respond_to?(:key?) && val.key?(attr_name)) + rescue Grape::Exceptions::Validation => e + array_errors << e + end end - end - raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors.any? - end + raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors.any? + end - def self.convert_to_short_name(klass) - ret = klass.name.gsub(/::/, '/') - 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 + def self.convert_to_short_name(klass) + ret = klass.name.gsub(/::/, '/') + 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 - def self.inherited(klass) - return unless klass.name.present? + def self.inherited(klass) + return unless klass.name.present? - Validations.register_validator(convert_to_short_name(klass), klass) - end + Validations.register_validator(convert_to_short_name(klass), klass) + end - def message(default_key = nil) - options = instance_variable_get(:@option) - options_key?(:message) ? options[:message] : default_key - end + def message(default_key = nil) + options = instance_variable_get(:@option) + options_key?(:message) ? options[:message] : default_key + end - def options_key?(key, options = nil) - options = instance_variable_get(:@option) if options.nil? - options.respond_to?(:key?) && options.key?(key) && !options[key].nil? - end + def options_key?(key, options = nil) + options = instance_variable_get(:@option) if options.nil? + options.respond_to?(:key?) && options.key?(key) && !options[key].nil? + end - def fail_fast? - @fail_fast + def fail_fast? + @fail_fast + end end end end diff --git a/lib/grape/validations/validators/coerce.rb b/lib/grape/validations/validators/coerce.rb index edbc6a06d..979ad47c6 100644 --- a/lib/grape/validations/validators/coerce.rb +++ b/lib/grape/validations/validators/coerce.rb @@ -1,86 +1,74 @@ # frozen_string_literal: true module Grape - class API - 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(attrs, options, required, scope, **opts) - super - - @converter = if type.is_a?(Grape::Validations::Types::VariantCollectionCoercer) - type - else - Types.build_coercer(type, method: @option[:method]) - end - end - - def validate_param!(attr_name, params) - raise validation_exception(attr_name) unless params.is_a? Hash - - new_value = coerce_value(params[attr_name]) - - raise validation_exception(attr_name, new_value.message) unless valid_type?(new_value) - - # Don't assign a value if it is identical. It fixes a problem with Hashie::Mash - # which looses wrappers for hashes and arrays after reassigning values + module Validators + class CoerceValidator < Base + def initialize(attrs, options, required, scope, **opts) + super + + @converter = if type.is_a?(Grape::Validations::Types::VariantCollectionCoercer) + type + else + Types.build_coercer(type, method: @option[:method]) + end + end + + def validate_param!(attr_name, params) + raise validation_exception(attr_name) unless params.is_a? Hash + + new_value = coerce_value(params[attr_name]) + + raise validation_exception(attr_name, new_value.message) unless valid_type?(new_value) + + # Don't assign a value if it is identical. It fixes a problem with Hashie::Mash + # which looses wrappers for hashes and arrays after reassigning values + # + # h = Hashie::Mash.new(list: [1, 2, 3, 4]) + # => #> + # list = h.list + # h[:list] = list + # h + # => # + return if params[attr_name].instance_of?(new_value.class) && params[attr_name] == new_value + + params[attr_name] = new_value + end + + private + + # @!attribute [r] converter + # Object that will be used for parameter coercion and type checking. # - # h = Hashie::Mash.new(list: [1, 2, 3, 4]) - # => #> - # list = h.list - # h[:list] = list - # h - # => # - return if params[attr_name].instance_of?(new_value.class) && params[attr_name] == new_value - - params[attr_name] = new_value - end - - private - - # @!attribute [r] converter - # Object that will be used for parameter coercion and type checking. - # - # See {Types.build_coercer} - # - # @return [Object] - attr_reader :converter - - def valid_type?(val) - !val.is_a?(Types::InvalidValue) - end + # See {Types.build_coercer} + # + # @return [Object] + attr_reader :converter - def coerce_value(val) - converter.call(val) - # Some custom types might fail, so it should be treated as an invalid value - rescue StandardError - Types::InvalidValue.new - end + def valid_type?(val) + !val.is_a?(Types::InvalidValue) + end - # Type to which the parameter will be coerced. - # - # @return [Class] - def type - @option[:type].is_a?(Hash) ? @option[:type][:value] : @option[:type] - end + def coerce_value(val) + converter.call(val) + # Some custom types might fail, so it should be treated as an invalid value + rescue StandardError + Types::InvalidValue.new + end - def validation_exception(attr_name, custom_msg = nil) - Grape::Exceptions::Validation.new( - params: [@scope.full_name(attr_name)], - message: custom_msg || message(:coerce) - ) + # Type to which the parameter will be coerced. + # + # @return [Class] + def type + @option[:type].is_a?(Hash) ? @option[:type][:value] : @option[:type] + end + + def validation_exception(attr_name, custom_msg = nil) + Grape::Exceptions::Validation.new( + params: [@scope.full_name(attr_name)], + message: custom_msg || message(:coerce) + ) + end end end end diff --git a/lib/grape/validations/validators/default.rb b/lib/grape/validations/validators/default.rb index 5f004d685..8ed593675 100644 --- a/lib/grape/validations/validators/default.rb +++ b/lib/grape/validations/validators/default.rb @@ -2,47 +2,49 @@ module Grape module Validations - class DefaultValidator < Base - def initialize(attrs, options, required, scope, **opts) - @default = options - super - end + module Validators + class DefaultValidator < Base + def initialize(attrs, options, required, scope, **opts) + @default = options + super + end - def validate_param!(attr_name, params) - params[attr_name] = if @default.is_a? Proc - @default.call - elsif @default.frozen? || !duplicatable?(@default) - @default - else - duplicate(@default) - end - end + def validate_param!(attr_name, params) + params[attr_name] = if @default.is_a? Proc + @default.call + elsif @default.frozen? || !duplicatable?(@default) + @default + else + duplicate(@default) + end + end - def validate!(params) - attrs = SingleAttributeIterator.new(self, @scope, params) - attrs.each do |resource_params, attr_name| - next unless @scope.meets_dependency?(resource_params, params) + def validate!(params) + attrs = SingleAttributeIterator.new(self, @scope, params) + attrs.each do |resource_params, attr_name| + next unless @scope.meets_dependency?(resource_params, params) - validate_param!(attr_name, resource_params) if resource_params.is_a?(Hash) && resource_params[attr_name].nil? + validate_param!(attr_name, resource_params) if resource_params.is_a?(Hash) && resource_params[attr_name].nil? + end end - end - private + private - # return true if we might be able to dup this object - def duplicatable?(obj) - !obj.nil? && - obj != true && - obj != false && - !obj.is_a?(Symbol) && - !obj.is_a?(Numeric) - end + # return true if we might be able to dup this object + def duplicatable?(obj) + !obj.nil? && + obj != true && + obj != false && + !obj.is_a?(Symbol) && + !obj.is_a?(Numeric) + end - # make a best effort to dup the object - def duplicate(obj) - obj.dup - rescue TypeError - obj + # make a best effort to dup the object + def duplicate(obj) + obj.dup + rescue TypeError + obj + end end end end diff --git a/lib/grape/validations/validators/exactly_one_of.rb b/lib/grape/validations/validators/exactly_one_of.rb index 2a6f95614..84d6142fb 100644 --- a/lib/grape/validations/validators/exactly_one_of.rb +++ b/lib/grape/validations/validators/exactly_one_of.rb @@ -4,13 +4,15 @@ 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? + module Validators + class ExactlyOneOfValidator < MultipleParamsBase + def validate_params!(params) + keys = keys_in_common(params) + return if keys.length == 1 + raise Grape::Exceptions::Validation.new(params: all_keys, message: message(:exactly_one)) if keys.length.zero? - raise Grape::Exceptions::Validation.new(params: keys, message: message(:mutual_exclusion)) + raise Grape::Exceptions::Validation.new(params: keys, message: message(:mutual_exclusion)) + end end end end diff --git a/lib/grape/validations/validators/except_values.rb b/lib/grape/validations/validators/except_values.rb index 5ba1e306b..b125c12cf 100644 --- a/lib/grape/validations/validators/except_values.rb +++ b/lib/grape/validations/validators/except_values.rb @@ -2,20 +2,22 @@ module Grape module Validations - class ExceptValuesValidator < Base - def initialize(attrs, options, required, scope, **opts) - @except = options.is_a?(Hash) ? options[:value] : options - super - end + module Validators + class ExceptValuesValidator < Base + def initialize(attrs, options, required, scope, **opts) + @except = options.is_a?(Hash) ? options[:value] : options + super + end - def validate_param!(attr_name, params) - return unless params.respond_to?(:key?) && params.key?(attr_name) + def validate_param!(attr_name, params) + return unless params.respond_to?(:key?) && params.key?(attr_name) - excepts = @except.is_a?(Proc) ? @except.call : @except - return if excepts.nil? + excepts = @except.is_a?(Proc) ? @except.call : @except + return if excepts.nil? - param_array = params[attr_name].nil? ? [nil] : Array.wrap(params[attr_name]) - raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:except_values)) if param_array.any? { |param| excepts.include?(param) } + param_array = params[attr_name].nil? ? [nil] : Array.wrap(params[attr_name]) + raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:except_values)) if param_array.any? { |param| excepts.include?(param) } + end end end end diff --git a/lib/grape/validations/validators/multiple_params_base.rb b/lib/grape/validations/validators/multiple_params_base.rb index 1a3f44bd2..c0b02ac50 100644 --- a/lib/grape/validations/validators/multiple_params_base.rb +++ b/lib/grape/validations/validators/multiple_params_base.rb @@ -2,34 +2,36 @@ module Grape module Validations - class MultipleParamsBase < Base - def validate!(params) - attributes = MultipleAttributesIterator.new(self, @scope, params) - array_errors = [] - - attributes.each do |resource_params, skip_value| - next if skip_value - - begin - validate_params!(resource_params) - rescue Grape::Exceptions::Validation => e - array_errors << e + module Validators + class MultipleParamsBase < Base + def validate!(params) + attributes = MultipleAttributesIterator.new(self, @scope, params) + array_errors = [] + + 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 - end - raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors.any? - end + raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors.any? + end - private + private - def keys_in_common(resource_params) - return [] unless resource_params.is_a?(Hash) + def keys_in_common(resource_params) + return [] unless resource_params.is_a?(Hash) - all_keys & resource_params.keys.map! { |attr| @scope.full_name(attr) } - end + all_keys & resource_params.keys.map! { |attr| @scope.full_name(attr) } + end - def all_keys - attrs.map { |attr| @scope.full_name(attr) } + def all_keys + attrs.map { |attr| @scope.full_name(attr) } + end end end end diff --git a/lib/grape/validations/validators/mutual_exclusion.rb b/lib/grape/validations/validators/mutual_exclusion.rb index e2817bb1d..e0f49278b 100644 --- a/lib/grape/validations/validators/mutual_exclusion.rb +++ b/lib/grape/validations/validators/mutual_exclusion.rb @@ -4,12 +4,14 @@ module Grape module Validations - class MutualExclusionValidator < MultipleParamsBase - def validate_params!(params) - keys = keys_in_common(params) - return if keys.length <= 1 + module Validators + class MutualExclusionValidator < MultipleParamsBase + def validate_params!(params) + keys = keys_in_common(params) + return if keys.length <= 1 - raise Grape::Exceptions::Validation.new(params: keys, message: message(:mutual_exclusion)) + raise Grape::Exceptions::Validation.new(params: keys, message: message(:mutual_exclusion)) + end end end end diff --git a/lib/grape/validations/validators/presence.rb b/lib/grape/validations/validators/presence.rb index a75d3d0d6..ae31dc3fb 100644 --- a/lib/grape/validations/validators/presence.rb +++ b/lib/grape/validations/validators/presence.rb @@ -2,11 +2,13 @@ module Grape module Validations - class PresenceValidator < Base - def validate_param!(attr_name, params) - return if params.respond_to?(:key?) && params.key?(attr_name) + module Validators + class PresenceValidator < Base + def validate_param!(attr_name, params) + return if params.respond_to?(:key?) && params.key?(attr_name) - raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:presence)) + raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:presence)) + end end end end diff --git a/lib/grape/validations/validators/regexp.rb b/lib/grape/validations/validators/regexp.rb index e796d3d57..ce7af87b6 100644 --- a/lib/grape/validations/validators/regexp.rb +++ b/lib/grape/validations/validators/regexp.rb @@ -2,12 +2,14 @@ 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.match?((options_key?(:value) ? @option[:value] : @option)) } + module Validators + 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.match?((options_key?(:value) ? @option[:value] : @option)) } - raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:regexp)) + raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:regexp)) + end end end end diff --git a/lib/grape/validations/validators/same_as.rb b/lib/grape/validations/validators/same_as.rb index ae98617c6..5a65afa60 100644 --- a/lib/grape/validations/validators/same_as.rb +++ b/lib/grape/validations/validators/same_as.rb @@ -2,24 +2,26 @@ 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] + module Validators + class SameAsValidator < Base + def validate_param!(attr_name, params) + confirmation = options_key?(:value) ? @option[:value] : @option + return if params[attr_name] == params[confirmation] - raise Grape::Exceptions::Validation.new( - params: [@scope.full_name(attr_name)], - message: build_message - ) - end + raise Grape::Exceptions::Validation.new( + params: [@scope.full_name(attr_name)], + message: build_message + ) + end - private + private - def build_message - if options_key?(:message) - @option[:message] - else - format I18n.t(:same_as, scope: 'grape.errors.messages'), parameter: @option + 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 diff --git a/lib/grape/validations/validators/values.rb b/lib/grape/validations/validators/values.rb index b72ffd4c3..b8b05fb6b 100644 --- a/lib/grape/validations/validators/values.rb +++ b/lib/grape/validations/validators/values.rb @@ -2,84 +2,86 @@ module Grape module Validations - class ValuesValidator < Base - def initialize(attrs, options, required, scope, **opts) - if options.is_a?(Hash) - @excepts = options[:except] - @values = options[:value] - @proc = options[:proc] - - warn '[DEPRECATION] The values validator except option is deprecated. ' \ - 'Use the except validator instead.' if @excepts - - raise ArgumentError, 'proc must be a Proc' if @proc && !@proc.is_a?(Proc) - - warn '[DEPRECATION] The values validator proc option is deprecated. ' \ - 'The lambda expression can now be assigned directly to values.' if @proc - else - @excepts = nil - @values = nil - @proc = nil - @values = options + module Validators + class ValuesValidator < Base + def initialize(attrs, options, required, scope, **opts) + if options.is_a?(Hash) + @excepts = options[:except] + @values = options[:value] + @proc = options[:proc] + + warn '[DEPRECATION] The values validator except option is deprecated. ' \ + 'Use the except validator instead.' if @excepts + + raise ArgumentError, 'proc must be a Proc' if @proc && !@proc.is_a?(Proc) + + warn '[DEPRECATION] The values validator proc option is deprecated. ' \ + 'The lambda expression can now be assigned directly to values.' if @proc + else + @excepts = nil + @values = nil + @proc = nil + @values = options + end + super end - super - end - def validate_param!(attr_name, params) - return unless params.is_a?(Hash) + def validate_param!(attr_name, params) + return unless params.is_a?(Hash) - val = params[attr_name] + val = params[attr_name] - return if val.nil? && !required_for_root_scope? + return if val.nil? && !required_for_root_scope? - # don't forget that +false.blank?+ is true - return if val != false && val.blank? && @allow_blank + # don't forget that +false.blank?+ is true + return if val != false && val.blank? && @allow_blank - param_array = val.nil? ? [nil] : Array.wrap(val) + param_array = val.nil? ? [nil] : Array.wrap(val) - raise validation_exception(attr_name, except_message) \ + raise validation_exception(attr_name, except_message) \ unless check_excepts(param_array) - raise validation_exception(attr_name, message(:values)) \ + raise validation_exception(attr_name, message(:values)) \ unless check_values(param_array, attr_name) - raise validation_exception(attr_name, message(:values)) \ + raise validation_exception(attr_name, message(:values)) \ if @proc && !param_array.all? { |param| @proc.call(param) } - end + end - private + private - def check_values(param_array, attr_name) - values = @values.is_a?(Proc) && @values.arity.zero? ? @values.call : @values - return true if values.nil? + def check_values(param_array, attr_name) + values = @values.is_a?(Proc) && @values.arity.zero? ? @values.call : @values + return true if values.nil? - begin - return param_array.all? { |param| values.call(param) } if values.is_a? Proc - rescue StandardError => e - warn "Error '#{e}' raised while validating attribute '#{attr_name}'" - return false + begin + return param_array.all? { |param| values.call(param) } if values.is_a? Proc + rescue StandardError => e + warn "Error '#{e}' raised while validating attribute '#{attr_name}'" + return false + end + param_array.all? { |param| values.include?(param) } end - param_array.all? { |param| values.include?(param) } - end - def check_excepts(param_array) - excepts = @excepts.is_a?(Proc) ? @excepts.call : @excepts - return true if excepts.nil? + def check_excepts(param_array) + excepts = @excepts.is_a?(Proc) ? @excepts.call : @excepts + return true if excepts.nil? - param_array.none? { |param| excepts.include?(param) } - end + param_array.none? { |param| excepts.include?(param) } + end - def except_message - options = instance_variable_get(:@option) - options_key?(:except_message) ? options[:except_message] : message(:except_values) - end + def except_message + options = instance_variable_get(:@option) + options_key?(:except_message) ? options[:except_message] : message(:except_values) + end - def required_for_root_scope? - @required && @scope.root? - end + 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) + 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/spec/grape/api/custom_validations_spec.rb b/spec/grape/api/custom_validations_spec.rb index 8a5b1ec5c..f9000ec65 100644 --- a/spec/grape/api/custom_validations_spec.rb +++ b/spec/grape/api/custom_validations_spec.rb @@ -4,18 +4,25 @@ describe Grape::Validations do context 'using a custom length validator' do - before do - module CustomValidationsSpec - 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.new(params: [@scope.full_name(attr_name)], message: "must be at the most #{@option} characters long") - end + let(:default_length_validator) do + Class.new(Grape::Validations::Validators::Base) do + def validate_param!(attr_name, params) + @option = params[:max].to_i if params.key?(:max) + return if params[attr_name].length <= @option + + raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: "must be at the most #{@option} characters long") end end end + + before do + Grape::Validations.register_validator('default_length', default_length_validator) + end + + after do + Grape::Validations.deregister_validator('default_length') + end + subject do Class.new(Grape::API) do params do @@ -49,15 +56,22 @@ def app end context 'using a custom body-only validator' do - before do - module CustomValidationsSpec - class InBody < Grape::Validations::PresenceValidator - def validate(request) - validate!(request.env['api.request.body']) - end + let(:in_body_validator) do + Class.new(Grape::Validations::Validators::PresenceValidator) do + def validate(request) + validate!(request.env['api.request.body']) end end end + + before do + Grape::Validations.register_validator('in_body', in_body_validator) + end + + after do + Grape::Validations.deregister_validator('in_body') + end + subject do Class.new(Grape::API) do params do @@ -86,15 +100,22 @@ def app end context 'using a custom validator with message_key' do - before do - module CustomValidationsSpec - class WithMessageKey < Grape::Validations::PresenceValidator - def validate_param!(attr_name, _params) - raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: :presence) - end + let(:message_key_validator) do + Class.new(Grape::Validations::Validators::PresenceValidator) do + def validate_param!(attr_name, _params) + raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: :presence) end end end + + before do + Grape::Validations.register_validator('with_message_key', message_key_validator) + end + + after do + Grape::Validations.deregister_validator('with_message_key') + end + subject do Class.new(Grape::API) do params do @@ -118,22 +139,29 @@ def app end context 'using a custom request/param validator' do - before do - module CustomValidationsSpec - class Admin < Grape::Validations::Base - def validate(request) - # return if the param we are checking was not in request - # @attrs is a list containing the attribute we are currently validating - return unless request.params.key? @attrs.first - # check if admin flag is set to true - return unless @option - # check if user is admin or not - # as an example get a token from request and check if it's admin or not - raise Grape::Exceptions::Validation.new(params: @attrs, message: 'Can not set Admin only field.') unless request.headers['X-Access-Token'] == 'admin' - end + let(:admin_validator) do + Class.new(Grape::Validations::Validators::Base) do + def validate(request) + # return if the param we are checking was not in request + # @attrs is a list containing the attribute we are currently validating + return unless request.params.key? @attrs.first + # check if admin flag is set to true + return unless @option + # check if user is admin or not + # as an example get a token from request and check if it's admin or not + raise Grape::Exceptions::Validation.new(params: @attrs, message: 'Can not set Admin only field.') unless request.headers['X-Access-Token'] == 'admin' end end end + + before do + Grape::Validations.register_validator('admin', admin_validator) + end + + after do + Grape::Validations.deregister_validator('admin') + end + subject do Class.new(Grape::API) do params do diff --git a/spec/grape/validations/instance_behaivour_spec.rb b/spec/grape/validations/instance_behaivour_spec.rb index 9f2038dce..f6ba28c94 100644 --- a/spec/grape/validations/instance_behaivour_spec.rb +++ b/spec/grape/validations/instance_behaivour_spec.rb @@ -4,7 +4,7 @@ describe 'Validator with instance variables' do let(:validator_type) do - Class.new(Grape::Validations::Base) do + Class.new(Grape::Validations::Validators::Base) do def validate_param!(_attr_name, _params) if instance_variable_defined?(:@instance_variable) && @instance_variable raise Grape::Exceptions::Validation.new(params: ['params'], diff --git a/spec/grape/validations/validators/all_or_none_spec.rb b/spec/grape/validations/validators/all_or_none_spec.rb index ce4124946..4623d88c6 100644 --- a/spec/grape/validations/validators/all_or_none_spec.rb +++ b/spec/grape/validations/validators/all_or_none_spec.rb @@ -2,73 +2,67 @@ require 'spec_helper' -describe Grape::Validations::AllOrNoneOfValidator do - describe '#validate!' do - 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 +describe Grape::Validations::Validators::AllOrNoneOfValidator do + let_it_be(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end - params do - optional :beer, :wine, type: Boolean - all_or_none_of :beer, :wine - end - post do - end + params do + optional :beer, :wine, type: Grape::API::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, :other, type: Grape::API::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 + optional :beer, :wine, type: Grape::API::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 + params do + requires :item, type: Hash do + optional :beer, :wine, type: Grape::API::Boolean + all_or_none_of :beer, :wine + end + end + post '/nested-hash' do + end - 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 + optional :beer, :wine, type: Grape::API::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 + params do + requires :items, type: Array do + requires :nested_items, type: Array do + optional :beer, :wine, type: Grape::API::Boolean + all_or_none_of :beer, :wine end end end + post '/deeply-nested-array' do + end end + end - def app - ValidationsSpec::AllOrNoneOfValidatorSpec::API - end + describe '#validate!' do + subject(:validate) { post path, params } context 'when all restricted params are present' do let(:path) { '/' } diff --git a/spec/grape/validations/validators/allow_blank_spec.rb b/spec/grape/validations/validators/allow_blank_spec.rb index cb74e332c..bab81a26c 100644 --- a/spec/grape/validations/validators/allow_blank_spec.rb +++ b/spec/grape/validations/validators/allow_blank_spec.rb @@ -2,24 +2,139 @@ require 'spec_helper' -describe Grape::Validations::AllowBlankValidator do - module ValidationsSpec - module AllowBlankValidatorSpec - class API < Grape::API - default_format :json +describe Grape::Validations::Validators::AllowBlankValidator do + let_it_be(:app) do + @app = Class.new(Grape::API) do + default_format :json - params do + params do + requires :name, allow_blank: false + end + get '/disallow_blank' + + params do + optional :name, type: String, allow_blank: false + end + get '/opt_disallow_string_blank' + + params do + optional :name, allow_blank: false + end + get '/disallow_blank_optional_param' + + params do + requires :name, allow_blank: true + end + get '/allow_blank' + + params do + requires :val, type: DateTime, allow_blank: true + end + get '/allow_datetime_blank' + + params do + requires :val, type: DateTime, allow_blank: false + end + get '/disallow_datetime_blank' + + params do + requires :val, type: DateTime + end + get '/default_allow_datetime_blank' + + params do + requires :val, type: Date, allow_blank: true + end + get '/allow_date_blank' + + params do + requires :val, type: Integer, allow_blank: true + end + get '/allow_integer_blank' + + params do + requires :val, type: Float, allow_blank: true + end + get '/allow_float_blank' + + params do + requires :val, type: Integer, allow_blank: true + end + get '/allow_integer_blank' + + params do + requires :val, type: Symbol, allow_blank: true + end + get '/allow_symbol_blank' + + params do + requires :val, type: Grape::API::Boolean, allow_blank: true + end + get '/allow_boolean_blank' + + params do + requires :val, type: Grape::API::Boolean, allow_blank: false + end + get '/disallow_boolean_blank' + + params do + optional :user, type: Hash do + requires :name, allow_blank: false + end + end + get '/disallow_blank_required_param_in_an_optional_group' + + params do + optional :user, type: Hash do + requires :name, type: Date, allow_blank: true + end + end + get '/allow_blank_date_param_in_an_optional_group' + + params do + optional :user, type: Hash do + optional :name, allow_blank: false + requires :age + end + end + get '/disallow_blank_optional_param_in_an_optional_group' + + params do + requires :user, type: Hash do requires :name, allow_blank: false end - get '/disallow_blank' + end + get '/disallow_blank_required_param_in_a_required_group' + + params do + requires :user, type: Hash do + requires :name, allow_blank: false + end + end + get '/disallow_string_value_in_a_required_hash_group' + + params do + requires :user, type: Hash do + optional :name, allow_blank: false + end + end + get '/disallow_blank_optional_param_in_a_required_group' + + params do + optional :user, type: Hash do + optional :name, allow_blank: false + end + end + get '/disallow_string_value_in_an_optional_hash_group' + resources :custom_message do params do - optional :name, type: String, allow_blank: false + requires :name, allow_blank: { value: false, message: 'has no value' } end - get '/opt_disallow_string_blank' + get params do - optional :name, allow_blank: false + optional :name, allow_blank: { value: false, message: 'has no value' } end get '/disallow_blank_optional_param' @@ -34,7 +149,7 @@ class API < Grape::API get '/allow_datetime_blank' params do - requires :val, type: DateTime, allow_blank: false + requires :val, type: DateTime, allow_blank: { value: false, message: 'has no value' } end get '/disallow_datetime_blank' @@ -69,18 +184,18 @@ class API < Grape::API get '/allow_symbol_blank' params do - requires :val, type: Boolean, allow_blank: true + requires :val, type: Grape::API::Boolean, allow_blank: true end get '/allow_boolean_blank' params do - requires :val, type: Boolean, allow_blank: false + requires :val, type: Grape::API::Boolean, allow_blank: { value: false, message: 'has no value' } end get '/disallow_boolean_blank' params do optional :user, type: Hash do - requires :name, allow_blank: false + requires :name, allow_blank: { value: false, message: 'has no value' } end end get '/disallow_blank_required_param_in_an_optional_group' @@ -94,7 +209,7 @@ class API < Grape::API params do optional :user, type: Hash do - optional :name, allow_blank: false + optional :name, allow_blank: { value: false, message: 'has no value' } requires :age end end @@ -102,156 +217,35 @@ class API < Grape::API params do requires :user, type: Hash do - requires :name, allow_blank: false + requires :name, allow_blank: { value: false, message: 'has no value' } end end get '/disallow_blank_required_param_in_a_required_group' params do requires :user, type: Hash do - requires :name, allow_blank: false + requires :name, allow_blank: { value: false, message: 'has no value' } end end get '/disallow_string_value_in_a_required_hash_group' params do requires :user, type: Hash do - optional :name, allow_blank: false + optional :name, allow_blank: { value: false, message: 'has no value' } end end get '/disallow_blank_optional_param_in_a_required_group' params do optional :user, type: Hash do - optional :name, allow_blank: false - end - end - get '/disallow_string_value_in_an_optional_hash_group' - - resources :custom_message do - params do - requires :name, allow_blank: { value: false, message: 'has no value' } - end - get - - params do optional :name, allow_blank: { value: false, message: 'has no value' } end - get '/disallow_blank_optional_param' - - params do - requires :name, allow_blank: true - end - get '/allow_blank' - - params do - requires :val, type: DateTime, allow_blank: true - end - get '/allow_datetime_blank' - - params do - requires :val, type: DateTime, allow_blank: { value: false, message: 'has no value' } - end - get '/disallow_datetime_blank' - - params do - requires :val, type: DateTime - end - get '/default_allow_datetime_blank' - - params do - requires :val, type: Date, allow_blank: true - end - get '/allow_date_blank' - - params do - requires :val, type: Integer, allow_blank: true - end - get '/allow_integer_blank' - - params do - requires :val, type: Float, allow_blank: true - end - get '/allow_float_blank' - - params do - requires :val, type: Integer, allow_blank: true - end - get '/allow_integer_blank' - - params do - requires :val, type: Symbol, allow_blank: true - end - get '/allow_symbol_blank' - - params do - requires :val, type: Boolean, allow_blank: true - end - get '/allow_boolean_blank' - - params do - requires :val, type: Boolean, allow_blank: { value: false, message: 'has no value' } - end - get '/disallow_boolean_blank' - - params do - optional :user, type: Hash do - requires :name, allow_blank: { value: false, message: 'has no value' } - end - end - get '/disallow_blank_required_param_in_an_optional_group' - - params do - optional :user, type: Hash do - requires :name, type: Date, allow_blank: true - end - end - get '/allow_blank_date_param_in_an_optional_group' - - params do - optional :user, type: Hash do - optional :name, allow_blank: { value: false, message: 'has no value' } - requires :age - end - end - get '/disallow_blank_optional_param_in_an_optional_group' - - params do - requires :user, type: Hash do - requires :name, allow_blank: { value: false, message: 'has no value' } - end - end - get '/disallow_blank_required_param_in_a_required_group' - - params do - requires :user, type: Hash do - requires :name, allow_blank: { value: false, message: 'has no value' } - end - end - get '/disallow_string_value_in_a_required_hash_group' - - params do - requires :user, type: Hash do - optional :name, allow_blank: { value: false, message: 'has no value' } - end - end - get '/disallow_blank_optional_param_in_a_required_group' - - params do - optional :user, type: Hash do - optional :name, allow_blank: { value: false, message: 'has no value' } - end - end - get '/disallow_string_value_in_an_optional_hash_group' end + get '/disallow_string_value_in_an_optional_hash_group' end end end - def app - ValidationsSpec::AllowBlankValidatorSpec::API - end - context 'invalid input' do it 'refuses empty string' do get '/disallow_blank', name: '' 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 b6468e3e5..4189ec069 100644 --- a/spec/grape/validations/validators/at_least_one_of_spec.rb +++ b/spec/grape/validations/validators/at_least_one_of_spec.rb @@ -2,73 +2,67 @@ require 'spec_helper' -describe Grape::Validations::AtLeastOneOfValidator do - describe '#validate!' do - 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 +describe Grape::Validations::Validators::AtLeastOneOfValidator do + let_it_be(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end - params do - optional :beer, :wine, :grapefruit - at_least_one_of :beer, :wine, :grapefruit - end - post do - end + 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, :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 + params do + optional :beer, :wine, :grapefruit + at_least_one_of :beer, :wine, :grapefruit, message: 'you should choose something' + end + post '/custom-message' do + 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 :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 + 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 + 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 - def app - ValidationsSpec::AtLeastOneOfValidatorSpec::API - end + describe '#validate!' do + subject(:validate) { post path, params } context 'when all restricted params are present' do let(:path) { '/' } diff --git a/spec/grape/validations/validators/coerce_spec.rb b/spec/grape/validations/validators/coerce_spec.rb index a24ca94cf..626859420 100644 --- a/spec/grape/validations/validators/coerce_spec.rb +++ b/spec/grape/validations/validators/coerce_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Grape::Validations::CoerceValidator do +describe Grape::Validations::Validators::CoerceValidator do subject do Class.new(Grape::API) end @@ -83,7 +83,7 @@ def self.parsed?(value) context 'on custom coercion rules' do before do subject.params do - requires :a, types: { value: [Boolean, String], message: 'type cast is invalid' }, coerce_with: (lambda do |val| + requires :a, types: { value: [Grape::API::Boolean, String], message: 'type cast is invalid' }, coerce_with: (lambda do |val| case val when 'yup' true @@ -171,9 +171,9 @@ def self.parsed?(value) expect(last_response.body).to eq('BigDecimal 45.1') end - it 'Boolean' do + it 'Grape::API::Boolean' do subject.params do - requires :boolean, type: Boolean + requires :boolean, type: Grape::API::Boolean end subject.post '/boolean' do params[:boolean] @@ -370,9 +370,9 @@ def self.parse(_val) end end - it 'Boolean' do + it 'Grape::API::Boolean' do subject.params do - requires :boolean, type: Boolean + requires :boolean, type: Grape::API::Boolean end subject.get '/boolean' do params[:boolean].class @@ -1018,11 +1018,9 @@ def self.parse(_val) end context 'multiple types' do - Boolean = Grape::API::Boolean - it 'coerces to first possible type' do subject.params do - requires :a, types: [Boolean, Integer, String] + requires :a, types: [Grape::API::Boolean, Integer, String] end subject.get '/' do params[:a].class.to_s @@ -1043,7 +1041,7 @@ def self.parse(_val) it 'fails when no coercion is possible' do subject.params do - requires :a, types: [Boolean, Integer] + requires :a, types: [Grape::API::Boolean, Integer] end subject.get '/' do params[:a].class.to_s @@ -1202,7 +1200,7 @@ def self.parse(_val) context 'custom coercion rules' do before do subject.params do - requires :a, types: [Boolean, String], coerce_with: (lambda do |val| + requires :a, types: [Grape::API::Boolean, String], coerce_with: (lambda do |val| case val when 'yup' true diff --git a/spec/grape/validations/validators/default_spec.rb b/spec/grape/validations/validators/default_spec.rb index ae16445eb..c6e399c5c 100644 --- a/spec/grape/validations/validators/default_spec.rb +++ b/spec/grape/validations/validators/default_spec.rb @@ -2,104 +2,98 @@ require 'spec_helper' -describe Grape::Validations::DefaultValidator do - module ValidationsSpec - module DefaultValidatorSpec - class API < Grape::API - default_format :json - - params do - optional :id - optional :type, default: 'default-type' - end - get '/' do - { id: params[:id], type: params[:type] } - end +describe Grape::Validations::Validators::DefaultValidator do + let_it_be(:app) do + Class.new(Grape::API) do + default_format :json + + params do + optional :id + optional :type, default: 'default-type' + end + get '/' do + { id: params[:id], type: params[:type] } + end - params do - optional :type1, default: 'default-type1' - optional :type2, default: 'default-type2' - end - get '/user' do - { type1: params[:type1], type2: params[:type2] } - end + params do + optional :type1, default: 'default-type1' + optional :type2, default: 'default-type2' + end + get '/user' do + { type1: params[:type1], type2: params[:type2] } + end - params do - requires :id - optional :type1, default: 'default-type1' - optional :type2, default: 'default-type2' - end + params do + requires :id + optional :type1, default: 'default-type1' + optional :type2, default: 'default-type2' + end - get '/message' do - { id: params[:id], type1: params[:type1], type2: params[:type2] } - end + get '/message' do + { id: params[:id], type1: params[:type1], type2: params[:type2] } + end - params do - optional :random, default: -> { Random.rand } - optional :not_random, default: Random.rand - end - get '/numbers' do - { random_number: params[:random], non_random_number: params[:non_random_number] } - end + params do + optional :random, default: -> { Random.rand } + optional :not_random, default: Random.rand + end + get '/numbers' do + { random_number: params[:random], non_random_number: params[:non_random_number] } + end - params do - optional :array, type: Array do - requires :name - optional :with_default, default: 'default' - end - end - get '/array' do - { array: params[:array] } + params do + optional :array, type: Array do + requires :name + optional :with_default, default: 'default' end + end + get '/array' do + { array: params[:array] } + end - params do - requires :thing1 - optional :more_things, type: Array do - requires :nested_thing - requires :other_thing, default: 1 - end - end - get '/optional_array' do - { thing1: params[:thing1] } + params do + requires :thing1 + optional :more_things, type: Array do + requires :nested_thing + requires :other_thing, default: 1 end + end + get '/optional_array' do + { thing1: params[:thing1] } + end - params do - requires :root, type: Hash do - optional :some_things, type: Array do - requires :foo - optional :options, type: Array do - requires :name, type: String - requires :value, type: String - end + params do + requires :root, type: Hash do + optional :some_things, type: Array do + requires :foo + optional :options, type: Array do + requires :name, type: String + requires :value, type: String end end end - get '/nested_optional_array' do - { root: params[:root] } - end + end + get '/nested_optional_array' do + { root: params[:root] } + end - params do - requires :root, type: Hash do - optional :some_things, type: Array do - requires :foo - optional :options, type: Array do - optional :name, type: String - optional :value, type: String - end + params do + requires :root, type: Hash do + optional :some_things, type: Array do + requires :foo + optional :options, type: Array do + optional :name, type: String + optional :value, type: String end end end - get '/another_nested_optional_array' do - { root: params[:root] } - end + end + get '/another_nested_optional_array' do + { root: params[:root] } end end end - def app - ValidationsSpec::DefaultValidatorSpec::API - end - it 'lets you leave required values nested inside an optional blank' do get '/optional_array', thing1: 'stuff' expect(last_response.status).to eq(200) diff --git a/spec/grape/validations/validators/exactly_one_of_spec.rb b/spec/grape/validations/validators/exactly_one_of_spec.rb index 87eba59d3..8076c703b 100644 --- a/spec/grape/validations/validators/exactly_one_of_spec.rb +++ b/spec/grape/validations/validators/exactly_one_of_spec.rb @@ -2,95 +2,89 @@ require 'spec_helper' -describe Grape::Validations::ExactlyOneOfValidator do - describe '#validate!' do - 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 +describe Grape::Validations::Validators::ExactlyOneOfValidator do + let_it_be(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end - params do - optional :beer - optional :wine - optional :grapefruit - exactly_one_of :beer, :wine, :grapefruit - end - post do - end + 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 + 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 + 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 + 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 + 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 + 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 - 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 + params do + requires :items, type: Array do + requires :nested_items, type: Array do + optional :beer, :wine, :grapefruit, type: Grape::API::Boolean + exactly_one_of :beer, :wine, :grapefruit end end end + post '/deeply-nested-array' do + end end + end - def app - ValidationsSpec::ExactlyOneOfValidatorSpec::API - end + describe '#validate!' do + subject(:validate) { post path, params } context 'when all params are present' do let(:path) { '/' } diff --git a/spec/grape/validations/validators/except_values_spec.rb b/spec/grape/validations/validators/except_values_spec.rb index bcc756dd5..6bed034c9 100644 --- a/spec/grape/validations/validators/except_values_spec.rb +++ b/spec/grape/validations/validators/except_values_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Grape::Validations::ExceptValuesValidator do +describe Grape::Validations::Validators::ExceptValuesValidator do module ValidationsSpec class ExceptValuesModel DEFAULT_EXCEPTS = %w[invalid-type1 invalid-type2 invalid-type3].freeze diff --git a/spec/grape/validations/validators/mutual_exclusion_spec.rb b/spec/grape/validations/validators/mutual_exclusion_spec.rb index ac1b46989..e01dee282 100644 --- a/spec/grape/validations/validators/mutual_exclusion_spec.rb +++ b/spec/grape/validations/validators/mutual_exclusion_spec.rb @@ -2,95 +2,89 @@ require 'spec_helper' -describe Grape::Validations::MutualExclusionValidator do - describe '#validate!' do - 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 +describe Grape::Validations::Validators::MutualExclusionValidator do + let_it_be(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end - params do - optional :beer - optional :wine - optional :grapefruit - mutually_exclusive :beer, :wine, :grapefruit - end - post do - end + 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 + 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 + 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 + 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 + 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 + 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 + params do + requires :items, type: Array do + requires :nested_items, type: Array do + optional :beer, :wine, :grapefruit, type: Grape::API::Boolean + mutually_exclusive :beer, :wine, :grapefruit end end end + post '/deeply-nested-array' do + end end + end - def app - ValidationsSpec::MutualExclusionValidatorSpec::API - end + describe '#validate!' do + subject(:validate) { post path, params } context 'when all mutually exclusive params are present' do let(:path) { '/' } diff --git a/spec/grape/validations/validators/presence_spec.rb b/spec/grape/validations/validators/presence_spec.rb index 5eaabe72a..a83399588 100644 --- a/spec/grape/validations/validators/presence_spec.rb +++ b/spec/grape/validations/validators/presence_spec.rb @@ -2,12 +2,13 @@ require 'spec_helper' -describe Grape::Validations::PresenceValidator do +describe Grape::Validations::Validators::PresenceValidator do subject do Class.new(Grape::API) do format :json end end + def app subject end diff --git a/spec/grape/validations/validators/regexp_spec.rb b/spec/grape/validations/validators/regexp_spec.rb index b4ffc99df..5a6aec73a 100644 --- a/spec/grape/validations/validators/regexp_spec.rb +++ b/spec/grape/validations/validators/regexp_spec.rb @@ -2,53 +2,47 @@ require 'spec_helper' -describe Grape::Validations::RegexpValidator do - module ValidationsSpec - module RegexpValidatorSpec - class API < Grape::API - default_format :json - - resources :custom_message do - params do - requires :name, regexp: { value: /^[a-z]+$/, message: 'format is invalid' } - end - get do - end - - params do - requires :names, type: { value: Array[String], message: 'can\'t be nil' }, regexp: { value: /^[a-z]+$/, message: 'format is invalid' } - end - get 'regexp_with_array' do - end - end +describe Grape::Validations::Validators::RegexpValidator do + let_it_be(:app) do + Class.new(Grape::API) do + default_format :json + resources :custom_message do params do - requires :name, regexp: /^[a-z]+$/ + requires :name, regexp: { value: /^[a-z]+$/, message: 'format is invalid' } end get do end params do - requires :names, type: Array[String], regexp: /^[a-z]+$/ + requires :names, type: { value: Array[String], message: 'can\'t be nil' }, regexp: { value: /^[a-z]+$/, message: 'format is invalid' } end get 'regexp_with_array' do end + end - params do - requires :people, type: Hash do - requires :names, type: Array[String], regexp: /^[a-z]+$/ - end - end - get 'nested_regexp_with_array' do + params do + requires :name, regexp: /^[a-z]+$/ + end + get do + end + + params do + requires :names, type: Array[String], regexp: /^[a-z]+$/ + end + get 'regexp_with_array' do + end + + params do + requires :people, type: Hash do + requires :names, type: Array[String], regexp: /^[a-z]+$/ end end + get 'nested_regexp_with_array' do + end end end - def app - ValidationsSpec::RegexpValidatorSpec::API - end - context 'custom validation message' do context 'with invalid input' do it 'refuses inapppopriate' do diff --git a/spec/grape/validations/validators/same_as_spec.rb b/spec/grape/validations/validators/same_as_spec.rb index da4c945c0..f21fd94b5 100644 --- a/spec/grape/validations/validators/same_as_spec.rb +++ b/spec/grape/validations/validators/same_as_spec.rb @@ -2,31 +2,25 @@ 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 +describe Grape::Validations::Validators::SameAsValidator do + let_it_be(:app) do + Class.new(Grape::API) do + 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 + params do + requires :password + requires :password_confirmation, same_as: { value: :password, message: 'not match' } + end + post '/custom-message' do end end end - def app - ValidationsSpec::SameAsValidatorSpec::API - end - describe '/' do context 'is the same' do it do diff --git a/spec/grape/validations/validators/values_spec.rb b/spec/grape/validations/validators/values_spec.rb index 73c35b390..ca59c6f09 100644 --- a/spec/grape/validations/validators/values_spec.rb +++ b/spec/grape/validations/validators/values_spec.rb @@ -2,11 +2,12 @@ require 'spec_helper' -describe Grape::Validations::ValuesValidator do - module ValidationsSpec - class ValuesModel +describe Grape::Validations::Validators::ValuesValidator do + let_it_be(:values_model) do + Class.new do DEFAULT_VALUES = %w[valid-type1 valid-type2 valid-type3].freeze DEFAULT_EXCEPTS = %w[invalid-type1 invalid-type2 invalid-type3].freeze + class << self def values @values ||= [] @@ -33,212 +34,213 @@ def include?(value) end end end + end - module ValuesValidatorSpec - class API < Grape::API - default_format :json - - resources :custom_message do - params do - requires :type, values: { value: ValuesModel.values, message: 'value does not include in values' } - end - get '/' do - { type: params[:type] } - end - - params do - optional :type, values: { value: -> { ValuesModel.values }, message: 'value does not include in values' }, default: 'valid-type2' - end - get '/lambda' do - { type: params[:type] } - end - - params do - requires :type, values: { except: ValuesModel.excepts, except_message: 'value is on exclusions list', message: 'default exclude message' } - end - get '/exclude/exclude_message' - - params do - requires :type, values: { except: -> { ValuesModel.excepts }, except_message: 'value is on exclusions list' } - end - get '/exclude/lambda/exclude_message' - - params do - requires :type, values: { except: ValuesModel.excepts, message: 'default exclude message' } - end - get '/exclude/fallback_message' - end + before do + stub_const('ValuesModel', values_model) + end + + let_it_be(:app) do + ValuesModel = values_model + Class.new(Grape::API) do + default_format :json + resources :custom_message do params do - requires :type, values: ValuesModel.values + requires :type, values: { value: ValuesModel.values, message: 'value does not include in values' } end get '/' do { type: params[:type] } end params do - requires :type, values: [] + optional :type, values: { value: -> { ValuesModel.values }, message: 'value does not include in values' }, default: 'valid-type2' end - get '/empty' - - params do - optional :type, values: { value: ValuesModel.values }, default: 'valid-type2' - end - get '/default/hash/valid' do + get '/lambda' do { type: params[:type] } end params do - optional :type, values: ValuesModel.values, default: 'valid-type2' - end - get '/default/valid' do - { type: params[:type] } + requires :type, values: { except: ValuesModel.excepts, except_message: 'value is on exclusions list', message: 'default exclude message' } end + get '/exclude/exclude_message' params do - optional :type, values: { except: ValuesModel.excepts }, default: 'valid-type2' - end - get '/default/except' do - { type: params[:type] } + requires :type, values: { except: -> { ValuesModel.excepts }, except_message: 'value is on exclusions list' } end + get '/exclude/lambda/exclude_message' params do - optional :type, values: -> { ValuesModel.values }, default: 'valid-type2' - end - get '/lambda' do - { type: params[:type] } + requires :type, values: { except: ValuesModel.excepts, message: 'default exclude message' } end + get '/exclude/fallback_message' + end - params do - requires :type, values: ->(v) { ValuesModel.include? v } - end - get '/lambda_val' do - { type: params[:type] } - end + params do + requires :type, values: ValuesModel.values + end + get '/' do + { type: params[:type] } + end - params do - requires :number, type: Integer, values: ->(v) { v > 0 } - end - get '/lambda_int_val' do - { number: params[:number] } - end + params do + requires :type, values: [] + end + get '/empty' - params do - requires :type, values: -> { [] } - end - get '/empty_lambda' + params do + optional :type, values: { value: ValuesModel.values }, default: 'valid-type2' + end + get '/default/hash/valid' do + { type: params[:type] } + end - params do - optional :type, values: ValuesModel.values, default: -> { ValuesModel.values.sample } - end - get '/default_lambda' do - { type: params[:type] } - end + params do + optional :type, values: ValuesModel.values, default: 'valid-type2' + end + get '/default/valid' do + { type: params[:type] } + end - params do - optional :type, values: -> { ValuesModel.values }, default: -> { ValuesModel.values.sample } - end - get '/default_and_values_lambda' do - { type: params[:type] } - end + params do + optional :type, values: { except: ValuesModel.excepts }, default: 'valid-type2' + end + get '/default/except' do + { type: params[:type] } + end - params do - optional :type, type: Boolean, desc: 'A boolean', values: [true] - end - get '/values/optional_boolean' do - { type: params[:type] } - end + params do + optional :type, values: -> { ValuesModel.values }, default: 'valid-type2' + end + get '/lambda' do + { type: params[:type] } + end - params do - requires :type, type: Integer, desc: 'An integer', values: [10, 11], default: 10 - end - get '/values/coercion' do - { type: params[:type] } - end + params do + requires :type, values: ->(v) { ValuesModel.include? v } + end + get '/lambda_val' do + { type: params[:type] } + end - params do - requires :type, type: Array[Integer], desc: 'An integer', values: [10, 11], default: 10 - end - get '/values/array_coercion' do - { type: params[:type] } - end + params do + requires :number, type: Integer, values: ->(v) { v > 0 } + end + get '/lambda_int_val' do + { number: params[:number] } + end - params do - optional :optional, type: Array do - requires :type, values: %w[a b] - end - end - get '/optional_with_required_values' + params do + requires :type, values: -> { [] } + end + get '/empty_lambda' - params do - requires :type, values: { except: ValuesModel.excepts } - end - get '/except/exclusive' do - { type: params[:type] } - end + params do + optional :type, values: ValuesModel.values, default: -> { ValuesModel.values.sample } + end + get '/default_lambda' do + { type: params[:type] } + end - params do - requires :type, type: String, values: { except: ValuesModel.excepts } - end - get '/except/exclusive/type' do - { type: params[:type] } - end + params do + optional :type, values: -> { ValuesModel.values }, default: -> { ValuesModel.values.sample } + end + get '/default_and_values_lambda' do + { type: params[:type] } + end - params do - requires :type, values: { except: -> { ValuesModel.excepts } } - end - get '/except/exclusive/lambda' do - { type: params[:type] } - end + params do + optional :type, type: Grape::API::Boolean, desc: 'A boolean', values: [true] + end + get '/values/optional_boolean' do + { type: params[:type] } + end - params do - requires :type, type: String, values: { except: -> { ValuesModel.excepts } } - end - get '/except/exclusive/lambda/type' do - { type: params[:type] } - end + params do + requires :type, type: Integer, desc: 'An integer', values: [10, 11], default: 10 + end + get '/values/coercion' do + { type: params[:type] } + end - params do - requires :type, type: Integer, values: { except: -> { [3, 4, 5] } } - end - get '/except/exclusive/lambda/coercion' do - { type: params[:type] } - end + params do + requires :type, type: Array[Integer], desc: 'An integer', values: [10, 11], default: 10 + end + get '/values/array_coercion' do + { type: params[:type] } + end - params do - requires :type, type: Integer, values: { value: 1..5, except: [3] } - end - get '/mixed/value/except' do - { type: params[:type] } + params do + optional :optional, type: Array do + requires :type, values: %w[a b] end + end + get '/optional_with_required_values' - params do - optional :optional, type: Array[String], values: %w[a b c] - end - put '/optional_with_array_of_string_values' + params do + requires :type, values: { except: ValuesModel.excepts } + end + get '/except/exclusive' do + { type: params[:type] } + end - params do - requires :type, values: { proc: ->(v) { ValuesModel.include? v } } - end - get '/proc' do - { type: params[:type] } - end + params do + requires :type, type: String, values: { except: ValuesModel.excepts } + end + get '/except/exclusive/type' do + { type: params[:type] } + end - params do - requires :type, values: { proc: ->(v) { ValuesModel.include? v }, message: 'failed check' } - end - get '/proc/message' + params do + requires :type, values: { except: -> { ValuesModel.excepts } } + end + get '/except/exclusive/lambda' do + { type: params[:type] } + end - params do - optional :name, type: String, values: %w[a b], allow_blank: true - end - get '/allow_blank' + params do + requires :type, type: String, values: { except: -> { ValuesModel.excepts } } + end + get '/except/exclusive/lambda/type' do + { type: params[:type] } end - end - end - def app - ValidationsSpec::ValuesValidatorSpec::API + params do + requires :type, type: Integer, values: { except: -> { [3, 4, 5] } } + end + get '/except/exclusive/lambda/coercion' do + { type: params[:type] } + end + + params do + requires :type, type: Integer, values: { value: 1..5, except: [3] } + end + get '/mixed/value/except' do + { type: params[:type] } + end + + params do + optional :optional, type: Array[String], values: %w[a b c] + end + put '/optional_with_array_of_string_values' + + params do + requires :type, values: { proc: ->(v) { ValuesModel.include? v } } + end + get '/proc' do + { type: params[:type] } + end + + params do + requires :type, values: { proc: ->(v) { ValuesModel.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 context 'with a custom validation message' do @@ -255,7 +257,7 @@ def app end it 'validates against values in a proc' do - ValidationsSpec::ValuesModel.add_value('valid-type4') + ValuesModel.add_value('valid-type4') get('/custom_message/lambda', type: 'valid-type4') expect(last_response.status).to eq 200 @@ -354,15 +356,14 @@ def app end it 'does not validate updated values without proc' do - ValidationsSpec::ValuesModel.add_value('valid-type4') - + ValuesModel.add_value('valid-type4') get('/', type: 'valid-type4') expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end it 'validates against values in a proc' do - ValidationsSpec::ValuesModel.add_value('valid-type4') + ValuesModel.add_value('valid-type4') get('/lambda', type: 'valid-type4') expect(last_response.status).to eq 200 @@ -424,7 +425,7 @@ def app it 'raises IncompatibleOptionValues on an invalid default value from proc' do subject = Class.new(Grape::API) expect do - subject.params { optional :type, values: %w[valid-type1 valid-type2 valid-type3], default: "#{ValidationsSpec::ValuesModel.values.sample}_invalid" } + subject.params { optional :type, values: %w[valid-type1 valid-type2 valid-type3], default: "#{ValuesModel.values.sample}_invalid" } end.to raise_error Grape::Exceptions::IncompatibleOptionValues end diff --git a/spec/grape/validations_spec.rb b/spec/grape/validations_spec.rb index 5d6ee7f13..9c2486f88 100644 --- a/spec/grape/validations_spec.rb +++ b/spec/grape/validations_spec.rb @@ -489,18 +489,24 @@ def define_requires_none end context 'custom validator for a Hash' do - module ValuesSpec - module DateRangeValidations - class DateRangeValidator < Grape::Validations::Base - def validate_param!(attr_name, params) - return if params[attr_name][:from] <= params[attr_name][:to] + let(:date_range_validator) do + Class.new(Grape::Validations::Validators::Base) do + def validate_param!(attr_name, params) + return if params[attr_name][:from] <= params[attr_name][:to] - raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: "'from' must be lower or equal to 'to'") - end + raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: "'from' must be lower or equal to 'to'") end end end + before do + Grape::Validations.register_validator('date_range', date_range_validator) + end + + after do + Grape::Validations.deregister_validator('date_range') + end + before do subject.params do optional :date_range, date_range: true, type: Hash do @@ -1183,8 +1189,8 @@ def validate_param!(attr_name, params) end context 'custom validation' do - module CustomValidations - class Customvalidator < Grape::Validations::Base + let(:custom_validator) do + Class.new(Grape::Validations::Validators::Base) do def validate_param!(attr_name, params) return if params[attr_name] == 'im custom' @@ -1193,6 +1199,14 @@ def validate_param!(attr_name, params) end end + before do + Grape::Validations.register_validator('customvalidator', custom_validator) + end + + after do + Grape::Validations.deregister_validator('customvalidator') + end + context 'when using optional with a custom validator' do before do subject.params do @@ -1332,8 +1346,8 @@ def validate_param!(attr_name, params) end context 'when using options on param' do - module CustomValidations - class CustomvalidatorWithOptions < Grape::Validations::Base + let(:custom_validator_with_options) do + Class.new(Grape::Validations::Validators::Base) do def validate_param!(attr_name, params) return if params[attr_name] == @option[:text] @@ -1342,6 +1356,14 @@ def validate_param!(attr_name, params) end end + before do + Grape::Validations.register_validator('customvalidator_with_options', custom_validator_with_options) + end + + after do + Grape::Validations.deregister_validator('customvalidator_with_options') + end + before do subject.params do optional :custom, customvalidator_with_options: { text: 'im custom with options', message: 'is not custom with options!' } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ce4e84d31..d8e256691 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -5,6 +5,15 @@ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'support')) require 'grape' +require 'test_prof/recipes/rspec/let_it_be' + +class NullAdapter + def begin_transaction; end + + def rollback_transaction; end +end + +TestProf::BeforeAll.adapter = NullAdapter.new require 'rubygems' require 'bundler' From 26035cbc6b06bc5764c94d178f22fcb7ee89f4a3 Mon Sep 17 00:00:00 2001 From: eproulx Date: Sun, 5 Dec 2021 13:47:50 +0100 Subject: [PATCH 069/304] Add test-prof to appraisal's gemfile --- gemfiles/multi_json.gemfile | 1 + gemfiles/multi_xml.gemfile | 1 + gemfiles/rack1.gemfile | 1 + gemfiles/rack2.gemfile | 1 + gemfiles/rack2_2.gemfile | 1 + gemfiles/rack_edge.gemfile | 1 + gemfiles/rails_5.gemfile | 1 + gemfiles/rails_6.gemfile | 1 + gemfiles/rails_6_1.gemfile | 1 + gemfiles/rails_edge.gemfile | 1 + 10 files changed, 10 insertions(+) diff --git a/gemfiles/multi_json.gemfile b/gemfiles/multi_json.gemfile index 726c8be19..28ee62972 100644 --- a/gemfiles/multi_json.gemfile +++ b/gemfiles/multi_json.gemfile @@ -34,6 +34,7 @@ group :test do gem 'rack-test', '~> 1.1.0' gem 'rspec', '~> 3.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false + gem 'test-prof', require: false end gemspec path: '../' diff --git a/gemfiles/multi_xml.gemfile b/gemfiles/multi_xml.gemfile index 72d4e3f30..9f2631e2f 100644 --- a/gemfiles/multi_xml.gemfile +++ b/gemfiles/multi_xml.gemfile @@ -34,6 +34,7 @@ group :test do gem 'rack-test', '~> 1.1.0' gem 'rspec', '~> 3.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false + gem 'test-prof', require: false end gemspec path: '../' diff --git a/gemfiles/rack1.gemfile b/gemfiles/rack1.gemfile index bb72144a0..87d0558d6 100644 --- a/gemfiles/rack1.gemfile +++ b/gemfiles/rack1.gemfile @@ -34,6 +34,7 @@ group :test do gem 'rack-test', '~> 1.1.0' gem 'rspec', '~> 3.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false + gem 'test-prof', require: false end gemspec path: '../' diff --git a/gemfiles/rack2.gemfile b/gemfiles/rack2.gemfile index 6522140d9..a40bc53d6 100644 --- a/gemfiles/rack2.gemfile +++ b/gemfiles/rack2.gemfile @@ -34,6 +34,7 @@ group :test do gem 'rack-test', '~> 1.1.0' gem 'rspec', '~> 3.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false + gem 'test-prof', require: false end gemspec path: '../' diff --git a/gemfiles/rack2_2.gemfile b/gemfiles/rack2_2.gemfile index 88cfa240d..1c8214330 100644 --- a/gemfiles/rack2_2.gemfile +++ b/gemfiles/rack2_2.gemfile @@ -34,6 +34,7 @@ group :test do gem 'rack-test', '~> 1.1.0' gem 'rspec', '~> 3.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false + gem 'test-prof', require: false end gemspec path: '../' diff --git a/gemfiles/rack_edge.gemfile b/gemfiles/rack_edge.gemfile index 25b275b6c..06955ea3c 100644 --- a/gemfiles/rack_edge.gemfile +++ b/gemfiles/rack_edge.gemfile @@ -34,6 +34,7 @@ group :test do gem 'rack-test', '~> 1.1.0' gem 'rspec', '~> 3.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false + gem 'test-prof', require: false end gemspec path: '../' diff --git a/gemfiles/rails_5.gemfile b/gemfiles/rails_5.gemfile index 7dc94e695..5d7c30e67 100644 --- a/gemfiles/rails_5.gemfile +++ b/gemfiles/rails_5.gemfile @@ -34,6 +34,7 @@ group :test do gem 'rack-test', '~> 1.1.0' gem 'rspec', '~> 3.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false + gem 'test-prof', require: false end gemspec path: '../' diff --git a/gemfiles/rails_6.gemfile b/gemfiles/rails_6.gemfile index 5db9b049d..9ffed6ac9 100644 --- a/gemfiles/rails_6.gemfile +++ b/gemfiles/rails_6.gemfile @@ -34,6 +34,7 @@ group :test do gem 'rack-test', '~> 1.1.0' gem 'rspec', '~> 3.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false + gem 'test-prof', require: false end gemspec path: '../' diff --git a/gemfiles/rails_6_1.gemfile b/gemfiles/rails_6_1.gemfile index d27f9ed02..d441b184d 100644 --- a/gemfiles/rails_6_1.gemfile +++ b/gemfiles/rails_6_1.gemfile @@ -34,6 +34,7 @@ group :test do gem 'rack-test', '~> 1.1.0' gem 'rspec', '~> 3.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false + gem 'test-prof', require: false end gemspec path: '../' diff --git a/gemfiles/rails_edge.gemfile b/gemfiles/rails_edge.gemfile index 5d179f078..e4c5e6919 100644 --- a/gemfiles/rails_edge.gemfile +++ b/gemfiles/rails_edge.gemfile @@ -34,6 +34,7 @@ group :test do gem 'rack-test', '~> 1.1.0' gem 'rspec', '~> 3.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false + gem 'test-prof', require: false end gemspec path: '../' From aeaaa8bf404ee5e8ec927860d35a01061489bb15 Mon Sep 17 00:00:00 2001 From: eproulx Date: Sun, 5 Dec 2021 13:55:20 +0100 Subject: [PATCH 070/304] Remove unnecessary @app --- spec/grape/validations/validators/allow_blank_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/grape/validations/validators/allow_blank_spec.rb b/spec/grape/validations/validators/allow_blank_spec.rb index bab81a26c..c23a755a6 100644 --- a/spec/grape/validations/validators/allow_blank_spec.rb +++ b/spec/grape/validations/validators/allow_blank_spec.rb @@ -4,7 +4,7 @@ describe Grape::Validations::Validators::AllowBlankValidator do let_it_be(:app) do - @app = Class.new(Grape::API) do + Class.new(Grape::API) do default_format :json params do From 7a647293ab829ee14647252c304747bb09d7e912 Mon Sep 17 00:00:00 2001 From: eproulx Date: Sun, 5 Dec 2021 13:58:57 +0100 Subject: [PATCH 071/304] ADd CHANGELOG entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1448c755c..5bea546a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * [#2193](https://github.com/ruby-grape/grape/pull/2193): Fixed the broken ruby-head NoMethodError spec - [@Jack12816](https://github.com/Jack12816). * [#2192](https://github.com/ruby-grape/grape/pull/2192): Memoize the result of Grape::Middleware::Base#response - [@Jack12816](https://github.com/Jack12816). +* [#2200](https://github.com/ruby-grape/grape/pull/2200): Add validators module to all validators - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 1.6.0 (2021/10/04) From 0d1cd88a743a8510a163e6b1c22640ec111837dd Mon Sep 17 00:00:00 2001 From: eproulx Date: Sun, 5 Dec 2021 17:41:00 +0100 Subject: [PATCH 072/304] Add reset_global! in before :all hooks Refactor logger_spec.rb --- spec/grape/dsl/logger_spec.rb | 34 ++++++++++++++++------------------ spec/spec_helper.rb | 1 + 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/spec/grape/dsl/logger_spec.rb b/spec/grape/dsl/logger_spec.rb index 1992e3277..b7e419541 100644 --- a/spec/grape/dsl/logger_spec.rb +++ b/spec/grape/dsl/logger_spec.rb @@ -2,27 +2,25 @@ require 'spec_helper' -module Grape - module DSL - module LoggerSpec - class Dummy - extend Grape::DSL::Logger - end +describe Grape::DSL::Logger do + subject { Class.new(dummy_logger) } + + let(:dummy_logger) do + Class.new do + extend Grape::DSL::Logger end - describe Logger do - subject { Class.new(LoggerSpec::Dummy) } - let(:logger) { double(:logger) } + end - describe '.logger' do - it 'sets a logger' do - subject.logger logger - expect(subject.logger).to eq logger - end + let(:logger) { instance_double(::Logger) } + + describe '.logger' do + it 'sets a logger' do + subject.logger logger + expect(subject.logger).to eq logger + end - it 'returns a logger' do - expect(subject.logger(logger)).to eq logger - end - end + it 'returns a logger' do + expect(subject.logger(logger)).to eq logger end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d8e256691..1dd8aee2b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -36,6 +36,7 @@ def rollback_transaction; end config.filter_run_when_matching :focus config.warnings = true + config.before(:all) { Grape::Util::InheritableSetting.reset_global! } config.before(:each) { Grape::Util::InheritableSetting.reset_global! } # Enable flags like --only-failures and --next-failure From b33173b0f7844e7c94c262cf58a8eedf61a5cd08 Mon Sep 17 00:00:00 2001 From: eproulx Date: Sun, 5 Dec 2021 17:44:23 +0100 Subject: [PATCH 073/304] Add CHANGELOG entry. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bea546a8..72327e5a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ * [#2193](https://github.com/ruby-grape/grape/pull/2193): Fixed the broken ruby-head NoMethodError spec - [@Jack12816](https://github.com/Jack12816). * [#2192](https://github.com/ruby-grape/grape/pull/2192): Memoize the result of Grape::Middleware::Base#response - [@Jack12816](https://github.com/Jack12816). * [#2200](https://github.com/ruby-grape/grape/pull/2200): Add validators module to all validators - [@ericproulx](https://github.com/ericproulx). +* [#2202](https://github.com/ruby-grape/grape/pull/2202): Fix random mock spec error - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 1.6.0 (2021/10/04) From 783fa592a65f42efbad53d05fdd3309d0df028d2 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Mon, 6 Dec 2021 14:24:29 +0100 Subject: [PATCH 074/304] Adding rubocop-rspec (#2203) * Update rubocop gems Add rubocop-rspec Regenerate autoconfig * Add CHANGELOG entry * Add back rack2_2.gemfile * Fix all RSpec/DescribedClass --- .rubocop.yml | 4 + .rubocop_todo.yml | 215 +++++++- CHANGELOG.md | 1 + Gemfile | 7 +- gemfiles/multi_json.gemfile | 11 +- gemfiles/multi_xml.gemfile | 11 +- gemfiles/rack1.gemfile | 11 +- gemfiles/rack2.gemfile | 11 +- gemfiles/rack2_2.gemfile | 11 +- gemfiles/rack_edge.gemfile | 11 +- gemfiles/rails_5.gemfile | 11 +- gemfiles/rails_6.gemfile | 11 +- gemfiles/rails_6_1.gemfile | 11 +- gemfiles/rails_edge.gemfile | 11 +- spec/grape/api/custom_validations_spec.rb | 111 ++-- .../grape/api/deeply_included_options_spec.rb | 6 +- .../api/defines_boolean_in_params_spec.rb | 3 +- spec/grape/api/invalid_format_spec.rb | 2 + spec/grape/api/recognize_path_spec.rb | 2 +- .../api/shared_helpers_exactly_one_of_spec.rb | 24 +- spec/grape/api_remount_spec.rb | 31 +- spec/grape/api_spec.rb | 476 +++++++++++------- spec/grape/dsl/callbacks_spec.rb | 1 + spec/grape/dsl/headers_spec.rb | 4 + spec/grape/dsl/helpers_spec.rb | 5 +- spec/grape/dsl/inside_route_spec.rb | 10 +- spec/grape/dsl/middleware_spec.rb | 1 + spec/grape/dsl/parameters_spec.rb | 1 + spec/grape/dsl/request_response_spec.rb | 1 + spec/grape/dsl/routing_spec.rb | 15 +- spec/grape/endpoint/declared_spec.rb | 24 +- spec/grape/endpoint_spec.rb | 109 ++-- spec/grape/entity_spec.rb | 26 +- .../exceptions/body_parse_errors_spec.rb | 3 + .../exceptions/invalid_accept_header_spec.rb | 83 ++- .../exceptions/validation_errors_spec.rb | 23 +- spec/grape/exceptions/validation_spec.rb | 8 +- .../extensions/param_builders/hash_spec.rb | 14 +- .../hash_with_indifferent_access_spec.rb | 16 +- .../param_builders/hashie/mash_spec.rb | 16 +- spec/grape/integration/rack_sendfile_spec.rb | 2 +- spec/grape/loading_spec.rb | 16 +- spec/grape/middleware/base_spec.rb | 23 +- spec/grape/middleware/error_spec.rb | 1 + spec/grape/middleware/exception_spec.rb | 272 ++++------ spec/grape/middleware/formatter_spec.rb | 29 +- spec/grape/middleware/globals_spec.rb | 11 +- spec/grape/middleware/stack_spec.rb | 22 +- .../versioner/accept_version_header_spec.rb | 3 +- .../grape/middleware/versioner/header_spec.rb | 27 +- spec/grape/middleware/versioner/param_spec.rb | 8 +- spec/grape/middleware/versioner/path_spec.rb | 6 +- spec/grape/middleware/versioner_spec.rb | 2 +- spec/grape/parser_spec.rb | 4 + spec/grape/path_spec.rb | 104 ++-- spec/grape/presenters/presenter_spec.rb | 13 +- spec/grape/request_spec.rb | 10 +- spec/grape/util/inheritable_setting_spec.rb | 14 +- spec/grape/util/inheritable_values_spec.rb | 5 +- .../util/reverse_stackable_values_spec.rb | 4 +- spec/grape/util/stackable_values_spec.rb | 12 +- .../validations/instance_behaivour_spec.rb | 17 +- .../multiple_attributes_iterator_spec.rb | 1 + spec/grape/validations/params_scope_spec.rb | 16 +- .../single_attribute_iterator_spec.rb | 1 + .../types/primitive_coercer_spec.rb | 4 +- spec/grape/validations/types_spec.rb | 16 +- .../validators/allow_blank_spec.rb | 2 + .../validations/validators/coerce_spec.rb | 2 +- .../validations/validators/presence_spec.rb | 14 + spec/grape/validations_spec.rb | 29 +- .../integration/eager_load/eager_load_spec.rb | 4 +- spec/integration/multi_json/json_spec.rb | 2 +- spec/integration/multi_xml/xml_spec.rb | 2 +- spec/shared/versioning_examples.rb | 17 +- spec/spec_helper.rb | 2 +- 76 files changed, 1282 insertions(+), 787 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 16744c161..86493d132 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -8,6 +8,7 @@ AllCops: require: - rubocop-performance + - rubocop-rspec inherit_from: .rubocop_todo.yml @@ -32,3 +33,6 @@ Style/HashTransformValues: Metrics/BlockLength: Exclude: - spec/**/*_spec.rb + +RSpec/Capybara/FeatureMethods: + Enabled: false diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 26c7d0988..b6477131a 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,18 +1,26 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2021-09-26 08:03:47 UTC using RuboCop version 1.21.0. +# on 2021-12-05 16:55:50 UTC using RuboCop version 1.23.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: Include. +# Include: **/*.gemspec +Gemspec/RequireMFA: + Exclude: + - 'grape.gemspec' + # Offense count: 1 # Configuration parameters: IgnoredMethods. Lint/AmbiguousBlockAssociation: Exclude: - 'spec/grape/dsl/routing_spec.rb' -# Offense count: 56 +# Offense count: 41 # Configuration parameters: AllowedMethods. # AllowedMethods: enums Lint/ConstantDefinitionInBlock: @@ -61,17 +69,17 @@ Metrics/AbcSize: Metrics/BlockLength: Max: 182 -# Offense count: 11 +# Offense count: 9 # Configuration parameters: CountComments, CountAsOne. Metrics/ClassLength: - Max: 310 + Max: 298 # Offense count: 30 # Configuration parameters: IgnoredMethods. Metrics/CyclomaticComplexity: Max: 15 -# Offense count: 71 +# Offense count: 68 # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. Metrics/MethodLength: Max: 32 @@ -133,6 +141,201 @@ Performance/MethodObjectAsBlock: Exclude: - 'lib/grape/middleware/stack.rb' +# Offense count: 4 +RSpec/AnyInstance: + Exclude: + - 'spec/grape/api_spec.rb' + - 'spec/grape/middleware/base_spec.rb' + +# Offense count: 332 +# Configuration parameters: Prefixes. +# Prefixes: when, with, without +RSpec/ContextWording: + Enabled: false + +# Offense count: 3 +# Configuration parameters: IgnoredMetadata. +RSpec/DescribeClass: + Exclude: + - '**/spec/features/**/*' + - '**/spec/requests/**/*' + - '**/spec/routing/**/*' + - '**/spec/system/**/*' + - '**/spec/views/**/*' + - 'spec/grape/config_spec.rb' + - 'spec/grape/named_api_spec.rb' + - 'spec/grape/validations/instance_behaivour_spec.rb' + +# Offense count: 3 +RSpec/EmptyExampleGroup: + Exclude: + - 'spec/grape/api_spec.rb' + - 'spec/grape/dsl/configuration_spec.rb' + - 'spec/grape/validations/attributes_iterator_spec.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +RSpec/EmptyLineAfterSubject: + Exclude: + - 'spec/grape/dsl/logger_spec.rb' + +# Offense count: 500 +# Configuration parameters: CountAsOne. +RSpec/ExampleLength: + Max: 57 + +# Offense count: 7 +# Cop supports --auto-correct. +RSpec/ExpectActual: + Exclude: + - 'spec/routing/**/*' + - 'spec/grape/endpoint/declared_spec.rb' + - 'spec/grape/middleware/exception_spec.rb' + +# Offense count: 3 +RSpec/ExpectInHook: + Exclude: + - 'spec/grape/api_spec.rb' + - 'spec/grape/validations/validators/values_spec.rb' + +# Offense count: 41 +# Configuration parameters: Include, CustomTransform, IgnoreMethods, SpecSuffixOnly. +# Include: **/*_spec*rb*, **/spec/**/* +RSpec/FilePath: + Enabled: false + +# Offense count: 2 +RSpec/IdenticalEqualityAssertion: + Exclude: + - 'spec/grape/middleware/base_spec.rb' + +# Offense count: 38 +# Configuration parameters: AssignmentOnly. +RSpec/InstanceVariable: + Exclude: + - 'spec/grape/api_spec.rb' + - 'spec/grape/endpoint_spec.rb' + - 'spec/grape/middleware/base_spec.rb' + - 'spec/grape/middleware/versioner/accept_version_header_spec.rb' + - 'spec/grape/middleware/versioner/header_spec.rb' + - 'spec/grape/validations/validators/except_values_spec.rb' + +# Offense count: 4 +RSpec/IteratedExpectation: + Exclude: + - 'spec/grape/middleware/formatter_spec.rb' + +# Offense count: 90 +RSpec/LeakyConstantDeclaration: + Enabled: false + +# Offense count: 1 +RSpec/LetSetup: + Exclude: + - 'spec/grape/integration/rack_spec.rb' + +# Offense count: 2 +RSpec/MessageChain: + Exclude: + - 'spec/grape/middleware/formatter_spec.rb' + +# Offense count: 137 +# Configuration parameters: . +# SupportedStyles: have_received, receive +RSpec/MessageSpies: + EnforcedStyle: receive + +# Offense count: 12 +RSpec/MissingExampleGroupArgument: + Exclude: + - 'spec/grape/middleware/exception_spec.rb' + +# Offense count: 755 +RSpec/MultipleExpectations: + Max: 16 + +# Offense count: 11 +# Configuration parameters: AllowSubject. +RSpec/MultipleMemoizedHelpers: + Max: 10 + +# Offense count: 2118 +# Configuration parameters: IgnoreSharedExamples. +RSpec/NamedSubject: + Enabled: false + +# Offense count: 157 +RSpec/NestedGroups: + Max: 6 + +# Offense count: 12 +RSpec/RepeatedDescription: + Exclude: + - 'spec/grape/api_spec.rb' + - 'spec/grape/endpoint_spec.rb' + - 'spec/grape/validations/validators/allow_blank_spec.rb' + - 'spec/grape/validations/validators/values_spec.rb' + +# Offense count: 10 +RSpec/RepeatedExample: + Exclude: + - 'spec/grape/api_spec.rb' + - 'spec/grape/dsl/request_response_spec.rb' + - 'spec/grape/middleware/versioner/accept_version_header_spec.rb' + - 'spec/grape/validations/validators/allow_blank_spec.rb' + +# Offense count: 10 +RSpec/RepeatedExampleGroupDescription: + Exclude: + - 'spec/grape/api_spec.rb' + - 'spec/grape/endpoint_spec.rb' + - 'spec/grape/util/inheritable_setting_spec.rb' + - 'spec/grape/validations/validators/values_spec.rb' + +# Offense count: 6 +RSpec/ScatteredSetup: + Exclude: + - 'spec/grape/util/inheritable_setting_spec.rb' + - 'spec/grape/validations_spec.rb' + +# Offense count: 9 +RSpec/StubbedMock: + Exclude: + - 'spec/grape/api_spec.rb' + - 'spec/grape/dsl/inside_route_spec.rb' + - 'spec/grape/dsl/routing_spec.rb' + - 'spec/grape/middleware/formatter_spec.rb' + - 'spec/grape/parser_spec.rb' + +# Offense count: 29 +RSpec/SubjectStub: + Exclude: + - 'spec/grape/api_spec.rb' + - 'spec/grape/dsl/inside_route_spec.rb' + - 'spec/grape/middleware/base_spec.rb' + - 'spec/grape/middleware/formatter_spec.rb' + - 'spec/grape/middleware/globals_spec.rb' + - 'spec/grape/middleware/stack_spec.rb' + - 'spec/grape/parser_spec.rb' + +# Offense count: 25 +# Configuration parameters: IgnoreNameless, IgnoreSymbolicNames. +RSpec/VerifiedDoubles: + Exclude: + - 'spec/grape/api_spec.rb' + - 'spec/grape/dsl/inside_route_spec.rb' + - 'spec/grape/dsl/logger_spec.rb' + - 'spec/grape/integration/rack_sendfile_spec.rb' + - 'spec/grape/middleware/formatter_spec.rb' + - 'spec/grape/validations/multiple_attributes_iterator_spec.rb' + - 'spec/grape/validations/single_attribute_iterator_spec.rb' + +# Offense count: 2 +RSpec/VoidExpect: + Exclude: + - 'spec/grape/api_spec.rb' + - 'spec/grape/dsl/headers_spec.rb' + # Offense count: 1 Style/CombinableLoops: Exclude: @@ -161,7 +364,7 @@ Style/OptionalBooleanParameter: - 'lib/grape/validations/types/primitive_coercer.rb' - 'lib/grape/validations/types/set_coercer.rb' -# Offense count: 132 +# Offense count: 146 # Cop supports --auto-correct. # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. # URISchemes: http, https diff --git a/CHANGELOG.md b/CHANGELOG.md index 72327e5a5..8b194db26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ * [#2192](https://github.com/ruby-grape/grape/pull/2192): Memoize the result of Grape::Middleware::Base#response - [@Jack12816](https://github.com/Jack12816). * [#2200](https://github.com/ruby-grape/grape/pull/2200): Add validators module to all validators - [@ericproulx](https://github.com/ericproulx). * [#2202](https://github.com/ruby-grape/grape/pull/2202): Fix random mock spec error - [@ericproulx](https://github.com/ericproulx). +* [#2203](https://github.com/ruby-grape/grape/pull/2203): Add rubocop-rspec - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 1.6.0 (2021/10/04) diff --git a/Gemfile b/Gemfile index fded526d1..ee9970adf 100644 --- a/Gemfile +++ b/Gemfile @@ -10,9 +10,10 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '~> 1.21.0' - gem 'rubocop-ast', '~> 1.11.0' - gem 'rubocop-performance', '~> 1.11.5', require: false + gem 'rubocop', '~> 1.23.0' + gem 'rubocop-ast', '~> 1.14.0' + gem 'rubocop-performance', require: false + gem 'rubocop-rspec', require: false end group :development do diff --git a/gemfiles/multi_json.gemfile b/gemfiles/multi_json.gemfile index 28ee62972..e14434a60 100644 --- a/gemfiles/multi_json.gemfile +++ b/gemfiles/multi_json.gemfile @@ -10,9 +10,10 @@ 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 + gem 'rubocop', '~> 1.23.0' + gem 'rubocop-ast', '~> 1.14.0' + gem 'rubocop-performance', require: false + gem 'rubocop-rspec', require: false end group :development do @@ -37,4 +38,8 @@ group :test do gem 'test-prof', require: false end +platforms :jruby do + gem 'racc' +end + gemspec path: '../' diff --git a/gemfiles/multi_xml.gemfile b/gemfiles/multi_xml.gemfile index 9f2631e2f..6d7593f05 100644 --- a/gemfiles/multi_xml.gemfile +++ b/gemfiles/multi_xml.gemfile @@ -10,9 +10,10 @@ 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 + gem 'rubocop', '~> 1.23.0' + gem 'rubocop-ast', '~> 1.14.0' + gem 'rubocop-performance', require: false + gem 'rubocop-rspec', require: false end group :development do @@ -37,4 +38,8 @@ group :test do gem 'test-prof', require: false end +platforms :jruby do + gem 'racc' +end + gemspec path: '../' diff --git a/gemfiles/rack1.gemfile b/gemfiles/rack1.gemfile index 87d0558d6..688c9866a 100644 --- a/gemfiles/rack1.gemfile +++ b/gemfiles/rack1.gemfile @@ -10,9 +10,10 @@ 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 + gem 'rubocop', '~> 1.23.0' + gem 'rubocop-ast', '~> 1.14.0' + gem 'rubocop-performance', require: false + gem 'rubocop-rspec', require: false end group :development do @@ -37,4 +38,8 @@ group :test do gem 'test-prof', require: false end +platforms :jruby do + gem 'racc' +end + gemspec path: '../' diff --git a/gemfiles/rack2.gemfile b/gemfiles/rack2.gemfile index a40bc53d6..1115914cf 100644 --- a/gemfiles/rack2.gemfile +++ b/gemfiles/rack2.gemfile @@ -10,9 +10,10 @@ 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 + gem 'rubocop', '~> 1.23.0' + gem 'rubocop-ast', '~> 1.14.0' + gem 'rubocop-performance', require: false + gem 'rubocop-rspec', require: false end group :development do @@ -37,4 +38,8 @@ group :test do gem 'test-prof', require: false end +platforms :jruby do + gem 'racc' +end + gemspec path: '../' diff --git a/gemfiles/rack2_2.gemfile b/gemfiles/rack2_2.gemfile index 1c8214330..78a013c03 100644 --- a/gemfiles/rack2_2.gemfile +++ b/gemfiles/rack2_2.gemfile @@ -10,9 +10,10 @@ 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 + gem 'rubocop', '~> 1.23.0' + gem 'rubocop-ast', '~> 1.14.0' + gem 'rubocop-performance', require: false + gem 'rubocop-rspec', require: false end group :development do @@ -37,4 +38,8 @@ group :test do gem 'test-prof', require: false end +platforms :jruby do + gem 'racc' +end + gemspec path: '../' diff --git a/gemfiles/rack_edge.gemfile b/gemfiles/rack_edge.gemfile index 06955ea3c..91ad3bdef 100644 --- a/gemfiles/rack_edge.gemfile +++ b/gemfiles/rack_edge.gemfile @@ -10,9 +10,10 @@ 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 + gem 'rubocop', '~> 1.23.0' + gem 'rubocop-ast', '~> 1.14.0' + gem 'rubocop-performance', require: false + gem 'rubocop-rspec', require: false end group :development do @@ -37,4 +38,8 @@ group :test do gem 'test-prof', require: false end +platforms :jruby do + gem 'racc' +end + gemspec path: '../' diff --git a/gemfiles/rails_5.gemfile b/gemfiles/rails_5.gemfile index 5d7c30e67..330213fba 100644 --- a/gemfiles/rails_5.gemfile +++ b/gemfiles/rails_5.gemfile @@ -10,9 +10,10 @@ 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 + gem 'rubocop', '~> 1.23.0' + gem 'rubocop-ast', '~> 1.14.0' + gem 'rubocop-performance', require: false + gem 'rubocop-rspec', require: false end group :development do @@ -37,4 +38,8 @@ group :test do gem 'test-prof', require: false end +platforms :jruby do + gem 'racc' +end + gemspec path: '../' diff --git a/gemfiles/rails_6.gemfile b/gemfiles/rails_6.gemfile index 9ffed6ac9..dcd1b7714 100644 --- a/gemfiles/rails_6.gemfile +++ b/gemfiles/rails_6.gemfile @@ -10,9 +10,10 @@ 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 + gem 'rubocop', '~> 1.23.0' + gem 'rubocop-ast', '~> 1.14.0' + gem 'rubocop-performance', require: false + gem 'rubocop-rspec', require: false end group :development do @@ -37,4 +38,8 @@ group :test do gem 'test-prof', require: false end +platforms :jruby do + gem 'racc' +end + gemspec path: '../' diff --git a/gemfiles/rails_6_1.gemfile b/gemfiles/rails_6_1.gemfile index d441b184d..bbb35078f 100644 --- a/gemfiles/rails_6_1.gemfile +++ b/gemfiles/rails_6_1.gemfile @@ -10,9 +10,10 @@ 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 + gem 'rubocop', '~> 1.23.0' + gem 'rubocop-ast', '~> 1.14.0' + gem 'rubocop-performance', require: false + gem 'rubocop-rspec', require: false end group :development do @@ -37,4 +38,8 @@ group :test do gem 'test-prof', require: false end +platforms :jruby do + gem 'racc' +end + gemspec path: '../' diff --git a/gemfiles/rails_edge.gemfile b/gemfiles/rails_edge.gemfile index e4c5e6919..ca650a61a 100644 --- a/gemfiles/rails_edge.gemfile +++ b/gemfiles/rails_edge.gemfile @@ -10,9 +10,10 @@ 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 + gem 'rubocop', '~> 1.23.0' + gem 'rubocop-ast', '~> 1.14.0' + gem 'rubocop-performance', require: false + gem 'rubocop-rspec', require: false end group :development do @@ -37,4 +38,8 @@ group :test do gem 'test-prof', require: false end +platforms :jruby do + gem 'racc' +end + gemspec path: '../' diff --git a/spec/grape/api/custom_validations_spec.rb b/spec/grape/api/custom_validations_spec.rb index f9000ec65..8524f86f2 100644 --- a/spec/grape/api/custom_validations_spec.rb +++ b/spec/grape/api/custom_validations_spec.rb @@ -4,6 +4,17 @@ describe Grape::Validations do context 'using a custom length validator' do + subject do + Class.new(Grape::API) do + params do + requires :text, default_length: 140 + end + get do + 'bacon' + end + end + end + let(:default_length_validator) do Class.new(Grape::Validations::Validators::Base) do def validate_param!(attr_name, params) @@ -16,22 +27,11 @@ def validate_param!(attr_name, params) end before do - Grape::Validations.register_validator('default_length', default_length_validator) + described_class.register_validator('default_length', default_length_validator) end after do - Grape::Validations.deregister_validator('default_length') - end - - subject do - Class.new(Grape::API) do - params do - requires :text, default_length: 140 - end - get do - 'bacon' - end - end + described_class.deregister_validator('default_length') end def app @@ -43,11 +43,13 @@ def app expect(last_response.status).to eq 200 expect(last_response.body).to eq 'bacon' end + it 'over 140 characters' do get '/', text: 'a' * 141 expect(last_response.status).to eq 400 expect(last_response.body).to eq 'text must be at the most 140 characters long' end + it 'specified in the query string' do get '/', text: 'a' * 141, max: 141 expect(last_response.status).to eq 200 @@ -56,6 +58,17 @@ def app end context 'using a custom body-only validator' do + subject do + Class.new(Grape::API) do + params do + requires :text, in_body: true + end + get do + 'bacon' + end + end + end + let(:in_body_validator) do Class.new(Grape::Validations::Validators::PresenceValidator) do def validate(request) @@ -65,22 +78,11 @@ def validate(request) end before do - Grape::Validations.register_validator('in_body', in_body_validator) + described_class.register_validator('in_body', in_body_validator) end after do - Grape::Validations.deregister_validator('in_body') - end - - subject do - Class.new(Grape::API) do - params do - requires :text, in_body: true - end - get do - 'bacon' - end - end + described_class.deregister_validator('in_body') end def app @@ -92,6 +94,7 @@ def app expect(last_response.status).to eq 200 expect(last_response.body).to eq 'bacon' end + it 'ignores field in query' do get '/', nil, text: 'abc' expect(last_response.status).to eq 400 @@ -100,6 +103,17 @@ def app end context 'using a custom validator with message_key' do + subject do + Class.new(Grape::API) do + params do + requires :text, with_message_key: true + end + get do + 'bacon' + end + end + end + let(:message_key_validator) do Class.new(Grape::Validations::Validators::PresenceValidator) do def validate_param!(attr_name, _params) @@ -109,22 +123,11 @@ def validate_param!(attr_name, _params) end before do - Grape::Validations.register_validator('with_message_key', message_key_validator) + described_class.register_validator('with_message_key', message_key_validator) end after do - Grape::Validations.deregister_validator('with_message_key') - end - - subject do - Class.new(Grape::API) do - params do - requires :text, with_message_key: true - end - get do - 'bacon' - end - end + described_class.deregister_validator('with_message_key') end def app @@ -139,6 +142,19 @@ def app end context 'using a custom request/param validator' do + subject do + Class.new(Grape::API) do + params do + optional :admin_field, type: String, admin: true + optional :non_admin_field, type: String + optional :admin_false_field, type: String, admin: false + end + get do + 'bacon' + end + end + end + let(:admin_validator) do Class.new(Grape::Validations::Validators::Base) do def validate(request) @@ -155,24 +171,11 @@ def validate(request) end before do - Grape::Validations.register_validator('admin', admin_validator) + described_class.register_validator('admin', admin_validator) end after do - Grape::Validations.deregister_validator('admin') - end - - subject do - Class.new(Grape::API) do - params do - optional :admin_field, type: String, admin: true - optional :non_admin_field, type: String - optional :admin_false_field, type: String, admin: false - end - get do - 'bacon' - end - end + described_class.deregister_validator('admin') end def app diff --git a/spec/grape/api/deeply_included_options_spec.rb b/spec/grape/api/deeply_included_options_spec.rb index 71cc1385b..4f27d04ff 100644 --- a/spec/grape/api/deeply_included_options_spec.rb +++ b/spec/grape/api/deeply_included_options_spec.rb @@ -41,18 +41,18 @@ def app it 'works for unspecified format' do get '/users' - expect(last_response.status).to eql 200 + expect(last_response.status).to be 200 expect(last_response.content_type).to eql 'application/json' end it 'works for specified format' do get '/users.json' - expect(last_response.status).to eql 200 + expect(last_response.status).to be 200 expect(last_response.content_type).to eql 'application/json' end it "doesn't work for format different than specified" do get '/users.txt' - expect(last_response.status).to eql 404 + expect(last_response.status).to be 404 end end diff --git a/spec/grape/api/defines_boolean_in_params_spec.rb b/spec/grape/api/defines_boolean_in_params_spec.rb index 8a0302a23..951865a87 100644 --- a/spec/grape/api/defines_boolean_in_params_spec.rb +++ b/spec/grape/api/defines_boolean_in_params_spec.rb @@ -31,8 +31,9 @@ def app 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' + expect(subject).to eq 'Grape::API::Boolean' end end end diff --git a/spec/grape/api/invalid_format_spec.rb b/spec/grape/api/invalid_format_spec.rb index e3e78f7be..23e5dbadb 100644 --- a/spec/grape/api/invalid_format_spec.rb +++ b/spec/grape/api/invalid_format_spec.rb @@ -31,11 +31,13 @@ def app expect(last_response.status).to eq 200 expect(last_response.body).to eq(::Grape::Json.dump(id: 'foo', format: nil)) end + it 'json format' do get '/foo.json' expect(last_response.status).to eq 200 expect(last_response.body).to eq(::Grape::Json.dump(id: 'foo', format: 'json')) end + it 'invalid format' do get '/foo.invalid' expect(last_response.status).to eq 200 diff --git a/spec/grape/api/recognize_path_spec.rb b/spec/grape/api/recognize_path_spec.rb index 0d821f57b..c521cbb95 100644 --- a/spec/grape/api/recognize_path_spec.rb +++ b/spec/grape/api/recognize_path_spec.rb @@ -4,7 +4,7 @@ describe Grape::API do describe '.recognize_path' do - subject { Class.new(Grape::API) } + subject { Class.new(described_class) } it 'fetches endpoint by given path' do subject.get('/foo/:id') {} 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 049fd8706..461e79d76 100644 --- a/spec/grape/api/shared_helpers_exactly_one_of_spec.rb +++ b/spec/grape/api/shared_helpers_exactly_one_of_spec.rb @@ -3,19 +3,17 @@ require 'spec_helper' describe Grape::API::Helpers do - subject do - shared_params = Module.new do - extend Grape::API::Helpers + let(:app) do + Class.new(Grape::API) do + helpers Module.new do + extend Grape::API::Helpers - params :drink do - optional :beer - optional :wine - exactly_one_of :beer, :wine + params :drink do + optional :beer + optional :wine + exactly_one_of :beer, :wine + end end - end - - Class.new(Grape::API) do - helpers shared_params format :json params do @@ -35,10 +33,6 @@ end end - def app - subject - end - it 'defines parameters' do get '/', orderType: 'food', pizza: 'mista' expect(last_response.status).to eq 200 diff --git a/spec/grape/api_remount_spec.rb b/spec/grape/api_remount_spec.rb index ff764293c..715cb8a07 100644 --- a/spec/grape/api_remount_spec.rb +++ b/spec/grape/api_remount_spec.rb @@ -4,8 +4,9 @@ require 'shared/versioning_examples' describe Grape::API do - subject(:a_remounted_api) { Class.new(Grape::API) } - let(:root_api) { Class.new(Grape::API) } + subject(:a_remounted_api) { Class.new(described_class) } + + let(:root_api) { Class.new(described_class) } def app root_api @@ -68,7 +69,7 @@ def app 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 + Class.new(described_class) do get 'always' do 'success' end @@ -101,7 +102,7 @@ def app context 'when using an expression derived from a configuration' do subject(:a_remounted_api) do - Class.new(Grape::API) do + Class.new(described_class) do get(mounted { "api_name_#{configuration[:api_name]}" }) do 'success' end @@ -126,7 +127,7 @@ def app context 'when the expression lives in a namespace' do subject(:a_remounted_api) do - Class.new(Grape::API) do + Class.new(described_class) do namespace :base do get(mounted { "api_name_#{configuration[:api_name]}" }) do 'success' @@ -149,7 +150,7 @@ def app 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 + Class.new(described_class) do mounted do desc configuration[:description] do headers configuration[:headers] @@ -191,7 +192,7 @@ def app context 'when executing a custom block on mount' do subject(:a_remounted_api) do - Class.new(Grape::API) do + Class.new(described_class) do get 'always' do 'success' end @@ -215,7 +216,7 @@ def app context 'when the configuration is part of the arguments of a method' do subject(:a_remounted_api) do - Class.new(Grape::API) do + Class.new(described_class) do get configuration[:endpoint_name] do 'success' end @@ -237,7 +238,7 @@ def app context 'when the configuration is the value in a key-arg pair' do subject(:a_remounted_api) do - Class.new(Grape::API) do + Class.new(described_class) do version 'v1', using: :param, parameter: configuration[:version_param] get 'endpoint' do 'version 1' @@ -267,7 +268,7 @@ def app context 'on the DescSCope' do subject(:a_remounted_api) do - Class.new(Grape::API) do + Class.new(described_class) do desc 'The description of this' do tags ['not_configurable_tag', configuration[:a_configurable_tag]] end @@ -284,7 +285,7 @@ def app context 'on the ParamScope' do subject(:a_remounted_api) do - Class.new(Grape::API) do + Class.new(described_class) do params do requires configuration[:required_param], type: configuration[:required_type] end @@ -314,7 +315,7 @@ def app context 'on dynamic checks' do subject(:a_remounted_api) do - Class.new(Grape::API) do + Class.new(described_class) do params do optional :restricted_values, values: -> { [configuration[:allowed_value], 'always'] } end @@ -363,7 +364,7 @@ def app context 'a very complex configuration example' do before do - top_level_api = Class.new(Grape::API) do + top_level_api = Class.new(described_class) do remounted_api = Class.new(Grape::API) do get configuration[:endpoint_name] do configuration[:response] @@ -431,7 +432,7 @@ def app context 'when the configuration is read in a helper' do subject(:a_remounted_api) do - Class.new(Grape::API) do + Class.new(described_class) do helpers do def printed_response configuration[:some_value] @@ -454,7 +455,7 @@ def printed_response context 'when the configuration is read within the response block' do subject(:a_remounted_api) do - Class.new(Grape::API) do + Class.new(described_class) do get 'location' do configuration[:some_value] end diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index 576925e9f..fb4086aec 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -4,7 +4,10 @@ require 'shared/versioning_examples' describe Grape::API do - subject { Class.new(Grape::API) } + subject do + puts described_class + Class.new(described_class) + end def app subject @@ -18,7 +21,7 @@ def app end get 'awesome/sauce/' - expect(last_response.status).to eql 200 + expect(last_response.status).to be 200 expect(last_response.body).to eql 'Hello there.' end @@ -32,7 +35,7 @@ def app expect(last_response.body).to eql 'Hello there.' get '/hello' - expect(last_response.status).to eql 404 + expect(last_response.status).to be 404 end it 'supports OPTIONS' do @@ -42,7 +45,7 @@ def app end options 'awesome/sauce' - expect(last_response.status).to eql 204 + expect(last_response.status).to be 204 expect(last_response.body).to be_blank end @@ -51,7 +54,7 @@ def app subject.get post 'awesome/sauce' - expect(last_response.status).to eql 405 + expect(last_response.status).to be 405 end end @@ -71,7 +74,7 @@ def app end describe '.version using path' do - it_should_behave_like 'versioning' do + it_behaves_like 'versioning' do let(:macro_options) do { using: :path @@ -81,7 +84,7 @@ def app end describe '.version using param' do - it_should_behave_like 'versioning' do + it_behaves_like 'versioning' do let(:macro_options) do { using: :param, @@ -92,7 +95,7 @@ def app end describe '.version using header' do - it_should_behave_like 'versioning' do + it_behaves_like 'versioning' do let(:macro_options) do { using: :header, @@ -120,7 +123,7 @@ def app end describe '.version using accept_version_header' do - it_should_behave_like 'versioning' do + it_behaves_like 'versioning' do let(:macro_options) do { using: :accept_version_header @@ -389,7 +392,7 @@ class DummyFormatClass end end - before(:each) do + before do allow_any_instance_of(ApiSpec::DummyFormatClass).to receive(:to_json).and_return('abc') allow_any_instance_of(ApiSpec::DummyFormatClass).to receive(:to_txt).and_return('def') @@ -447,7 +450,7 @@ class DummyFormatClass end %i[put post].each do |verb| - context verb do + context verb.to_s do ['string', :symbol, 1, -1.1, {}, [], true, false, nil].each do |object| it "allows a(n) #{object.class} json object in params" do subject.format :json @@ -459,6 +462,7 @@ class DummyFormatClass expect(last_response.body).to eql ::Grape::Json.dump(object) expect(last_request.params).to eql({}) end + it 'stores input in api.request.input' do subject.format :json subject.send(verb) do @@ -468,6 +472,7 @@ class DummyFormatClass expect(last_response.status).to eq(verb == :post ? 201 : 200) expect(last_response.body).to eql ::Grape::Json.dump(object).to_json end + context 'chunked transfer encoding' do it 'stores input in api.request.input' do subject.format :json @@ -562,7 +567,8 @@ class DummyFormatClass send(other_verb, '/example') expected_rc = if other_verb == 'options' then 204 elsif other_verb == 'head' && verb == 'get' then 200 - else 405 + else + 405 end expect(last_response.status).to eql expected_rc end @@ -575,7 +581,7 @@ class DummyFormatClass end post '/example' - expect(last_response.status).to eql 201 + expect(last_response.status).to be 201 expect(last_response.body).to eql 'Created' end @@ -585,7 +591,7 @@ class DummyFormatClass 'example' end put '/example' - expect(last_response.status).to eql 405 + expect(last_response.status).to be 405 expect(last_response.body).to eql '405 Not Allowed' expect(last_response.headers['X-Custom-Header']).to eql 'foo' end @@ -593,15 +599,17 @@ class DummyFormatClass it 'runs only the before filter on 405 bad method' do subject.namespace :example do before { header 'X-Custom-Header', 'foo' } + before_validation { raise 'before_validation filter should not run' } after_validation { raise 'after_validation filter should not run' } after { raise 'after filter should not run' } + params { requires :only_for_get } get end post '/example' - expect(last_response.status).to eql 405 + expect(last_response.status).to be 405 expect(last_response.headers['X-Custom-Header']).to eql 'foo' end @@ -614,26 +622,29 @@ class DummyFormatClass already_run = true header 'X-Custom-Header', 'foo' end + get end post '/example' - expect(last_response.status).to eql 405 + expect(last_response.status).to be 405 expect(last_response.headers['X-Custom-Header']).to eql 'foo' end it 'runs all filters and body with a custom OPTIONS method' do subject.namespace :example do before { header 'X-Custom-Header-1', 'foo' } + before_validation { header 'X-Custom-Header-2', 'foo' } after_validation { header 'X-Custom-Header-3', 'foo' } after { header 'X-Custom-Header-4', 'foo' } + options { 'yup' } get end options '/example' - expect(last_response.status).to eql 200 + expect(last_response.status).to be 200 expect(last_response.body).to eql 'yup' expect(last_response.headers['Allow']).to be_nil expect(last_response.headers['X-Custom-Header-1']).to eql 'foo' @@ -650,7 +661,7 @@ class DummyFormatClass end put '/example' - expect(last_response.status).to eql 405 + expect(last_response.status).to be 405 expect(last_response.body).to eq <<~XML @@ -670,7 +681,7 @@ class DummyFormatClass 'example' end put '/example' - expect(last_response.status).to eql 405 + expect(last_response.status).to be 405 expect(last_response.body).to eql '405 Not Allowed' end end @@ -714,7 +725,7 @@ class DummyFormatClass end it 'returns a 204' do - expect(last_response.status).to eql 204 + expect(last_response.status).to be 204 end it 'has an empty body' do @@ -778,7 +789,7 @@ class DummyFormatClass describe 'it adds an OPTIONS route for namespaced endpoints that' do it 'returns a 204' do - expect(last_response.status).to eql 204 + expect(last_response.status).to be 204 end it 'has an empty body' do @@ -796,6 +807,7 @@ class DummyFormatClass subject.before { header 'X-Custom-Header', 'foo' } subject.namespace :example do before { header 'X-Custom-Header-2', 'foo' } + get :inner do 'example/inner' end @@ -804,7 +816,7 @@ class DummyFormatClass end it 'returns a 204' do - expect(last_response.status).to eql 204 + expect(last_response.status).to be 204 end it 'has an empty body' do @@ -842,7 +854,7 @@ class DummyFormatClass end it 'returns a 405' do - expect(last_response.status).to eql 405 + expect(last_response.status).to be 405 end it 'contains error message in body' do @@ -858,7 +870,7 @@ class DummyFormatClass end end - describe 'when hook behaviour is controlled by attributes on the route ' do + 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] @@ -881,28 +893,31 @@ class DummyFormatClass let(:response) { delete('/example') } it 'responds with a 405 status' do - expect(response.status).to eql 405 + expect(response.status).to be 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 + expect(response.status).to be 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 + expect(response.status).to be 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 + expect(response.status).to be 201 end end @@ -910,7 +925,7 @@ class DummyFormatClass 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 + expect(response.status).to be 401 end end @@ -918,7 +933,7 @@ class DummyFormatClass 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 + expect(response.status).to be 200 end end end @@ -935,7 +950,7 @@ class DummyFormatClass end it 'returns a 200' do - expect(last_response.status).to eql 200 + expect(last_response.status).to be 200 end it 'has an empty body' do @@ -951,31 +966,33 @@ class DummyFormatClass 'example' end head '/example' - expect(last_response.status).to eql 400 + expect(last_response.status).to be 400 end end context 'do_not_route_head!' do - before :each do + before do subject.do_not_route_head! subject.get 'example' do 'example' end end + it 'options does not contain HEAD' do options '/example' - expect(last_response.status).to eql 204 + expect(last_response.status).to be 204 expect(last_response.body).to eql '' expect(last_response.headers['Allow']).to eql 'OPTIONS, GET' end + it 'does not allow HEAD on a GET request' do head '/example' - expect(last_response.status).to eql 405 + expect(last_response.status).to be 405 end end context 'do_not_route_options!' do - before :each do + before do subject.do_not_route_options! subject.get 'example' do 'example' @@ -984,19 +1001,19 @@ class DummyFormatClass it 'does not create an OPTIONS route' do options '/example' - expect(last_response.status).to eql 405 + expect(last_response.status).to be 405 end it 'does not include OPTIONS in Allow header' do options '/example' - expect(last_response.status).to eql 405 + expect(last_response.status).to be 405 expect(last_response.headers['Allow']).to eql 'GET, HEAD' end end describe '.compile!' do it 'requires the grape/eager_load file' do - expect(app).to receive(:require).with('grape/eager_load') { nil } + expect(app).to receive(:require).with('grape/eager_load').and_return(nil) app.compile! end @@ -1018,7 +1035,7 @@ class DummyFormatClass context 'when the app was mounted' do it 'returns the first mounted instance' do mounted_app = app - Class.new(Grape::API) do + Class.new(described_class) do namespace 'new_namespace' do mount mounted_app end @@ -1046,6 +1063,7 @@ class DummyFormatClass end subject.namespace :blah do before { @foo = 'foo' } + get '/' do "blah - #{@foo}" end @@ -1087,7 +1105,7 @@ class DummyFormatClass @var ||= 'default' end - expect(m).to receive(:do_something!).exactly(2).times + expect(m).to receive(:do_something!).twice get '/' expect(last_response.body).to eql 'default' end @@ -1103,21 +1121,23 @@ class DummyFormatClass end subject.resource ':id' do before { a.do_something! } + before_validation { b.do_something! } after_validation { c.do_something! } after { d.do_something! } + get do 'got it' end end - expect(a).to receive(:do_something!).exactly(1).times - expect(b).to receive(:do_something!).exactly(1).times - expect(c).to receive(:do_something!).exactly(1).times - expect(d).to receive(:do_something!).exactly(1).times + expect(a).to receive(:do_something!).once + expect(b).to receive(:do_something!).once + expect(c).to receive(:do_something!).once + expect(d).to receive(:do_something!).once get '/123' - expect(last_response.status).to eql 200 + expect(last_response.status).to be 200 expect(last_response.body).to eql 'got it' end @@ -1132,21 +1152,23 @@ class DummyFormatClass end subject.resource ':id' do before { a.do_something! } + before_validation { b.do_something! } after_validation { c.do_something! } after { d.do_something! } + get do 'got it' end end - expect(a).to receive(:do_something!).exactly(1).times - expect(b).to receive(:do_something!).exactly(1).times + expect(a).to receive(:do_something!).once + expect(b).to receive(:do_something!).once expect(c).to receive(:do_something!).exactly(0).times expect(d).to receive(:do_something!).exactly(0).times get '/abc' - expect(last_response.status).to eql 400 + expect(last_response.status).to be 400 expect(last_response.body).to eql 'id is invalid' end @@ -1162,21 +1184,23 @@ class DummyFormatClass end subject.resource ':id' do before { a.here(i += 1) } + before_validation { b.here(i += 1) } after_validation { c.here(i += 1) } after { d.here(i += 1) } + get do 'got it' end end - expect(a).to receive(:here).with(1).exactly(1).times - expect(b).to receive(:here).with(2).exactly(1).times - expect(c).to receive(:here).with(3).exactly(1).times - expect(d).to receive(:here).with(4).exactly(1).times + expect(a).to receive(:here).with(1).once + expect(b).to receive(:here).with(2).once + expect(c).to receive(:here).with(3).once + expect(d).to receive(:here).with(4).once get '/123' - expect(last_response.status).to eql 200 + expect(last_response.status).to be 200 expect(last_response.body).to eql 'got it' end end @@ -1267,7 +1291,7 @@ class DummyFormatClass subject.format :json subject.get('/error') { error!('error in json', 500) } get '/error.json' - expect(last_response.status).to eql 500 + expect(last_response.status).to be 500 expect(last_response.headers['Content-Type']).to eql 'application/json' end @@ -1275,7 +1299,7 @@ class DummyFormatClass subject.format :xml subject.get('/error') { error!('error in xml', 500) } get '/error' - expect(last_response.status).to eql 500 + expect(last_response.status).to be 500 expect(last_response.headers['Content-Type']).to eql 'application/xml' end @@ -1534,9 +1558,9 @@ def call(env) end subject.get(:hello) { 'Hello, world.' } get '/hello' - expect(last_response.status).to eql 401 + expect(last_response.status).to be 401 get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever') - expect(last_response.status).to eql 200 + expect(last_response.status).to be 200 end it 'is scopable' do @@ -1550,9 +1574,9 @@ def call(env) end get '/hello' - expect(last_response.status).to eql 200 + expect(last_response.status).to be 200 get '/admin/hello' - expect(last_response.status).to eql 401 + expect(last_response.status).to be 401 end it 'is callable via .auth as well' do @@ -1562,9 +1586,9 @@ def call(env) subject.get(:hello) { 'Hello, world.' } get '/hello' - expect(last_response.status).to eql 401 + expect(last_response.status).to be 401 get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever') - expect(last_response.status).to eql 200 + expect(last_response.status).to be 200 end it 'has access to the current endpoint' do @@ -1594,9 +1618,9 @@ def authorize(u, p) subject.get(:hello) { 'Hello, world.' } get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever') - expect(last_response.status).to eql 200 + expect(last_response.status).to be 200 get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('disallow', 'whatever') - expect(last_response.status).to eql 401 + expect(last_response.status).to be 401 end it 'can set instance variables accessible to routes' do @@ -1608,14 +1632,14 @@ def authorize(u, p) subject.get(:hello) { @hello } get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever') - expect(last_response.status).to eql 200 + expect(last_response.status).to be 200 expect(last_response.body).to eql 'Hello, world.' end end describe '.logger' do subject do - Class.new(Grape::API) do + Class.new(described_class) do def self.io @io ||= StringIO.new end @@ -1630,7 +1654,7 @@ def self.io it 'allows setting a custom logger' do mylogger = Class.new subject.logger mylogger - expect(mylogger).to receive(:info).exactly(1).times + expect(mylogger).to receive(:info).once subject.logger.info 'this will be logged' end @@ -1645,7 +1669,7 @@ def self.io it 'does not unnecessarily retain duplicate setup blocks' do subject.logger - expect { subject.logger }.to_not change(subject.instance_variable_get(:@setup), :size) + expect { subject.logger }.not_to change(subject.instance_variable_get(:@setup), :size) end end @@ -1771,13 +1795,13 @@ def three end get '/new/abc' - expect(last_response.status).to eql 404 + expect(last_response.status).to be 404 get '/legacy/abc' - expect(last_response.status).to eql 200 + expect(last_response.status).to be 200 get '/legacy/def' - expect(last_response.status).to eql 404 + expect(last_response.status).to be 404 get '/new/def' - expect(last_response.status).to eql 200 + expect(last_response.status).to be 200 end end @@ -1997,8 +2021,8 @@ def custom_error!(name) end context 'with multiple apis' do - let(:a) { Class.new(Grape::API) } - let(:b) { Class.new(Grape::API) } + let(:a) { Class.new(described_class) } + let(:b) { Class.new(described_class) } before do a.helpers do @@ -2032,7 +2056,7 @@ def foo raise 'rain!' end get '/exception' - expect(last_response.status).to eql 500 + expect(last_response.status).to be 500 expect(last_response.body).to eq 'rain!' end @@ -2044,7 +2068,7 @@ def foo raise 'rain!' end get '/exception' - expect(last_response.status).to eql 500 + expect(last_response.status).to be 500 expect(last_response.body).to eq({ error: 'rain!' }.to_json) end @@ -2054,7 +2078,7 @@ def foo subject.get('/unrescued') { raise 'beefcake' } get '/rescued' - expect(last_response.status).to eql 500 + expect(last_response.status).to be 500 expect { get '/unrescued' }.to raise_error(RuntimeError, 'beefcake') end @@ -2073,10 +2097,10 @@ def foo subject.get('/standard_error') { raise StandardError } get '/child_of_standard_error' - expect(last_response.status).to eql 402 + expect(last_response.status).to be 402 get '/standard_error' - expect(last_response.status).to eql 401 + expect(last_response.status).to be 401 end context 'CustomError subclass of Grape::Exceptions::Base' do @@ -2117,7 +2141,7 @@ class CustomError < Grape::Exceptions::Base; end subject.get('/formatter_exception') { 'Hello world' } get '/formatter_exception' - expect(last_response.status).to eql 500 + expect(last_response.status).to be 500 expect(last_response.body).to eq('Formatter Error') end @@ -2127,7 +2151,7 @@ class CustomError < Grape::Exceptions::Base; end 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.status).to be 500 expect(last_response.body).to eql 'Invalid response' end end @@ -2141,7 +2165,7 @@ class CustomError < Grape::Exceptions::Base; end raise 'rain!' end get '/exception' - expect(last_response.status).to eql 202 + expect(last_response.status).to be 202 expect(last_response.body).to eq('rescued from rain!') end @@ -2162,9 +2186,10 @@ class CommunicationError < StandardError; end raise ConnectionError end get '/exception' - expect(last_response.status).to eql 500 + expect(last_response.status).to be 500 expect(last_response.body).to eq('rescued from ConnectionError') end + it 'rescues a specific error' do subject.rescue_from ConnectionError do |e| rack_response("rescued from #{e.class.name}", 500) @@ -2173,9 +2198,10 @@ class CommunicationError < StandardError; end raise ConnectionError end get '/exception' - expect(last_response.status).to eql 500 + expect(last_response.status).to be 500 expect(last_response.body).to eq('rescued from ConnectionError') end + it 'rescues a subclass of an error by default' do subject.rescue_from RuntimeError do |e| rack_response("rescued from #{e.class.name}", 500) @@ -2184,9 +2210,10 @@ class CommunicationError < StandardError; end raise ConnectionError end get '/exception' - expect(last_response.status).to eql 500 + expect(last_response.status).to be 500 expect(last_response.body).to eq('rescued from ConnectionError') end + it 'rescues multiple specific errors' do subject.rescue_from ConnectionError do |e| rack_response("rescued from #{e.class.name}", 500) @@ -2201,12 +2228,13 @@ class CommunicationError < StandardError; end raise DatabaseError end get '/connection' - expect(last_response.status).to eql 500 + expect(last_response.status).to be 500 expect(last_response.body).to eq('rescued from ConnectionError') get '/database' - expect(last_response.status).to eql 500 + expect(last_response.status).to be 500 expect(last_response.body).to eq('rescued from DatabaseError') end + it 'does not rescue a different error' do subject.rescue_from RuntimeError do |e| rack_response("rescued from #{e.class.name}", 500) @@ -2327,9 +2355,9 @@ class ChildError < ParentError; end end get '/caught_child' - expect(last_response.status).to eql 500 + expect(last_response.status).to be 500 get '/caught_parent' - expect(last_response.status).to eql 500 + expect(last_response.status).to be 500 expect { get '/uncaught_parent' }.to raise_error(StandardError) end @@ -2342,7 +2370,7 @@ class ChildError < ParentError; end end get '/caught_child' - expect(last_response.status).to eql 500 + expect(last_response.status).to be 500 end it 'does not rescue child errors if rescue_subclasses is false' do @@ -2437,7 +2465,7 @@ class ChildError < ParentError; end end context 'class' do - before :each do + before do module ApiSpec class CustomErrorFormatter def self.call(message, _backtrace, _options, _env, _original_exception) @@ -2446,6 +2474,7 @@ def self.call(message, _backtrace, _options, _env, _original_exception) end end end + it 'returns a custom error format' do subject.rescue_from :all, backtrace: true subject.error_formatter :txt, ApiSpec::CustomErrorFormatter @@ -2459,7 +2488,7 @@ def self.call(message, _backtrace, _options, _env, _original_exception) describe 'with' do context 'class' do - before :each do + before do module ApiSpec class CustomErrorFormatter def self.call(message, _backtrace, _option, _env, _original_exception) @@ -2489,6 +2518,7 @@ def self.call(message, _backtrace, _option, _env, _original_exception) get '/exception' expect(last_response.body).to eql '{"error":"rain!"}' end + it 'rescues all errors and return :json with backtrace' do subject.rescue_from :all, backtrace: true subject.format :json @@ -2500,6 +2530,7 @@ def self.call(message, _backtrace, _option, _env, _original_exception) expect(json['error']).to eql 'rain!' expect(json['backtrace'].length).to be > 0 end + it 'rescues error! and return txt' do subject.format :txt subject.get '/error' do @@ -2508,23 +2539,26 @@ def self.call(message, _backtrace, _option, _env, _original_exception) get '/error' expect(last_response.body).to eql 'Access Denied' end + context 'with json format' do before { subject.format :json } + after do + get '/error' + expect(last_response.body).to eql('{"error":"failure"}') + end + it 'rescues error! called with a string and returns json' do subject.get('/error') { error!(:failure, 401) } end + it 'rescues error! called with a symbol and returns json' do subject.get('/error') { error!(:failure, 401) } end + it 'rescues error! called with a hash and returns json' do subject.get('/error') { error!({ error: :failure }, 401) } end - - after do - get '/error' - expect(last_response.body).to eql('{"error":"failure"}') - end end end @@ -2537,6 +2571,7 @@ def self.call(message, _backtrace, _option, _env, _original_exception) get '/excel.xls' expect(last_response.content_type).to eq('application/vnd.ms-excel') end + it 'allows to override content-type' do subject.get :content do content_type 'text/javascript' @@ -2545,6 +2580,7 @@ def self.call(message, _backtrace, _option, _env, _original_exception) get '/content' expect(last_response.content_type).to eq('text/javascript') end + it 'removes existing content types' do subject.content_type :xls, 'application/vnd.ms-excel' subject.get :excel do @@ -2562,24 +2598,27 @@ def self.call(message, _backtrace, _option, _env, _original_exception) describe '.formatter' do context 'multiple formatters' do - before :each do + before do subject.formatter :json, ->(object, _env) { "{\"custom_formatter\":\"#{object[:some]}\"}" } subject.formatter :txt, ->(object, _env) { "custom_formatter: #{object[:some]}" } subject.get :simple do { some: 'hash' } end end + it 'sets one formatter' do get '/simple.json' expect(last_response.body).to eql '{"custom_formatter":"hash"}' end + it 'sets another formatter' do get '/simple.txt' expect(last_response.body).to eql 'custom_formatter: hash' end end + context 'custom formatter' do - before :each do + before do subject.content_type :json, 'application/json' subject.content_type :custom, 'application/custom' subject.formatter :custom, ->(object, _env) { "{\"custom_formatter\":\"#{object[:some]}\"}" } @@ -2587,15 +2626,18 @@ def self.call(message, _backtrace, _option, _env, _original_exception) { some: 'hash' } end end + it 'uses json' do get '/simple.json' expect(last_response.body).to eql '{"some":"hash"}' end + it 'uses custom formatter' do get '/simple.custom', 'HTTP_ACCEPT' => 'application/custom' expect(last_response.body).to eql '{"custom_formatter":"hash"}' end end + context 'custom formatter class' do module ApiSpec module CustomFormatter @@ -2604,7 +2646,7 @@ def self.call(object, _env) end end end - before :each do + before do subject.content_type :json, 'application/json' subject.content_type :custom, 'application/custom' subject.formatter :custom, ApiSpec::CustomFormatter @@ -2612,10 +2654,12 @@ def self.call(object, _env) { some: 'hash' } end end + it 'uses json' do get '/simple.json' expect(last_response.body).to eql '{"some":"hash"}' end + it 'uses custom formatter' do get '/simple.custom', 'HTTP_ACCEPT' => 'application/custom' expect(last_response.body).to eql '{"custom_formatter":"hash"}' @@ -2633,8 +2677,9 @@ def self.call(object, _env) expect(last_response.status).to eq(201) expect(last_response.body).to eq('{"x":42}') end + context 'lambda parser' do - before :each do + before do subject.content_type :txt, 'text/plain' subject.content_type :custom, 'text/custom' subject.parser :custom, ->(object, _env) { { object.to_sym => object.to_s.reverse } } @@ -2642,6 +2687,7 @@ def self.call(object, _env) params[:simple] end end + ['text/custom', 'text/custom; charset=UTF-8'].each do |content_type| it "uses parser for #{content_type}" do put '/simple', 'simple', 'CONTENT_TYPE' => content_type @@ -2650,6 +2696,7 @@ def self.call(object, _env) end end end + context 'custom parser class' do module ApiSpec module CustomParser @@ -2658,7 +2705,7 @@ def self.call(object, _env) end end end - before :each do + before do subject.content_type :txt, 'text/plain' subject.content_type :custom, 'text/custom' subject.parser :custom, ApiSpec::CustomParser @@ -2666,12 +2713,14 @@ def self.call(object, _env) params[:simple] end end + it 'uses custom parser' do put '/simple', 'simple', 'CONTENT_TYPE' => 'text/custom' expect(last_response.status).to eq(200) expect(last_response.body).to eql 'elpmis' end end + if Object.const_defined? :MultiXml context 'multi_xml' do it "doesn't parse yaml" do @@ -2696,12 +2745,13 @@ def self.call(object, _env) end end context 'none parser class' do - before :each do + before do subject.parser :json, nil subject.put 'data' do "body: #{env['api.request.body']}" end end + it 'does not parse data' do put '/data', 'not valid json', 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq(200) @@ -2711,10 +2761,11 @@ def self.call(object, _env) end describe '.default_format' do - before :each do + before do subject.format :json subject.default_format :json end + it 'returns data in default format' do subject.get '/data' do { x: 42 } @@ -2723,6 +2774,7 @@ def self.call(object, _env) expect(last_response.status).to eq(200) expect(last_response.body).to eq('{"x":42}') end + it 'parses data in default format' do subject.post '/data' do { x: params[:x] } @@ -2741,16 +2793,18 @@ def self.call(object, _env) raise 'rain!' end get '/exception' - expect(last_response.status).to eql 200 + expect(last_response.status).to be 200 end + it 'has a default error status' do subject.rescue_from :all subject.get '/exception' do raise 'rain!' end get '/exception' - expect(last_response.status).to eql 500 + expect(last_response.status).to be 500 end + it 'uses the default error status in error!' do subject.rescue_from :all subject.default_error_status 400 @@ -2758,7 +2812,7 @@ def self.call(object, _env) error! 'rain!' end get '/exception' - expect(last_response.status).to eql 400 + expect(last_response.status).to be 400 end end @@ -2784,7 +2838,7 @@ def static end get '/exception' - expect(last_response.status).to eql 408 + expect(last_response.status).to be 408 expect(last_response.body).to eql({ code: 408, static: 'some static text' }.to_json) end @@ -2795,7 +2849,7 @@ def static end get '/exception' - expect(last_response.status).to eql 408 + expect(last_response.status).to be 408 expect(last_response.body).to eql({ code: 408, static: 'some static text' }.to_json) end end @@ -2806,12 +2860,14 @@ def static expect(subject.routes).to eq([]) end end + describe 'single method api structure' do - before(:each) do + before do subject.get :ping do 'pong' end end + it 'returns one route' do expect(subject.routes.size).to eq(1) route = subject.routes[0] @@ -2820,8 +2876,9 @@ def static expect(route.request_method).to eq('GET') end end + describe 'api structure with two versions and a namespace' do - before :each do + before do subject.version 'v1', using: :path subject.get 'version' do api.version @@ -2837,30 +2894,37 @@ def static end end end + it 'returns the latest version set' do expect(subject.version).to eq('v2') end + it 'returns versions' do expect(subject.versions).to eq(%w[v1 v2]) end + it 'sets route paths' do expect(subject.routes.size).to be >= 2 expect(subject.routes[0].path).to eq('/:version/version(.:format)') expect(subject.routes[1].path).to eq('/p/:version/n1/n2/version(.:format)') end + it 'sets route versions' do expect(subject.routes[0].version).to eq('v1') expect(subject.routes[1].version).to eq('v2') end + it 'sets a nested namespace' do expect(subject.routes[1].namespace).to eq('/n1/n2') end + it 'sets prefix' do expect(subject.routes[1].prefix).to eq('p') end end + describe 'api structure with additional parameters' do - before(:each) do + before do subject.params do requires :token, desc: 'a token' optional :limit, desc: 'the limit' @@ -2869,14 +2933,17 @@ def static params[:string].split(params[:token], (params[:limit] || 0).to_i) end end + it 'splits a string' do get '/split/a,b,c.json', token: ',' expect(last_response.body).to eq('["a","b","c"]') end + it 'splits a string with limit' do get '/split/a,b,c.json', token: ',', limit: '2' expect(last_response.body).to eq('["a","b,c"]') end + it 'sets params' do expect(subject.routes.map do |route| { params: route.params } @@ -2891,8 +2958,9 @@ def static ] end end + describe 'api structure with multiple apis' do - before(:each) do + before do subject.params do requires :one, desc: 'a token' optional :two, desc: 'the limit' @@ -2907,6 +2975,7 @@ def static subject.get 'two' do end end + it 'sets params' do expect(subject.routes.map do |route| { params: route.params } @@ -2926,8 +2995,9 @@ def static ] end end + describe 'api structure with an api without params' do - before(:each) do + before do subject.params do requires :one, desc: 'a token' optional :two, desc: 'the limit' @@ -2938,6 +3008,7 @@ def static subject.get 'two' do end end + it 'sets params' do expect(subject.routes.map do |route| { params: route.params } @@ -2954,17 +3025,20 @@ def static ] end end + describe 'api with a custom route setting' do - before(:each) do + before do subject.route_setting :custom, key: 'value' subject.get 'one' end + it 'exposed' do expect(subject.routes.count).to eq 1 route = subject.routes.first expect(route.settings[:custom]).to eq(key: 'value') end end + describe 'status' do it 'can be set to arbitrary Integer value' do subject.get '/foo' do @@ -2973,6 +3047,7 @@ def static get '/foo' expect(last_response.status).to eq 210 end + it 'can be set with a status code symbol' do subject.get '/foo' do status :see_other @@ -2987,10 +3062,12 @@ def static it 'empty array of routes' do expect(subject.routes).to eq([]) end + it 'empty array of routes' do subject.desc 'grape api' expect(subject.routes).to eq([]) end + it 'describes a method' do subject.desc 'first method' subject.get :first @@ -3001,6 +3078,7 @@ def static expect(route.params).to eq({}) expect(route.options).to be_a_kind_of(Hash) end + it 'has params which does not include format and version as named captures' do subject.version :v1, using: :path subject.get :first @@ -3008,6 +3086,7 @@ def static expect(param_keys).not_to include('format') expect(param_keys).not_to include('version') end + it 'describes methods separately' do subject.desc 'first method' subject.get :first @@ -3021,6 +3100,7 @@ def static { description: 'second method', params: {} } ] end + it 'resets desc' do subject.desc 'first method' subject.get :first @@ -3032,6 +3112,7 @@ def static { description: nil, params: {} } ] end + it 'namespaces and describe arbitrary parameters' do subject.namespace 'ns' do desc 'ns second', foo: 'bar' @@ -3043,6 +3124,7 @@ def static { description: 'ns second', foo: 'bar', params: {} } ] end + it 'includes details' do subject.desc 'method', details: 'method details' subject.get 'method' @@ -3052,6 +3134,7 @@ def static { description: 'method', details: 'method details', params: {} } ] end + it 'describes a method with parameters' do subject.desc 'Reverses a string.', params: { 's' => { desc: 'string to reverse', type: 'string' } } subject.get 'reverse' do @@ -3063,6 +3146,7 @@ def static { description: 'Reverses a string.', params: { 's' => { desc: 'string to reverse', type: 'string' } } } ] end + it 'does not inherit param descriptions in consequent namespaces' do subject.desc 'global description' subject.params do @@ -3093,6 +3177,7 @@ def static } } ] end + it 'merges the parameters of the namespace with the parameters of the method' do subject.desc 'namespace' subject.params do @@ -3117,6 +3202,7 @@ def static } } ] end + it 'merges the parameters of nested namespaces' do subject.desc 'ns1' subject.params do @@ -3149,6 +3235,7 @@ def static } } ] end + it 'groups nested params and prevents overwriting of params with same name in different groups' do subject.desc 'method' subject.params do @@ -3172,6 +3259,7 @@ def static 'group2[param2]' => { required: true, desc: 'group2 param2 desc' } }] end + it 'uses full name of parameters in nested groups' do subject.desc 'nesting' subject.params do @@ -3192,6 +3280,7 @@ def static } } ] end + it 'allows to set the type attribute on :group element' do subject.params do group :foo, type: Array do @@ -3199,6 +3288,7 @@ def static end end end + it 'parses parameters when no description is given' do subject.params do requires :one_param, desc: 'one param' @@ -3210,6 +3300,7 @@ def static { description: nil, params: { 'one_param' => { required: true, desc: 'one param' } } } ] end + it 'does not symbolize params' do subject.desc 'Reverses a string.', params: { 's' => { desc: 'string to reverse', type: 'string' } } subject.get 'reverse/:s' do @@ -3268,7 +3359,7 @@ def static subject.version 'v1', using: :path subject.namespace :cool do - app = Class.new(Grape::API) + app = Class.new(Grape::API) # rubocop:disable RSpec/DescribedClass app.get('/awesome') do 'yo' end @@ -3284,12 +3375,12 @@ def static subject.version 'v1', using: :path subject.namespace :cool do - inner_app = Class.new(Grape::API) + inner_app = Class.new(Grape::API) # rubocop:disable RSpec/DescribedClass inner_app.get('/awesome') do 'yo' end - app = Class.new(Grape::API) + app = Class.new(Grape::API) # rubocop:disable RSpec/DescribedClass app.mount inner_app mount app end @@ -3304,7 +3395,7 @@ def static rack_response("rescued from #{e.message}", 202) end - app = Class.new(Grape::API) + app = Class.new(described_class) subject.namespace :mounted do app.rescue_from ArgumentError @@ -3313,15 +3404,16 @@ def static end get '/mounted/fail' - expect(last_response.status).to eql 202 + expect(last_response.status).to be 202 expect(last_response.body).to eq('rescued from doh!') end + it 'prefers rescues defined by mounted if they rescue similar error class' do subject.rescue_from StandardError do rack_response('outer rescue') end - app = Class.new(Grape::API) + app = Class.new(described_class) subject.namespace :mounted do rescue_from StandardError do @@ -3334,12 +3426,13 @@ def static get '/mounted/fail' expect(last_response.body).to eq('inner rescue') end + it 'prefers rescues defined by mounted even if outer is more specific' do subject.rescue_from ArgumentError do rack_response('outer rescue') end - app = Class.new(Grape::API) + app = Class.new(described_class) subject.namespace :mounted do rescue_from StandardError do @@ -3352,12 +3445,13 @@ def static get '/mounted/fail' expect(last_response.body).to eq('inner rescue') end + it 'prefers more specific rescues defined by mounted' do subject.rescue_from StandardError do rack_response('outer rescue') end - app = Class.new(Grape::API) + app = Class.new(described_class) subject.namespace :mounted do rescue_from ArgumentError do @@ -3374,7 +3468,7 @@ def static it 'collects the routes of the mounted api' do subject.namespace :cool do - app = Class.new(Grape::API) + app = Class.new(Grape::API) # rubocop:disable RSpec/DescribedClass app.get('/awesome') {} app.post('/sauce') {} mount app @@ -3386,7 +3480,7 @@ def static it 'mounts on a path' do subject.namespace :cool do - app = Class.new(Grape::API) + app = Class.new(Grape::API) # rubocop:disable RSpec/DescribedClass app.get '/awesome' do 'sauce' end @@ -3398,8 +3492,8 @@ def static end it 'mounts on a nested path' do - APP1 = Class.new(Grape::API) - APP2 = Class.new(Grape::API) + APP1 = Class.new(described_class) + APP2 = Class.new(described_class) APP2.get '/nice' do 'play' end @@ -3415,7 +3509,7 @@ def static end it 'responds to options' do - app = Class.new(Grape::API) + app = Class.new(described_class) app.get '/colour' do 'red' end @@ -3429,21 +3523,21 @@ def static end get '/apples/colour' - expect(last_response.status).to eql 200 + expect(last_response.status).to be 200 expect(last_response.body).to eq('red') options '/apples/colour' - expect(last_response.status).to eql 204 + expect(last_response.status).to be 204 get '/apples/pears/colour' - expect(last_response.status).to eql 200 + expect(last_response.status).to be 200 expect(last_response.body).to eq('green') options '/apples/pears/colour' - expect(last_response.status).to eql 204 + expect(last_response.status).to be 204 end it 'responds to options with path versioning' do subject.version 'v1', using: :path subject.namespace :apples do - app = Class.new(Grape::API) + app = Class.new(Grape::API) # rubocop:disable RSpec/DescribedClass app.get('/colour') do 'red' end @@ -3451,14 +3545,14 @@ def static end get '/v1/apples/colour' - expect(last_response.status).to eql 200 + expect(last_response.status).to be 200 expect(last_response.body).to eq('red') options '/v1/apples/colour' - expect(last_response.status).to eql 204 + expect(last_response.status).to be 204 end it 'mounts a versioned API with nested resources' do - api = Class.new(Grape::API) do + api = Class.new(described_class) do version 'v1' resources :users do get :hello do @@ -3473,7 +3567,7 @@ def static end it 'mounts a prefixed API with nested resources' do - api = Class.new(Grape::API) do + api = Class.new(described_class) do prefix 'api' resources :users do get :hello do @@ -3488,7 +3582,7 @@ def static end it 'applies format to a mounted API with nested resources' do - api = Class.new(Grape::API) do + api = Class.new(described_class) do format :json resources :users do get do @@ -3503,7 +3597,7 @@ def static end it 'applies auth to a mounted API with nested resources' do - api = Class.new(Grape::API) do + api = Class.new(described_class) do format :json http_basic do |username, password| username == 'username' && password == 'password' @@ -3524,7 +3618,7 @@ def static end it 'mounts multiple versioned APIs with nested resources' do - api1 = Class.new(Grape::API) do + api1 = Class.new(described_class) do version 'one', using: :header, vendor: 'test' resources :users do get :hello do @@ -3533,7 +3627,7 @@ def static end end - api2 = Class.new(Grape::API) do + api2 = Class.new(described_class) do version 'two', using: :header, vendor: 'test' resources :users do get :hello do @@ -3552,7 +3646,7 @@ def static end it 'recognizes potential versions with mounted path' do - a = Class.new(Grape::API) do + a = Class.new(described_class) do version :v1, using: :path get '/hello' do @@ -3560,7 +3654,7 @@ def static end end - b = Class.new(Grape::API) do + b = Class.new(described_class) do version :v1, using: :path get '/world' do @@ -3580,11 +3674,11 @@ def static context 'when mounting class extends a subclass of Grape::API' do it 'mounts APIs with the same superclass' do - base_api = Class.new(Grape::API) + base_api = Class.new(described_class) a = Class.new(base_api) b = Class.new(base_api) - expect { a.mount b }.to_not raise_error + expect { a.mount b }.not_to raise_error end end @@ -3603,22 +3697,22 @@ def my_method end end - it 'should correctly include module in nested mount' do + it 'correctlies include module in nested mount' do module_to_include = included_module - v1 = Class.new(Grape::API) do + v1 = Class.new(described_class) do version :v1, using: :path include module_to_include my_method end - v2 = Class.new(Grape::API) do + v2 = Class.new(described_class) do version :v2, using: :path end - segment_base = Class.new(Grape::API) do + segment_base = Class.new(described_class) do mount v1 mount v2 end - Class.new(Grape::API) do + Class.new(described_class) do mount segment_base end @@ -3653,7 +3747,7 @@ def my_method end describe '.endpoint' do - before(:each) do + before do subject.format :json subject.get '/endpoint/options' do { @@ -3662,6 +3756,7 @@ def my_method } end end + it 'path' do get '/endpoint/options' options = ::Grape::Json.load(last_response.body) @@ -3673,7 +3768,7 @@ def my_method describe '.route' do context 'plain' do - before(:each) do + before do subject.get '/' do route.path end @@ -3681,6 +3776,7 @@ def my_method route.path end end + it 'provides access to route info' do get '/' expect(last_response.body).to eq('/(.:format)') @@ -3688,8 +3784,9 @@ def my_method expect(last_response.body).to eq('/path(.:format)') end end + context 'with desc' do - before(:each) do + before do subject.desc 'returns description' subject.get '/description' do route.description @@ -3699,82 +3796,98 @@ def my_method route.params[params[:id]] end end + it 'returns route description' do get '/description' expect(last_response.body).to eq('returns description') end + it 'returns route parameters' do get '/params/x' expect(last_response.body).to eq('y') end end end + describe '.format' do context ':txt' do - before(:each) do + before do subject.format :txt subject.content_type :json, 'application/json' subject.get '/meaning_of_life' do { meaning_of_life: 42 } end end + it 'forces txt without an extension' do get '/meaning_of_life' expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s) end + it 'does not force txt with an extension' do get '/meaning_of_life.json' expect(last_response.body).to eq({ meaning_of_life: 42 }.to_json) end + it 'forces txt from a non-accepting header' do get '/meaning_of_life', {}, 'HTTP_ACCEPT' => 'application/json' expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s) end end + context ':txt only' do - before(:each) do + before do subject.format :txt subject.get '/meaning_of_life' do { meaning_of_life: 42 } end end + it 'forces txt without an extension' do get '/meaning_of_life' expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s) end + it 'accepts specified extension' do get '/meaning_of_life.txt' expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s) end + it 'does not accept extensions other than specified' do get '/meaning_of_life.json' expect(last_response.status).to eq(404) end + it 'forces txt from a non-accepting header' do get '/meaning_of_life', {}, 'HTTP_ACCEPT' => 'application/json' expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s) end end + context ':json' do - before(:each) do + before do subject.format :json subject.content_type :txt, 'text/plain' subject.get '/meaning_of_life' do { meaning_of_life: 42 } end end + it 'forces json without an extension' do get '/meaning_of_life' expect(last_response.body).to eq({ meaning_of_life: 42 }.to_json) end + it 'does not force json with an extension' do get '/meaning_of_life.txt' expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s) end + it 'forces json from a non-accepting header' do get '/meaning_of_life', {}, 'HTTP_ACCEPT' => 'text/html' expect(last_response.body).to eq({ meaning_of_life: 42 }.to_json) end + it 'can be overwritten with an explicit content type' do subject.get '/meaning_of_life_with_content_type' do content_type 'text/plain' @@ -3783,6 +3896,7 @@ def my_method get '/meaning_of_life_with_content_type' expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s) end + it 'raised :error from middleware' do middleware = Class.new(Grape::Middleware::Base) do def before @@ -3797,6 +3911,7 @@ def before expect(last_response.body).to eq({ error: 'Unauthorized' }.to_json) end end + context ':serializable_hash' do class SerializableHashExample def serializable_hash @@ -3804,9 +3919,10 @@ def serializable_hash end end - before(:each) do + before do subject.format :serializable_hash end + it 'instance' do subject.get '/example' do SerializableHashExample.new @@ -3814,6 +3930,7 @@ def serializable_hash get '/example' expect(last_response.body).to eq('{"abc":"def"}') end + it 'root' do subject.get '/example' do { 'root' => SerializableHashExample.new } @@ -3821,6 +3938,7 @@ def serializable_hash get '/example' expect(last_response.body).to eq('{"root":{"abc":"def"}}') end + it 'array' do subject.get '/examples' do [SerializableHashExample.new, SerializableHashExample.new] @@ -3829,10 +3947,12 @@ def serializable_hash expect(last_response.body).to eq('[{"abc":"def"},{"abc":"def"}]') end end + context ':xml' do - before(:each) do + before do subject.format :xml end + it 'string' do subject.get '/example' do 'example' @@ -3846,6 +3966,7 @@ def serializable_hash XML end + it 'hash' do subject.get '/example' do { @@ -3863,6 +3984,7 @@ def serializable_hash XML end + it 'array' do subject.get '/example' do %w[example1 example2] @@ -3877,6 +3999,7 @@ def serializable_hash XML end + it 'raised :error from middleware' do middleware = Class.new(Grape::Middleware::Base) do def before @@ -3938,12 +4061,12 @@ def before context 'catch-all' do before do - api1 = Class.new(Grape::API) + api1 = Class.new(described_class) api1.version 'v1', using: :path api1.get 'hello' do 'v1' end - api2 = Class.new(Grape::API) + api2 = Class.new(described_class) api2.version 'v2', using: :path api2.get 'hello' do 'v2' @@ -3951,6 +4074,7 @@ def before subject.mount api1 subject.mount api2 end + [true, false].each do |anchor| it "anchor=#{anchor}" do subject.route :any, '*path', anchor: anchor do @@ -3983,6 +4107,7 @@ def before expect(last_response.status).to eq(404) expect(last_response.headers['X-Cascade']).to eq('pass') end + it 'does not cascade' do subject.version 'v2', using: :path, cascade: false get '/v2/hello' @@ -3990,6 +4115,7 @@ def before expect(last_response.headers.keys).not_to include 'X-Cascade' end end + context 'via endpoint' do it 'cascades' do subject.cascade true @@ -3997,6 +4123,7 @@ def before expect(last_response.status).to eq(404) expect(last_response.headers['X-Cascade']).to eq('pass') end + it 'does not cascade' do subject.cascade false get '/hello' @@ -4046,12 +4173,14 @@ def before body false end end + it 'returns blank body' do get '/blank' expect(last_response.status).to eq(204) expect(last_response.body).to be_blank end end + context 'plain text' do before do subject.get '/text' do @@ -4060,6 +4189,7 @@ def before 'ignored' end end + it 'returns blank body' do get '/text' expect(last_response.status).to eq(200) @@ -4069,7 +4199,7 @@ def before end describe 'normal class methods' do - subject(:grape_api) { Class.new(Grape::API) } + subject(:grape_api) { Class.new(described_class) } before do stub_const('MyAPI', grape_api) @@ -4089,7 +4219,7 @@ def before describe '.inherited' do context 'overriding within class' do let(:root_api) do - Class.new(Grape::API) do + Class.new(described_class) do @bar = 'Hello, world' def self.inherited(child_api) @@ -4115,7 +4245,7 @@ def inherited(api) end let(:root_api) do - Class.new(Grape::API) do + Class.new(described_class) do @bar = 'Hello, world' extend Inherited end @@ -4130,9 +4260,10 @@ def inherited(api) end describe 'const_missing' do - subject(:grape_api) { Class.new(Grape::API) } + subject(:grape_api) { Class.new(described_class) } + let(:mounted) do - Class.new(Grape::API) do + Class.new(described_class) do get '/missing' do SomeRandomConstant end @@ -4147,6 +4278,12 @@ def inherited(api) end describe 'custom route helpers on nested APIs' do + subject(:grape_api) do + Class.new(described_class) do + version 'v1', using: :path + end + end + let(:shared_api_module) do Module.new do # rubocop:disable Style/ExplicitBlockArgument because this causes @@ -4180,7 +4317,7 @@ def uniqe_id_route let(:orders_root) do shared = shared_api_definitions find = orders_find_endpoint - Class.new(Grape::API) do + Class.new(described_class) do include shared namespace(:orders) do @@ -4190,7 +4327,7 @@ def uniqe_id_route end let(:orders_find_endpoint) do shared = shared_api_definitions - Class.new(Grape::API) do + Class.new(described_class) do include shared uniqe_id_route do @@ -4201,11 +4338,6 @@ def uniqe_id_route 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) diff --git a/spec/grape/dsl/callbacks_spec.rb b/spec/grape/dsl/callbacks_spec.rb index 8844b02d7..a83ae9351 100644 --- a/spec/grape/dsl/callbacks_spec.rb +++ b/spec/grape/dsl/callbacks_spec.rb @@ -12,6 +12,7 @@ class Dummy describe Callbacks do subject { Class.new(CallbacksSpec::Dummy) } + let(:proc) { -> {} } describe '.before' do diff --git a/spec/grape/dsl/headers_spec.rb b/spec/grape/dsl/headers_spec.rb index d96c9be0e..221fed09f 100644 --- a/spec/grape/dsl/headers_spec.rb +++ b/spec/grape/dsl/headers_spec.rb @@ -11,6 +11,7 @@ class Dummy end describe Headers do subject { HeadersSpec::Dummy.new } + let(:header_data) do { 'First Key' => 'First Value', 'Second Key' => 'Second Value' } @@ -21,6 +22,7 @@ class Dummy before do header_data.each { |k, v| subject.header(k, v) } end + describe 'get' do it 'returns a specifc value' do expect(subject.header['First Key']).to eq 'First Value' @@ -32,12 +34,14 @@ class Dummy expect(subject.headers).to eq header_data end end + describe 'set' do it 'returns value' do expect(subject.header('Third Key', 'Third Value')) expect(subject.header['Third Key']).to eq 'Third Value' end end + describe 'delete' do it 'deletes a header key-value pair' do expect(subject.header('First Key')).to eq header_data['First Key'] diff --git a/spec/grape/dsl/helpers_spec.rb b/spec/grape/dsl/helpers_spec.rb index 2f7bbf5a1..837605bc0 100644 --- a/spec/grape/dsl/helpers_spec.rb +++ b/spec/grape/dsl/helpers_spec.rb @@ -34,6 +34,7 @@ class Child < Base; end describe Helpers do subject { Class.new(HelpersSpec::Dummy) } + let(:proc) do lambda do |*| def test @@ -54,7 +55,7 @@ def test it 'uses provided modules' do mod = Module.new - expect(subject).to receive(:namespace_stackable).with(:helpers, kind_of(Grape::DSL::Helpers::BaseHelper)).and_call_original.exactly(2).times + expect(subject).to receive(:namespace_stackable).with(:helpers, kind_of(Grape::DSL::Helpers::BaseHelper)).and_call_original.twice expect(subject).to receive(:namespace_stackable).with(:helpers).and_call_original subject.helpers(mod, &proc) @@ -92,7 +93,7 @@ def test use :requires_toggle_prm end end - end.to_not raise_exception + end.not_to raise_exception end end end diff --git a/spec/grape/dsl/inside_route_spec.rb b/spec/grape/dsl/inside_route_spec.rb index 9d83c148d..e35df773f 100644 --- a/spec/grape/dsl/inside_route_spec.rb +++ b/spec/grape/dsl/inside_route_spec.rb @@ -43,6 +43,7 @@ def initialize before do catch(:error) { subject.error! 'Not Found', 404 } end + it 'sets status' do expect(subject.status).to eq 404 end @@ -53,6 +54,7 @@ def initialize subject.namespace_inheritable(:default_error_status, 500) catch(:error) { subject.error! 'Unknown' } end + it 'sets status to default_error_status' do expect(subject.status).to eq 500 end @@ -136,7 +138,7 @@ def initialize end it 'accepts unknown Integer status codes' do - expect { subject.status 210 }.to_not raise_error + expect { subject.status 210 }.not_to raise_error end it 'raises error if status is not a integer or symbol' do @@ -273,7 +275,7 @@ def initialize end it 'sends no deprecation warnings' do - expect(subject).to_not receive(:warn) + expect(subject).not_to receive(:warn) subject.sendfile file_path end @@ -334,7 +336,7 @@ def initialize end it 'emits no deprecation warnings' do - expect(subject).to_not receive(:warn) + expect(subject).not_to receive(:warn) subject.stream file_path end @@ -384,7 +386,7 @@ def initialize end it 'emits no deprecation warnings' do - expect(subject).to_not receive(:warn) + expect(subject).not_to receive(:warn) subject.stream stream_object end diff --git a/spec/grape/dsl/middleware_spec.rb b/spec/grape/dsl/middleware_spec.rb index a9b11d74c..309042f99 100644 --- a/spec/grape/dsl/middleware_spec.rb +++ b/spec/grape/dsl/middleware_spec.rb @@ -12,6 +12,7 @@ class Dummy describe Middleware do subject { Class.new(MiddlewareSpec::Dummy) } + let(:proc) { -> {} } let(:foo_middleware) { Class.new } let(:bar_middleware) { Class.new } diff --git a/spec/grape/dsl/parameters_spec.rb b/spec/grape/dsl/parameters_spec.rb index 51967d3ff..69df048fd 100644 --- a/spec/grape/dsl/parameters_spec.rb +++ b/spec/grape/dsl/parameters_spec.rb @@ -55,6 +55,7 @@ def extract_message_option(attrs) allow_message_expectations_on_nil allow(subject.api).to receive(:namespace_stackable).with(:named_params) end + let(:options) { { option: 'value' } } let(:named_params) { { params_group: proc {} } } diff --git a/spec/grape/dsl/request_response_spec.rb b/spec/grape/dsl/request_response_spec.rb index 00d9e3a6a..822fbbe8b 100644 --- a/spec/grape/dsl/request_response_spec.rb +++ b/spec/grape/dsl/request_response_spec.rb @@ -20,6 +20,7 @@ def self.imbue(key, value) describe RequestResponse do subject { Class.new(RequestResponseSpec::Dummy) } + let(:c_type) { 'application/json' } let(:format) { 'txt' } diff --git a/spec/grape/dsl/routing_spec.rb b/spec/grape/dsl/routing_spec.rb index ea225eaff..4ea286f85 100644 --- a/spec/grape/dsl/routing_spec.rb +++ b/spec/grape/dsl/routing_spec.rb @@ -12,6 +12,7 @@ class Dummy describe Routing do subject { Class.new(RoutingSpec::Dummy) } + let(:proc) { -> {} } let(:options) { { a: :b } } let(:path) { '/dummy' } @@ -109,7 +110,7 @@ class Dummy it 'does not duplicate identical endpoints' do subject.route(:any) expect { subject.route(:any) } - .to_not change(subject.endpoints, :count) + .not_to change(subject.endpoints, :count) end it 'generates correct endpoint options' do @@ -233,21 +234,23 @@ class Dummy allow(subject).to receive(:prepare_routes).and_return(routes) subject.routes end - it 'it does not call prepare_routes again' do - expect(subject).to_not receive(:prepare_routes) + + it 'does not call prepare_routes again' do + expect(subject).not_to receive(:prepare_routes) expect(subject.routes).to eq routes end end end describe '.route_param' do + let!(:options) { { requirements: regex } } + let(:regex) { /(.*)/ } + it 'calls #namespace with given params' do expect(subject).to receive(:namespace).with(':foo', {}).and_yield subject.route_param('foo', {}, &proc {}) end - let(:regex) { /(.*)/ } - let!(:options) { { requirements: regex } } it 'nests requirements option under param name' do expect(subject).to receive(:namespace) do |_param, options| expect(options[:requirements][:foo]).to eq regex @@ -258,7 +261,7 @@ class Dummy it 'does not modify options parameter' do allow(subject).to receive(:namespace) expect { subject.route_param('foo', options, &proc {}) } - .to_not change { options } + .not_to change { options } end end diff --git a/spec/grape/endpoint/declared_spec.rb b/spec/grape/endpoint/declared_spec.rb index e50f45bb8..5cd37a136 100644 --- a/spec/grape/endpoint/declared_spec.rb +++ b/spec/grape/endpoint/declared_spec.rb @@ -87,7 +87,7 @@ def app end end - it 'should show nil for nested params if include_missing is true' do + it 'shows nil for nested params if include_missing is true' do subject.get '/declared' do declared(params, include_missing: true) end @@ -97,7 +97,7 @@ def app 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 + it 'shows nil for multiple allowed types if include_missing is true' do subject.get '/declared' do declared(params, include_missing: true) end @@ -568,34 +568,34 @@ def app 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 + expect(json).to be_key(:id) + expect(json).not_to be_key(:artist_id) end it 'return only :artist_id without :id' do get '/artists/1/compositions' json = JSON.parse(last_response.body, symbolize_names: true) - expect(json.key?(:artist_id)).to be_truthy - expect(json.key?(:id)).not_to be_truthy + expect(json).to be_key(:artist_id) + expect(json).not_to be_key(:id) end it 'return :filter and :id parameters in declared for second enpoint inside route_param' do get '/artists/1/some_route', filter: 'some_filter' json = JSON.parse(last_response.body, symbolize_names: true) - expect(json.key?(:filter)).to be_truthy - expect(json.key?(:id)).to be_truthy - expect(json.key?(:artist_id)).not_to be_truthy + expect(json).to be_key(:filter) + expect(json).to be_key(:id) + expect(json).not_to be_key(:artist_id) end it 'return :compositor_id for mounter in route_param' do get '/artists/1/albums' json = JSON.parse(last_response.body, symbolize_names: true) - expect(json.key?(:compositor_id)).to be_truthy - expect(json.key?(:id)).not_to be_truthy - expect(json.key?(:artist_id)).not_to be_truthy + expect(json).to be_key(:compositor_id) + expect(json).not_to be_key(:id) + expect(json).not_to be_key(:artist_id) end end diff --git a/spec/grape/endpoint_spec.rb b/spec/grape/endpoint_spec.rb index 08f7c3529..20efdc505 100644 --- a/spec/grape/endpoint_spec.rb +++ b/spec/grape/endpoint_spec.rb @@ -10,32 +10,32 @@ def app end describe '.before_each' do - after { Grape::Endpoint.before_each.clear } + after { described_class.before_each.clear } it 'is settable via block' do block = ->(_endpoint) { 'noop' } - Grape::Endpoint.before_each(&block) - expect(Grape::Endpoint.before_each.first).to eq(block) + described_class.before_each(&block) + expect(described_class.before_each.first).to eq(block) end it 'is settable via reference' do block = ->(_endpoint) { 'noop' } - Grape::Endpoint.before_each block - expect(Grape::Endpoint.before_each.first).to eq(block) + described_class.before_each block + expect(described_class.before_each.first).to eq(block) end it 'is able to override a helper' do subject.get('/') { current_user } expect { get '/' }.to raise_error(NameError) - Grape::Endpoint.before_each do |endpoint| + described_class.before_each do |endpoint| allow(endpoint).to receive(:current_user).and_return('Bob') end get '/' expect(last_response.body).to eq('Bob') - Grape::Endpoint.before_each(nil) + described_class.before_each(nil) expect { get '/' }.to raise_error(NameError) end @@ -46,18 +46,18 @@ def app end expect { get '/' }.to raise_error(NameError) - Grape::Endpoint.before_each do |endpoint| + described_class.before_each do |endpoint| allow(endpoint).to receive(:current_user).and_return('Bob') end - Grape::Endpoint.before_each do |endpoint| + described_class.before_each do |endpoint| allow(endpoint).to receive(:authenticate_user!).and_return(true) end get '/' expect(last_response.body).to eq('Bob') - Grape::Endpoint.before_each(nil) + described_class.before_each(nil) expect { get '/' }.to raise_error(NameError) end end @@ -66,7 +66,7 @@ def app it 'takes a settings stack, options, and a block' do p = proc {} expect do - Grape::Endpoint.new(Grape::Util::InheritableSetting.new, { + described_class.new(Grape::Util::InheritableSetting.new, { path: '/', method: :get }, &p) @@ -77,7 +77,7 @@ def app it 'sets itself in the env upon call' do subject.get('/') { 'Hello world.' } get '/' - expect(last_request.env['api.endpoint']).to be_kind_of(Grape::Endpoint) + expect(last_request.env['api.endpoint']).to be_kind_of(described_class) end describe '#status' do @@ -137,6 +137,7 @@ def app headers.to_json end end + it 'includes request headers' do get '/headers' expect(JSON.parse(last_response.body)).to eq( @@ -144,10 +145,12 @@ def app 'Cookie' => '' ) end + it 'includes additional request headers' do get '/headers', nil, 'HTTP_X_GRAPE_CLIENT' => '1' expect(JSON.parse(last_response.body)['X-Grape-Client']).to eq('1') end + it 'includes headers passed as symbols' do env = Rack::MockRequest.env_for('/headers') env[:HTTP_SYMBOL_HEADER] = 'Goliath passes symbols' @@ -253,7 +256,7 @@ def app describe '#params' do context 'default class' do - it 'should be a ActiveSupport::HashWithIndifferentAccess' do + it 'is a ActiveSupport::HashWithIndifferentAccess' do subject.get '/foo' do params.class end @@ -339,7 +342,7 @@ def app end context 'namespace requirements' do - before :each do + before do subject.namespace :outer, requirements: { person_email: /abc@(.*).com/ } do get('/:person_email') do params[:person_email] @@ -358,7 +361,7 @@ def app expect(last_response.body).to eq('abc@example.com') end - it "should override outer namespace's requirements" do + it "overrides outer namespace's requirements" do get '/outer/inner/someone@testing.wrong/test/1' expect(last_response.status).to eq(404) @@ -370,7 +373,7 @@ def app end context 'from body parameters' do - before(:each) do + before do subject.post '/request_body' do params[:user] end @@ -469,11 +472,11 @@ def app post '/', ::Grape::Json.dump(data: { some: 'payload' }), 'CONTENT_TYPE' => 'application/json' end - it 'should not response with 406 for same type without params' do + it 'does not response with 406 for same type without params' do expect(last_response.status).not_to be 406 end - it 'should response with given content type in headers' do + it 'responses with given content type in headers' do expect(last_response.headers['Content-Type']).to eq 'application/json; charset=utf-8' end end @@ -709,16 +712,18 @@ def memoized describe '.generate_api_method' do it 'raises NameError if the method name is already in use' do expect do - Grape::Endpoint.generate_api_method('version', &proc {}) + described_class.generate_api_method('version', &proc {}) end.to raise_error(NameError) end + it 'raises ArgumentError if a block is not given' do expect do - Grape::Endpoint.generate_api_method('GET without a block method') + described_class.generate_api_method('GET without a block method') end.to raise_error(ArgumentError) end + it 'returns a Proc' do - expect(Grape::Endpoint.generate_api_method('GET test for a proc', &proc {})).to be_a Proc + expect(described_class.generate_api_method('GET test for a proc', &proc {})).to be_a Proc end end @@ -777,7 +782,7 @@ def memoized end get '/error_filters' - expect(last_response.status).to eql 500 + expect(last_response.status).to be 500 expect(called).to match_array %w[before before_validation] end @@ -786,8 +791,11 @@ def memoized subject.before { called << 'parent' } subject.namespace :parent do before { called << 'prior' } + before { error! :oops, 500 } + before { called << 'subsequent' } + get :hello do called << :endpoint 'Hello!' @@ -795,7 +803,7 @@ def memoized end get '/parent/hello' - expect(last_response.status).to eql 500 + expect(last_response.status).to be 500 expect(called).to match_array %w[parent prior] end end @@ -806,19 +814,19 @@ def memoized it 'allows for the anchoring option with a delete method' do subject.send(:delete, '/example', anchor: true) {} send(:delete, '/example/and/some/more') - expect(last_response.status).to eql 404 + expect(last_response.status).to be 404 end it 'anchors paths by default for the delete method' do subject.send(:delete, '/example') {} send(:delete, '/example/and/some/more') - expect(last_response.status).to eql 404 + expect(last_response.status).to be 404 end it 'responds to /example/and/some/more for the non-anchored delete method' do subject.send(:delete, '/example', anchor: false) {} send(:delete, '/example/and/some/more') - expect(last_response.status).to eql 204 + expect(last_response.status).to be 204 expect(last_response.body).to be_empty end end @@ -830,7 +838,7 @@ def memoized body 'deleted' end send(:delete, '/example/and/some/more') - expect(last_response.status).to eql 200 + expect(last_response.status).to be 200 expect(last_response.body).not_to be_empty end end @@ -839,7 +847,7 @@ def memoized it 'responds to /example delete method' do subject.delete(:example) { 'deleted' } delete '/example' - expect(last_response.status).to eql 200 + expect(last_response.status).to be 200 expect(last_response.body).not_to be_empty end end @@ -848,7 +856,7 @@ def memoized it 'responds to /example delete method' do subject.delete(:example) { nil } delete '/example' - expect(last_response.status).to eql 204 + expect(last_response.status).to be 204 expect(last_response.body).to be_empty end end @@ -857,7 +865,7 @@ def memoized it 'responds to /example delete method' do subject.delete(:example) { '' } delete '/example' - expect(last_response.status).to eql 204 + expect(last_response.status).to be 204 expect(last_response.body).to be_empty end end @@ -869,7 +877,7 @@ def memoized verb end send(verb, '/example/and/some/more') - expect(last_response.status).to eql 404 + expect(last_response.status).to be 404 end it "anchors paths by default for the #{verb.upcase} method" do @@ -877,7 +885,7 @@ def memoized verb end send(verb, '/example/and/some/more') - expect(last_response.status).to eql 404 + expect(last_response.status).to be 404 end it "responds to /example/and/some/more for the non-anchored #{verb.upcase} method" do @@ -900,8 +908,9 @@ def memoized get '/url' expect(last_response.body).to eq('http://example.org/url') end + ['v1', :v1].each do |version| - it "should include version #{version}" do + it "includes version #{version}" do subject.version version, using: :path subject.get('/url') do request.url @@ -910,7 +919,7 @@ def memoized expect(last_response.body).to eq("http://example.org/#{version}/url") end end - it 'should include prefix' do + it 'includes prefix' do subject.version 'v1', using: :path subject.prefix 'api' subject.get('/url') do @@ -1000,26 +1009,26 @@ def memoized # In order that the events finalized (time each block ended) expect(@events).to contain_exactly( - have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(Grape::Endpoint), + have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(described_class), filters: a_collection_containing_exactly(an_instance_of(Proc)), type: :before }), - have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(Grape::Endpoint), + have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(described_class), filters: [], type: :before_validation }), - have_attributes(name: 'endpoint_run_validators.grape', payload: { endpoint: a_kind_of(Grape::Endpoint), + have_attributes(name: 'endpoint_run_validators.grape', payload: { endpoint: a_kind_of(described_class), validators: [], request: a_kind_of(Grape::Request) }), - have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(Grape::Endpoint), + have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(described_class), filters: [], type: :after_validation }), - have_attributes(name: 'endpoint_render.grape', payload: { endpoint: a_kind_of(Grape::Endpoint) }), - have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(Grape::Endpoint), + have_attributes(name: 'endpoint_render.grape', payload: { endpoint: a_kind_of(described_class) }), + have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(described_class), filters: [], type: :after }), - have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(Grape::Endpoint), + have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(described_class), filters: [], type: :finally }), - have_attributes(name: 'endpoint_run.grape', payload: { endpoint: a_kind_of(Grape::Endpoint), + have_attributes(name: 'endpoint_run.grape', payload: { endpoint: a_kind_of(described_class), env: an_instance_of(Hash) }), have_attributes(name: 'format_response.grape', payload: { env: an_instance_of(Hash), formatter: a_kind_of(Module) }) @@ -1027,25 +1036,25 @@ def memoized # In order that events were initialized expect(@events.sort_by(&:time)).to contain_exactly( - have_attributes(name: 'endpoint_run.grape', payload: { endpoint: a_kind_of(Grape::Endpoint), + have_attributes(name: 'endpoint_run.grape', payload: { endpoint: a_kind_of(described_class), env: an_instance_of(Hash) }), - have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(Grape::Endpoint), + have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(described_class), filters: a_collection_containing_exactly(an_instance_of(Proc)), type: :before }), - have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(Grape::Endpoint), + have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(described_class), filters: [], type: :before_validation }), - have_attributes(name: 'endpoint_run_validators.grape', payload: { endpoint: a_kind_of(Grape::Endpoint), + have_attributes(name: 'endpoint_run_validators.grape', payload: { endpoint: a_kind_of(described_class), validators: [], request: a_kind_of(Grape::Request) }), - have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(Grape::Endpoint), + have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(described_class), filters: [], type: :after_validation }), - have_attributes(name: 'endpoint_render.grape', payload: { endpoint: a_kind_of(Grape::Endpoint) }), - have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(Grape::Endpoint), + have_attributes(name: 'endpoint_render.grape', payload: { endpoint: a_kind_of(described_class) }), + have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(described_class), filters: [], type: :after }), - have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(Grape::Endpoint), + have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(described_class), filters: [], type: :finally }), have_attributes(name: 'format_response.grape', payload: { env: an_instance_of(Hash), diff --git a/spec/grape/entity_spec.rb b/spec/grape/entity_spec.rb index a099cdaa3..d69042244 100644 --- a/spec/grape/entity_spec.rb +++ b/spec/grape/entity_spec.rb @@ -32,7 +32,7 @@ def app end it 'pulls a representation from the class options if it exists' do - entity = Class.new(Grape::Entity) + entity = Class.new(described_class) allow(entity).to receive(:represent).and_return('Hiya') subject.represent Object, with: entity @@ -44,7 +44,7 @@ def app end it 'pulls a representation from the class options if the presented object is a collection of objects' do - entity = Class.new(Grape::Entity) + entity = Class.new(described_class) allow(entity).to receive(:represent).and_return('Hiya') module EntitySpec @@ -75,7 +75,7 @@ def first end it 'pulls a representation from the class ancestor if it exists' do - entity = Class.new(Grape::Entity) + entity = Class.new(described_class) allow(entity).to receive(:represent).and_return('Hiya') subclass = Class.new(Object) @@ -90,7 +90,7 @@ def first it 'automatically uses Klass::Entity if that exists' do some_model = Class.new - entity = Class.new(Grape::Entity) + entity = Class.new(described_class) allow(entity).to receive(:represent).and_return('Auto-detect!') some_model.const_set :Entity, entity @@ -104,7 +104,7 @@ def first it 'automatically uses Klass::Entity based on the first object in the collection being presented' do some_model = Class.new - entity = Class.new(Grape::Entity) + entity = Class.new(described_class) allow(entity).to receive(:represent).and_return('Auto-detect!') some_model.const_set :Entity, entity @@ -117,7 +117,7 @@ def first end it 'does not run autodetection for Entity when explicitly provided' do - entity = Class.new(Grape::Entity) + entity = Class.new(described_class) some_array = [] subject.get '/example' do @@ -129,7 +129,7 @@ def first end it 'does not use #first method on ActiveRecord::Relation to prevent needless sql query' do - entity = Class.new(Grape::Entity) + entity = Class.new(described_class) some_relation = Class.new some_model = Class.new @@ -173,7 +173,7 @@ def first %i[json serializable_hash].each do |format| it "presents with #{format}" do - entity = Class.new(Grape::Entity) + entity = Class.new(described_class) entity.root 'examples', 'example' entity.expose :id @@ -195,7 +195,7 @@ def initialize(id) end it "presents with #{format} collection" do - entity = Class.new(Grape::Entity) + entity = Class.new(described_class) entity.root 'examples', 'example' entity.expose :id @@ -219,7 +219,7 @@ def initialize(id) end it 'presents with xml' do - entity = Class.new(Grape::Entity) + entity = Class.new(described_class) entity.root 'examples', 'example' entity.expose :name @@ -249,7 +249,7 @@ def initialize(args) end it 'presents with json' do - entity = Class.new(Grape::Entity) + entity = Class.new(described_class) entity.root 'examples', 'example' entity.expose :name @@ -275,7 +275,7 @@ def initialize(args) # Include JSONP middleware subject.use Rack::JSONP - entity = Class.new(Grape::Entity) + entity = Class.new(described_class) entity.root 'examples', 'example' entity.expose :name @@ -315,7 +315,7 @@ def initialize(args) user1 = user.new(name: 'user1') user2 = user.new(name: 'user2') - entity = Class.new(Grape::Entity) + entity = Class.new(described_class) entity.expose :name subject.format :json diff --git a/spec/grape/exceptions/body_parse_errors_spec.rb b/spec/grape/exceptions/body_parse_errors_spec.rb index 990758a5f..43d48d069 100644 --- a/spec/grape/exceptions/body_parse_errors_spec.rb +++ b/spec/grape/exceptions/body_parse_errors_spec.rb @@ -5,6 +5,7 @@ describe Grape::Exceptions::ValidationErrors do context 'api with rescue_from :all handler' do subject { Class.new(Grape::API) } + before do subject.rescue_from :all do |_e| rack_response 'message was processed', 400 @@ -56,6 +57,7 @@ def app context 'api with rescue_from :grape_exceptions handler' do subject { Class.new(Grape::API) } + before do subject.rescue_from :all do |_e| rack_response 'message was processed', 400 @@ -93,6 +95,7 @@ def app context 'api without a rescue handler' do subject { Class.new(Grape::API) } + before do subject.params do requires :beer diff --git a/spec/grape/exceptions/invalid_accept_header_spec.rb b/spec/grape/exceptions/invalid_accept_header_spec.rb index 3a93ce3e1..69404d72b 100644 --- a/spec/grape/exceptions/invalid_accept_header_spec.rb +++ b/spec/grape/exceptions/invalid_accept_header_spec.rb @@ -7,6 +7,7 @@ it 'does return with status 200' do expect(last_response.status).to eq 200 end + it 'does return the expected result' do expect(last_response.body).to eq('beer received') end @@ -20,6 +21,7 @@ it 'does not include the X-Cascade=pass header' do expect(last_response.headers['X-Cascade']).to be_nil end + it 'does not accept the request' do expect(last_response.status).to eq 406 end @@ -28,6 +30,7 @@ it 'does not include the X-Cascade=pass header' do expect(last_response.headers['X-Cascade']).to be_nil end + it 'does show rescue handler processing' do expect(last_response.status).to eq 400 expect(last_response.body).to eq('message was processed') @@ -36,6 +39,7 @@ context 'API with cascade=false and rescue_from :all handler' do subject { Class.new(Grape::API) } + before do subject.version 'v99', using: :header, vendor: 'vendorname', format: :json, cascade: false subject.rescue_from :all do |e| @@ -52,7 +56,8 @@ def app context 'that received a request with correct vendor and version' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } - it_should_behave_like 'a valid request' + + it_behaves_like 'a valid request' end context 'that receives' do @@ -61,13 +66,15 @@ def app get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99', 'CONTENT_TYPE' => 'application/json' end - it_should_behave_like 'a rescued request' + + it_behaves_like 'a rescued request' end end end context 'API with cascade=false and without a rescue handler' do subject { Class.new(Grape::API) } + before do subject.version 'v99', using: :header, vendor: 'vendorname', format: :json, cascade: false subject.get '/beer' do @@ -81,23 +88,28 @@ def app context 'that received a request with correct vendor and version' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } - it_should_behave_like 'a valid request' + + it_behaves_like 'a valid request' end context 'that receives' do context 'an invalid version in the request' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77' } - it_should_behave_like 'a not-cascaded request' + + it_behaves_like 'a not-cascaded request' end + context 'an invalid vendor in the request' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99' } - it_should_behave_like 'a not-cascaded request' + + it_behaves_like 'a not-cascaded request' end end end context 'API with cascade=false and with rescue_from :all handler and http_codes' do subject { Class.new(Grape::API) } + before do subject.version 'v99', using: :header, vendor: 'vendorname', format: :json, cascade: false subject.rescue_from :all do |e| @@ -119,7 +131,8 @@ def app context 'that received a request with correct vendor and version' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } - it_should_behave_like 'a valid request' + + it_behaves_like 'a valid request' end context 'that receives' do @@ -128,13 +141,15 @@ def app get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99', 'CONTENT_TYPE' => 'application/json' end - it_should_behave_like 'a rescued request' + + it_behaves_like 'a rescued request' end end end context 'API with cascade=false, http_codes but without a rescue handler' do subject { Class.new(Grape::API) } + before do subject.version 'v99', using: :header, vendor: 'vendorname', format: :json, cascade: false subject.desc 'Get beer' do @@ -153,23 +168,28 @@ def app context 'that received a request with correct vendor and version' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } - it_should_behave_like 'a valid request' + + it_behaves_like 'a valid request' end context 'that receives' do context 'an invalid version in the request' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77' } - it_should_behave_like 'a not-cascaded request' + + it_behaves_like 'a not-cascaded request' end + context 'an invalid vendor in the request' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99' } - it_should_behave_like 'a not-cascaded request' + + it_behaves_like 'a not-cascaded request' end end end context 'API with cascade=true and rescue_from :all handler' do subject { Class.new(Grape::API) } + before do subject.version 'v99', using: :header, vendor: 'vendorname', format: :json, cascade: true subject.rescue_from :all do |e| @@ -186,7 +206,8 @@ def app context 'that received a request with correct vendor and version' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } - it_should_behave_like 'a valid request' + + it_behaves_like 'a valid request' end context 'that receives' do @@ -195,20 +216,24 @@ def app get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77', 'CONTENT_TYPE' => 'application/json' end - it_should_behave_like 'a cascaded request' + + it_behaves_like 'a cascaded request' end + context 'an invalid vendor in the request' do before do get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99', 'CONTENT_TYPE' => 'application/json' end - it_should_behave_like 'a cascaded request' + + it_behaves_like 'a cascaded request' end end end context 'API with cascade=true and without a rescue handler' do subject { Class.new(Grape::API) } + before do subject.version 'v99', using: :header, vendor: 'vendorname', format: :json, cascade: true subject.get '/beer' do @@ -222,23 +247,28 @@ def app context 'that received a request with correct vendor and version' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } - it_should_behave_like 'a valid request' + + it_behaves_like 'a valid request' end context 'that receives' do context 'an invalid version in the request' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77' } - it_should_behave_like 'a cascaded request' + + it_behaves_like 'a cascaded request' end + context 'an invalid vendor in the request' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99' } - it_should_behave_like 'a cascaded request' + + it_behaves_like 'a cascaded request' end end end context 'API with cascade=true and with rescue_from :all handler and http_codes' do subject { Class.new(Grape::API) } + before do subject.version 'v99', using: :header, vendor: 'vendorname', format: :json, cascade: true subject.rescue_from :all do |e| @@ -260,7 +290,8 @@ def app context 'that received a request with correct vendor and version' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } - it_should_behave_like 'a valid request' + + it_behaves_like 'a valid request' end context 'that receives' do @@ -269,20 +300,24 @@ def app get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77', 'CONTENT_TYPE' => 'application/json' end - it_should_behave_like 'a cascaded request' + + it_behaves_like 'a cascaded request' end + context 'an invalid vendor in the request' do before do get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99', 'CONTENT_TYPE' => 'application/json' end - it_should_behave_like 'a cascaded request' + + it_behaves_like 'a cascaded request' end end end context 'API with cascade=true, http_codes but without a rescue handler' do subject { Class.new(Grape::API) } + before do subject.version 'v99', using: :header, vendor: 'vendorname', format: :json, cascade: true subject.desc 'Get beer' do @@ -301,17 +336,21 @@ def app context 'that received a request with correct vendor and version' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } - it_should_behave_like 'a valid request' + + it_behaves_like 'a valid request' end context 'that receives' do context 'an invalid version in the request' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77' } - it_should_behave_like 'a cascaded request' + + it_behaves_like 'a cascaded request' end + context 'an invalid vendor in the request' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99' } - it_should_behave_like 'a cascaded request' + + it_behaves_like 'a cascaded request' end end end diff --git a/spec/grape/exceptions/validation_errors_spec.rb b/spec/grape/exceptions/validation_errors_spec.rb index cf7e1e540..8c3661da0 100644 --- a/spec/grape/exceptions/validation_errors_spec.rb +++ b/spec/grape/exceptions/validation_errors_spec.rb @@ -5,30 +5,31 @@ describe Grape::Exceptions::ValidationErrors do let(:validation_message) { 'FooBar is invalid' } - let(:validation_error) { OpenStruct.new(params: [validation_message]) } + let(:validation_error) { instance_double Grape::Exceptions::Validation, params: [validation_message], message: '' } context 'initialize' do + subject do + described_class.new(errors: [validation_error], headers: headers) + end + let(:headers) do { 'A-Header-Key' => 'A-Header-Value' } end - subject do - described_class.new(errors: [validation_error], headers: headers) - end - - it 'should assign headers through base class' do + it 'assigns headers through base class' do expect(subject.headers).to eq(headers) end end context 'message' do context 'is not repeated' do + subject(:message) { error.message.split(',').map(&:strip) } + let(:error) do described_class.new(errors: [validation_error, validation_error]) end - subject(:message) { error.message.split(',').map(&:strip) } it { expect(message).to include validation_message } it { expect(message.size).to eq 1 } @@ -37,9 +38,10 @@ describe '#full_messages' do context 'with errors' do + subject { described_class.new(errors: [validation_error_1, validation_error_2]).full_messages } + let(:validation_error_1) { Grape::Exceptions::Validation.new(params: ['id'], message: :presence) } let(:validation_error_2) { Grape::Exceptions::Validation.new(params: ['name'], message: :presence) } - subject { described_class.new(errors: [validation_error_1, validation_error_2]).full_messages } it 'returns an array with each errors full message' do expect(subject).to contain_exactly('id is missing', 'name is missing') @@ -47,9 +49,10 @@ end context 'when attributes is an array of symbols' do - let(:validation_error) { Grape::Exceptions::Validation.new(params: [:admin_field], message: 'Can not set admin-only field') } subject { described_class.new(errors: [validation_error]).full_messages } + let(:validation_error) { Grape::Exceptions::Validation.new(params: [:admin_field], message: 'Can not set admin-only field') } + it 'returns an array with an error full message' do expect(subject.first).to eq('admin_field Can not set admin-only field') end @@ -65,7 +68,7 @@ def app it 'can return structured json with separate fields' do subject.format :json - subject.rescue_from Grape::Exceptions::ValidationErrors do |e| + subject.rescue_from described_class do |e| error!(e, 400) end subject.params do diff --git a/spec/grape/exceptions/validation_spec.rb b/spec/grape/exceptions/validation_spec.rb index 5486cbf64..71cd4ef98 100644 --- a/spec/grape/exceptions/validation_spec.rb +++ b/spec/grape/exceptions/validation_spec.rb @@ -4,16 +4,18 @@ 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 { described_class.new(message: 'presence') }.to raise_error(ArgumentError, /missing keyword:.+?params/) end + context 'when message is a symbol' do it 'stores message_key' do - expect(Grape::Exceptions::Validation.new(params: ['id'], message: :presence).message_key).to eq(:presence) + expect(described_class.new(params: ['id'], message: :presence).message_key).to eq(:presence) end end + context 'when message is a String' do it 'does not store the message_key' do - expect(Grape::Exceptions::Validation.new(params: ['id'], message: 'presence').message_key).to eq(nil) + expect(described_class.new(params: ['id'], message: 'presence').message_key).to eq(nil) end end end diff --git a/spec/grape/extensions/param_builders/hash_spec.rb b/spec/grape/extensions/param_builders/hash_spec.rb index 2b68397d0..6cb53dee7 100644 --- a/spec/grape/extensions/param_builders/hash_spec.rb +++ b/spec/grape/extensions/param_builders/hash_spec.rb @@ -10,10 +10,10 @@ def app end describe 'in an endpoint' do - context '#params' do + describe '#params' do before do subject.params do - build_with Grape::Extensions::Hash::ParamBuilder + build_with Grape::Extensions::Hash::ParamBuilder # rubocop:disable RSpec/DescribedClass end subject.get do @@ -21,7 +21,7 @@ def app end end - it 'should be of type Hash' do + it 'is of type Hash' do get '/' expect(last_response.status).to eq(200) expect(last_response.body).to eq('Hash') @@ -31,17 +31,17 @@ def app describe 'in an api' do before do - subject.send(:include, Grape::Extensions::Hash::ParamBuilder) + subject.send(:include, Grape::Extensions::Hash::ParamBuilder) # rubocop:disable RSpec/DescribedClass end - context '#params' do + describe '#params' do before do subject.get do params.class end end - it 'should be Hash' do + it 'is Hash' do get '/' expect(last_response.status).to eq(200) expect(last_response.body).to eq('Hash') @@ -69,7 +69,7 @@ def app it 'symbolizes the params' do subject.params do - build_with Grape::Extensions::Hash::ParamBuilder + build_with Grape::Extensions::Hash::ParamBuilder # rubocop:disable RSpec/DescribedClass requires :a, type: String end 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 4e5b8e6b1..ae64dcc9e 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 @@ -10,10 +10,10 @@ def app end describe 'in an endpoint' do - context '#params' do + describe '#params' do before do subject.params do - build_with Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder + build_with Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder # rubocop:disable RSpec/DescribedClass end subject.get do @@ -21,7 +21,7 @@ def app end end - it 'should be of type Hash' do + it 'is of type Hash' do get '/' expect(last_response.status).to eq(200) expect(last_response.body).to eq('ActiveSupport::HashWithIndifferentAccess') @@ -31,10 +31,10 @@ def app describe 'in an api' do before do - subject.send(:include, Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder) + subject.send(:include, Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder) # rubocop:disable RSpec/DescribedClass end - context '#params' do + describe '#params' do before do subject.get do params.class @@ -49,7 +49,7 @@ def app it 'parses sub hash params' do subject.params do - build_with Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder + build_with Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder # rubocop:disable RSpec/DescribedClass optional :a, type: Hash do optional :b, type: Hash do @@ -70,7 +70,7 @@ def app it 'params are indifferent to symbol or string keys' do subject.params do - build_with Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder + build_with Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder # rubocop:disable RSpec/DescribedClass optional :a, type: Hash do optional :b, type: Hash do optional :c, type: String @@ -90,7 +90,7 @@ def app it 'responds to string keys' do subject.params do - build_with Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder + build_with Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder # rubocop:disable RSpec/DescribedClass requires :a, type: String end diff --git a/spec/grape/extensions/param_builders/hashie/mash_spec.rb b/spec/grape/extensions/param_builders/hashie/mash_spec.rb index b533f5657..9e7fa19ba 100644 --- a/spec/grape/extensions/param_builders/hashie/mash_spec.rb +++ b/spec/grape/extensions/param_builders/hashie/mash_spec.rb @@ -10,10 +10,10 @@ def app end describe 'in an endpoint' do - context '#params' do + describe '#params' do before do subject.params do - build_with Grape::Extensions::Hashie::Mash::ParamBuilder + build_with Grape::Extensions::Hashie::Mash::ParamBuilder # rubocop:disable RSpec/DescribedClass end subject.get do @@ -21,7 +21,7 @@ def app end end - it 'should be of type Hashie::Mash' do + it 'is of type Hashie::Mash' do get '/' expect(last_response.status).to eq(200) expect(last_response.body).to eq('Hashie::Mash') @@ -31,17 +31,17 @@ def app describe 'in an api' do before do - subject.send(:include, Grape::Extensions::Hashie::Mash::ParamBuilder) + subject.send(:include, Grape::Extensions::Hashie::Mash::ParamBuilder) # rubocop:disable RSpec/DescribedClass end - context '#params' do + describe '#params' do before do subject.get do params.class end end - it 'should be Hashie::Mash' do + it 'is Hashie::Mash' do get '/' expect(last_response.status).to eq(200) expect(last_response.body).to eq('Hashie::Mash') @@ -57,7 +57,7 @@ def app end end - it 'should be Hashie::Mash' do + it 'is Hashie::Mash' do get '/foo' expect(last_response.status).to eq(200) expect(last_response.body).to eq('Hashie::Mash') @@ -66,7 +66,7 @@ def app it 'is indifferent to key or symbol access' do subject.params do - build_with Grape::Extensions::Hashie::Mash::ParamBuilder + build_with Grape::Extensions::Hashie::Mash::ParamBuilder # rubocop:disable RSpec/DescribedClass requires :a, type: String end subject.get '/foo' do diff --git a/spec/grape/integration/rack_sendfile_spec.rb b/spec/grape/integration/rack_sendfile_spec.rb index ab7cb1ba0..879797404 100644 --- a/spec/grape/integration/rack_sendfile_spec.rb +++ b/spec/grape/integration/rack_sendfile_spec.rb @@ -44,7 +44,7 @@ it 'not contains Sendfile headers' do headers = subject[1] - expect(headers).to_not include('X-Accel-Redirect') + expect(headers).not_to include('X-Accel-Redirect') end end end diff --git a/spec/grape/loading_spec.rb b/spec/grape/loading_spec.rb index e28891cce..718f5833b 100644 --- a/spec/grape/loading_spec.rb +++ b/spec/grape/loading_spec.rb @@ -3,6 +3,14 @@ require 'spec_helper' describe Grape::API do + subject do + CombinedApi = combined_api + Class.new(Grape::API) do + format :json + mount CombinedApi => '/' + end + end + let(:jobs_api) do Class.new(Grape::API) do namespace :one do @@ -26,14 +34,6 @@ end end - subject do - CombinedApi = combined_api - Class.new(Grape::API) do - format :json - mount CombinedApi => '/' - end - end - def app subject end diff --git a/spec/grape/middleware/base_spec.rb b/spec/grape/middleware/base_spec.rb index 57cba82d1..ac0269a66 100644 --- a/spec/grape/middleware/base_spec.rb +++ b/spec/grape/middleware/base_spec.rb @@ -3,7 +3,8 @@ require 'spec_helper' describe Grape::Middleware::Base do - subject { Grape::Middleware::Base.new(blank_app) } + subject { described_class.new(blank_app) } + let(:blank_app) { ->(_) { [200, {}, 'Hi there.'] } } before do @@ -20,6 +21,8 @@ end context 'callbacks' do + after { subject.call!({}) } + it 'calls #before' do expect(subject).to receive(:before) end @@ -27,8 +30,6 @@ it 'calls #after' do expect(subject).to receive(:after) end - - after { subject.call!({}) } end context 'callbacks on error' do @@ -58,7 +59,7 @@ context 'with patched warnings' do before do @warnings = warnings = [] - allow_any_instance_of(Grape::Middleware::Base).to receive(:warn) { |m| warnings << m } + allow_any_instance_of(described_class).to receive(:warn) { |m| warnings << m } allow(subject).to receive(:after).and_raise(StandardError) end @@ -75,11 +76,14 @@ end describe '#response' do - subject { Grape::Middleware::Base.new(response) } + subject do + puts described_class + described_class.new(response) + end before { subject.call({}) } - context Array do + context 'when Array' do let(:response) { ->(_) { [204, { abc: 1 }, 'test'] } } it 'status' do @@ -99,7 +103,7 @@ end end - context Rack::Response do + context 'when Rack::Response' do let(:response) { ->(_) { Rack::Response.new('test', 204, abc: 1) } } it 'status' do @@ -121,7 +125,8 @@ end describe '#context' do - subject { Grape::Middleware::Base.new(blank_app) } + subject { described_class.new(blank_app) } + it 'allows access to response context' do subject.call(Grape::Env::API_ENDPOINT => { header: 'some header' }) expect(subject.context).to eq(header: 'some header') @@ -130,7 +135,7 @@ context 'options' do it 'persists options passed at initialization' do - expect(Grape::Middleware::Base.new(blank_app, abc: true).options[:abc]).to be true + expect(described_class.new(blank_app, abc: true).options[:abc]).to be true end context 'defaults' do diff --git a/spec/grape/middleware/error_spec.rb b/spec/grape/middleware/error_spec.rb index cb2befbf9..1fe4e3e3e 100644 --- a/spec/grape/middleware/error_spec.rb +++ b/spec/grape/middleware/error_spec.rb @@ -62,6 +62,7 @@ def app context 'with http code' do let(:options) { { default_message: 'Aww, hamburgers.' } } + it 'adds the status code if wanted' do ErrorSpec::ErrApp.error = { message: { code: 200 } } get '/' diff --git a/spec/grape/middleware/exception_spec.rb b/spec/grape/middleware/exception_spec.rb index 11aa8e5aa..cb9f20650 100644 --- a/spec/grape/middleware/exception_spec.rb +++ b/spec/grape/middleware/exception_spec.rb @@ -3,76 +3,82 @@ require 'spec_helper' describe Grape::Middleware::Error do - # raises a text exception - module ExceptionSpec - class ExceptionApp + let(:exception_app) do + Class.new do class << self def call(_env) raise 'rain!' end end end + end - # raises a non-StandardError (ScriptError) exception - class OtherExceptionApp + let(:other_exception_app) do + Class.new do class << self def call(_env) raise NotImplementedError, 'snow!' end end end + end - # raises a hash error - class ErrorHashApp + let(:custom_error_app) do + Class.new do class << self - def error!(message, status) - throw :error, message: { error: message, detail: 'missing widget' }, status: status - end + class CustomError < Grape::Exceptions::Base; end def call(_env) - error!('rain!', 401) + raise CustomError.new(status: 400, message: 'failed validation') end end end + end - # raises an error! - class AccessDeniedApp + let(:error_hash_app) do + Class.new do class << self def error!(message, status) - throw :error, message: message, status: status + throw :error, message: { error: message, detail: 'missing widget' }, status: status end def call(_env) - error!('Access Denied', 401) + error!('rain!', 401) end end end + end - # raises a custom error - class CustomError < Grape::Exceptions::Base - end - - class CustomErrorApp + let(:access_denied_app) do + Class.new do class << self + def error!(message, status) + throw :error, message: message, status: status + end + def call(_env) - raise CustomError.new(status: 400, message: 'failed validation') + error!('Access Denied', 401) end end end end - def app - subject + let(:app) do + builder = Rack::Builder.new + builder.use Spec::Support::EndpointFaker + if options.any? + builder.use described_class, options + else + builder.use described_class + end + builder.run running_app + builder.to_app end context 'with defaults' do - subject do - Rack::Builder.app do - use Spec::Support::EndpointFaker - use Grape::Middleware::Error - run ExceptionSpec::ExceptionApp - end - end + let(:running_app) { exception_app } + let(:options) { {} } + it 'does not trap errors by default' do expect { get '/' }.to raise_error(RuntimeError, 'rain!') end @@ -80,17 +86,14 @@ def app context 'with rescue_all' do context 'StandardError exception' do - subject do - Rack::Builder.app do - use Spec::Support::EndpointFaker - use Grape::Middleware::Error, rescue_all: true - run ExceptionSpec::ExceptionApp - end - end + let(:running_app) { exception_app } + let(:options) { { rescue_all: true } } + it 'sets the message appropriately' do get '/' expect(last_response.body).to eq('rain!') end + it 'defaults to a 500 status' do get '/' expect(last_response.status).to eq(500) @@ -98,13 +101,9 @@ def app end context 'Non-StandardError exception' do - subject do - Rack::Builder.app do - use Spec::Support::EndpointFaker - use Grape::Middleware::Error, rescue_all: true - run ExceptionSpec::OtherExceptionApp - end - end + let(:running_app) { other_exception_app } + let(:options) { { rescue_all: true } } + it 'does not trap errors other than StandardError' do expect { get '/' }.to raise_error(NotImplementedError, 'snow!') end @@ -113,13 +112,9 @@ def app context 'Non-StandardError exception with a provided rescue handler' do context 'default error response' do - subject do - Rack::Builder.app do - use Spec::Support::EndpointFaker - use Grape::Middleware::Error, rescue_handlers: { NotImplementedError => nil } - run ExceptionSpec::OtherExceptionApp - end - end + let(:running_app) { other_exception_app } + let(:options) { { rescue_handlers: { NotImplementedError => nil } } } + it 'rescues the exception using the default handler' do get '/' expect(last_response.body).to eq('snow!') @@ -127,13 +122,9 @@ def app end context 'custom error response' do - subject do - Rack::Builder.app do - use Spec::Support::EndpointFaker - use Grape::Middleware::Error, rescue_handlers: { NotImplementedError => -> { Rack::Response.new('rescued', 200, {}) } } - run ExceptionSpec::OtherExceptionApp - end - end + let(:running_app) { other_exception_app } + let(:options) { { rescue_handlers: { NotImplementedError => -> { Rack::Response.new('rescued', 200, {}) } } } } + it 'rescues the exception using the provided handler' do get '/' expect(last_response.body).to eq('rescued') @@ -142,13 +133,9 @@ def app end context do - subject do - Rack::Builder.app do - use Spec::Support::EndpointFaker - use Grape::Middleware::Error, rescue_all: true, default_status: 500 - run ExceptionSpec::ExceptionApp - end - end + let(:running_app) { exception_app } + let(:options) { { rescue_all: true, default_status: 500 } } + it 'is possible to specify a different default status code' do get '/' expect(last_response.status).to eq(500) @@ -156,13 +143,9 @@ def app end context do - subject do - Rack::Builder.app do - use Spec::Support::EndpointFaker - use Grape::Middleware::Error, rescue_all: true, format: :json - run ExceptionSpec::ExceptionApp - end - end + let(:running_app) { exception_app } + let(:options) { { rescue_all: true, format: :json } } + it 'is possible to return errors in json format' do get '/' expect(last_response.body).to eq('{"error":"rain!"}') @@ -170,13 +153,9 @@ def app end context do - subject do - Rack::Builder.app do - use Spec::Support::EndpointFaker - use Grape::Middleware::Error, rescue_all: true, format: :json - run ExceptionSpec::ErrorHashApp - end - end + let(:running_app) { error_hash_app } + let(:options) { { rescue_all: true, format: :json } } + it 'is possible to return hash errors in json format' do get '/' expect(['{"error":"rain!","detail":"missing widget"}', @@ -185,13 +164,9 @@ def app end context do - subject do - Rack::Builder.app do - use Spec::Support::EndpointFaker - use Grape::Middleware::Error, rescue_all: true, format: :jsonapi - run ExceptionSpec::ExceptionApp - end - end + let(:running_app) { exception_app } + let(:options) { { rescue_all: true, format: :jsonapi } } + it 'is possible to return errors in jsonapi format' do get '/' expect(last_response.body).to eq('{"error":"rain!"}') @@ -199,13 +174,8 @@ def app end context do - subject do - Rack::Builder.app do - use Spec::Support::EndpointFaker - use Grape::Middleware::Error, rescue_all: true, format: :jsonapi - run ExceptionSpec::ErrorHashApp - end - end + let(:running_app) { error_hash_app } + let(:options) { { rescue_all: true, format: :jsonapi } } it 'is possible to return hash errors in jsonapi format' do get '/' @@ -215,13 +185,9 @@ def app end context do - subject do - Rack::Builder.app do - use Spec::Support::EndpointFaker - use Grape::Middleware::Error, rescue_all: true, format: :xml - run ExceptionSpec::ExceptionApp - end - end + let(:running_app) { exception_app } + let(:options) { { rescue_all: true, format: :xml } } + it 'is possible to return errors in xml format' do get '/' expect(last_response.body).to eq("\n\n rain!\n\n") @@ -229,13 +195,9 @@ def app end context do - subject do - Rack::Builder.app do - use Spec::Support::EndpointFaker - use Grape::Middleware::Error, rescue_all: true, format: :xml - run ExceptionSpec::ErrorHashApp - end - end + let(:running_app) { error_hash_app } + let(:options) { { rescue_all: true, format: :xml } } + it 'is possible to return hash errors in xml format' do get '/' expect(["\n\n missing widget\n rain!\n\n", @@ -244,20 +206,19 @@ def app end context do - subject do - Rack::Builder.app do - use Spec::Support::EndpointFaker - use Grape::Middleware::Error, - rescue_all: true, - format: :custom, - error_formatters: { - custom: lambda do |message, _backtrace, _options, _env, _original_exception| - { custom_formatter: message }.inspect - end - } - run ExceptionSpec::ExceptionApp - end + let(:running_app) { exception_app } + let(:options) do + { + rescue_all: true, + format: :custom, + error_formatters: { + custom: lambda do |message, _backtrace, _options, _env, _original_exception| + { custom_formatter: message }.inspect + end + } + } end + it 'is possible to specify a custom formatter' do get '/' expect(last_response.body).to eq('{:custom_formatter=>"rain!"}') @@ -265,13 +226,9 @@ def app end context do - subject do - Rack::Builder.app do - use Spec::Support::EndpointFaker - use Grape::Middleware::Error - run ExceptionSpec::AccessDeniedApp - end - end + let(:running_app) { access_denied_app } + let(:options) { {} } + it 'does not trap regular error! codes' do get '/' expect(last_response.status).to eq(401) @@ -279,13 +236,9 @@ def app end context do - subject do - Rack::Builder.app do - use Spec::Support::EndpointFaker - use Grape::Middleware::Error, rescue_all: false - run ExceptionSpec::CustomErrorApp - end - end + let(:running_app) { custom_error_app } + let(:options) { { rescue_all: false } } + it 'responds to custom Grape exceptions appropriately' do get '/' expect(last_response.status).to eq(400) @@ -294,16 +247,15 @@ def app end context 'with rescue_options :backtrace and :exception set to true' do - subject do - Rack::Builder.app do - use Spec::Support::EndpointFaker - use Grape::Middleware::Error, - rescue_all: true, - format: :json, - rescue_options: { backtrace: true, original_exception: true } - run ExceptionSpec::ExceptionApp - end + let(:running_app) { exception_app } + let(:options) do + { + rescue_all: true, + format: :json, + rescue_options: { backtrace: true, original_exception: true } + } end + it 'is possible to return the backtrace and the original exception in json format' do get '/' expect(last_response.body).to include('error', 'rain!', 'backtrace', 'original_exception', 'RuntimeError') @@ -311,16 +263,15 @@ def app end context do - subject do - Rack::Builder.app do - use Spec::Support::EndpointFaker - use Grape::Middleware::Error, - rescue_all: true, - format: :xml, - rescue_options: { backtrace: true, original_exception: true } - run ExceptionSpec::ExceptionApp - end + let(:running_app) { exception_app } + let(:options) do + { + rescue_all: true, + format: :xml, + rescue_options: { backtrace: true, original_exception: true } + } end + it 'is possible to return the backtrace and the original exception in xml format' do get '/' expect(last_response.body).to include('error', 'rain!', 'backtrace', 'original-exception', 'RuntimeError') @@ -328,16 +279,15 @@ def app end context do - subject do - Rack::Builder.app do - use Spec::Support::EndpointFaker - use Grape::Middleware::Error, - rescue_all: true, - format: :txt, - rescue_options: { backtrace: true, original_exception: true } - run ExceptionSpec::ExceptionApp - end + let(:running_app) { exception_app } + let(:options) do + { + rescue_all: true, + format: :txt, + rescue_options: { backtrace: true, original_exception: true } + } end + it 'is possible to return the backtrace and the original exception in txt format' do get '/' expect(last_response.body).to include('error', 'rain!', 'backtrace', 'original exception', 'RuntimeError') diff --git a/spec/grape/middleware/formatter_spec.rb b/spec/grape/middleware/formatter_spec.rb index 8040d8e87..d112e33b1 100644 --- a/spec/grape/middleware/formatter_spec.rb +++ b/spec/grape/middleware/formatter_spec.rb @@ -3,7 +3,8 @@ require 'spec_helper' describe Grape::Middleware::Formatter do - subject { Grape::Middleware::Formatter.new(app) } + subject { described_class.new(app) } + before { allow(subject).to receive(:dup).and_return(subject) } let(:body) { { 'foo' => 'bar' } } @@ -11,6 +12,7 @@ context 'serialization' do let(:body) { { 'abc' => 'def' } } + it 'looks at the bodies for possibly serializable data' do _, _, bodies = *subject.call('PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/json') bodies.each { |b| expect(b).to eq(::Grape::Json.dump(body)) } @@ -18,6 +20,7 @@ context 'default format' do let(:body) { ['foo'] } + it 'calls #to_json since default format is json' do body.instance_eval do def to_json(*_args) @@ -31,6 +34,7 @@ def to_json(*_args) context 'jsonapi' do let(:body) { { 'foos' => [{ 'bar' => 'baz' }] } } + it 'calls #to_json if the content type is jsonapi' do body.instance_eval do def to_json(*_args) @@ -44,6 +48,7 @@ def to_json(*_args) context 'xml' do let(:body) { +'string' } + it 'calls #to_xml if the content type is xml' do body.instance_eval do def to_xml @@ -58,6 +63,7 @@ def to_xml context 'error handling' do let(:formatter) { double(:formatter) } + before do allow(Grape::Formatter).to receive(:formatter_for) { formatter } end @@ -67,7 +73,7 @@ def to_xml expect do catch(:error) { subject.call('PATH_INFO' => '/somewhere.xml', 'HTTP_ACCEPT' => 'application/json') } - end.to_not raise_error + end.not_to raise_error end it 'does not rescue other exceptions' do @@ -147,7 +153,7 @@ def to_xml subject.options[:content_types][:custom] = 'application/vnd.test+json' end - it 'it uses the custom type' do + it 'uses the custom type' do subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/vnd.test+json') expect(subject.env['api.format']).to eq(:custom) end @@ -164,26 +170,31 @@ def to_xml _, headers, = subject.call('PATH_INFO' => '/info.json') expect(headers['Content-type']).to eq('application/json') end + it 'is set for xml' do _, headers, = subject.call('PATH_INFO' => '/info.xml') expect(headers['Content-type']).to eq('application/xml') end + it 'is set for txt' do _, headers, = subject.call('PATH_INFO' => '/info.txt') expect(headers['Content-type']).to eq('text/plain') end + it 'is set for custom' do subject.options[:content_types] = {} subject.options[:content_types][:custom] = 'application/x-custom' _, headers, = subject.call('PATH_INFO' => '/info.custom') expect(headers['Content-type']).to eq('application/x-custom') end + it 'is set for vendored with registered type' do subject.options[:content_types] = {} subject.options[:content_types][:custom] = 'application/vnd.test+json' _, headers, = subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/vnd.test+json') expect(headers['Content-type']).to eq('application/vnd.test+json') end + it 'is set to closest generic for custom vendored/versioned without registered type' do _, headers, = subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/vnd.test+json') expect(headers['Content-type']).to eq('application/json') @@ -198,13 +209,16 @@ def to_xml _, _, body = subject.call('PATH_INFO' => '/info.custom') 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(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') @@ -272,6 +286,7 @@ def to_xml context 'when body is nil' do let(:io) { double } + before do allow(io).to receive_message_chain(:rewind, :read).and_return(nil) end @@ -290,6 +305,7 @@ def to_xml context 'when body is empty' do let(:io) { double } + before do allow(io).to receive_message_chain(:rewind, :read).and_return('') end @@ -334,6 +350,7 @@ def to_xml expect(subject.env['rack.request.form_hash']['is_boolean']).to be true expect(subject.env['rack.request.form_hash']['string']).to eq('thing') end + it 'rewinds IO' do io = StringIO.new('{"is_boolean":true,"string":"thing"}') io.read @@ -347,6 +364,7 @@ def to_xml expect(subject.env['rack.request.form_hash']['is_boolean']).to be true expect(subject.env['rack.request.form_hash']['string']).to eq('thing') end + it "parses the body from an xml #{method} and copies values into rack.request.from_hash" do io = StringIO.new('Test') subject.call( @@ -362,6 +380,7 @@ def to_xml expect(subject.env['rack.request.form_hash']['thing']['name']['__content__']).to eq('Test') end end + [Rack::Request::FORM_DATA_MEDIA_TYPES, Rack::Request::PARSEABLE_DATA_MEDIA_TYPES].flatten.each do |content_type| it "ignores #{content_type}" do io = StringIO.new('name=Other+Test+Thing') @@ -400,10 +419,12 @@ def self.call(_, _) end end let(:app) { ->(_env) { [200, {}, ['']] } } + before do Grape::Formatter.register :invalid, InvalidFormatter Grape::ContentTypes.register :invalid, 'application/x-invalid' end + after do Grape::ContentTypes.default_elements.delete(:invalid) Grape::Formatter.default_elements.delete(:invalid) @@ -418,7 +439,7 @@ def self.call(_, _) context 'custom parser raises exception and rescue options are enabled for backtrace and original_exception' do it 'adds the backtrace and original_exception to the error output' do - subject = Grape::Middleware::Formatter.new( + subject = described_class.new( app, rescue_options: { backtrace: true, original_exception: true }, parsers: { json: ->(_object, _env) { raise StandardError, 'fail' } } diff --git a/spec/grape/middleware/globals_spec.rb b/spec/grape/middleware/globals_spec.rb index e50eff171..9188953ac 100644 --- a/spec/grape/middleware/globals_spec.rb +++ b/spec/grape/middleware/globals_spec.rb @@ -3,7 +3,8 @@ require 'spec_helper' describe Grape::Middleware::Globals do - subject { Grape::Middleware::Globals.new(blank_app) } + subject { described_class.new(blank_app) } + before { allow(subject).to receive(:dup).and_return(subject) } let(:blank_app) { ->(_env) { [200, {}, 'Hi there.'] } } @@ -13,15 +14,17 @@ end context 'environment' do - it 'should set the grape.request environment' do + it 'sets the grape.request environment' do subject.call({}) expect(subject.env['grape.request']).to be_a(Grape::Request) end - it 'should set the grape.request.headers environment' do + + it 'sets the grape.request.headers environment' do subject.call({}) expect(subject.env['grape.request.headers']).to be_a(Hash) end - it 'should set the grape.request.params environment' do + + it 'sets the grape.request.params environment' do subject.call('QUERY_STRING' => 'test=1', 'rack.input' => StringIO.new) expect(subject.env['grape.request.params']).to be_a(Hash) end diff --git a/spec/grape/middleware/stack_spec.rb b/spec/grape/middleware/stack_spec.rb index 27af97526..94e579e72 100644 --- a/spec/grape/middleware/stack_spec.rb +++ b/spec/grape/middleware/stack_spec.rb @@ -17,11 +17,11 @@ def initialize(&block) end end + subject { described_class.new } + let(:proc) { -> {} } let(:others) { [[:use, StackSpec::BarMiddleware], [:insert_before, StackSpec::BarMiddleware, StackSpec::BlockMiddleware, proc]] } - subject { Grape::Middleware::Stack.new } - before do subject.use StackSpec::FooMiddleware end @@ -29,20 +29,20 @@ def initialize(&block) describe '#use' do it 'pushes a middleware class onto the stack' do expect { subject.use StackSpec::BarMiddleware } - .to change { subject.size }.by(1) + .to change(subject, :size).by(1) expect(subject.last).to eq(StackSpec::BarMiddleware) end it 'pushes a middleware class with arguments onto the stack' do expect { subject.use StackSpec::BarMiddleware, false, my_arg: 42 } - .to change { subject.size }.by(1) + .to change(subject, :size).by(1) expect(subject.last).to eq(StackSpec::BarMiddleware) expect(subject.last.args).to eq([false, { my_arg: 42 }]) end it 'pushes a middleware class with block arguments onto the stack' do expect { subject.use StackSpec::BlockMiddleware, &proc } - .to change { subject.size }.by(1) + .to change(subject, :size).by(1) expect(subject.last).to eq(StackSpec::BlockMiddleware) expect(subject.last.args).to eq([]) expect(subject.last.block).to eq(proc) @@ -52,7 +52,7 @@ def initialize(&block) describe '#insert' do it 'inserts a middleware class at the integer index' do expect { subject.insert 0, StackSpec::BarMiddleware } - .to change { subject.size }.by(1) + .to change(subject, :size).by(1) expect(subject[0]).to eq(StackSpec::BarMiddleware) expect(subject[1]).to eq(StackSpec::FooMiddleware) end @@ -61,7 +61,7 @@ def initialize(&block) describe '#insert_before' do it 'inserts a middleware before another middleware class' do expect { subject.insert_before StackSpec::FooMiddleware, StackSpec::BarMiddleware } - .to change { subject.size }.by(1) + .to change(subject, :size).by(1) expect(subject[0]).to eq(StackSpec::BarMiddleware) expect(subject[1]).to eq(StackSpec::FooMiddleware) end @@ -70,7 +70,7 @@ def initialize(&block) subject.use Class.new(StackSpec::BlockMiddleware) expect { subject.insert_before StackSpec::BlockMiddleware, StackSpec::BarMiddleware } - .to change { subject.size }.by(1) + .to change(subject, :size).by(1) expect(subject[1]).to eq(StackSpec::BarMiddleware) expect(subject[2]).to eq(StackSpec::BlockMiddleware) @@ -85,7 +85,7 @@ def initialize(&block) describe '#insert_after' do it 'inserts a middleware after another middleware class' do expect { subject.insert_after StackSpec::FooMiddleware, StackSpec::BarMiddleware } - .to change { subject.size }.by(1) + .to change(subject, :size).by(1) expect(subject[1]).to eq(StackSpec::BarMiddleware) expect(subject[0]).to eq(StackSpec::FooMiddleware) end @@ -94,7 +94,7 @@ def initialize(&block) subject.use Class.new(StackSpec::BlockMiddleware) expect { subject.insert_after StackSpec::BlockMiddleware, StackSpec::BarMiddleware } - .to change { subject.size }.by(1) + .to change(subject, :size).by(1) expect(subject[1]).to eq(StackSpec::BlockMiddleware) expect(subject[2]).to eq(StackSpec::BarMiddleware) @@ -109,7 +109,7 @@ def initialize(&block) describe '#merge_with' do it 'applies a collection of operations and middlewares' do expect { subject.merge_with(others) } - .to change { subject.size }.by(2) + .to change(subject, :size).by(2) expect(subject[0]).to eq(StackSpec::FooMiddleware) expect(subject[1]).to eq(StackSpec::BlockMiddleware) expect(subject[2]).to eq(StackSpec::BarMiddleware) diff --git a/spec/grape/middleware/versioner/accept_version_header_spec.rb b/spec/grape/middleware/versioner/accept_version_header_spec.rb index f13d44175..9966299f1 100644 --- a/spec/grape/middleware/versioner/accept_version_header_spec.rb +++ b/spec/grape/middleware/versioner/accept_version_header_spec.rb @@ -3,8 +3,9 @@ require 'spec_helper' describe Grape::Middleware::Versioner::AcceptVersionHeader do + subject { described_class.new(app, **(@options || {})) } + let(:app) { ->(env) { [200, env, env] } } - 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 b2349a712..d34462bd0 100644 --- a/spec/grape/middleware/versioner/header_spec.rb +++ b/spec/grape/middleware/versioner/header_spec.rb @@ -3,8 +3,9 @@ require 'spec_helper' describe Grape::Middleware::Versioner::Header do + subject { described_class.new(app, **(@options || {})) } + let(:app) { ->(env) { [200, env, env] } } - subject { Grape::Middleware::Versioner::Header.new(app, **(@options || {})) } before do @options = { @@ -47,7 +48,7 @@ it 'is nil if not provided' do status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor') - expect(env['api.format']).to eql nil + expect(env['api.format']).to be nil expect(status).to eq(200) end @@ -65,7 +66,7 @@ it 'is nil if not provided' do status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1') - expect(env['api.format']).to eql nil + expect(env['api.format']).to be nil expect(status).to eq(200) end end @@ -90,7 +91,7 @@ .to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) expect(exception.headers).to eql('X-Cascade' => 'pass') - expect(exception.status).to eql 406 + expect(exception.status).to be 406 expect(exception.message).to include 'API vendor not found' end end @@ -117,7 +118,7 @@ .to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) expect(exception.headers).to eql('X-Cascade' => 'pass') - expect(exception.status).to eql 406 + expect(exception.status).to be 406 expect(exception.message).to include('API vendor not found') end end @@ -145,7 +146,7 @@ expect { subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v2+json').last }.to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidVersionHeader) expect(exception.headers).to eql('X-Cascade' => 'pass') - expect(exception.status).to eql 406 + expect(exception.status).to be 406 expect(exception.message).to include('API version not found') end end @@ -178,7 +179,7 @@ expect { subject.call({}).last }.to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) expect(exception.headers).to eql('X-Cascade' => 'pass') - expect(exception.status).to eql 406 + expect(exception.status).to be 406 expect(exception.message).to include('Accept header must be set.') end end @@ -187,7 +188,7 @@ expect { subject.call('HTTP_ACCEPT' => '').last }.to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) expect(exception.headers).to eql('X-Cascade' => 'pass') - expect(exception.status).to eql 406 + expect(exception.status).to be 406 expect(exception.message).to include('Accept header must be set.') end end @@ -208,7 +209,7 @@ expect { subject.call({}).last }.to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) expect(exception.headers).to eql({}) - expect(exception.status).to eql 406 + expect(exception.status).to be 406 expect(exception.message).to include('Accept header must be set.') end end @@ -218,7 +219,7 @@ .to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) expect(exception.headers).to eql({}) - expect(exception.status).to eql 406 + expect(exception.status).to be 406 expect(exception.message).to include('API vendor or version not found.') end end @@ -227,7 +228,7 @@ expect { subject.call('HTTP_ACCEPT' => '').last }.to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) expect(exception.headers).to eql({}) - expect(exception.status).to eql 406 + expect(exception.status).to be 406 expect(exception.message).to include('Accept header must be set.') end end @@ -237,7 +238,7 @@ .to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) expect(exception.headers).to eql({}) - expect(exception.status).to eql 406 + expect(exception.status).to be 406 expect(exception.message).to include('API vendor or version not found.') end end @@ -264,7 +265,7 @@ expect { subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v3+json') }.to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidVersionHeader) expect(exception.headers).to eql('X-Cascade' => 'pass') - expect(exception.status).to eql 406 + expect(exception.status).to be 406 expect(exception.message).to include('API version not found') end end diff --git a/spec/grape/middleware/versioner/param_spec.rb b/spec/grape/middleware/versioner/param_spec.rb index 328696128..f2f287428 100644 --- a/spec/grape/middleware/versioner/param_spec.rb +++ b/spec/grape/middleware/versioner/param_spec.rb @@ -3,9 +3,10 @@ require 'spec_helper' describe Grape::Middleware::Versioner::Param do + subject { described_class.new(app, **options) } + let(:app) { ->(env) { [200, env, env['api.version']] } } let(: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' }) @@ -26,10 +27,12 @@ context 'with specified parameter name' do let(:options) { { version_options: { parameter: 'v' } } } + it 'sets the API version based on the custom parameter name' do env = Rack::MockRequest.env_for('/awesome', params: { 'v' => 'v1' }) expect(subject.call(env)[1]['api.version']).to eq('v1') end + it 'does not set the API version based on the default param' do env = Rack::MockRequest.env_for('/awesome', params: { 'apiver' => 'v1' }) expect(subject.call(env)[1]['api.version']).to be_nil @@ -38,10 +41,12 @@ context 'with specified versions' do let(:options) { { versions: %w[v1 v2] } } + it 'throws an error if a non-allowed version is specified' do env = Rack::MockRequest.env_for('/awesome', params: { 'apiver' => 'v3' }) expect(catch(:error) { subject.call(env) }[:status]).to eq(404) end + it 'allows versions that have been specified' do env = Rack::MockRequest.env_for('/awesome', params: { 'apiver' => 'v1' }) expect(subject.call(env)[1]['api.version']).to eq('v1') @@ -55,6 +60,7 @@ version_options: { using: :header } } end + it 'returns a 200 (matches the first version found)' do env = Rack::MockRequest.env_for('/awesome', params: {}) expect(subject.call(env).first).to eq(200) diff --git a/spec/grape/middleware/versioner/path_spec.rb b/spec/grape/middleware/versioner/path_spec.rb index e703931f3..93b56a37e 100644 --- a/spec/grape/middleware/versioner/path_spec.rb +++ b/spec/grape/middleware/versioner/path_spec.rb @@ -3,9 +3,10 @@ require 'spec_helper' describe Grape::Middleware::Versioner::Path do + subject { described_class.new(app, **options) } + let(:app) { ->(env) { [200, env, env['api.version']] } } let(: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') @@ -21,6 +22,7 @@ context 'with a pattern' do let(:options) { { pattern: /v./i } } + it 'sets the version if it matches' do expect(subject.call('PATH_INFO' => '/v1/awesome').last).to eq('v1') end @@ -46,6 +48,7 @@ context 'with prefix, but requested version is not matched' do let(:options) { { prefix: '/v1', pattern: /v./i } } + it 'recognizes potential version' do expect(subject.call('PATH_INFO' => '/v3/foo').last).to eq('v3') end @@ -53,6 +56,7 @@ context 'with mount path' do let(:options) { { mount_path: '/mounted', versions: [:v1] } } + it 'recognizes potential version' do expect(subject.call('PATH_INFO' => '/mounted/v1/foo').last).to eq('v1') end diff --git a/spec/grape/middleware/versioner_spec.rb b/spec/grape/middleware/versioner_spec.rb index 198f15a3a..8399aeeea 100644 --- a/spec/grape/middleware/versioner_spec.rb +++ b/spec/grape/middleware/versioner_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Grape::Middleware::Versioner do - let(:klass) { Grape::Middleware::Versioner } + let(:klass) { described_class } it 'recognizes :path' do expect(klass.using(:path)).to eq(Grape::Middleware::Versioner::Path) diff --git a/spec/grape/parser_spec.rb b/spec/grape/parser_spec.rb index ace8954fa..9e55e4a4c 100644 --- a/spec/grape/parser_spec.rb +++ b/spec/grape/parser_spec.rb @@ -26,6 +26,7 @@ context 'with :parsers option' do let(:parsers) { { customized: Class.new } } + it 'includes passed :parsers values' do expect(subject.parsers(parsers: parsers)).to include(parsers) end @@ -33,7 +34,9 @@ context 'with added parser by using `register` keyword' do let(:added_parser) { Class.new } + before { subject.register :added, added_parser } + it 'includes added parser' do expect(subject.parsers(**{})).to include(added: added_parser) end @@ -54,6 +57,7 @@ context 'when parser is available' do before { subject.register :customized_json, Grape::Parser::Json } + it 'returns registered parser if available' do expect(subject.parser_for(:customized_json)).to eq(Grape::Parser::Json) end diff --git a/spec/grape/path_spec.rb b/spec/grape/path_spec.rb index e43e1df4a..515c66b45 100644 --- a/spec/grape/path_spec.rb +++ b/spec/grape/path_spec.rb @@ -6,63 +6,63 @@ module Grape describe Path do describe '#initialize' do it 'remembers the path' do - path = Path.new('/:id', anything, anything) + path = described_class.new('/:id', anything, anything) expect(path.raw_path).to eql('/:id') end it 'remembers the namespace' do - path = Path.new(anything, '/users', anything) + path = described_class.new(anything, '/users', anything) expect(path.namespace).to eql('/users') end it 'remebers the settings' do - path = Path.new(anything, anything, foo: 'bar') + path = described_class.new(anything, anything, foo: 'bar') expect(path.settings).to eql(foo: 'bar') end end describe '#mount_path' do it 'is nil when no mount path setting exists' do - path = Path.new(anything, anything, {}) + path = described_class.new(anything, anything, {}) expect(path.mount_path).to be_nil end it 'is nil when the mount path is nil' do - path = Path.new(anything, anything, mount_path: nil) + path = described_class.new(anything, anything, mount_path: nil) expect(path.mount_path).to be_nil end it 'splits the mount path' do - path = Path.new(anything, anything, mount_path: %w[foo bar]) + path = described_class.new(anything, anything, mount_path: %w[foo bar]) expect(path.mount_path).to eql(%w[foo bar]) end end describe '#root_prefix' do it 'is nil when no root prefix setting exists' do - path = Path.new(anything, anything, {}) + path = described_class.new(anything, anything, {}) expect(path.root_prefix).to be_nil end it 'is nil when the mount path is nil' do - path = Path.new(anything, anything, root_prefix: nil) + path = described_class.new(anything, anything, root_prefix: nil) expect(path.root_prefix).to be_nil end it 'splits the mount path' do - path = Path.new(anything, anything, root_prefix: 'hello/world') + path = described_class.new(anything, anything, root_prefix: 'hello/world') expect(path.root_prefix).to eql(%w[hello world]) end end describe '#uses_path_versioning?' do it 'is false when the version setting is nil' do - path = Path.new(anything, anything, version: nil) + path = described_class.new(anything, anything, version: nil) expect(path.uses_path_versioning?).to be false end it 'is false when the version option is header' do - path = Path.new( + path = described_class.new( anything, anything, version: 'v1', @@ -73,7 +73,7 @@ module Grape end it 'is true when the version option is path' do - path = Path.new( + path = described_class.new( anything, anything, version: 'v1', @@ -86,44 +86,44 @@ 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_falsey + path = described_class.new(anything, nil, anything) + expect(path).not_to be_namespace end it 'is false when the namespace starts with whitespace' do - path = Path.new(anything, ' /foo', anything) - expect(path.namespace?).to be_falsey + path = described_class.new(anything, ' /foo', anything) + expect(path).not_to be_namespace end it 'is false when the namespace is the root path' do - path = Path.new(anything, '/', anything) + path = described_class.new(anything, '/', anything) expect(path.namespace?).to be false end it 'is true otherwise' do - path = Path.new(anything, '/world', anything) + path = described_class.new(anything, '/world', anything) expect(path.namespace?).to be true end end describe '#path?' do it 'is false when the path is nil' do - path = Path.new(nil, anything, anything) - expect(path.path?).to be_falsey + path = described_class.new(nil, anything, anything) + expect(path).not_to be_path end it 'is false when the path starts with whitespace' do - path = Path.new(' /foo', anything, anything) - expect(path.path?).to be_falsey + path = described_class.new(' /foo', anything, anything) + expect(path).not_to be_path end it 'is false when the path is the root path' do - path = Path.new('/', anything, anything) + path = described_class.new('/', anything, anything) expect(path.path?).to be false end it 'is true otherwise' do - path = Path.new('/hello', anything, anything) + path = described_class.new('/hello', anything, anything) expect(path.path?).to be true end end @@ -131,24 +131,24 @@ module Grape describe '#path' do context 'mount_path' do it 'is not included when it is nil' do - path = Path.new(nil, nil, mount_path: '/foo/bar') + path = described_class.new(nil, nil, mount_path: '/foo/bar') expect(path.path).to eql '/foo/bar' end it 'is included when it is not nil' do - path = Path.new(nil, nil, {}) + path = described_class.new(nil, nil, {}) expect(path.path).to eql('/') end end context 'root_prefix' do it 'is not included when it is nil' do - path = Path.new(nil, nil, {}) + path = described_class.new(nil, nil, {}) expect(path.path).to eql('/') end it 'is included after the mount path' do - path = Path.new( + path = described_class.new( nil, nil, mount_path: '/foo', @@ -160,7 +160,7 @@ module Grape end it 'uses the namespace after the mount path and root prefix' do - path = Path.new( + path = described_class.new( nil, 'namespace', mount_path: '/foo', @@ -171,7 +171,7 @@ module Grape end it 'uses the raw path after the namespace' do - path = Path.new( + path = described_class.new( 'raw_path', 'namespace', mount_path: '/foo', @@ -185,9 +185,9 @@ module Grape describe '#suffix' do context 'when using a specific format' do it 'accepts specified format' do - path = Path.new(nil, nil, {}) - allow(path).to receive(:uses_specific_format?) { true } - allow(path).to receive(:settings) { { format: :json } } + path = described_class.new(nil, nil, {}) + allow(path).to receive(:uses_specific_format?).and_return(true) + allow(path).to receive(:settings).and_return({ format: :json }) expect(path.suffix).to eql('(.json)') end @@ -195,9 +195,9 @@ module Grape context 'when path versioning is used' do it "includes a '/'" do - path = Path.new(nil, nil, {}) - allow(path).to receive(:uses_specific_format?) { false } - allow(path).to receive(:uses_path_versioning?) { true } + path = described_class.new(nil, nil, {}) + allow(path).to receive(:uses_specific_format?).and_return(false) + allow(path).to receive(:uses_path_versioning?).and_return(true) expect(path.suffix).to eql('(/.:format)') end @@ -205,25 +205,25 @@ module Grape context 'when path versioning is not used' do it "does not include a '/' when the path has a namespace" do - path = Path.new(nil, 'namespace', {}) - allow(path).to receive(:uses_specific_format?) { false } - allow(path).to receive(:uses_path_versioning?) { true } + path = described_class.new(nil, 'namespace', {}) + allow(path).to receive(:uses_specific_format?).and_return(false) + allow(path).to receive(:uses_path_versioning?).and_return(true) expect(path.suffix).to eql('(.:format)') end it "does not include a '/' when the path has a path" do - path = Path.new('/path', nil, {}) - allow(path).to receive(:uses_specific_format?) { false } - allow(path).to receive(:uses_path_versioning?) { true } + path = described_class.new('/path', nil, {}) + allow(path).to receive(:uses_specific_format?).and_return(false) + allow(path).to receive(:uses_path_versioning?).and_return(true) expect(path.suffix).to eql('(.:format)') end it "includes a '/' otherwise" do - path = Path.new(nil, nil, {}) - allow(path).to receive(:uses_specific_format?) { false } - allow(path).to receive(:uses_path_versioning?) { true } + path = described_class.new(nil, nil, {}) + allow(path).to receive(:uses_specific_format?).and_return(false) + allow(path).to receive(:uses_path_versioning?).and_return(true) expect(path.suffix).to eql('(/.:format)') end @@ -232,19 +232,19 @@ module Grape describe '#path_with_suffix' do it 'combines the path and suffix' do - path = Path.new(nil, nil, {}) - allow(path).to receive(:path) { '/the/path' } - allow(path).to receive(:suffix) { 'suffix' } + path = described_class.new(nil, nil, {}) + allow(path).to receive(:path).and_return('/the/path') + allow(path).to receive(:suffix).and_return('suffix') expect(path.path_with_suffix).to eql('/the/pathsuffix') end context 'when using a specific format' do it 'might have a suffix with specified format' do - path = Path.new(nil, nil, {}) - allow(path).to receive(:path) { '/the/path' } - allow(path).to receive(:uses_specific_format?) { true } - allow(path).to receive(:settings) { { format: :json } } + path = described_class.new(nil, nil, {}) + allow(path).to receive(:path).and_return('/the/path') + allow(path).to receive(:uses_specific_format?).and_return(true) + allow(path).to receive(:settings).and_return({ format: :json }) expect(path.path_with_suffix).to eql('/the/path(.json)') end diff --git a/spec/grape/presenters/presenter_spec.rb b/spec/grape/presenters/presenter_spec.rb index 4e73e8e5b..c5a606052 100644 --- a/spec/grape/presenters/presenter_spec.rb +++ b/spec/grape/presenters/presenter_spec.rb @@ -19,18 +19,18 @@ def initialize end describe Presenter do + subject { PresenterSpec::Dummy.new } + describe 'represent' do let(:object_mock) do Object.new end it 'represent object' do - expect(Presenter.represent(object_mock)).to eq object_mock + expect(described_class.represent(object_mock)).to eq object_mock end end - subject { PresenterSpec::Dummy.new } - describe 'present' do let(:hash_mock) do { key: :value } @@ -38,8 +38,9 @@ def initialize describe 'instance' do before do - subject.present hash_mock, with: Grape::Presenters::Presenter + subject.present hash_mock, with: described_class end + it 'presents dummy hash' do expect(subject.body).to eq hash_mock end @@ -56,8 +57,8 @@ def initialize describe 'instance' do before do - subject.present hash_mock1, with: Grape::Presenters::Presenter - subject.present hash_mock2, with: Grape::Presenters::Presenter + subject.present hash_mock1, with: described_class + subject.present hash_mock2, with: described_class end it 'presents both dummy presenter' do diff --git a/spec/grape/request_spec.rb b/spec/grape/request_spec.rb index 1bfce035e..1f9eac94e 100644 --- a/spec/grape/request_spec.rb +++ b/spec/grape/request_spec.rb @@ -21,7 +21,7 @@ module Grape let(:env) { default_env } let(:request) do - Grape::Request.new(env) + described_class.new(env) end describe '#params' do @@ -38,7 +38,7 @@ module Grape context 'when build_params_with: Grape::Extensions::Hash::ParamBuilder is specified' do let(:request) do - Grape::Request.new(env, build_params_with: Grape::Extensions::Hash::ParamBuilder) + described_class.new(env, build_params_with: Grape::Extensions::Hash::ParamBuilder) end it 'returns symbolized params' do @@ -65,6 +65,8 @@ module Grape end describe 'when the param_builder is set to Hashie' do + subject(:request_params) { described_class.new(env, **opts).params } + before do Grape.configure do |config| config.param_builder = Grape::Extensions::Hashie::Mash::ParamBuilder @@ -75,15 +77,15 @@ module Grape 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 diff --git a/spec/grape/util/inheritable_setting_spec.rb b/spec/grape/util/inheritable_setting_spec.rb index 0e0b672e7..76e862d1a 100644 --- a/spec/grape/util/inheritable_setting_spec.rb +++ b/spec/grape/util/inheritable_setting_spec.rb @@ -4,12 +4,12 @@ module Grape module Util describe InheritableSetting do - before :each do - InheritableSetting.reset_global! + before do + described_class.reset_global! end let(:parent) do - Grape::Util::InheritableSetting.new.tap do |settings| + described_class.new.tap do |settings| settings.global[:global_thing] = :global_foo_bar settings.namespace[:namespace_thing] = :namespace_foo_bar settings.namespace_inheritable[:namespace_inheritable_thing] = :namespace_inheritable_foo_bar @@ -20,7 +20,7 @@ module Util end let(:other_parent) do - Grape::Util::InheritableSetting.new.tap do |settings| + described_class.new.tap do |settings| settings.namespace[:namespace_thing] = :namespace_foo_bar_other settings.namespace_inheritable[:namespace_inheritable_thing] = :namespace_inheritable_foo_bar_other settings.namespace_stackable[:namespace_stackable_thing] = :namespace_stackable_foo_bar_other @@ -29,7 +29,7 @@ module Util end end - before :each do + before do subject.inherit_from parent end @@ -50,7 +50,7 @@ module Util expect(parent.global[:global_thing]).to eq :global_new_foo_bar end - it 'should handle different parents' do + it 'handles different parents' do subject.global[:global_thing] = :global_new_foo_bar subject.inherit_from other_parent @@ -89,7 +89,7 @@ module Util expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :namespace_inheritable_foo_bar end - it 'should handle different parents' do + it 'handles different parents' do expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :namespace_inheritable_foo_bar subject.inherit_from other_parent diff --git a/spec/grape/util/inheritable_values_spec.rb b/spec/grape/util/inheritable_values_spec.rb index a5003d875..984475aba 100644 --- a/spec/grape/util/inheritable_values_spec.rb +++ b/spec/grape/util/inheritable_values_spec.rb @@ -4,8 +4,9 @@ module Grape module Util describe InheritableValues do - let(:parent) { InheritableValues.new } - subject { InheritableValues.new(parent) } + subject { described_class.new(parent) } + + let(:parent) { described_class.new } describe '#delete' do it 'deletes a key' do diff --git a/spec/grape/util/reverse_stackable_values_spec.rb b/spec/grape/util/reverse_stackable_values_spec.rb index c5ffbd293..92b4f2988 100644 --- a/spec/grape/util/reverse_stackable_values_spec.rb +++ b/spec/grape/util/reverse_stackable_values_spec.rb @@ -4,9 +4,10 @@ module Grape module Util describe ReverseStackableValues do - let(:parent) { described_class.new } subject { described_class.new(parent) } + let(:parent) { described_class.new } + describe '#keys' do it 'returns all keys' do subject[:some_thing] = :foo_bar @@ -102,6 +103,7 @@ module Util describe '#clone' do let(:obj_cloned) { subject.clone } + it 'copies all values' do parent = described_class.new child = described_class.new parent diff --git a/spec/grape/util/stackable_values_spec.rb b/spec/grape/util/stackable_values_spec.rb index c7defdf75..f43e94a37 100644 --- a/spec/grape/util/stackable_values_spec.rb +++ b/spec/grape/util/stackable_values_spec.rb @@ -4,8 +4,9 @@ module Grape module Util describe StackableValues do - let(:parent) { StackableValues.new } - subject { StackableValues.new(parent) } + subject { described_class.new(parent) } + + let(:parent) { described_class.new } describe '#keys' do it 'returns all keys' do @@ -99,10 +100,11 @@ module Util describe '#clone' do let(:obj_cloned) { subject.clone } + it 'copies all values' do - parent = StackableValues.new - child = StackableValues.new parent - grandchild = StackableValues.new child + parent = described_class.new + child = described_class.new parent + grandchild = described_class.new child parent[:some_thing] = :foo child[:some_thing] = %i[bar more] diff --git a/spec/grape/validations/instance_behaivour_spec.rb b/spec/grape/validations/instance_behaivour_spec.rb index f6ba28c94..bbbddbfd6 100644 --- a/spec/grape/validations/instance_behaivour_spec.rb +++ b/spec/grape/validations/instance_behaivour_spec.rb @@ -14,15 +14,6 @@ def validate_param!(_attr_name, _params) end end end - - before do - Grape::Validations.register_validator('instance_validator', validator_type) - end - - after do - Grape::Validations.deregister_validator('instance_validator') - end - let(:app) do Class.new(Grape::API) do params do @@ -35,6 +26,14 @@ def validate_param!(_attr_name, _params) end end + before do + Grape::Validations.register_validator('instance_validator', validator_type) + end + + after do + Grape::Validations.deregister_validator('instance_validator') + end + it 'passes validation every time' do expect(validator_type).to receive(:new).exactly(4).times.and_call_original diff --git a/spec/grape/validations/multiple_attributes_iterator_spec.rb b/spec/grape/validations/multiple_attributes_iterator_spec.rb index 1508a76ed..af8e802f1 100644 --- a/spec/grape/validations/multiple_attributes_iterator_spec.rb +++ b/spec/grape/validations/multiple_attributes_iterator_spec.rb @@ -5,6 +5,7 @@ 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]) } diff --git a/spec/grape/validations/params_scope_spec.rb b/spec/grape/validations/params_scope_spec.rb index 290d646bf..f2169bf03 100644 --- a/spec/grape/validations/params_scope_spec.rb +++ b/spec/grape/validations/params_scope_spec.rb @@ -292,7 +292,7 @@ def initialize(value) it 'does not raise an exception' do expect do subject.params { optional :numbers, type: Array[Integer], values: 0..2, default: 0..2 } - end.to_not raise_error + end.not_to raise_error end end @@ -300,7 +300,7 @@ def initialize(value) it 'does not raise an exception' do expect do subject.params { optional :numbers, type: Array[Integer], values: [0, 1, 2], default: [1, 0] } - end.to_not raise_error + end.not_to raise_error end end end @@ -524,7 +524,7 @@ def initialize(value) requires :c end end - end.to_not raise_error + end.not_to raise_error end it 'does not raise an error if when using nested given' do @@ -540,7 +540,7 @@ def initialize(value) end end end - end.to_not raise_error + end.not_to raise_error end it 'allows nested dependent parameters' do @@ -585,7 +585,7 @@ def initialize(value) body = JSON.parse(last_response.body) expect(body.keys).to include('c') - expect(body.keys).to_not include('b') + expect(body.keys).not_to include('b') end it 'allows renaming of dependent on parameter' do @@ -787,7 +787,7 @@ def initialize(value) subject.get('/test') { 'ok' } end - it 'should pass none Hash params' do + it 'passes none Hash params' do get '/test', foos: [''] expect(last_response.status).to eq(200) expect(last_response.body).to eq('ok') @@ -963,6 +963,7 @@ def initialize(value) expect(last_response.body).to eq('one is missing, two is missing, three is missing') end end + context 'when fail_fast is defined it stops the validation' do it 'of other params' do subject.params do @@ -975,6 +976,7 @@ def initialize(value) expect(last_response.status).to eq(400) expect(last_response.body).to eq('one is missing') end + it 'for a single param' do subject.params do requires :one, allow_blank: false, regexp: /[0-9]+/, fail_fast: true @@ -1025,7 +1027,7 @@ def initialize(value) end it 'prioritizes parameter validation over group validation' do - expect(last_response.body).to_not include('address is empty') + expect(last_response.body).not_to include('address is empty') end end end diff --git a/spec/grape/validations/single_attribute_iterator_spec.rb b/spec/grape/validations/single_attribute_iterator_spec.rb index e23b9a359..7784e7dc1 100644 --- a/spec/grape/validations/single_attribute_iterator_spec.rb +++ b/spec/grape/validations/single_attribute_iterator_spec.rb @@ -5,6 +5,7 @@ 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]) } diff --git a/spec/grape/validations/types/primitive_coercer_spec.rb b/spec/grape/validations/types/primitive_coercer_spec.rb index 88f22efc0..3a071d800 100644 --- a/spec/grape/validations/types/primitive_coercer_spec.rb +++ b/spec/grape/validations/types/primitive_coercer_spec.rb @@ -3,10 +3,10 @@ require 'spec_helper' describe Grape::Validations::Types::PrimitiveCoercer do - let(:strict) { false } - subject { described_class.new(type, strict) } + let(:strict) { false } + describe '#call' do context 'BigDecimal' do let(:type) { BigDecimal } diff --git a/spec/grape/validations/types_spec.rb b/spec/grape/validations/types_spec.rb index 1bee89e77..71e2cae32 100644 --- a/spec/grape/validations/types_spec.rb +++ b/spec/grape/validations/types_spec.rb @@ -20,13 +20,13 @@ def self.parse; end Date, DateTime, Time ].each do |type| it "recognizes #{type} as a primitive" do - expect(described_class.primitive?(type)).to be_truthy + expect(described_class).to be_primitive(type) end end it 'identifies unknown types' do - expect(described_class.primitive?(Object)).to be_falsy - expect(described_class.primitive?(TypesSpec::FooType)).to be_falsy + expect(described_class).not_to be_primitive(Object) + expect(described_class).not_to be_primitive(TypesSpec::FooType) end end @@ -35,7 +35,7 @@ def self.parse; end Hash, Array, Set ].each do |type| it "recognizes #{type} as a structure" do - expect(described_class.structure?(type)).to be_truthy + expect(described_class).to be_structure(type) end end end @@ -45,22 +45,22 @@ def self.parse; end JSON, Array[JSON], File, Rack::Multipart::UploadedFile ].each do |type| it "provides special handling for #{type.inspect}" do - expect(described_class.special?(type)).to be_truthy + expect(described_class).to be_special(type) end end end describe '::custom?' do it 'returns false if the type does not respond to :parse' do - expect(described_class.custom?(Object)).to be_falsy + expect(described_class).not_to be_custom(Object) end it 'returns true if the type responds to :parse with one argument' do - expect(described_class.custom?(TypesSpec::FooType)).to be_truthy + expect(described_class).to be_custom(TypesSpec::FooType) end it 'returns false if the type\'s #parse method takes other than one argument' do - expect(described_class.custom?(TypesSpec::BarType)).to be_falsy + expect(described_class).not_to be_custom(TypesSpec::BarType) end end diff --git a/spec/grape/validations/validators/allow_blank_spec.rb b/spec/grape/validations/validators/allow_blank_spec.rb index c23a755a6..2fca98c3b 100644 --- a/spec/grape/validations/validators/allow_blank_spec.rb +++ b/spec/grape/validations/validators/allow_blank_spec.rb @@ -283,10 +283,12 @@ get '/custom_message', name: '' expect(last_response.body).to eq('{"error":"name has no value"}') end + it 'refuses empty string for an optional param' do get '/custom_message/disallow_blank_optional_param', name: '' expect(last_response.body).to eq('{"error":"name has no value"}') end + it 'refuses only whitespaces' do get '/custom_message', name: ' ' expect(last_response.body).to eq('{"error":"name has no value"}') diff --git a/spec/grape/validations/validators/coerce_spec.rb b/spec/grape/validations/validators/coerce_spec.rb index 626859420..6c4cc29fe 100644 --- a/spec/grape/validations/validators/coerce_spec.rb +++ b/spec/grape/validations/validators/coerce_spec.rb @@ -23,7 +23,7 @@ def self.parsed?(value) end context 'i18n' do - after :each do + after do I18n.available_locales = %i[en] I18n.locale = :en I18n.default_locale = :en diff --git a/spec/grape/validations/validators/presence_spec.rb b/spec/grape/validations/validators/presence_spec.rb index a83399588..ebd0a1296 100644 --- a/spec/grape/validations/validators/presence_spec.rb +++ b/spec/grape/validations/validators/presence_spec.rb @@ -21,6 +21,7 @@ def app end end end + it 'does not validate for any params' do get '/bacons' expect(last_response.status).to eq(200) @@ -39,15 +40,18 @@ def app end end end + it 'requires when missing' do get '/requires' expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"email is required, email has no value"}') end + it 'requires when empty' do get '/requires', email: '' expect(last_response.body).to eq('{"error":"email has no value, email format is invalid"}') end + it 'valid when set' do get '/requires', email: 'bob@example.com' expect(last_response.status).to eq(200) @@ -65,6 +69,7 @@ def app { ret: params[:id] } end end + it 'validates id' do post '/' expect(last_response.status).to eq(400) @@ -91,16 +96,19 @@ def app 'Hello' end end + it 'requires when missing' do get '/' expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"email is missing, email is empty"}') end + it 'requires when empty' do get '/', email: '' expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"email is empty, email is invalid"}') end + it 'valid when set' do get '/', email: 'bob@example.com' expect(last_response.status).to eq(200) @@ -125,6 +133,7 @@ def app 'Hello' end end + it 'validates for all defined params' do get '/single-requires' expect(last_response.status).to eq(400) @@ -145,6 +154,7 @@ def app 'Hello' end end + it 'validates name, company' do get '/' expect(last_response.status).to eq(400) @@ -172,6 +182,7 @@ def app 'Nested' end end + it 'validates nested parameters' do get '/nested' expect(last_response.status).to eq(400) @@ -204,6 +215,7 @@ def app 'Nested triple' end end + it 'validates triple nested parameters' do get '/nested_triple' expect(last_response.status).to eq(400) @@ -253,6 +265,7 @@ def app 'Hello optional' end end + it 'works with required' do get '/required' expect(last_response.status).to eq(400) @@ -262,6 +275,7 @@ def app expect(last_response.status).to eq(200) expect(last_response.body).to eq('Hello required'.to_json) end + it 'works with optional' do get '/optional' expect(last_response.status).to eq(200) diff --git a/spec/grape/validations_spec.rb b/spec/grape/validations_spec.rb index 9c2486f88..ac1f1df33 100644 --- a/spec/grape/validations_spec.rb +++ b/spec/grape/validations_spec.rb @@ -500,11 +500,11 @@ def validate_param!(attr_name, params) end before do - Grape::Validations.register_validator('date_range', date_range_validator) + described_class.register_validator('date_range', date_range_validator) end after do - Grape::Validations.deregister_validator('date_range') + described_class.deregister_validator('date_range') end before do @@ -1200,11 +1200,11 @@ def validate_param!(attr_name, params) end before do - Grape::Validations.register_validator('customvalidator', custom_validator) + described_class.register_validator('customvalidator', custom_validator) end after do - Grape::Validations.deregister_validator('customvalidator') + described_class.deregister_validator('customvalidator') end context 'when using optional with a custom validator' do @@ -1357,11 +1357,11 @@ def validate_param!(attr_name, params) end before do - Grape::Validations.register_validator('customvalidator_with_options', custom_validator_with_options) + described_class.register_validator('customvalidator_with_options', custom_validator_with_options) end after do - Grape::Validations.deregister_validator('customvalidator_with_options') + described_class.deregister_validator('customvalidator_with_options') end before do @@ -1458,21 +1458,25 @@ def validate_param!(attr_name, params) } end end + it 'returns defaults' do get '/order' expect(last_response.status).to eq(200) expect(last_response.body).to eq({ order: :asc, order_by: :created_at }.to_json) end + it 'overrides default value for order' do get '/order?order=desc' expect(last_response.status).to eq(200) expect(last_response.body).to eq({ order: :desc, order_by: :created_at }.to_json) end + it 'overrides default value for order_by' do get '/order?order_by=name' expect(last_response.status).to eq(200) expect(last_response.body).to eq({ order: :asc, order_by: :name }.to_json) end + it 'fails with invalid value' do get '/order?order=invalid' expect(last_response.status).to eq(400) @@ -1497,7 +1501,7 @@ def validate_param!(attr_name, params) context 'all or none' do context 'optional params' do - before :each do + before do subject.resource :custom_message do params do optional :beer @@ -1510,17 +1514,20 @@ def validate_param!(attr_name, params) end end end + context 'with a custom validation message' do it 'errors when any one is present' do get '/custom_message/all_or_none', beer: 'string' expect(last_response.status).to eq(400) expect(last_response.body).to eq 'beer, wine, juice all params are required or none is required' end + it 'works when all params are present' do get '/custom_message/all_or_none', beer: 'string', wine: 'anotherstring', juice: 'anotheranotherstring' expect(last_response.status).to eq(200) expect(last_response.body).to eq 'all_or_none works!' end + it 'works when none are present' do get '/custom_message/all_or_none' expect(last_response.status).to eq(200) @@ -1684,7 +1691,7 @@ def validate_param!(attr_name, params) context 'exactly one of' do context 'params' do - before :each do + before do subject.resources :custom_message do params do optional :beer @@ -1748,7 +1755,7 @@ def validate_param!(attr_name, params) end context 'nested params' do - before :each do + before do subject.params do requires :nested, type: Hash do optional :beer_nested @@ -1790,7 +1797,7 @@ def validate_param!(attr_name, params) context 'at least one of' do context 'params' do - before :each do + before do subject.resources :custom_message do params do optional :beer @@ -1854,7 +1861,7 @@ def validate_param!(attr_name, params) end context 'nested params' do - before :each do + before do subject.params do requires :nested, type: Hash do optional :beer diff --git a/spec/integration/eager_load/eager_load_spec.rb b/spec/integration/eager_load/eager_load_spec.rb index 94b78e3e0..174ba9944 100644 --- a/spec/integration/eager_load/eager_load_spec.rb +++ b/spec/integration/eager_load/eager_load_spec.rb @@ -6,10 +6,10 @@ describe Grape do it 'eager_load!' do require 'grape/eager_load' - expect { Grape.eager_load! }.to_not raise_error + expect { described_class.eager_load! }.not_to raise_error end it 'compile!' do - expect { Class.new(Grape::API).compile! }.to_not raise_error + expect { Class.new(Grape::API).compile! }.not_to raise_error end end diff --git a/spec/integration/multi_json/json_spec.rb b/spec/integration/multi_json/json_spec.rb index fc06602fd..ab0224d6c 100644 --- a/spec/integration/multi_json/json_spec.rb +++ b/spec/integration/multi_json/json_spec.rb @@ -4,6 +4,6 @@ describe Grape::Json do it 'uses multi_json' do - expect(Grape::Json).to eq(::MultiJson) + expect(described_class).to eq(::MultiJson) end end diff --git a/spec/integration/multi_xml/xml_spec.rb b/spec/integration/multi_xml/xml_spec.rb index dde1fec5c..7aa11e2ba 100644 --- a/spec/integration/multi_xml/xml_spec.rb +++ b/spec/integration/multi_xml/xml_spec.rb @@ -4,6 +4,6 @@ describe Grape::Xml do it 'uses multi_xml' do - expect(Grape::Xml).to eq(::MultiXml) + expect(described_class).to eq(::MultiXml) end end diff --git a/spec/shared/versioning_examples.rb b/spec/shared/versioning_examples.rb index d7b61dc1a..192d7bd03 100644 --- a/spec/shared/versioning_examples.rb +++ b/spec/shared/versioning_examples.rb @@ -35,14 +35,14 @@ end versioned_get '/awesome', 'v1', **macro_options - expect(last_response.status).to eql 404 + expect(last_response.status).to be 404 versioned_get '/awesome', 'v2', **macro_options - expect(last_response.status).to eql 200 + expect(last_response.status).to be 200 versioned_get '/legacy', 'v1', **macro_options - expect(last_response.status).to eql 200 + expect(last_response.status).to be 200 versioned_get '/legacy', 'v2', **macro_options - expect(last_response.status).to eql 404 + expect(last_response.status).to be 404 end it 'is able to specify multiple versions' do @@ -52,11 +52,11 @@ end versioned_get '/awesome', 'v1', **macro_options - expect(last_response.status).to eql 200 + expect(last_response.status).to be 200 versioned_get '/awesome', 'v2', **macro_options - expect(last_response.status).to eql 200 + expect(last_response.status).to be 200 versioned_get '/awesome', 'v3', **macro_options - expect(last_response.status).to eql 404 + expect(last_response.status).to be 404 end context 'with different versions for the same endpoint' do @@ -117,6 +117,7 @@ before do @output ||= 'v2-' end + get 'version' do @output += 'version' end @@ -126,6 +127,7 @@ before do @output ||= 'v1-' end + get 'version' do @output += 'version' end @@ -170,6 +172,7 @@ end klass end + before do subject.format :txt diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 1dd8aee2b..66c56727b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -37,7 +37,7 @@ def rollback_transaction; end config.warnings = true config.before(:all) { Grape::Util::InheritableSetting.reset_global! } - config.before(:each) { Grape::Util::InheritableSetting.reset_global! } + config.before { Grape::Util::InheritableSetting.reset_global! } # Enable flags like --only-failures and --next-failure config.example_status_persistence_file_path = '.rspec_status' From f98ea2c24dd25b4574af809dce0860ff373217c6 Mon Sep 17 00:00:00 2001 From: Baptiste Courtois Date: Mon, 20 Dec 2021 21:36:58 +0100 Subject: [PATCH 075/304] Require main active_support lib before any of its extension definitions It seems to be the recommended way with active_support. See https://guides.rubyonrails.org/active_support_core_extensions.html#cherry-picking-a-definition Fix #2205. Signed-off-by: Baptiste Courtois --- CHANGELOG.md | 1 + lib/grape.rb | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b194db26..68b439dfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ #### Fixes +* [#2206](https://github.com/ruby-grape/grape/pull/2206): Require main active_support lib before any of its extension definitions - [@annih](https://github.com/Annih). * [#2193](https://github.com/ruby-grape/grape/pull/2193): Fixed the broken ruby-head NoMethodError spec - [@Jack12816](https://github.com/Jack12816). * [#2192](https://github.com/ruby-grape/grape/pull/2192): Memoize the result of Grape::Middleware::Base#response - [@Jack12816](https://github.com/Jack12816). * [#2200](https://github.com/ruby-grape/grape/pull/2200): Add validators module to all validators - [@ericproulx](https://github.com/ericproulx). diff --git a/lib/grape.rb b/lib/grape.rb index 2fc201e6c..e7b500649 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -7,6 +7,7 @@ require 'rack/auth/basic' require 'rack/auth/digest/md5' require 'set' +require 'active_support' require 'active_support/version' require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/object/blank' From 6d417fad0acd3564213371ddd5b4ec561c7519f2 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Mon, 27 Dec 2021 17:43:11 +0100 Subject: [PATCH 076/304] Validations Autoload (#2207) --- CHANGELOG.md | 1 + lib/grape.rb | 62 ++++++++++++------- lib/grape/dsl/helpers.rb | 2 +- lib/grape/util/json.rb | 2 + lib/grape/util/strict_hash_configuration.rb | 2 +- lib/grape/validations.rb | 6 -- lib/grape/validations/types/json.rb | 2 - ...or_none.rb => all_or_none_of_validator.rb} | 2 - ...llow_blank.rb => allow_blank_validator.rb} | 0 .../validators/{as.rb => as_validator.rb} | 0 ...one_of.rb => at_least_one_of_validator.rb} | 2 - .../{coerce.rb => coerce_validator.rb} | 0 .../{default.rb => default_validator.rb} | 0 ..._one_of.rb => exactly_one_of_validator.rb} | 2 - ...t_values.rb => except_values_validator.rb} | 0 ...usion.rb => mutual_exclusion_validator.rb} | 2 - .../{presence.rb => presence_validator.rb} | 0 .../{regexp.rb => regexp_validator.rb} | 0 .../{same_as.rb => same_as_validator.rb} | 0 .../{values.rb => values_validator.rb} | 0 20 files changed, 43 insertions(+), 42 deletions(-) rename lib/grape/validations/validators/{all_or_none.rb => all_or_none_of_validator.rb} (87%) rename lib/grape/validations/validators/{allow_blank.rb => allow_blank_validator.rb} (100%) rename lib/grape/validations/validators/{as.rb => as_validator.rb} (100%) rename lib/grape/validations/validators/{at_least_one_of.rb => at_least_one_of_validator.rb} (86%) rename lib/grape/validations/validators/{coerce.rb => coerce_validator.rb} (100%) rename lib/grape/validations/validators/{default.rb => default_validator.rb} (100%) rename lib/grape/validations/validators/{exactly_one_of.rb => exactly_one_of_validator.rb} (89%) rename lib/grape/validations/validators/{except_values.rb => except_values_validator.rb} (100%) rename lib/grape/validations/validators/{mutual_exclusion.rb => mutual_exclusion_validator.rb} (86%) rename lib/grape/validations/validators/{presence.rb => presence_validator.rb} (100%) rename lib/grape/validations/validators/{regexp.rb => regexp_validator.rb} (100%) rename lib/grape/validations/validators/{same_as.rb => same_as_validator.rb} (100%) rename lib/grape/validations/validators/{values.rb => values_validator.rb} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68b439dfd..c6b40408f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ * [#2200](https://github.com/ruby-grape/grape/pull/2200): Add validators module to all validators - [@ericproulx](https://github.com/ericproulx). * [#2202](https://github.com/ruby-grape/grape/pull/2202): Fix random mock spec error - [@ericproulx](https://github.com/ericproulx). * [#2203](https://github.com/ruby-grape/grape/pull/2203): Add rubocop-rspec - [@ericproulx](https://github.com/ericproulx). +* [#2207](https://github.com/ruby-grape/grape/pull/2207): Autoload Validations/Validators - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 1.6.0 (2021/10/04) diff --git a/lib/grape.rb b/lib/grape.rb index e7b500649..8e342bff3 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -11,14 +11,14 @@ require 'active_support/version' require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/array/conversions' 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/conversions' require 'active_support/core_ext/hash/deep_merge' -require 'active_support/core_ext/hash/reverse_merge' require 'active_support/core_ext/hash/except' +require 'active_support/core_ext/hash/reverse_merge' require 'active_support/core_ext/hash/slice' -require 'active_support/core_ext/hash/conversions' require 'active_support/dependencies/autoload' require 'active_support/notifications' require 'i18n' @@ -217,6 +217,41 @@ module ServeStream autoload :StreamResponse end end + + module Validations + extend ::ActiveSupport::Autoload + + eager_autoload do + autoload :AttributesIterator + autoload :MultipleAttributesIterator + autoload :SingleAttributeIterator + autoload :ParamsScope + autoload :Types + autoload :ValidatorFactory + end + + module Validators + extend ::ActiveSupport::Autoload + + eager_autoload do + autoload :Base + autoload :MultipleParamsBase + autoload :AllOrNoneOfValidator + autoload :AllowBlankValidator + autoload :AsValidator + autoload :AtLeastOneOfValidator + autoload :CoerceValidator + autoload :DefaultValidator + autoload :ExactlyOneOfValidator + autoload :ExceptValuesValidator + autoload :MutualExclusionValidator + autoload :PresenceValidator + autoload :RegexpValidator + autoload :SameAsValidator + autoload :ValuesValidator + end + end + end end require 'grape/config' @@ -226,25 +261,4 @@ module ServeStream 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' -require 'grape/validations/validators/coerce' -require 'grape/validations/validators/default' -require 'grape/validations/validators/exactly_one_of' -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' -require 'grape/validations/validators/all_or_none' -require 'grape/validations/types' -require 'grape/validations/validator_factory' - require 'grape/version' diff --git a/lib/grape/dsl/helpers.rb b/lib/grape/dsl/helpers.rb index c51940842..a6c4dd6ca 100644 --- a/lib/grape/dsl/helpers.rb +++ b/lib/grape/dsl/helpers.rb @@ -68,7 +68,7 @@ def include_all_in_scope def define_boolean_in_mod(mod) return if defined? mod::Boolean - mod.const_set('Boolean', Grape::API::Boolean) + mod.const_set(:Boolean, Grape::API::Boolean) end def inject_api_helpers_to_mod(mod, &block) diff --git a/lib/grape/util/json.rb b/lib/grape/util/json.rb index 9381d841a..26695e92a 100644 --- a/lib/grape/util/json.rb +++ b/lib/grape/util/json.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'json' + module Grape if Object.const_defined? :MultiJson Json = ::MultiJson diff --git a/lib/grape/util/strict_hash_configuration.rb b/lib/grape/util/strict_hash_configuration.rb index 91fa41399..3d096897a 100644 --- a/lib/grape/util/strict_hash_configuration.rb +++ b/lib/grape/util/strict_hash_configuration.rb @@ -65,7 +65,7 @@ def self.nested_settings_methods(setting_name, new_config_class) end end - define_method 'to_hash' do + define_method :to_hash do merge_hash = {} setting_name.each_key { |k| merge_hash[k] = send("#{k}_context").to_hash } diff --git a/lib/grape/validations.rb b/lib/grape/validations.rb index c0736ef22..bd55c0611 100644 --- a/lib/grape/validations.rb +++ b/lib/grape/validations.rb @@ -1,11 +1,5 @@ # frozen_string_literal: true -require 'grape/validations/attributes_iterator' -require 'grape/validations/single_attribute_iterator' -require 'grape/validations/multiple_attributes_iterator' -require 'grape/validations/params_scope' -require 'grape/validations/types' - module Grape # Registry to store and locate known Validators. module Validations diff --git a/lib/grape/validations/types/json.rb b/lib/grape/validations/types/json.rb index 3240de27b..61b01131c 100644 --- a/lib/grape/validations/types/json.rb +++ b/lib/grape/validations/types/json.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'json' - module Grape module Validations module Types diff --git a/lib/grape/validations/validators/all_or_none.rb b/lib/grape/validations/validators/all_or_none_of_validator.rb similarity index 87% rename from lib/grape/validations/validators/all_or_none.rb rename to lib/grape/validations/validators/all_or_none_of_validator.rb index 24dc4f8b6..2fe553a15 100644 --- a/lib/grape/validations/validators/all_or_none.rb +++ b/lib/grape/validations/validators/all_or_none_of_validator.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'grape/validations/validators/multiple_params_base' - module Grape module Validations module Validators diff --git a/lib/grape/validations/validators/allow_blank.rb b/lib/grape/validations/validators/allow_blank_validator.rb similarity index 100% rename from lib/grape/validations/validators/allow_blank.rb rename to lib/grape/validations/validators/allow_blank_validator.rb diff --git a/lib/grape/validations/validators/as.rb b/lib/grape/validations/validators/as_validator.rb similarity index 100% rename from lib/grape/validations/validators/as.rb rename to lib/grape/validations/validators/as_validator.rb diff --git a/lib/grape/validations/validators/at_least_one_of.rb b/lib/grape/validations/validators/at_least_one_of_validator.rb similarity index 86% rename from lib/grape/validations/validators/at_least_one_of.rb rename to lib/grape/validations/validators/at_least_one_of_validator.rb index 6fedbef46..3467e4f1d 100644 --- a/lib/grape/validations/validators/at_least_one_of.rb +++ b/lib/grape/validations/validators/at_least_one_of_validator.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'grape/validations/validators/multiple_params_base' - module Grape module Validations module Validators diff --git a/lib/grape/validations/validators/coerce.rb b/lib/grape/validations/validators/coerce_validator.rb similarity index 100% rename from lib/grape/validations/validators/coerce.rb rename to lib/grape/validations/validators/coerce_validator.rb diff --git a/lib/grape/validations/validators/default.rb b/lib/grape/validations/validators/default_validator.rb similarity index 100% rename from lib/grape/validations/validators/default.rb rename to lib/grape/validations/validators/default_validator.rb diff --git a/lib/grape/validations/validators/exactly_one_of.rb b/lib/grape/validations/validators/exactly_one_of_validator.rb similarity index 89% rename from lib/grape/validations/validators/exactly_one_of.rb rename to lib/grape/validations/validators/exactly_one_of_validator.rb index 84d6142fb..735c45701 100644 --- a/lib/grape/validations/validators/exactly_one_of.rb +++ b/lib/grape/validations/validators/exactly_one_of_validator.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'grape/validations/validators/multiple_params_base' - module Grape module Validations module Validators diff --git a/lib/grape/validations/validators/except_values.rb b/lib/grape/validations/validators/except_values_validator.rb similarity index 100% rename from lib/grape/validations/validators/except_values.rb rename to lib/grape/validations/validators/except_values_validator.rb diff --git a/lib/grape/validations/validators/mutual_exclusion.rb b/lib/grape/validations/validators/mutual_exclusion_validator.rb similarity index 86% rename from lib/grape/validations/validators/mutual_exclusion.rb rename to lib/grape/validations/validators/mutual_exclusion_validator.rb index e0f49278b..8d19da34c 100644 --- a/lib/grape/validations/validators/mutual_exclusion.rb +++ b/lib/grape/validations/validators/mutual_exclusion_validator.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'grape/validations/validators/multiple_params_base' - module Grape module Validations module Validators diff --git a/lib/grape/validations/validators/presence.rb b/lib/grape/validations/validators/presence_validator.rb similarity index 100% rename from lib/grape/validations/validators/presence.rb rename to lib/grape/validations/validators/presence_validator.rb diff --git a/lib/grape/validations/validators/regexp.rb b/lib/grape/validations/validators/regexp_validator.rb similarity index 100% rename from lib/grape/validations/validators/regexp.rb rename to lib/grape/validations/validators/regexp_validator.rb diff --git a/lib/grape/validations/validators/same_as.rb b/lib/grape/validations/validators/same_as_validator.rb similarity index 100% rename from lib/grape/validations/validators/same_as.rb rename to lib/grape/validations/validators/same_as_validator.rb diff --git a/lib/grape/validations/validators/values.rb b/lib/grape/validations/validators/values_validator.rb similarity index 100% rename from lib/grape/validations/validators/values.rb rename to lib/grape/validations/validators/values_validator.rb From 8c809fa45a72677daa0a1c83403a908c5d87f68f Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Mon, 27 Dec 2021 17:51:15 +0100 Subject: [PATCH 077/304] Rails 7 - uninitialized constant ActiveSupport::XmlMini::IsolatedExecutionState (#2208) * Add require 'active_support/isolated_execution_state' when ActiveSupport::VERSION::MAJOR > 6 Add rails-7 * Add require 'active_support/isolated_execution_state' when ActiveSupport::VERSION::MAJOR > 6 Add rails-7 appraisals Fix rubocop Performance/StringIdentifierArgument * Add test Rails 7 Add CHANGELOG --- .github/workflows/test.yml | 6 +++++ Appraisals | 4 ++++ CHANGELOG.md | 1 + gemfiles/rails_7.gemfile | 45 ++++++++++++++++++++++++++++++++++++++ lib/grape.rb | 1 + 5 files changed, 57 insertions(+) create mode 100644 gemfiles/rails_7.gemfile diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ea8cdf787..353d7109e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -46,12 +46,18 @@ jobs: - ruby: "3.0" gemfile: 'gemfiles/multi_xml.gemfile' experimental: false + - ruby: "3.0" + gemfile: 'gemfiles/rails_7.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_7.gemfile' + experimental: false - ruby: 2.7 gemfile: 'gemfiles/rails_edge.gemfile' experimental: false diff --git a/Appraisals b/Appraisals index 1613cfa03..403831c68 100644 --- a/Appraisals +++ b/Appraisals @@ -12,6 +12,10 @@ appraise 'rails-6-1' do gem 'rails', '~> 6.1' end +appraise 'rails-7' do + gem 'rails', '~> 7.0' +end + appraise 'rails-edge' do gem 'rails', github: 'rails/rails' end diff --git a/CHANGELOG.md b/CHANGELOG.md index c6b40408f..08d7cb9df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ #### Features * [#2196](https://github.com/ruby-grape/grape/pull/2196): Add support for `passwords_hashed` param for `digest_auth` - [@lHydra](https://github.com/lhydra). +* [#2208](https://github.com/ruby-grape/grape/pull/2208): Added Rails 7 support - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/gemfiles/rails_7.gemfile b/gemfiles/rails_7.gemfile new file mode 100644 index 000000000..5dd4290d9 --- /dev/null +++ b/gemfiles/rails_7.gemfile @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +# This file was generated by Appraisal + +source 'https://rubygems.org' + +gem 'rails', '~> 7.0' + +group :development, :test do + gem 'bundler' + gem 'hashie' + gem 'rake' + gem 'rubocop', '~> 1.23.0' + gem 'rubocop-ast', '~> 1.14.0' + gem 'rubocop-performance', require: false + gem 'rubocop-rspec', 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 + gem 'test-prof', require: false +end + +platforms :jruby do + gem 'racc' +end + +gemspec path: '../' diff --git a/lib/grape.rb b/lib/grape.rb index 8e342bff3..6e7fd5e31 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -9,6 +9,7 @@ require 'set' require 'active_support' require 'active_support/version' +require 'active_support/isolated_execution_state' if ActiveSupport::VERSION::MAJOR > 6 require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/array/conversions' From 190fbf2bbe35569919a77aa2ed005cc5a446bd1a Mon Sep 17 00:00:00 2001 From: dm1try Date: Mon, 27 Dec 2021 23:32:51 +0300 Subject: [PATCH 078/304] rely less on Logger internal interface simplify spec --- spec/grape/api_spec.rb | 36 ++++++++++++++---------------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index fb4086aec..727ceb8e3 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -1638,33 +1638,25 @@ def authorize(u, p) end describe '.logger' do - subject do - Class.new(described_class) do - def self.io - @io ||= StringIO.new - end - logger ::Logger.new(io) - end - end - it 'returns an instance of Logger class by default' do expect(subject.logger.class).to eql Logger end - it 'allows setting a custom logger' do - mylogger = Class.new - subject.logger mylogger - expect(mylogger).to receive(:info).once - subject.logger.info 'this will be logged' - end + context 'with a custom logger' do + subject do + Class.new(described_class) do + def self.io + @io ||= StringIO.new + end + logger ::Logger.new(io) + end + end - it 'defaults to a standard logger log format' do - t = Time.at(100) - allow(Time).to receive(:now).and_return(t) - message = "this will be logged\n" - message = "I, [#{Logger::Formatter.new.send(:format_datetime, t)}\##{Process.pid}] INFO -- : #{message}" if !defined?(Rails) || Gem::Version.new(Rails::VERSION::STRING) >= Gem::Version.new('4.0') - expect(subject.io).to receive(:write).with(message) - subject.logger.info 'this will be logged' + it 'exposes its interaface' do + message = 'this will be logged' + subject.logger.info message + expect(subject.io.string).to include(message) + end end it 'does not unnecessarily retain duplicate setup blocks' do From 224402f0ba945017a391be5d9cd5bd273ce6593e Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Tue, 28 Dec 2021 01:06:04 +0100 Subject: [PATCH 079/304] Autoload Types (#2209) --- CHANGELOG.md | 1 + lib/grape.rb | 19 ++++ lib/grape/dry_types.rb | 12 +++ lib/grape/validations/types.rb | 92 ++++++++++++++++-- lib/grape/validations/types/array_coercer.rb | 2 - lib/grape/validations/types/build_coercer.rb | 94 ------------------- .../validations/types/dry_type_coercer.rb | 11 +-- .../validations/types/primitive_coercer.rb | 12 +-- lib/grape/validations/types/set_coercer.rb | 3 - 9 files changed, 121 insertions(+), 125 deletions(-) create mode 100644 lib/grape/dry_types.rb delete mode 100644 lib/grape/validations/types/build_coercer.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 08d7cb9df..6316d81ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ * [#2202](https://github.com/ruby-grape/grape/pull/2202): Fix random mock spec error - [@ericproulx](https://github.com/ericproulx). * [#2203](https://github.com/ruby-grape/grape/pull/2203): Add rubocop-rspec - [@ericproulx](https://github.com/ericproulx). * [#2207](https://github.com/ruby-grape/grape/pull/2207): Autoload Validations/Validators - [@ericproulx](https://github.com/ericproulx). +* [#2209](https://github.com/ruby-grape/grape/pull/2209): Autoload Validations/Types - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 1.6.0 (2021/10/04) diff --git a/lib/grape.rb b/lib/grape.rb index 6e7fd5e31..65333f0bc 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -45,6 +45,7 @@ module Grape autoload :Env, 'grape/util/env' autoload :Json, 'grape/util/json' autoload :Xml, 'grape/util/xml' + autoload :DryTypes end module Http @@ -222,6 +223,24 @@ module ServeStream module Validations extend ::ActiveSupport::Autoload + module Types + extend ::ActiveSupport::Autoload + + eager_autoload do + autoload :InvalidValue + autoload :File + autoload :Json + autoload :DryTypeCoercer + autoload :ArrayCoercer + autoload :SetCoercer + autoload :PrimitiveCoercer + autoload :CustomTypeCoercer + autoload :CustomTypeCollectionCoercer + autoload :MultipleTypeCoercer + autoload :VariantCollectionCoercer + end + end + eager_autoload do autoload :AttributesIterator autoload :MultipleAttributesIterator diff --git a/lib/grape/dry_types.rb b/lib/grape/dry_types.rb new file mode 100644 index 000000000..f0676c376 --- /dev/null +++ b/lib/grape/dry_types.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require 'dry-types' + +module Grape + 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 +end diff --git a/lib/grape/validations/types.rb b/lib/grape/validations/types.rb index 2240a7fa7..f822666e7 100644 --- a/lib/grape/validations/types.rb +++ b/lib/grape/validations/types.rb @@ -1,14 +1,5 @@ # frozen_string_literal: true -require_relative 'types/build_coercer' -require_relative 'types/custom_type_coercer' -require_relative 'types/custom_type_collection_coercer' -require_relative 'types/multiple_type_coercer' -require_relative 'types/variant_collection_coercer' -require_relative 'types/json' -require_relative 'types/file' -require_relative 'types/invalid_value' - module Grape module Validations # Module for code related to grape's system for @@ -143,6 +134,89 @@ def self.collection_of_custom?(type) def self.map_special(type) SPECIAL.fetch(type, type) end + + # Chooses the best coercer for the given type. For example, if the type + # is Integer, it will return a coercer which will be able to coerce a value + # to the integer. + # + # There are a few very special coercers which might be returned. + # + # +Grape::Types::MultipleTypeCoercer+ is a coercer which is returned when + # the given type implies values in an array with different types. + # For example, +[Integer, String]+ allows integer and string values in + # an array. + # + # +Grape::Types::CustomTypeCoercer+ is a coercer which is returned when + # a method is specified by a user with +coerce_with+ option or the user + # specifies a custom type which implements requirments of + # +Grape::Types::CustomTypeCoercer+. + # + # +Grape::Types::CustomTypeCollectionCoercer+ is a very similar to the + # previous one, but it expects an array or set of values having a custom + # type implemented by the user. + # + # There is also a group of custom types implemented by Grape, check + # +Grape::Validations::Types::SPECIAL+ to get the full list. + # + # @param type [Class] the type to which input strings + # should be coerced + # @param method [Class,#call] the coercion method to use + # @return [Object] object to be used + # for coercion and type validation + def 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, strict) + # Maps a custom type provided by Grape, it doesn't map types wrapped by collections!!! + type = Types.map_special(type) + + # Use a special coercer for multiply-typed parameters. + if Types.multiple?(type) + MultipleTypeCoercer.new(type, method) + + # Use a special coercer for custom types and coercion methods. + elsif method || Types.custom?(type) + CustomTypeCoercer.new(type, method) + + # Special coercer for collections of types that implement a parse method. + # CustomTypeCoercer (above) already handles such types when an explicit coercion + # method is supplied. + elsif Types.collection_of_custom?(type) + Types::CustomTypeCollectionCoercer.new( + Types.map_special(type.first), type.is_a?(Set) + ) + else + DryTypeCoercer.coercer_instance_for(type, strict) + end + end + + def self.cache_instance(type, method, strict, &_block) + key = cache_key(type, method, strict) + + return @__cache[key] if @__cache.key?(key) + + instance = yield + + @__cache_write_lock.synchronize do + @__cache[key] = instance + end + + instance + end + + 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, {}) + instance_variable_set(:@__cache_write_lock, Mutex.new) end end end diff --git a/lib/grape/validations/types/array_coercer.rb b/lib/grape/validations/types/array_coercer.rb index d3aeb2146..10a0761b6 100644 --- a/lib/grape/validations/types/array_coercer.rb +++ b/lib/grape/validations/types/array_coercer.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_relative 'dry_type_coercer' - module Grape module Validations module Types diff --git a/lib/grape/validations/types/build_coercer.rb b/lib/grape/validations/types/build_coercer.rb deleted file mode 100644 index c55e048db..000000000 --- a/lib/grape/validations/types/build_coercer.rb +++ /dev/null @@ -1,94 +0,0 @@ -# frozen_string_literal: true - -require_relative 'array_coercer' -require_relative 'set_coercer' -require_relative 'primitive_coercer' - -module Grape - module Validations - module Types - # Chooses the best coercer for the given type. For example, if the type - # is Integer, it will return a coercer which will be able to coerce a value - # to the integer. - # - # There are a few very special coercers which might be returned. - # - # +Grape::Types::MultipleTypeCoercer+ is a coercer which is returned when - # the given type implies values in an array with different types. - # For example, +[Integer, String]+ allows integer and string values in - # an array. - # - # +Grape::Types::CustomTypeCoercer+ is a coercer which is returned when - # a method is specified by a user with +coerce_with+ option or the user - # specifies a custom type which implements requirments of - # +Grape::Types::CustomTypeCoercer+. - # - # +Grape::Types::CustomTypeCollectionCoercer+ is a very similar to the - # previous one, but it expects an array or set of values having a custom - # type implemented by the user. - # - # There is also a group of custom types implemented by Grape, check - # +Grape::Validations::Types::SPECIAL+ to get the full list. - # - # @param type [Class] the type to which input strings - # should be coerced - # @param method [Class,#call] the coercion method to use - # @return [Object] object to be used - # for coercion and type validation - def 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, strict) - # Maps a custom type provided by Grape, it doesn't map types wrapped by collections!!! - type = Types.map_special(type) - - # Use a special coercer for multiply-typed parameters. - if Types.multiple?(type) - MultipleTypeCoercer.new(type, method) - - # Use a special coercer for custom types and coercion methods. - elsif method || Types.custom?(type) - CustomTypeCoercer.new(type, method) - - # Special coercer for collections of types that implement a parse method. - # CustomTypeCoercer (above) already handles such types when an explicit coercion - # method is supplied. - elsif Types.collection_of_custom?(type) - Types::CustomTypeCollectionCoercer.new( - Types.map_special(type.first), type.is_a?(Set) - ) - else - DryTypeCoercer.coercer_instance_for(type, strict) - end - end - - def self.cache_instance(type, method, strict, &_block) - key = cache_key(type, method, strict) - - return @__cache[key] if @__cache.key?(key) - - instance = yield - - @__cache_write_lock.synchronize do - @__cache[key] = instance - end - - instance - end - - 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, {}) - instance_variable_set(:@__cache_write_lock, Mutex.new) - end - end -end diff --git a/lib/grape/validations/types/dry_type_coercer.rb b/lib/grape/validations/types/dry_type_coercer.rb index 6b772378e..3ecaea88b 100644 --- a/lib/grape/validations/types/dry_type_coercer.rb +++ b/lib/grape/validations/types/dry_type_coercer.rb @@ -1,14 +1,5 @@ # 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 @@ -52,7 +43,7 @@ def collection_coercers def initialize(type, strict = false) @type = type @strict = strict - @scope = strict ? DryTypes::Strict : DryTypes::Params + @scope = strict ? Grape::DryTypes::Strict : Grape::DryTypes::Params end # Coerces the given value to a type which was specified during diff --git a/lib/grape/validations/types/primitive_coercer.rb b/lib/grape/validations/types/primitive_coercer.rb index 368ccff64..e6b02c63a 100644 --- a/lib/grape/validations/types/primitive_coercer.rb +++ b/lib/grape/validations/types/primitive_coercer.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_relative 'dry_type_coercer' - module Grape module Validations module Types @@ -10,16 +8,16 @@ module Types # that it has the proper type. class PrimitiveCoercer < DryTypeCoercer MAPPING = { - Grape::API::Boolean => DryTypes::Params::Bool, - BigDecimal => DryTypes::Params::Decimal, + Grape::API::Boolean => Grape::DryTypes::Params::Bool, + BigDecimal => Grape::DryTypes::Params::Decimal, # unfortunately, a +Params+ scope doesn't contain String - String => DryTypes::Coercible::String + String => Grape::DryTypes::Coercible::String }.freeze STRICT_MAPPING = { - Grape::API::Boolean => DryTypes::Strict::Bool, - BigDecimal => DryTypes::Strict::Decimal + Grape::API::Boolean => Grape::DryTypes::Strict::Bool, + BigDecimal => Grape::DryTypes::Strict::Decimal }.freeze def initialize(type, strict = false) diff --git a/lib/grape/validations/types/set_coercer.rb b/lib/grape/validations/types/set_coercer.rb index dc76fc773..a6d80306d 100644 --- a/lib/grape/validations/types/set_coercer.rb +++ b/lib/grape/validations/types/set_coercer.rb @@ -1,8 +1,5 @@ # frozen_string_literal: true -require 'set' -require_relative 'array_coercer' - module Grape module Validations module Types From b1d114bb27c0bbef42e88e0d10f9996b3910dbd8 Mon Sep 17 00:00:00 2001 From: dm1try Date: Tue, 28 Dec 2021 17:16:40 +0300 Subject: [PATCH 080/304] Preparing for release, 1.6.1. --- CHANGELOG.md | 4 +--- README.md | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6316d81ee..a2446503c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,9 @@ -### 1.6.1 (Next) +### 1.6.1 (2021/12/28) #### Features * [#2196](https://github.com/ruby-grape/grape/pull/2196): Add support for `passwords_hashed` param for `digest_auth` - [@lHydra](https://github.com/lhydra). * [#2208](https://github.com/ruby-grape/grape/pull/2208): Added Rails 7 support - [@ericproulx](https://github.com/ericproulx). -* Your contribution here. #### Fixes @@ -16,7 +15,6 @@ * [#2203](https://github.com/ruby-grape/grape/pull/2203): Add rubocop-rspec - [@ericproulx](https://github.com/ericproulx). * [#2207](https://github.com/ruby-grape/grape/pull/2207): Autoload Validations/Validators - [@ericproulx](https://github.com/ericproulx). * [#2209](https://github.com/ruby-grape/grape/pull/2209): Autoload Validations/Types - [@ericproulx](https://github.com/ericproulx). -* Your contribution here. ### 1.6.0 (2021/10/04) diff --git a/README.md b/README.md index 471659743..ceebd1eb7 100644 --- a/README.md +++ b/README.md @@ -158,9 +158,8 @@ content negotiation, versioning and much more. ## Stable Release -You're reading the documentation for the next release of Grape, which should be **1.6.1**. +You're reading the documentation for the stable release of Grape, **1.6.1**. Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. -The current stable release is [1.6.0](https://github.com/ruby-grape/grape/blob/v1.6.0/README.md). ## Project Resources From ac94b704325e5eb667fda3263c513b8c8df46fe7 Mon Sep 17 00:00:00 2001 From: dm1try Date: Tue, 28 Dec 2021 17:24:15 +0300 Subject: [PATCH 081/304] Preparing for next development iteration, 1.6.2 --- CHANGELOG.md | 10 ++++++++++ README.md | 3 ++- lib/grape/version.rb | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2446503c..51b0cc57a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +### 1.6.2 (Next) + +#### Features + +* Your contribution here. + +#### Fixes + +* Your contribution here. + ### 1.6.1 (2021/12/28) #### Features diff --git a/README.md b/README.md index ceebd1eb7..c29e6661c 100644 --- a/README.md +++ b/README.md @@ -158,8 +158,9 @@ content negotiation, versioning and much more. ## Stable Release -You're reading the documentation for the stable release of Grape, **1.6.1**. +You're reading the documentation for the next release of Grape, which should be **1.6.2**. Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. +The current stable release is [1.6.1](https://github.com/ruby-grape/grape/blob/v1.6.1/README.md). ## Project Resources diff --git a/lib/grape/version.rb b/lib/grape/version.rb index 653f9e487..15b86fd88 100644 --- a/lib/grape/version.rb +++ b/lib/grape/version.rb @@ -2,5 +2,5 @@ module Grape # The current version of Grape. - VERSION = '1.6.1' + VERSION = '1.6.2' end From 05d4410996e967b184665e6bda16de8422f8773f Mon Sep 17 00:00:00 2001 From: Matt Ewell Date: Wed, 29 Dec 2021 13:40:14 -0700 Subject: [PATCH 082/304] Fix some documentation typos (#2217) * Fix typos * Remove hard wraps --- CONTRIBUTING.md | 2 +- UPGRADING.md | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6b528c523..7cde59694 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -119,7 +119,7 @@ Go back to your pull request after a few minutes and see whether it passed muste #### Be Patient -It's likely that your change will not be merged and that the nitpicky maintainers will ask you to do more, or fix seemingly benign problems. Hang on there! +It's likely that your change will not be merged and that the nitpicky maintainers will ask you to do more, or fix seemingly benign problems. Hang in there! #### Thank You diff --git a/UPGRADING.md b/UPGRADING.md index aceed0b51..8dc77ea67 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -27,7 +27,7 @@ declared(params, include_missing: false) # actual => { b: '5' } (uncasted, unvalidated, <= 1.5.3) ``` -Another implication of this change is the dependent parameter resolution. Prior to 1.6.0 the following code produced an `Grape::Exceptions::UnknownParameter` because `:a` was replace by `:b`: +Another implication of this change is the dependent parameter resolution. Prior to 1.6.0 the following code produced a `Grape::Exceptions::UnknownParameter` because `:a` was replaced by `:b`: ```ruby params do @@ -47,7 +47,7 @@ See [#2189](https://github.com/ruby-grape/grape/pull/2189) for more information. #### 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: +This behavior was not tested or documented. Version 1.3.0 quietly changed this behavior, in that `nil` values skipped the coercion. Version 1.5.3 fixes and documents this as follows: ```ruby class Api < Grape::API @@ -197,13 +197,13 @@ end #### Nil values for structures -Nil values always been a special case when dealing with types especially with the following structures: +Nil values have always been a special case when dealing with types, especially with the following structures: - Array - Hash - Set -The behavior for these structures has change through out the latest releases. For example: +The behavior for these structures has changed throughout the latest releases. For example: ```ruby class Api < Grape::API From 1c425e6e62b781179e3e837019013a7857c5cb8c Mon Sep 17 00:00:00 2001 From: dm1try Date: Thu, 30 Dec 2021 20:24:56 +0300 Subject: [PATCH 083/304] Revert "Autoload Types (#2209)" This reverts commit 224402f0ba945017a391be5d9cd5bd273ce6593e. --- lib/grape.rb | 19 ---- lib/grape/dry_types.rb | 12 --- lib/grape/validations/types.rb | 92 ++---------------- lib/grape/validations/types/array_coercer.rb | 2 + lib/grape/validations/types/build_coercer.rb | 94 +++++++++++++++++++ .../validations/types/dry_type_coercer.rb | 11 ++- .../validations/types/primitive_coercer.rb | 12 ++- lib/grape/validations/types/set_coercer.rb | 3 + 8 files changed, 125 insertions(+), 120 deletions(-) delete mode 100644 lib/grape/dry_types.rb create mode 100644 lib/grape/validations/types/build_coercer.rb diff --git a/lib/grape.rb b/lib/grape.rb index 65333f0bc..6e7fd5e31 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -45,7 +45,6 @@ module Grape autoload :Env, 'grape/util/env' autoload :Json, 'grape/util/json' autoload :Xml, 'grape/util/xml' - autoload :DryTypes end module Http @@ -223,24 +222,6 @@ module ServeStream module Validations extend ::ActiveSupport::Autoload - module Types - extend ::ActiveSupport::Autoload - - eager_autoload do - autoload :InvalidValue - autoload :File - autoload :Json - autoload :DryTypeCoercer - autoload :ArrayCoercer - autoload :SetCoercer - autoload :PrimitiveCoercer - autoload :CustomTypeCoercer - autoload :CustomTypeCollectionCoercer - autoload :MultipleTypeCoercer - autoload :VariantCollectionCoercer - end - end - eager_autoload do autoload :AttributesIterator autoload :MultipleAttributesIterator diff --git a/lib/grape/dry_types.rb b/lib/grape/dry_types.rb deleted file mode 100644 index f0676c376..000000000 --- a/lib/grape/dry_types.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -require 'dry-types' - -module Grape - 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 -end diff --git a/lib/grape/validations/types.rb b/lib/grape/validations/types.rb index f822666e7..2240a7fa7 100644 --- a/lib/grape/validations/types.rb +++ b/lib/grape/validations/types.rb @@ -1,5 +1,14 @@ # frozen_string_literal: true +require_relative 'types/build_coercer' +require_relative 'types/custom_type_coercer' +require_relative 'types/custom_type_collection_coercer' +require_relative 'types/multiple_type_coercer' +require_relative 'types/variant_collection_coercer' +require_relative 'types/json' +require_relative 'types/file' +require_relative 'types/invalid_value' + module Grape module Validations # Module for code related to grape's system for @@ -134,89 +143,6 @@ def self.collection_of_custom?(type) def self.map_special(type) SPECIAL.fetch(type, type) end - - # Chooses the best coercer for the given type. For example, if the type - # is Integer, it will return a coercer which will be able to coerce a value - # to the integer. - # - # There are a few very special coercers which might be returned. - # - # +Grape::Types::MultipleTypeCoercer+ is a coercer which is returned when - # the given type implies values in an array with different types. - # For example, +[Integer, String]+ allows integer and string values in - # an array. - # - # +Grape::Types::CustomTypeCoercer+ is a coercer which is returned when - # a method is specified by a user with +coerce_with+ option or the user - # specifies a custom type which implements requirments of - # +Grape::Types::CustomTypeCoercer+. - # - # +Grape::Types::CustomTypeCollectionCoercer+ is a very similar to the - # previous one, but it expects an array or set of values having a custom - # type implemented by the user. - # - # There is also a group of custom types implemented by Grape, check - # +Grape::Validations::Types::SPECIAL+ to get the full list. - # - # @param type [Class] the type to which input strings - # should be coerced - # @param method [Class,#call] the coercion method to use - # @return [Object] object to be used - # for coercion and type validation - def 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, strict) - # Maps a custom type provided by Grape, it doesn't map types wrapped by collections!!! - type = Types.map_special(type) - - # Use a special coercer for multiply-typed parameters. - if Types.multiple?(type) - MultipleTypeCoercer.new(type, method) - - # Use a special coercer for custom types and coercion methods. - elsif method || Types.custom?(type) - CustomTypeCoercer.new(type, method) - - # Special coercer for collections of types that implement a parse method. - # CustomTypeCoercer (above) already handles such types when an explicit coercion - # method is supplied. - elsif Types.collection_of_custom?(type) - Types::CustomTypeCollectionCoercer.new( - Types.map_special(type.first), type.is_a?(Set) - ) - else - DryTypeCoercer.coercer_instance_for(type, strict) - end - end - - def self.cache_instance(type, method, strict, &_block) - key = cache_key(type, method, strict) - - return @__cache[key] if @__cache.key?(key) - - instance = yield - - @__cache_write_lock.synchronize do - @__cache[key] = instance - end - - instance - end - - 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, {}) - instance_variable_set(:@__cache_write_lock, Mutex.new) end end end diff --git a/lib/grape/validations/types/array_coercer.rb b/lib/grape/validations/types/array_coercer.rb index 10a0761b6..d3aeb2146 100644 --- a/lib/grape/validations/types/array_coercer.rb +++ b/lib/grape/validations/types/array_coercer.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require_relative 'dry_type_coercer' + module Grape module Validations module Types diff --git a/lib/grape/validations/types/build_coercer.rb b/lib/grape/validations/types/build_coercer.rb new file mode 100644 index 000000000..c55e048db --- /dev/null +++ b/lib/grape/validations/types/build_coercer.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require_relative 'array_coercer' +require_relative 'set_coercer' +require_relative 'primitive_coercer' + +module Grape + module Validations + module Types + # Chooses the best coercer for the given type. For example, if the type + # is Integer, it will return a coercer which will be able to coerce a value + # to the integer. + # + # There are a few very special coercers which might be returned. + # + # +Grape::Types::MultipleTypeCoercer+ is a coercer which is returned when + # the given type implies values in an array with different types. + # For example, +[Integer, String]+ allows integer and string values in + # an array. + # + # +Grape::Types::CustomTypeCoercer+ is a coercer which is returned when + # a method is specified by a user with +coerce_with+ option or the user + # specifies a custom type which implements requirments of + # +Grape::Types::CustomTypeCoercer+. + # + # +Grape::Types::CustomTypeCollectionCoercer+ is a very similar to the + # previous one, but it expects an array or set of values having a custom + # type implemented by the user. + # + # There is also a group of custom types implemented by Grape, check + # +Grape::Validations::Types::SPECIAL+ to get the full list. + # + # @param type [Class] the type to which input strings + # should be coerced + # @param method [Class,#call] the coercion method to use + # @return [Object] object to be used + # for coercion and type validation + def 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, strict) + # Maps a custom type provided by Grape, it doesn't map types wrapped by collections!!! + type = Types.map_special(type) + + # Use a special coercer for multiply-typed parameters. + if Types.multiple?(type) + MultipleTypeCoercer.new(type, method) + + # Use a special coercer for custom types and coercion methods. + elsif method || Types.custom?(type) + CustomTypeCoercer.new(type, method) + + # Special coercer for collections of types that implement a parse method. + # CustomTypeCoercer (above) already handles such types when an explicit coercion + # method is supplied. + elsif Types.collection_of_custom?(type) + Types::CustomTypeCollectionCoercer.new( + Types.map_special(type.first), type.is_a?(Set) + ) + else + DryTypeCoercer.coercer_instance_for(type, strict) + end + end + + def self.cache_instance(type, method, strict, &_block) + key = cache_key(type, method, strict) + + return @__cache[key] if @__cache.key?(key) + + instance = yield + + @__cache_write_lock.synchronize do + @__cache[key] = instance + end + + instance + end + + 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, {}) + instance_variable_set(:@__cache_write_lock, Mutex.new) + end + end +end diff --git a/lib/grape/validations/types/dry_type_coercer.rb b/lib/grape/validations/types/dry_type_coercer.rb index 3ecaea88b..6b772378e 100644 --- a/lib/grape/validations/types/dry_type_coercer.rb +++ b/lib/grape/validations/types/dry_type_coercer.rb @@ -1,5 +1,14 @@ # 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 @@ -43,7 +52,7 @@ def collection_coercers def initialize(type, strict = false) @type = type @strict = strict - @scope = strict ? Grape::DryTypes::Strict : Grape::DryTypes::Params + @scope = strict ? DryTypes::Strict : DryTypes::Params end # Coerces the given value to a type which was specified during diff --git a/lib/grape/validations/types/primitive_coercer.rb b/lib/grape/validations/types/primitive_coercer.rb index e6b02c63a..368ccff64 100644 --- a/lib/grape/validations/types/primitive_coercer.rb +++ b/lib/grape/validations/types/primitive_coercer.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require_relative 'dry_type_coercer' + module Grape module Validations module Types @@ -8,16 +10,16 @@ module Types # that it has the proper type. class PrimitiveCoercer < DryTypeCoercer MAPPING = { - Grape::API::Boolean => Grape::DryTypes::Params::Bool, - BigDecimal => Grape::DryTypes::Params::Decimal, + Grape::API::Boolean => DryTypes::Params::Bool, + BigDecimal => DryTypes::Params::Decimal, # unfortunately, a +Params+ scope doesn't contain String - String => Grape::DryTypes::Coercible::String + String => DryTypes::Coercible::String }.freeze STRICT_MAPPING = { - Grape::API::Boolean => Grape::DryTypes::Strict::Bool, - BigDecimal => Grape::DryTypes::Strict::Decimal + Grape::API::Boolean => DryTypes::Strict::Bool, + BigDecimal => DryTypes::Strict::Decimal }.freeze def initialize(type, strict = false) diff --git a/lib/grape/validations/types/set_coercer.rb b/lib/grape/validations/types/set_coercer.rb index a6d80306d..dc76fc773 100644 --- a/lib/grape/validations/types/set_coercer.rb +++ b/lib/grape/validations/types/set_coercer.rb @@ -1,5 +1,8 @@ # frozen_string_literal: true +require 'set' +require_relative 'array_coercer' + module Grape module Validations module Types From 7ce8eafeadf1a6b5341f91ed0da132ff352480cd Mon Sep 17 00:00:00 2001 From: dm1try Date: Thu, 30 Dec 2021 20:28:33 +0300 Subject: [PATCH 084/304] Revert "Validations Autoload (#2207)" This reverts commit 6d417fad0acd3564213371ddd5b4ec561c7519f2. --- lib/grape.rb | 62 +++++++------------ lib/grape/dsl/helpers.rb | 2 +- lib/grape/util/json.rb | 2 - lib/grape/util/strict_hash_configuration.rb | 2 +- lib/grape/validations.rb | 6 ++ lib/grape/validations/types/json.rb | 2 + ...or_none_of_validator.rb => all_or_none.rb} | 2 + ...llow_blank_validator.rb => allow_blank.rb} | 0 .../validators/{as_validator.rb => as.rb} | 0 ...one_of_validator.rb => at_least_one_of.rb} | 2 + .../{coerce_validator.rb => coerce.rb} | 0 .../{default_validator.rb => default.rb} | 0 ..._one_of_validator.rb => exactly_one_of.rb} | 2 + ...t_values_validator.rb => except_values.rb} | 0 ...usion_validator.rb => mutual_exclusion.rb} | 2 + .../{presence_validator.rb => presence.rb} | 0 .../{regexp_validator.rb => regexp.rb} | 0 .../{same_as_validator.rb => same_as.rb} | 0 .../{values_validator.rb => values.rb} | 0 19 files changed, 42 insertions(+), 42 deletions(-) rename lib/grape/validations/validators/{all_or_none_of_validator.rb => all_or_none.rb} (87%) rename lib/grape/validations/validators/{allow_blank_validator.rb => allow_blank.rb} (100%) rename lib/grape/validations/validators/{as_validator.rb => as.rb} (100%) rename lib/grape/validations/validators/{at_least_one_of_validator.rb => at_least_one_of.rb} (86%) rename lib/grape/validations/validators/{coerce_validator.rb => coerce.rb} (100%) rename lib/grape/validations/validators/{default_validator.rb => default.rb} (100%) rename lib/grape/validations/validators/{exactly_one_of_validator.rb => exactly_one_of.rb} (89%) rename lib/grape/validations/validators/{except_values_validator.rb => except_values.rb} (100%) rename lib/grape/validations/validators/{mutual_exclusion_validator.rb => mutual_exclusion.rb} (86%) rename lib/grape/validations/validators/{presence_validator.rb => presence.rb} (100%) rename lib/grape/validations/validators/{regexp_validator.rb => regexp.rb} (100%) rename lib/grape/validations/validators/{same_as_validator.rb => same_as.rb} (100%) rename lib/grape/validations/validators/{values_validator.rb => values.rb} (100%) diff --git a/lib/grape.rb b/lib/grape.rb index 6e7fd5e31..a48008e9d 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -12,14 +12,14 @@ require 'active_support/isolated_execution_state' if ActiveSupport::VERSION::MAJOR > 6 require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/object/blank' -require 'active_support/core_ext/array/conversions' require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/array/wrap' -require 'active_support/core_ext/hash/conversions' +require 'active_support/core_ext/array/conversions' require 'active_support/core_ext/hash/deep_merge' -require 'active_support/core_ext/hash/except' require 'active_support/core_ext/hash/reverse_merge' +require 'active_support/core_ext/hash/except' require 'active_support/core_ext/hash/slice' +require 'active_support/core_ext/hash/conversions' require 'active_support/dependencies/autoload' require 'active_support/notifications' require 'i18n' @@ -218,41 +218,6 @@ module ServeStream autoload :StreamResponse end end - - module Validations - extend ::ActiveSupport::Autoload - - eager_autoload do - autoload :AttributesIterator - autoload :MultipleAttributesIterator - autoload :SingleAttributeIterator - autoload :ParamsScope - autoload :Types - autoload :ValidatorFactory - end - - module Validators - extend ::ActiveSupport::Autoload - - eager_autoload do - autoload :Base - autoload :MultipleParamsBase - autoload :AllOrNoneOfValidator - autoload :AllowBlankValidator - autoload :AsValidator - autoload :AtLeastOneOfValidator - autoload :CoerceValidator - autoload :DefaultValidator - autoload :ExactlyOneOfValidator - autoload :ExceptValuesValidator - autoload :MutualExclusionValidator - autoload :PresenceValidator - autoload :RegexpValidator - autoload :SameAsValidator - autoload :ValuesValidator - end - end - end end require 'grape/config' @@ -262,4 +227,25 @@ module Validators 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' +require 'grape/validations/validators/coerce' +require 'grape/validations/validators/default' +require 'grape/validations/validators/exactly_one_of' +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' +require 'grape/validations/validators/all_or_none' +require 'grape/validations/types' +require 'grape/validations/validator_factory' + require 'grape/version' diff --git a/lib/grape/dsl/helpers.rb b/lib/grape/dsl/helpers.rb index a6c4dd6ca..c51940842 100644 --- a/lib/grape/dsl/helpers.rb +++ b/lib/grape/dsl/helpers.rb @@ -68,7 +68,7 @@ def include_all_in_scope def define_boolean_in_mod(mod) return if defined? mod::Boolean - mod.const_set(:Boolean, Grape::API::Boolean) + mod.const_set('Boolean', Grape::API::Boolean) end def inject_api_helpers_to_mod(mod, &block) diff --git a/lib/grape/util/json.rb b/lib/grape/util/json.rb index 26695e92a..9381d841a 100644 --- a/lib/grape/util/json.rb +++ b/lib/grape/util/json.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'json' - module Grape if Object.const_defined? :MultiJson Json = ::MultiJson diff --git a/lib/grape/util/strict_hash_configuration.rb b/lib/grape/util/strict_hash_configuration.rb index 3d096897a..91fa41399 100644 --- a/lib/grape/util/strict_hash_configuration.rb +++ b/lib/grape/util/strict_hash_configuration.rb @@ -65,7 +65,7 @@ def self.nested_settings_methods(setting_name, new_config_class) end end - define_method :to_hash do + define_method 'to_hash' do merge_hash = {} setting_name.each_key { |k| merge_hash[k] = send("#{k}_context").to_hash } diff --git a/lib/grape/validations.rb b/lib/grape/validations.rb index bd55c0611..c0736ef22 100644 --- a/lib/grape/validations.rb +++ b/lib/grape/validations.rb @@ -1,5 +1,11 @@ # frozen_string_literal: true +require 'grape/validations/attributes_iterator' +require 'grape/validations/single_attribute_iterator' +require 'grape/validations/multiple_attributes_iterator' +require 'grape/validations/params_scope' +require 'grape/validations/types' + module Grape # Registry to store and locate known Validators. module Validations diff --git a/lib/grape/validations/types/json.rb b/lib/grape/validations/types/json.rb index 61b01131c..3240de27b 100644 --- a/lib/grape/validations/types/json.rb +++ b/lib/grape/validations/types/json.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'json' + module Grape module Validations module Types diff --git a/lib/grape/validations/validators/all_or_none_of_validator.rb b/lib/grape/validations/validators/all_or_none.rb similarity index 87% rename from lib/grape/validations/validators/all_or_none_of_validator.rb rename to lib/grape/validations/validators/all_or_none.rb index 2fe553a15..24dc4f8b6 100644 --- a/lib/grape/validations/validators/all_or_none_of_validator.rb +++ b/lib/grape/validations/validators/all_or_none.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'grape/validations/validators/multiple_params_base' + module Grape module Validations module Validators diff --git a/lib/grape/validations/validators/allow_blank_validator.rb b/lib/grape/validations/validators/allow_blank.rb similarity index 100% rename from lib/grape/validations/validators/allow_blank_validator.rb rename to lib/grape/validations/validators/allow_blank.rb diff --git a/lib/grape/validations/validators/as_validator.rb b/lib/grape/validations/validators/as.rb similarity index 100% rename from lib/grape/validations/validators/as_validator.rb rename to lib/grape/validations/validators/as.rb diff --git a/lib/grape/validations/validators/at_least_one_of_validator.rb b/lib/grape/validations/validators/at_least_one_of.rb similarity index 86% rename from lib/grape/validations/validators/at_least_one_of_validator.rb rename to lib/grape/validations/validators/at_least_one_of.rb index 3467e4f1d..6fedbef46 100644 --- a/lib/grape/validations/validators/at_least_one_of_validator.rb +++ b/lib/grape/validations/validators/at_least_one_of.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'grape/validations/validators/multiple_params_base' + module Grape module Validations module Validators diff --git a/lib/grape/validations/validators/coerce_validator.rb b/lib/grape/validations/validators/coerce.rb similarity index 100% rename from lib/grape/validations/validators/coerce_validator.rb rename to lib/grape/validations/validators/coerce.rb diff --git a/lib/grape/validations/validators/default_validator.rb b/lib/grape/validations/validators/default.rb similarity index 100% rename from lib/grape/validations/validators/default_validator.rb rename to lib/grape/validations/validators/default.rb diff --git a/lib/grape/validations/validators/exactly_one_of_validator.rb b/lib/grape/validations/validators/exactly_one_of.rb similarity index 89% rename from lib/grape/validations/validators/exactly_one_of_validator.rb rename to lib/grape/validations/validators/exactly_one_of.rb index 735c45701..84d6142fb 100644 --- a/lib/grape/validations/validators/exactly_one_of_validator.rb +++ b/lib/grape/validations/validators/exactly_one_of.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'grape/validations/validators/multiple_params_base' + module Grape module Validations module Validators diff --git a/lib/grape/validations/validators/except_values_validator.rb b/lib/grape/validations/validators/except_values.rb similarity index 100% rename from lib/grape/validations/validators/except_values_validator.rb rename to lib/grape/validations/validators/except_values.rb diff --git a/lib/grape/validations/validators/mutual_exclusion_validator.rb b/lib/grape/validations/validators/mutual_exclusion.rb similarity index 86% rename from lib/grape/validations/validators/mutual_exclusion_validator.rb rename to lib/grape/validations/validators/mutual_exclusion.rb index 8d19da34c..e0f49278b 100644 --- a/lib/grape/validations/validators/mutual_exclusion_validator.rb +++ b/lib/grape/validations/validators/mutual_exclusion.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'grape/validations/validators/multiple_params_base' + module Grape module Validations module Validators diff --git a/lib/grape/validations/validators/presence_validator.rb b/lib/grape/validations/validators/presence.rb similarity index 100% rename from lib/grape/validations/validators/presence_validator.rb rename to lib/grape/validations/validators/presence.rb diff --git a/lib/grape/validations/validators/regexp_validator.rb b/lib/grape/validations/validators/regexp.rb similarity index 100% rename from lib/grape/validations/validators/regexp_validator.rb rename to lib/grape/validations/validators/regexp.rb diff --git a/lib/grape/validations/validators/same_as_validator.rb b/lib/grape/validations/validators/same_as.rb similarity index 100% rename from lib/grape/validations/validators/same_as_validator.rb rename to lib/grape/validations/validators/same_as.rb diff --git a/lib/grape/validations/validators/values_validator.rb b/lib/grape/validations/validators/values.rb similarity index 100% rename from lib/grape/validations/validators/values_validator.rb rename to lib/grape/validations/validators/values.rb From 39098c0abfe61ab9b59977a44617bcde3c919ff0 Mon Sep 17 00:00:00 2001 From: dm1try Date: Thu, 30 Dec 2021 20:36:26 +0300 Subject: [PATCH 085/304] fix rubocop complains --- lib/grape/dsl/helpers.rb | 2 +- lib/grape/util/strict_hash_configuration.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/grape/dsl/helpers.rb b/lib/grape/dsl/helpers.rb index c51940842..a6c4dd6ca 100644 --- a/lib/grape/dsl/helpers.rb +++ b/lib/grape/dsl/helpers.rb @@ -68,7 +68,7 @@ def include_all_in_scope def define_boolean_in_mod(mod) return if defined? mod::Boolean - mod.const_set('Boolean', Grape::API::Boolean) + mod.const_set(:Boolean, Grape::API::Boolean) end def inject_api_helpers_to_mod(mod, &block) diff --git a/lib/grape/util/strict_hash_configuration.rb b/lib/grape/util/strict_hash_configuration.rb index 91fa41399..3d096897a 100644 --- a/lib/grape/util/strict_hash_configuration.rb +++ b/lib/grape/util/strict_hash_configuration.rb @@ -65,7 +65,7 @@ def self.nested_settings_methods(setting_name, new_config_class) end end - define_method 'to_hash' do + define_method :to_hash do merge_hash = {} setting_name.each_key { |k| merge_hash[k] = send("#{k}_context").to_hash } From a8d8ce8848c17ed01849f4607f673719d30ae809 Mon Sep 17 00:00:00 2001 From: dm1try Date: Thu, 30 Dec 2021 20:37:58 +0300 Subject: [PATCH 086/304] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51b0cc57a..fbec4a5e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ #### Fixes +* [#2219](https://github.com/ruby-grape/grape/pull/2219): Revert the changes for autoloading provided in 1.6.1 - [@dm1try](https://github.com/dm1try). * Your contribution here. ### 1.6.1 (2021/12/28) From a6cde5d5fb00ea7cf34f7adfbe81dcd2b3958e8d Mon Sep 17 00:00:00 2001 From: dm1try Date: Thu, 30 Dec 2021 21:01:09 +0300 Subject: [PATCH 087/304] Preparing for release, 1.6.2. --- CHANGELOG.md | 5 +---- README.md | 3 +-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbec4a5e6..cb605c0e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,10 @@ -### 1.6.2 (Next) +### 1.6.2 (2021/12/30) #### Features -* Your contribution here. - #### Fixes * [#2219](https://github.com/ruby-grape/grape/pull/2219): Revert the changes for autoloading provided in 1.6.1 - [@dm1try](https://github.com/dm1try). -* Your contribution here. ### 1.6.1 (2021/12/28) diff --git a/README.md b/README.md index c29e6661c..2e4f7d533 100644 --- a/README.md +++ b/README.md @@ -158,9 +158,8 @@ content negotiation, versioning and much more. ## Stable Release -You're reading the documentation for the next release of Grape, which should be **1.6.2**. +You're reading the documentation for the stable release of Grape, **1.6.2**. Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. -The current stable release is [1.6.1](https://github.com/ruby-grape/grape/blob/v1.6.1/README.md). ## Project Resources From 68172c0feae34b7ec5236e0c3bc9ff15bfdd6b7d Mon Sep 17 00:00:00 2001 From: dm1try Date: Thu, 30 Dec 2021 21:04:41 +0300 Subject: [PATCH 088/304] Preparing for next development iteration, 1.6.3. --- CHANGELOG.md | 10 ++++++++++ README.md | 3 ++- lib/grape/version.rb | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb605c0e8..1b86db320 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +### 1.6.3 (Next) + +#### Features + +* Your contribution here. + +#### Fixes + +* Your contribution here. + ### 1.6.2 (2021/12/30) #### Features diff --git a/README.md b/README.md index 2e4f7d533..0ceedfd29 100644 --- a/README.md +++ b/README.md @@ -158,8 +158,9 @@ content negotiation, versioning and much more. ## Stable Release -You're reading the documentation for the stable release of Grape, **1.6.2**. +You're reading the documentation for the next release of Grape, which should be **1.6.3**. Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. +The current stable release is [1.6.2](https://github.com/ruby-grape/grape/blob/v1.6.2/README.md). ## Project Resources diff --git a/lib/grape/version.rb b/lib/grape/version.rb index 15b86fd88..af7c1a7cd 100644 --- a/lib/grape/version.rb +++ b/lib/grape/version.rb @@ -2,5 +2,5 @@ module Grape # The current version of Grape. - VERSION = '1.6.2' + VERSION = '1.6.3' end From 814d7783618ff1f1ed04abdbf13e82e089efd9de Mon Sep 17 00:00:00 2001 From: mocaberos Date: Fri, 31 Dec 2021 11:42:58 +0900 Subject: [PATCH 089/304] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0ceedfd29..7318ea99a 100644 --- a/README.md +++ b/README.md @@ -1743,7 +1743,7 @@ end ### Custom Validators ```ruby -class AlphaNumeric < Grape::Validations::Base +class AlphaNumeric < Grape::Validations::Validators::Base def validate_param!(attr_name, params) unless params[attr_name] =~ /\A[[:alnum:]]+\z/ fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: 'must consist of alpha-numeric characters' @@ -1761,7 +1761,7 @@ end You can also create custom classes that take parameters. ```ruby -class Length < Grape::Validations::Base +class Length < Grape::Validations::Validators::Base def validate_param!(attr_name, params) unless params[attr_name].length <= @option fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: "must be at the most #{@option} characters long" @@ -1779,7 +1779,7 @@ end You can also create custom validation that use request to validate the attribute. For example if you want to have parameters that are available to only admins, you can do the following. ```ruby -class Admin < Grape::Validations::Base +class Admin < Grape::Validations::Validators::Base def validate(request) # return if the param we are checking was not in request # @attrs is a list containing the attribute we are currently validating From aa4140a9cc4a1d0ca0d7924d566a9da2ee1760fd Mon Sep 17 00:00:00 2001 From: eproulx Date: Sun, 2 Jan 2022 11:44:52 +0100 Subject: [PATCH 090/304] Remove all require 'spec_helper' and add require spec_helper in .rspec Rebuild rubocop todo --- .rspec | 1 + .rubocop_todo.yml | 37 ++++++++++--------- README.md | 2 +- spec/grape/api/custom_validations_spec.rb | 2 - .../grape/api/deeply_included_options_spec.rb | 2 - .../api/defines_boolean_in_params_spec.rb | 2 - spec/grape/api/inherited_helpers_spec.rb | 2 - spec/grape/api/instance_spec.rb | 1 - spec/grape/api/invalid_format_spec.rb | 2 - .../api/namespace_parameters_in_route_spec.rb | 2 - spec/grape/api/nested_helpers_spec.rb | 2 - .../api/optional_parameters_in_route_spec.rb | 2 - .../grape/api/parameters_modification_spec.rb | 2 - spec/grape/api/patch_method_helpers_spec.rb | 2 - spec/grape/api/recognize_path_spec.rb | 2 - .../api/required_parameters_in_route_spec.rb | 2 - ...red_parameters_with_invalid_method_spec.rb | 2 - .../api/routes_with_requirements_spec.rb | 2 - .../api/shared_helpers_exactly_one_of_spec.rb | 2 - spec/grape/api/shared_helpers_spec.rb | 2 - spec/grape/api_remount_spec.rb | 1 - spec/grape/api_spec.rb | 1 - spec/grape/config_spec.rb | 2 - spec/grape/dsl/callbacks_spec.rb | 2 - spec/grape/dsl/configuration_spec.rb | 2 - spec/grape/dsl/desc_spec.rb | 2 - spec/grape/dsl/headers_spec.rb | 2 - spec/grape/dsl/helpers_spec.rb | 2 - spec/grape/dsl/inside_route_spec.rb | 2 - spec/grape/dsl/logger_spec.rb | 2 - spec/grape/dsl/middleware_spec.rb | 2 - spec/grape/dsl/parameters_spec.rb | 2 - spec/grape/dsl/request_response_spec.rb | 2 - spec/grape/dsl/routing_spec.rb | 2 - spec/grape/dsl/settings_spec.rb | 2 - spec/grape/dsl/validations_spec.rb | 2 - spec/grape/endpoint/declared_spec.rb | 2 - spec/grape/endpoint_spec.rb | 2 - spec/grape/entity_spec.rb | 1 - spec/grape/exceptions/base_spec.rb | 2 - .../exceptions/body_parse_errors_spec.rb | 2 - .../exceptions/invalid_accept_header_spec.rb | 2 - .../exceptions/invalid_formatter_spec.rb | 2 - .../grape/exceptions/invalid_response_spec.rb | 2 - .../invalid_versioner_option_spec.rb | 2 - .../exceptions/missing_mime_type_spec.rb | 2 - spec/grape/exceptions/missing_option_spec.rb | 2 - spec/grape/exceptions/unknown_options_spec.rb | 2 - .../exceptions/unknown_validator_spec.rb | 2 - .../exceptions/validation_errors_spec.rb | 1 - spec/grape/exceptions/validation_spec.rb | 2 - .../extensions/param_builders/hash_spec.rb | 2 - .../hash_with_indifferent_access_spec.rb | 2 - .../param_builders/hashie/mash_spec.rb | 2 - .../global_namespace_function_spec.rb | 2 - spec/grape/integration/rack_sendfile_spec.rb | 2 - spec/grape/integration/rack_spec.rb | 2 - spec/grape/loading_spec.rb | 2 - spec/grape/middleware/auth/base_spec.rb | 1 - spec/grape/middleware/auth/dsl_spec.rb | 2 - spec/grape/middleware/auth/strategies_spec.rb | 2 - spec/grape/middleware/base_spec.rb | 2 - spec/grape/middleware/error_spec.rb | 1 - spec/grape/middleware/exception_spec.rb | 2 - spec/grape/middleware/formatter_spec.rb | 2 - spec/grape/middleware/globals_spec.rb | 2 - spec/grape/middleware/stack_spec.rb | 2 - .../versioner/accept_version_header_spec.rb | 2 - .../grape/middleware/versioner/header_spec.rb | 2 - spec/grape/middleware/versioner/param_spec.rb | 2 - spec/grape/middleware/versioner/path_spec.rb | 2 - spec/grape/middleware/versioner_spec.rb | 2 - spec/grape/named_api_spec.rb | 2 - spec/grape/parser_spec.rb | 2 - spec/grape/path_spec.rb | 2 - spec/grape/presenters/presenter_spec.rb | 2 - spec/grape/request_spec.rb | 2 - spec/grape/util/inheritable_setting_spec.rb | 1 - spec/grape/util/inheritable_values_spec.rb | 1 - .../util/reverse_stackable_values_spec.rb | 1 - spec/grape/util/stackable_values_spec.rb | 1 - .../util/strict_hash_configuration_spec.rb | 1 - .../validations/attributes_iterator_spec.rb | 2 - .../validations/instance_behaivour_spec.rb | 2 - .../multiple_attributes_iterator_spec.rb | 2 - spec/grape/validations/params_scope_spec.rb | 2 - .../single_attribute_iterator_spec.rb | 2 - .../validations/types/array_coercer_spec.rb | 2 - .../types/primitive_coercer_spec.rb | 2 - .../validations/types/set_coercer_spec.rb | 2 - spec/grape/validations/types_spec.rb | 2 - .../validators/all_or_none_spec.rb | 2 - .../validators/allow_blank_spec.rb | 2 - .../validators/at_least_one_of_spec.rb | 2 - .../validations/validators/coerce_spec.rb | 2 - .../validations/validators/default_spec.rb | 2 - .../validators/exactly_one_of_spec.rb | 2 - .../validators/except_values_spec.rb | 2 - .../validators/mutual_exclusion_spec.rb | 2 - .../validations/validators/presence_spec.rb | 2 - .../validations/validators/regexp_spec.rb | 2 - .../validations/validators/same_as_spec.rb | 2 - .../validations/validators/values_spec.rb | 2 - spec/grape/validations_spec.rb | 2 - spec/integration/multi_json/json_spec.rb | 2 - spec/integration/multi_xml/xml_spec.rb | 2 - 106 files changed, 21 insertions(+), 213 deletions(-) diff --git a/.rspec b/.rspec index 87d9ba441..d4e55c387 100644 --- a/.rspec +++ b/.rspec @@ -1,3 +1,4 @@ +--require spec_helper --color --format=documentation --order=rand diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index b6477131a..27d504550 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2021-12-05 16:55:50 UTC using RuboCop version 1.23.0. +# on 2022-01-02 10:41:35 UTC using RuboCop version 1.23.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 @@ -20,7 +20,7 @@ Lint/AmbiguousBlockAssociation: Exclude: - 'spec/grape/dsl/routing_spec.rb' -# Offense count: 41 +# Offense count: 40 # Configuration parameters: AllowedMethods. # AllowedMethods: enums Lint/ConstantDefinitionInBlock: @@ -173,13 +173,7 @@ RSpec/EmptyExampleGroup: - 'spec/grape/dsl/configuration_spec.rb' - 'spec/grape/validations/attributes_iterator_spec.rb' -# Offense count: 1 -# Cop supports --auto-correct. -RSpec/EmptyLineAfterSubject: - Exclude: - - 'spec/grape/dsl/logger_spec.rb' - -# Offense count: 500 +# Offense count: 499 # Configuration parameters: CountAsOne. RSpec/ExampleLength: Max: 57 @@ -225,7 +219,7 @@ RSpec/IteratedExpectation: Exclude: - 'spec/grape/middleware/formatter_spec.rb' -# Offense count: 90 +# Offense count: 84 RSpec/LeakyConstantDeclaration: Enabled: false @@ -239,7 +233,7 @@ RSpec/MessageChain: Exclude: - 'spec/grape/middleware/formatter_spec.rb' -# Offense count: 137 +# Offense count: 135 # Configuration parameters: . # SupportedStyles: have_received, receive RSpec/MessageSpies: @@ -254,17 +248,17 @@ RSpec/MissingExampleGroupArgument: RSpec/MultipleExpectations: Max: 16 -# Offense count: 11 +# Offense count: 32 # Configuration parameters: AllowSubject. RSpec/MultipleMemoizedHelpers: Max: 10 -# Offense count: 2118 +# Offense count: 2116 # Configuration parameters: IgnoreSharedExamples. RSpec/NamedSubject: Enabled: false -# Offense count: 157 +# Offense count: 161 RSpec/NestedGroups: Max: 6 @@ -307,24 +301,31 @@ RSpec/StubbedMock: - 'spec/grape/middleware/formatter_spec.rb' - 'spec/grape/parser_spec.rb' -# Offense count: 29 +# Offense count: 122 RSpec/SubjectStub: Exclude: - 'spec/grape/api_spec.rb' + - 'spec/grape/dsl/callbacks_spec.rb' + - 'spec/grape/dsl/desc_spec.rb' + - 'spec/grape/dsl/helpers_spec.rb' - 'spec/grape/dsl/inside_route_spec.rb' + - 'spec/grape/dsl/middleware_spec.rb' + - 'spec/grape/dsl/parameters_spec.rb' + - 'spec/grape/dsl/request_response_spec.rb' + - 'spec/grape/dsl/routing_spec.rb' + - 'spec/grape/dsl/settings_spec.rb' - 'spec/grape/middleware/base_spec.rb' - 'spec/grape/middleware/formatter_spec.rb' - 'spec/grape/middleware/globals_spec.rb' - 'spec/grape/middleware/stack_spec.rb' - 'spec/grape/parser_spec.rb' -# Offense count: 25 +# Offense count: 24 # Configuration parameters: IgnoreNameless, IgnoreSymbolicNames. RSpec/VerifiedDoubles: Exclude: - 'spec/grape/api_spec.rb' - 'spec/grape/dsl/inside_route_spec.rb' - - 'spec/grape/dsl/logger_spec.rb' - 'spec/grape/integration/rack_sendfile_spec.rb' - 'spec/grape/middleware/formatter_spec.rb' - 'spec/grape/validations/multiple_attributes_iterator_spec.rb' @@ -364,7 +365,7 @@ Style/OptionalBooleanParameter: - 'lib/grape/validations/types/primitive_coercer.rb' - 'lib/grape/validations/types/set_coercer.rb' -# Offense count: 146 +# Offense count: 144 # Cop supports --auto-correct. # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. # URISchemes: http, https diff --git a/README.md b/README.md index 7318ea99a..640cf4620 100644 --- a/README.md +++ b/README.md @@ -3702,7 +3702,7 @@ Use `rack-test` and define your API as `app`. You can test a Grape API with RSpec by making HTTP requests and examining the response. ```ruby -require 'spec_helper' + describe Twitter::API do include Rack::Test::Methods diff --git a/spec/grape/api/custom_validations_spec.rb b/spec/grape/api/custom_validations_spec.rb index 8524f86f2..75c8b8641 100644 --- a/spec/grape/api/custom_validations_spec.rb +++ b/spec/grape/api/custom_validations_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Validations do context 'using a custom length validator' do subject do diff --git a/spec/grape/api/deeply_included_options_spec.rb b/spec/grape/api/deeply_included_options_spec.rb index 4f27d04ff..08084811b 100644 --- a/spec/grape/api/deeply_included_options_spec.rb +++ b/spec/grape/api/deeply_included_options_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - module DeeplyIncludedOptionsSpec module Defaults extend ActiveSupport::Concern diff --git a/spec/grape/api/defines_boolean_in_params_spec.rb b/spec/grape/api/defines_boolean_in_params_spec.rb index 951865a87..e1f31332d 100644 --- a/spec/grape/api/defines_boolean_in_params_spec.rb +++ b/spec/grape/api/defines_boolean_in_params_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::API::Instance do describe 'boolean constant' do module DefinesBooleanInstanceSpec diff --git a/spec/grape/api/inherited_helpers_spec.rb b/spec/grape/api/inherited_helpers_spec.rb index be2cb9179..0b7933cb9 100644 --- a/spec/grape/api/inherited_helpers_spec.rb +++ b/spec/grape/api/inherited_helpers_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::API::Helpers do let(:user) { 'Miguel Caneo' } let(:id) { '42' } diff --git a/spec/grape/api/instance_spec.rb b/spec/grape/api/instance_spec.rb index f0a278a7b..56fd1b0a3 100644 --- a/spec/grape/api/instance_spec.rb +++ b/spec/grape/api/instance_spec.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' require 'shared/versioning_examples' describe Grape::API::Instance do diff --git a/spec/grape/api/invalid_format_spec.rb b/spec/grape/api/invalid_format_spec.rb index 23e5dbadb..79da1ac1d 100644 --- a/spec/grape/api/invalid_format_spec.rb +++ b/spec/grape/api/invalid_format_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Endpoint do subject { Class.new(Grape::API) } diff --git a/spec/grape/api/namespace_parameters_in_route_spec.rb b/spec/grape/api/namespace_parameters_in_route_spec.rb index e8496a4a5..f2562d5da 100644 --- a/spec/grape/api/namespace_parameters_in_route_spec.rb +++ b/spec/grape/api/namespace_parameters_in_route_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Endpoint do subject { Class.new(Grape::API) } diff --git a/spec/grape/api/nested_helpers_spec.rb b/spec/grape/api/nested_helpers_spec.rb index 2f9c61aba..2acddbcf0 100644 --- a/spec/grape/api/nested_helpers_spec.rb +++ b/spec/grape/api/nested_helpers_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::API::Helpers do module NestedHelpersSpec module HelperMethods diff --git a/spec/grape/api/optional_parameters_in_route_spec.rb b/spec/grape/api/optional_parameters_in_route_spec.rb index 6bc6bc47d..bb9a89440 100644 --- a/spec/grape/api/optional_parameters_in_route_spec.rb +++ b/spec/grape/api/optional_parameters_in_route_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Endpoint do subject { Class.new(Grape::API) } diff --git a/spec/grape/api/parameters_modification_spec.rb b/spec/grape/api/parameters_modification_spec.rb index 2d1ea1e20..2d64f9fd3 100644 --- a/spec/grape/api/parameters_modification_spec.rb +++ b/spec/grape/api/parameters_modification_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Endpoint do subject { Class.new(Grape::API) } diff --git a/spec/grape/api/patch_method_helpers_spec.rb b/spec/grape/api/patch_method_helpers_spec.rb index a369a284a..23b4338e7 100644 --- a/spec/grape/api/patch_method_helpers_spec.rb +++ b/spec/grape/api/patch_method_helpers_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::API::Helpers do module PatchHelpersSpec class PatchPublic < Grape::API diff --git a/spec/grape/api/recognize_path_spec.rb b/spec/grape/api/recognize_path_spec.rb index c521cbb95..b3e9afa96 100644 --- a/spec/grape/api/recognize_path_spec.rb +++ b/spec/grape/api/recognize_path_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::API do describe '.recognize_path' do subject { Class.new(described_class) } diff --git a/spec/grape/api/required_parameters_in_route_spec.rb b/spec/grape/api/required_parameters_in_route_spec.rb index 63544892a..3408a16fa 100644 --- a/spec/grape/api/required_parameters_in_route_spec.rb +++ b/spec/grape/api/required_parameters_in_route_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Endpoint do subject { Class.new(Grape::API) } 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 0829492cf..10b5a904f 100644 --- a/spec/grape/api/required_parameters_with_invalid_method_spec.rb +++ b/spec/grape/api/required_parameters_with_invalid_method_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Endpoint do subject { Class.new(Grape::API) } diff --git a/spec/grape/api/routes_with_requirements_spec.rb b/spec/grape/api/routes_with_requirements_spec.rb index 2ebac9609..f283aa2af 100644 --- a/spec/grape/api/routes_with_requirements_spec.rb +++ b/spec/grape/api/routes_with_requirements_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Endpoint do subject { Class.new(Grape::API) } 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 461e79d76..070265713 100644 --- a/spec/grape/api/shared_helpers_exactly_one_of_spec.rb +++ b/spec/grape/api/shared_helpers_exactly_one_of_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::API::Helpers do let(:app) do Class.new(Grape::API) do diff --git a/spec/grape/api/shared_helpers_spec.rb b/spec/grape/api/shared_helpers_spec.rb index 68626944d..842417138 100644 --- a/spec/grape/api/shared_helpers_spec.rb +++ b/spec/grape/api/shared_helpers_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::API::Helpers do subject do shared_params = Module.new do diff --git a/spec/grape/api_remount_spec.rb b/spec/grape/api_remount_spec.rb index 715cb8a07..9fea1c3f0 100644 --- a/spec/grape/api_remount_spec.rb +++ b/spec/grape/api_remount_spec.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' require 'shared/versioning_examples' describe Grape::API do diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index 727ceb8e3..7cf73b4a4 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' require 'shared/versioning_examples' describe Grape::API do diff --git a/spec/grape/config_spec.rb b/spec/grape/config_spec.rb index 07bed04a3..c09090cd7 100644 --- a/spec/grape/config_spec.rb +++ b/spec/grape/config_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe '.configure' do before do Grape.configure do |config| diff --git a/spec/grape/dsl/callbacks_spec.rb b/spec/grape/dsl/callbacks_spec.rb index a83ae9351..bec2d823f 100644 --- a/spec/grape/dsl/callbacks_spec.rb +++ b/spec/grape/dsl/callbacks_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - module Grape module DSL module CallbacksSpec diff --git a/spec/grape/dsl/configuration_spec.rb b/spec/grape/dsl/configuration_spec.rb index 32b015f75..9265fdc41 100644 --- a/spec/grape/dsl/configuration_spec.rb +++ b/spec/grape/dsl/configuration_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - module Grape module DSL module ConfigurationSpec diff --git a/spec/grape/dsl/desc_spec.rb b/spec/grape/dsl/desc_spec.rb index 9822620c0..8212add8f 100644 --- a/spec/grape/dsl/desc_spec.rb +++ b/spec/grape/dsl/desc_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - module Grape module DSL module DescSpec diff --git a/spec/grape/dsl/headers_spec.rb b/spec/grape/dsl/headers_spec.rb index 221fed09f..e6b90ebd9 100644 --- a/spec/grape/dsl/headers_spec.rb +++ b/spec/grape/dsl/headers_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - module Grape module DSL module HeadersSpec diff --git a/spec/grape/dsl/helpers_spec.rb b/spec/grape/dsl/helpers_spec.rb index 837605bc0..ce122e953 100644 --- a/spec/grape/dsl/helpers_spec.rb +++ b/spec/grape/dsl/helpers_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - module Grape module DSL module HelpersSpec diff --git a/spec/grape/dsl/inside_route_spec.rb b/spec/grape/dsl/inside_route_spec.rb index e35df773f..31e84a9ac 100644 --- a/spec/grape/dsl/inside_route_spec.rb +++ b/spec/grape/dsl/inside_route_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - module Grape module DSL module InsideRouteSpec diff --git a/spec/grape/dsl/logger_spec.rb b/spec/grape/dsl/logger_spec.rb index b7e419541..2c9739f75 100644 --- a/spec/grape/dsl/logger_spec.rb +++ b/spec/grape/dsl/logger_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::DSL::Logger do subject { Class.new(dummy_logger) } diff --git a/spec/grape/dsl/middleware_spec.rb b/spec/grape/dsl/middleware_spec.rb index 309042f99..8413eaaaf 100644 --- a/spec/grape/dsl/middleware_spec.rb +++ b/spec/grape/dsl/middleware_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - module Grape module DSL module MiddlewareSpec diff --git a/spec/grape/dsl/parameters_spec.rb b/spec/grape/dsl/parameters_spec.rb index 69df048fd..4fea57d20 100644 --- a/spec/grape/dsl/parameters_spec.rb +++ b/spec/grape/dsl/parameters_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - module Grape module DSL module ParametersSpec diff --git a/spec/grape/dsl/request_response_spec.rb b/spec/grape/dsl/request_response_spec.rb index 822fbbe8b..e2fb5f798 100644 --- a/spec/grape/dsl/request_response_spec.rb +++ b/spec/grape/dsl/request_response_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - module Grape module DSL module RequestResponseSpec diff --git a/spec/grape/dsl/routing_spec.rb b/spec/grape/dsl/routing_spec.rb index 4ea286f85..0695e4c45 100644 --- a/spec/grape/dsl/routing_spec.rb +++ b/spec/grape/dsl/routing_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - module Grape module DSL module RoutingSpec diff --git a/spec/grape/dsl/settings_spec.rb b/spec/grape/dsl/settings_spec.rb index b2520d36f..5de6febf1 100644 --- a/spec/grape/dsl/settings_spec.rb +++ b/spec/grape/dsl/settings_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - module Grape module DSL module SettingsSpec diff --git a/spec/grape/dsl/validations_spec.rb b/spec/grape/dsl/validations_spec.rb index e39069266..08b951d65 100644 --- a/spec/grape/dsl/validations_spec.rb +++ b/spec/grape/dsl/validations_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - module Grape module DSL module ValidationsSpec diff --git a/spec/grape/endpoint/declared_spec.rb b/spec/grape/endpoint/declared_spec.rb index 5cd37a136..5b0614f5c 100644 --- a/spec/grape/endpoint/declared_spec.rb +++ b/spec/grape/endpoint/declared_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Endpoint do subject { Class.new(Grape::API) } diff --git a/spec/grape/endpoint_spec.rb b/spec/grape/endpoint_spec.rb index 20efdc505..9fee973ca 100644 --- a/spec/grape/endpoint_spec.rb +++ b/spec/grape/endpoint_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Endpoint do subject { Class.new(Grape::API) } diff --git a/spec/grape/entity_spec.rb b/spec/grape/entity_spec.rb index d69042244..425edf003 100644 --- a/spec/grape/entity_spec.rb +++ b/spec/grape/entity_spec.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' require 'grape_entity' describe Grape::Entity do diff --git a/spec/grape/exceptions/base_spec.rb b/spec/grape/exceptions/base_spec.rb index db970a74d..8bf949d01 100644 --- a/spec/grape/exceptions/base_spec.rb +++ b/spec/grape/exceptions/base_spec.rb @@ -1,7 +1,5 @@ # 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) } diff --git a/spec/grape/exceptions/body_parse_errors_spec.rb b/spec/grape/exceptions/body_parse_errors_spec.rb index 43d48d069..285f3e81b 100644 --- a/spec/grape/exceptions/body_parse_errors_spec.rb +++ b/spec/grape/exceptions/body_parse_errors_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Exceptions::ValidationErrors do context 'api with rescue_from :all handler' do subject { Class.new(Grape::API) } diff --git a/spec/grape/exceptions/invalid_accept_header_spec.rb b/spec/grape/exceptions/invalid_accept_header_spec.rb index 69404d72b..574222259 100644 --- a/spec/grape/exceptions/invalid_accept_header_spec.rb +++ b/spec/grape/exceptions/invalid_accept_header_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Exceptions::InvalidAcceptHeader do shared_examples_for 'a valid request' do it 'does return with status 200' do diff --git a/spec/grape/exceptions/invalid_formatter_spec.rb b/spec/grape/exceptions/invalid_formatter_spec.rb index 3c60e3d13..03f019747 100644 --- a/spec/grape/exceptions/invalid_formatter_spec.rb +++ b/spec/grape/exceptions/invalid_formatter_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Exceptions::InvalidFormatter do describe '#message' do let(:error) do diff --git a/spec/grape/exceptions/invalid_response_spec.rb b/spec/grape/exceptions/invalid_response_spec.rb index 8a7c6879b..1e4d579a8 100644 --- a/spec/grape/exceptions/invalid_response_spec.rb +++ b/spec/grape/exceptions/invalid_response_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Exceptions::InvalidResponse do describe '#message' do let(:error) { described_class.new } diff --git a/spec/grape/exceptions/invalid_versioner_option_spec.rb b/spec/grape/exceptions/invalid_versioner_option_spec.rb index a17079349..19fff343d 100644 --- a/spec/grape/exceptions/invalid_versioner_option_spec.rb +++ b/spec/grape/exceptions/invalid_versioner_option_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Exceptions::InvalidVersionerOption do describe '#message' do let(:error) do diff --git a/spec/grape/exceptions/missing_mime_type_spec.rb b/spec/grape/exceptions/missing_mime_type_spec.rb index b5545cbcc..77441a4ef 100644 --- a/spec/grape/exceptions/missing_mime_type_spec.rb +++ b/spec/grape/exceptions/missing_mime_type_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Exceptions::MissingMimeType do describe '#message' do let(:error) do diff --git a/spec/grape/exceptions/missing_option_spec.rb b/spec/grape/exceptions/missing_option_spec.rb index 1ae125134..633c28a3f 100644 --- a/spec/grape/exceptions/missing_option_spec.rb +++ b/spec/grape/exceptions/missing_option_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Exceptions::MissingOption do describe '#message' do let(:error) do diff --git a/spec/grape/exceptions/unknown_options_spec.rb b/spec/grape/exceptions/unknown_options_spec.rb index 743553719..725d88cba 100644 --- a/spec/grape/exceptions/unknown_options_spec.rb +++ b/spec/grape/exceptions/unknown_options_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Exceptions::UnknownOptions do describe '#message' do let(:error) do diff --git a/spec/grape/exceptions/unknown_validator_spec.rb b/spec/grape/exceptions/unknown_validator_spec.rb index 2ef256f37..e36daa4d9 100644 --- a/spec/grape/exceptions/unknown_validator_spec.rb +++ b/spec/grape/exceptions/unknown_validator_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Exceptions::UnknownValidator do describe '#message' do let(:error) do diff --git a/spec/grape/exceptions/validation_errors_spec.rb b/spec/grape/exceptions/validation_errors_spec.rb index 8c3661da0..831ad80d7 100644 --- a/spec/grape/exceptions/validation_errors_spec.rb +++ b/spec/grape/exceptions/validation_errors_spec.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' require 'ostruct' describe Grape::Exceptions::ValidationErrors do diff --git a/spec/grape/exceptions/validation_spec.rb b/spec/grape/exceptions/validation_spec.rb index 71cd4ef98..1e2515939 100644 --- a/spec/grape/exceptions/validation_spec.rb +++ b/spec/grape/exceptions/validation_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Exceptions::Validation do it 'fails when params are missing' do expect { described_class.new(message: 'presence') }.to raise_error(ArgumentError, /missing keyword:.+?params/) diff --git a/spec/grape/extensions/param_builders/hash_spec.rb b/spec/grape/extensions/param_builders/hash_spec.rb index 6cb53dee7..e35096bf7 100644 --- a/spec/grape/extensions/param_builders/hash_spec.rb +++ b/spec/grape/extensions/param_builders/hash_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Extensions::Hash::ParamBuilder do subject { Class.new(Grape::API) } 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 ae64dcc9e..992a07789 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,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder do subject { Class.new(Grape::API) } diff --git a/spec/grape/extensions/param_builders/hashie/mash_spec.rb b/spec/grape/extensions/param_builders/hashie/mash_spec.rb index 9e7fa19ba..54a187982 100644 --- a/spec/grape/extensions/param_builders/hashie/mash_spec.rb +++ b/spec/grape/extensions/param_builders/hashie/mash_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Extensions::Hashie::Mash::ParamBuilder do subject { Class.new(Grape::API) } diff --git a/spec/grape/integration/global_namespace_function_spec.rb b/spec/grape/integration/global_namespace_function_spec.rb index 32a55f7de..0c429fb74 100644 --- a/spec/grape/integration/global_namespace_function_spec.rb +++ b/spec/grape/integration/global_namespace_function_spec.rb @@ -2,8 +2,6 @@ # see https://github.com/ruby-grape/grape/issues/1348 -require 'spec_helper' - def namespace raise end diff --git a/spec/grape/integration/rack_sendfile_spec.rb b/spec/grape/integration/rack_sendfile_spec.rb index 879797404..87041f7e1 100644 --- a/spec/grape/integration/rack_sendfile_spec.rb +++ b/spec/grape/integration/rack_sendfile_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Rack::Sendfile do subject do content_object = file_object diff --git a/spec/grape/integration/rack_spec.rb b/spec/grape/integration/rack_spec.rb index e1c46762f..83dbf7c59 100644 --- a/spec/grape/integration/rack_spec.rb +++ b/spec/grape/integration/rack_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Rack do it 'correctly populates params from a Tempfile' do input = Tempfile.new 'rubbish' diff --git a/spec/grape/loading_spec.rb b/spec/grape/loading_spec.rb index 718f5833b..767a0b793 100644 --- a/spec/grape/loading_spec.rb +++ b/spec/grape/loading_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::API do subject do CombinedApi = combined_api diff --git a/spec/grape/middleware/auth/base_spec.rb b/spec/grape/middleware/auth/base_spec.rb index d18433698..1bc888f11 100644 --- a/spec/grape/middleware/auth/base_spec.rb +++ b/spec/grape/middleware/auth/base_spec.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' require 'base64' describe Grape::Middleware::Auth::Base do diff --git a/spec/grape/middleware/auth/dsl_spec.rb b/spec/grape/middleware/auth/dsl_spec.rb index 5e694d08e..bd1961f14 100644 --- a/spec/grape/middleware/auth/dsl_spec.rb +++ b/spec/grape/middleware/auth/dsl_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Middleware::Auth::DSL do subject { Class.new(Grape::API) } diff --git a/spec/grape/middleware/auth/strategies_spec.rb b/spec/grape/middleware/auth/strategies_spec.rb index fcbe9e7bd..29749c551 100644 --- a/spec/grape/middleware/auth/strategies_spec.rb +++ b/spec/grape/middleware/auth/strategies_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - require 'base64' describe Grape::Middleware::Auth::Strategies do diff --git a/spec/grape/middleware/base_spec.rb b/spec/grape/middleware/base_spec.rb index ac0269a66..ee733b745 100644 --- a/spec/grape/middleware/base_spec.rb +++ b/spec/grape/middleware/base_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Middleware::Base do subject { described_class.new(blank_app) } diff --git a/spec/grape/middleware/error_spec.rb b/spec/grape/middleware/error_spec.rb index 1fe4e3e3e..cdfcc82d1 100644 --- a/spec/grape/middleware/error_spec.rb +++ b/spec/grape/middleware/error_spec.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' require 'grape-entity' describe Grape::Middleware::Error do diff --git a/spec/grape/middleware/exception_spec.rb b/spec/grape/middleware/exception_spec.rb index cb9f20650..18d8bff8b 100644 --- a/spec/grape/middleware/exception_spec.rb +++ b/spec/grape/middleware/exception_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Middleware::Error do let(:exception_app) do Class.new do diff --git a/spec/grape/middleware/formatter_spec.rb b/spec/grape/middleware/formatter_spec.rb index d112e33b1..6310d2d04 100644 --- a/spec/grape/middleware/formatter_spec.rb +++ b/spec/grape/middleware/formatter_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Middleware::Formatter do subject { described_class.new(app) } diff --git a/spec/grape/middleware/globals_spec.rb b/spec/grape/middleware/globals_spec.rb index 9188953ac..272664ef4 100644 --- a/spec/grape/middleware/globals_spec.rb +++ b/spec/grape/middleware/globals_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Middleware::Globals do subject { described_class.new(blank_app) } diff --git a/spec/grape/middleware/stack_spec.rb b/spec/grape/middleware/stack_spec.rb index 94e579e72..3325469fd 100644 --- a/spec/grape/middleware/stack_spec.rb +++ b/spec/grape/middleware/stack_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Middleware::Stack do module StackSpec class FooMiddleware; end diff --git a/spec/grape/middleware/versioner/accept_version_header_spec.rb b/spec/grape/middleware/versioner/accept_version_header_spec.rb index 9966299f1..a67c26f24 100644 --- a/spec/grape/middleware/versioner/accept_version_header_spec.rb +++ b/spec/grape/middleware/versioner/accept_version_header_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Middleware::Versioner::AcceptVersionHeader do subject { described_class.new(app, **(@options || {})) } diff --git a/spec/grape/middleware/versioner/header_spec.rb b/spec/grape/middleware/versioner/header_spec.rb index d34462bd0..b19ea0449 100644 --- a/spec/grape/middleware/versioner/header_spec.rb +++ b/spec/grape/middleware/versioner/header_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Middleware::Versioner::Header do subject { described_class.new(app, **(@options || {})) } diff --git a/spec/grape/middleware/versioner/param_spec.rb b/spec/grape/middleware/versioner/param_spec.rb index f2f287428..9c2e540ef 100644 --- a/spec/grape/middleware/versioner/param_spec.rb +++ b/spec/grape/middleware/versioner/param_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Middleware::Versioner::Param do subject { described_class.new(app, **options) } diff --git a/spec/grape/middleware/versioner/path_spec.rb b/spec/grape/middleware/versioner/path_spec.rb index 93b56a37e..d5b28a88b 100644 --- a/spec/grape/middleware/versioner/path_spec.rb +++ b/spec/grape/middleware/versioner/path_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Middleware::Versioner::Path do subject { described_class.new(app, **options) } diff --git a/spec/grape/middleware/versioner_spec.rb b/spec/grape/middleware/versioner_spec.rb index 8399aeeea..f42eaa5fe 100644 --- a/spec/grape/middleware/versioner_spec.rb +++ b/spec/grape/middleware/versioner_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Middleware::Versioner do let(:klass) { described_class } diff --git a/spec/grape/named_api_spec.rb b/spec/grape/named_api_spec.rb index 145066612..e3aac6cb5 100644 --- a/spec/grape/named_api_spec.rb +++ b/spec/grape/named_api_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe 'A named API' do subject(:api_name) { NamedAPI.endpoints.last.options[:for].to_s } diff --git a/spec/grape/parser_spec.rb b/spec/grape/parser_spec.rb index 9e55e4a4c..a3b43856f 100644 --- a/spec/grape/parser_spec.rb +++ b/spec/grape/parser_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Parser do subject { described_class } diff --git a/spec/grape/path_spec.rb b/spec/grape/path_spec.rb index 515c66b45..43672146f 100644 --- a/spec/grape/path_spec.rb +++ b/spec/grape/path_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - module Grape describe Path do describe '#initialize' do diff --git a/spec/grape/presenters/presenter_spec.rb b/spec/grape/presenters/presenter_spec.rb index c5a606052..da155190b 100644 --- a/spec/grape/presenters/presenter_spec.rb +++ b/spec/grape/presenters/presenter_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - module Grape module Presenters module PresenterSpec diff --git a/spec/grape/request_spec.rb b/spec/grape/request_spec.rb index 1f9eac94e..0c48cf2b2 100644 --- a/spec/grape/request_spec.rb +++ b/spec/grape/request_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - module Grape describe Request do let(:default_method) { 'GET' } diff --git a/spec/grape/util/inheritable_setting_spec.rb b/spec/grape/util/inheritable_setting_spec.rb index 76e862d1a..2941b3181 100644 --- a/spec/grape/util/inheritable_setting_spec.rb +++ b/spec/grape/util/inheritable_setting_spec.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' module Grape module Util describe InheritableSetting do diff --git a/spec/grape/util/inheritable_values_spec.rb b/spec/grape/util/inheritable_values_spec.rb index 984475aba..fed4a62c2 100644 --- a/spec/grape/util/inheritable_values_spec.rb +++ b/spec/grape/util/inheritable_values_spec.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' module Grape module Util describe InheritableValues do diff --git a/spec/grape/util/reverse_stackable_values_spec.rb b/spec/grape/util/reverse_stackable_values_spec.rb index 92b4f2988..e2f5b282f 100644 --- a/spec/grape/util/reverse_stackable_values_spec.rb +++ b/spec/grape/util/reverse_stackable_values_spec.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' module Grape module Util describe ReverseStackableValues do diff --git a/spec/grape/util/stackable_values_spec.rb b/spec/grape/util/stackable_values_spec.rb index f43e94a37..e857c7f90 100644 --- a/spec/grape/util/stackable_values_spec.rb +++ b/spec/grape/util/stackable_values_spec.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' module Grape module Util describe StackableValues do diff --git a/spec/grape/util/strict_hash_configuration_spec.rb b/spec/grape/util/strict_hash_configuration_spec.rb index c55fd616b..7f059eee1 100644 --- a/spec/grape/util/strict_hash_configuration_spec.rb +++ b/spec/grape/util/strict_hash_configuration_spec.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' module Grape module Util describe 'StrictHashConfiguration' do diff --git a/spec/grape/validations/attributes_iterator_spec.rb b/spec/grape/validations/attributes_iterator_spec.rb index 594c8ca19..40286c840 100644 --- a/spec/grape/validations/attributes_iterator_spec.rb +++ b/spec/grape/validations/attributes_iterator_spec.rb @@ -1,6 +1,4 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Validations::AttributesIterator do end diff --git a/spec/grape/validations/instance_behaivour_spec.rb b/spec/grape/validations/instance_behaivour_spec.rb index bbbddbfd6..c8df96756 100644 --- a/spec/grape/validations/instance_behaivour_spec.rb +++ b/spec/grape/validations/instance_behaivour_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe 'Validator with instance variables' do let(:validator_type) do Class.new(Grape::Validations::Validators::Base) do diff --git a/spec/grape/validations/multiple_attributes_iterator_spec.rb b/spec/grape/validations/multiple_attributes_iterator_spec.rb index af8e802f1..5e6322785 100644 --- a/spec/grape/validations/multiple_attributes_iterator_spec.rb +++ b/spec/grape/validations/multiple_attributes_iterator_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Validations::MultipleAttributesIterator do describe '#each' do subject(:iterator) { described_class.new(validator, scope, params) } diff --git a/spec/grape/validations/params_scope_spec.rb b/spec/grape/validations/params_scope_spec.rb index f2169bf03..87b9bb962 100644 --- a/spec/grape/validations/params_scope_spec.rb +++ b/spec/grape/validations/params_scope_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Validations::ParamsScope do subject do Class.new(Grape::API) diff --git a/spec/grape/validations/single_attribute_iterator_spec.rb b/spec/grape/validations/single_attribute_iterator_spec.rb index 7784e7dc1..1962a8e31 100644 --- a/spec/grape/validations/single_attribute_iterator_spec.rb +++ b/spec/grape/validations/single_attribute_iterator_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Validations::SingleAttributeIterator do describe '#each' do subject(:iterator) { described_class.new(validator, scope, params) } diff --git a/spec/grape/validations/types/array_coercer_spec.rb b/spec/grape/validations/types/array_coercer_spec.rb index f2bfb6c6d..14cca7e58 100644 --- a/spec/grape/validations/types/array_coercer_spec.rb +++ b/spec/grape/validations/types/array_coercer_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Validations::Types::ArrayCoercer do subject { described_class.new(type) } diff --git a/spec/grape/validations/types/primitive_coercer_spec.rb b/spec/grape/validations/types/primitive_coercer_spec.rb index 3a071d800..2b64277d2 100644 --- a/spec/grape/validations/types/primitive_coercer_spec.rb +++ b/spec/grape/validations/types/primitive_coercer_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Validations::Types::PrimitiveCoercer do subject { described_class.new(type, strict) } diff --git a/spec/grape/validations/types/set_coercer_spec.rb b/spec/grape/validations/types/set_coercer_spec.rb index d78f5f511..47646a00a 100644 --- a/spec/grape/validations/types/set_coercer_spec.rb +++ b/spec/grape/validations/types/set_coercer_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Validations::Types::SetCoercer do subject { described_class.new(type) } diff --git a/spec/grape/validations/types_spec.rb b/spec/grape/validations/types_spec.rb index 71e2cae32..d0ab42eef 100644 --- a/spec/grape/validations/types_spec.rb +++ b/spec/grape/validations/types_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Validations::Types do module TypesSpec class FooType diff --git a/spec/grape/validations/validators/all_or_none_spec.rb b/spec/grape/validations/validators/all_or_none_spec.rb index 4623d88c6..9c8fe78b1 100644 --- a/spec/grape/validations/validators/all_or_none_spec.rb +++ b/spec/grape/validations/validators/all_or_none_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Validations::Validators::AllOrNoneOfValidator do let_it_be(:app) do Class.new(Grape::API) do diff --git a/spec/grape/validations/validators/allow_blank_spec.rb b/spec/grape/validations/validators/allow_blank_spec.rb index 2fca98c3b..a699bdff9 100644 --- a/spec/grape/validations/validators/allow_blank_spec.rb +++ b/spec/grape/validations/validators/allow_blank_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Validations::Validators::AllowBlankValidator do let_it_be(:app) do Class.new(Grape::API) 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 4189ec069..4b0e3b0c7 100644 --- a/spec/grape/validations/validators/at_least_one_of_spec.rb +++ b/spec/grape/validations/validators/at_least_one_of_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Validations::Validators::AtLeastOneOfValidator do let_it_be(:app) do Class.new(Grape::API) do diff --git a/spec/grape/validations/validators/coerce_spec.rb b/spec/grape/validations/validators/coerce_spec.rb index 6c4cc29fe..cd1d9dcbd 100644 --- a/spec/grape/validations/validators/coerce_spec.rb +++ b/spec/grape/validations/validators/coerce_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Validations::Validators::CoerceValidator do subject do Class.new(Grape::API) diff --git a/spec/grape/validations/validators/default_spec.rb b/spec/grape/validations/validators/default_spec.rb index c6e399c5c..980fa2304 100644 --- a/spec/grape/validations/validators/default_spec.rb +++ b/spec/grape/validations/validators/default_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Validations::Validators::DefaultValidator do let_it_be(:app) do Class.new(Grape::API) do diff --git a/spec/grape/validations/validators/exactly_one_of_spec.rb b/spec/grape/validations/validators/exactly_one_of_spec.rb index 8076c703b..21e177e93 100644 --- a/spec/grape/validations/validators/exactly_one_of_spec.rb +++ b/spec/grape/validations/validators/exactly_one_of_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Validations::Validators::ExactlyOneOfValidator do let_it_be(:app) do Class.new(Grape::API) do diff --git a/spec/grape/validations/validators/except_values_spec.rb b/spec/grape/validations/validators/except_values_spec.rb index 6bed034c9..63c62dd1d 100644 --- a/spec/grape/validations/validators/except_values_spec.rb +++ b/spec/grape/validations/validators/except_values_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Validations::Validators::ExceptValuesValidator do module ValidationsSpec class ExceptValuesModel diff --git a/spec/grape/validations/validators/mutual_exclusion_spec.rb b/spec/grape/validations/validators/mutual_exclusion_spec.rb index e01dee282..a36a26c63 100644 --- a/spec/grape/validations/validators/mutual_exclusion_spec.rb +++ b/spec/grape/validations/validators/mutual_exclusion_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Validations::Validators::MutualExclusionValidator do let_it_be(:app) do Class.new(Grape::API) do diff --git a/spec/grape/validations/validators/presence_spec.rb b/spec/grape/validations/validators/presence_spec.rb index ebd0a1296..bbe9865bd 100644 --- a/spec/grape/validations/validators/presence_spec.rb +++ b/spec/grape/validations/validators/presence_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Validations::Validators::PresenceValidator do subject do Class.new(Grape::API) do diff --git a/spec/grape/validations/validators/regexp_spec.rb b/spec/grape/validations/validators/regexp_spec.rb index 5a6aec73a..98073b86e 100644 --- a/spec/grape/validations/validators/regexp_spec.rb +++ b/spec/grape/validations/validators/regexp_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Validations::Validators::RegexpValidator do let_it_be(:app) do Class.new(Grape::API) do diff --git a/spec/grape/validations/validators/same_as_spec.rb b/spec/grape/validations/validators/same_as_spec.rb index f21fd94b5..cc1ac984e 100644 --- a/spec/grape/validations/validators/same_as_spec.rb +++ b/spec/grape/validations/validators/same_as_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Validations::Validators::SameAsValidator do let_it_be(:app) do Class.new(Grape::API) do diff --git a/spec/grape/validations/validators/values_spec.rb b/spec/grape/validations/validators/values_spec.rb index ca59c6f09..efa1b90d2 100644 --- a/spec/grape/validations/validators/values_spec.rb +++ b/spec/grape/validations/validators/values_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Validations::Validators::ValuesValidator do let_it_be(:values_model) do Class.new do diff --git a/spec/grape/validations_spec.rb b/spec/grape/validations_spec.rb index ac1f1df33..07b6e5fd2 100644 --- a/spec/grape/validations_spec.rb +++ b/spec/grape/validations_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Validations do subject { Class.new(Grape::API) } diff --git a/spec/integration/multi_json/json_spec.rb b/spec/integration/multi_json/json_spec.rb index ab0224d6c..18ac22b72 100644 --- a/spec/integration/multi_json/json_spec.rb +++ b/spec/integration/multi_json/json_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Json do it 'uses multi_json' do expect(described_class).to eq(::MultiJson) diff --git a/spec/integration/multi_xml/xml_spec.rb b/spec/integration/multi_xml/xml_spec.rb index 7aa11e2ba..54d918e55 100644 --- a/spec/integration/multi_xml/xml_spec.rb +++ b/spec/integration/multi_xml/xml_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::Xml do it 'uses multi_xml' do expect(described_class).to eq(::MultiXml) From a414eb052b21b0ea0b97c2a2e7c2dc25674178a4 Mon Sep 17 00:00:00 2001 From: eproulx Date: Mon, 3 Jan 2022 13:26:07 +0100 Subject: [PATCH 091/304] Fix versions in rack2 and rails6 file --- Appraisals | 4 ++-- gemfiles/rack2.gemfile | 2 +- gemfiles/rails_6.gemfile | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Appraisals b/Appraisals index 403831c68..4968ae7c3 100644 --- a/Appraisals +++ b/Appraisals @@ -5,7 +5,7 @@ appraise 'rails-5' do end appraise 'rails-6' do - gem 'rails', '~> 6.0' + gem 'rails', '~> 6.0.0' end appraise 'rails-6-1' do @@ -37,5 +37,5 @@ appraise 'rack1' do end appraise 'rack2' do - gem 'rack', '~> 2.0' + gem 'rack', '~> 2.0.0' end diff --git a/gemfiles/rack2.gemfile b/gemfiles/rack2.gemfile index 1115914cf..d48a8bb0e 100644 --- a/gemfiles/rack2.gemfile +++ b/gemfiles/rack2.gemfile @@ -4,7 +4,7 @@ source 'https://rubygems.org' -gem 'rack', '~> 2.0' +gem 'rack', '~> 2.0.0' group :development, :test do gem 'bundler' diff --git a/gemfiles/rails_6.gemfile b/gemfiles/rails_6.gemfile index dcd1b7714..27efb6cbf 100644 --- a/gemfiles/rails_6.gemfile +++ b/gemfiles/rails_6.gemfile @@ -4,7 +4,7 @@ source 'https://rubygems.org' -gem 'rails', '~> 6.0' +gem 'rails', '~> 6.0.0' group :development, :test do gem 'bundler' From 37a19e4de796df471c4d7065e5f5990b51b69fd0 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Mon, 3 Jan 2022 13:41:57 +0100 Subject: [PATCH 092/304] Fix autoload types and validators (#2222) --- CHANGELOG.md | 1 + lib/grape.rb | 83 ++++++++---- lib/grape/dry_types.rb | 12 ++ lib/grape/util/json.rb | 2 + lib/grape/validations.rb | 28 ++-- lib/grape/validations/params_scope.rb | 6 +- lib/grape/validations/types.rb | 128 ++++++++++++++---- lib/grape/validations/types/array_coercer.rb | 2 - .../validations/types/dry_type_coercer.rb | 10 +- lib/grape/validations/types/set_coercer.rb | 2 - ...or_none.rb => all_or_none_of_validator.rb} | 2 - ...llow_blank.rb => allow_blank_validator.rb} | 0 .../validators/{as.rb => as_validator.rb} | 0 ...one_of.rb => at_least_one_of_validator.rb} | 2 - .../{coerce.rb => coerce_validator.rb} | 0 .../{default.rb => default_validator.rb} | 0 ..._one_of.rb => exactly_one_of_validator.rb} | 2 - ...t_values.rb => except_values_validator.rb} | 0 ...usion.rb => mutual_exclusion_validator.rb} | 2 - .../{presence.rb => presence_validator.rb} | 0 .../{regexp.rb => regexp_validator.rb} | 0 .../{same_as.rb => same_as_validator.rb} | 0 .../{values.rb => values_validator.rb} | 0 spec/grape/validations/types_spec.rb | 28 ++++ spec/grape/validations_spec.rb | 18 +++ spec/spec_helper.rb | 2 - spec/support/eager_load.rb | 19 --- 27 files changed, 236 insertions(+), 113 deletions(-) create mode 100644 lib/grape/dry_types.rb rename lib/grape/validations/validators/{all_or_none.rb => all_or_none_of_validator.rb} (87%) rename lib/grape/validations/validators/{allow_blank.rb => allow_blank_validator.rb} (100%) rename lib/grape/validations/validators/{as.rb => as_validator.rb} (100%) rename lib/grape/validations/validators/{at_least_one_of.rb => at_least_one_of_validator.rb} (86%) rename lib/grape/validations/validators/{coerce.rb => coerce_validator.rb} (100%) rename lib/grape/validations/validators/{default.rb => default_validator.rb} (100%) rename lib/grape/validations/validators/{exactly_one_of.rb => exactly_one_of_validator.rb} (89%) rename lib/grape/validations/validators/{except_values.rb => except_values_validator.rb} (100%) rename lib/grape/validations/validators/{mutual_exclusion.rb => mutual_exclusion_validator.rb} (86%) rename lib/grape/validations/validators/{presence.rb => presence_validator.rb} (100%) rename lib/grape/validations/validators/{regexp.rb => regexp_validator.rb} (100%) rename lib/grape/validations/validators/{same_as.rb => same_as_validator.rb} (100%) rename lib/grape/validations/validators/{values.rb => values_validator.rb} (100%) delete mode 100644 spec/support/eager_load.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b86db320..697580513 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ #### Fixes +* [#2222](https://github.com/ruby-grape/grape/pull/2222): Autoload types and validators - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 1.6.2 (2021/12/30) diff --git a/lib/grape.rb b/lib/grape.rb index a48008e9d..5c091a537 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -10,16 +10,16 @@ require 'active_support' require 'active_support/version' require 'active_support/isolated_execution_state' if ActiveSupport::VERSION::MAJOR > 6 -require 'active_support/core_ext/hash/indifferent_access' -require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/array/conversions' 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/conversions' require 'active_support/core_ext/hash/deep_merge' -require 'active_support/core_ext/hash/reverse_merge' require 'active_support/core_ext/hash/except' +require 'active_support/core_ext/hash/indifferent_access' +require 'active_support/core_ext/hash/reverse_merge' require 'active_support/core_ext/hash/slice' -require 'active_support/core_ext/hash/conversions' +require 'active_support/core_ext/object/blank' require 'active_support/dependencies/autoload' require 'active_support/notifications' require 'i18n' @@ -45,6 +45,7 @@ module Grape autoload :Env, 'grape/util/env' autoload :Json, 'grape/util/json' autoload :Xml, 'grape/util/xml' + autoload :DryTypes end module Http @@ -218,6 +219,57 @@ module ServeStream autoload :StreamResponse end end + + module Validations + extend ::ActiveSupport::Autoload + + eager_autoload do + autoload :AttributesIterator + autoload :MultipleAttributesIterator + autoload :SingleAttributeIterator + autoload :Types + autoload :ParamsScope + autoload :ValidatorFactory + end + + module Types + extend ::ActiveSupport::Autoload + + eager_autoload do + autoload :InvalidValue + autoload :DryTypeCoercer + autoload :ArrayCoercer + autoload :SetCoercer + autoload :PrimitiveCoercer + autoload :CustomTypeCoercer + autoload :CustomTypeCollectionCoercer + autoload :MultipleTypeCoercer + autoload :VariantCollectionCoercer + end + end + + module Validators + extend ::ActiveSupport::Autoload + + eager_autoload do + autoload :Base + autoload :MultipleParamsBase + autoload :AllOrNoneOfValidator + autoload :AllowBlankValidator + autoload :AsValidator + autoload :AtLeastOneOfValidator + autoload :CoerceValidator + autoload :DefaultValidator + autoload :ExactlyOneOfValidator + autoload :ExceptValuesValidator + autoload :MutualExclusionValidator + autoload :PresenceValidator + autoload :RegexpValidator + autoload :SameAsValidator + autoload :ValuesValidator + end + end + end end require 'grape/config' @@ -227,25 +279,4 @@ module ServeStream 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' -require 'grape/validations/validators/coerce' -require 'grape/validations/validators/default' -require 'grape/validations/validators/exactly_one_of' -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' -require 'grape/validations/validators/all_or_none' -require 'grape/validations/types' -require 'grape/validations/validator_factory' - require 'grape/version' diff --git a/lib/grape/dry_types.rb b/lib/grape/dry_types.rb new file mode 100644 index 000000000..f0676c376 --- /dev/null +++ b/lib/grape/dry_types.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require 'dry-types' + +module Grape + 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 +end diff --git a/lib/grape/util/json.rb b/lib/grape/util/json.rb index 9381d841a..26695e92a 100644 --- a/lib/grape/util/json.rb +++ b/lib/grape/util/json.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'json' + module Grape if Object.const_defined? :MultiJson Json = ::MultiJson diff --git a/lib/grape/validations.rb b/lib/grape/validations.rb index c0736ef22..4b2e97baf 100644 --- a/lib/grape/validations.rb +++ b/lib/grape/validations.rb @@ -1,30 +1,34 @@ # frozen_string_literal: true -require 'grape/validations/attributes_iterator' -require 'grape/validations/single_attribute_iterator' -require 'grape/validations/multiple_attributes_iterator' -require 'grape/validations/params_scope' -require 'grape/validations/types' - module Grape # Registry to store and locate known Validators. module Validations - class << self - attr_accessor :validators - end + module_function - self.validators = {} + def validators + @validators ||= {} + end # Register a new validator, so it can be used to validate parameters. # @param short_name [String] all lower-case, no spaces # @param klass [Class] the validator class. Should inherit from # Validations::Base. - def self.register_validator(short_name, klass) + def register_validator(short_name, klass) validators[short_name] = klass end - def self.deregister_validator(short_name) + def deregister_validator(short_name) validators.delete(short_name) end + + # Find a validator and if not found will try to load it + def require_validator(short_name) + str_name = short_name.to_s + validators.fetch(str_name) do + Grape::Validations::Validators.const_get("#{str_name.camelize}Validator") + end + rescue NameError + raise Grape::Exceptions::UnknownValidator.new(short_name) + end end end diff --git a/lib/grape/validations/params_scope.rb b/lib/grape/validations/params_scope.rb index a3fa9af0d..7b57ba42c 100644 --- a/lib/grape/validations/params_scope.rb +++ b/lib/grape/validations/params_scope.rb @@ -431,17 +431,13 @@ def check_incompatible_option_values(default, values, except_values, excepts) end def validate(type, options, attrs, doc_attrs, opts) - validator_class = Validations.validators[type.to_s] - - raise Grape::Exceptions::UnknownValidator.new(type) unless validator_class - validator_options = { attributes: attrs, options: options, required: doc_attrs[:required], params_scope: self, opts: opts, - validator_class: validator_class + validator_class: Validations.require_validator(type) } @api.namespace_stackable(:validations, validator_options) end diff --git a/lib/grape/validations/types.rb b/lib/grape/validations/types.rb index 2240a7fa7..1e45c4f7f 100644 --- a/lib/grape/validations/types.rb +++ b/lib/grape/validations/types.rb @@ -1,13 +1,7 @@ # frozen_string_literal: true -require_relative 'types/build_coercer' -require_relative 'types/custom_type_coercer' -require_relative 'types/custom_type_collection_coercer' -require_relative 'types/multiple_type_coercer' -require_relative 'types/variant_collection_coercer' -require_relative 'types/json' -require_relative 'types/file' -require_relative 'types/invalid_value' +require 'grape/validations/types/json' +require 'grape/validations/types/file' module Grape module Validations @@ -22,7 +16,8 @@ module Validations # and {Grape::Dsl::Parameters#optional}. The main # entry point for this process is {Types.build_coercer}. module Types - # Types representing a single value, which are coerced. + module_function + PRIMITIVES = [ # Numerical Integer, @@ -44,33 +39,23 @@ module Types ].freeze # Types representing data structures. - STRUCTURES = [ - Hash, - Array, - Set - ].freeze + STRUCTURES = [Hash, Array, Set].freeze - # Special custom types provided by Grape. SPECIAL = { - JSON => Json, + ::JSON => Json, Array[JSON] => JsonArray, ::File => File, Rack::Multipart::UploadedFile => File }.freeze - GROUPS = [ - Array, - Hash, - JSON, - Array[JSON] - ].freeze + GROUPS = [Array, Hash, JSON, Array[JSON]].freeze # Is the given class a primitive type as recognized by Grape? # # @param type [Class] type to check # @return [Boolean] whether or not the type is known by Grape as a valid # type for a single value - def self.primitive?(type) + def primitive?(type) PRIMITIVES.include?(type) end @@ -80,7 +65,7 @@ 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 - def self.structure?(type) + def structure?(type) STRUCTURES.include?(type) end @@ -92,7 +77,7 @@ def self.structure?(type) # @param type [Array,Set] type (or type list!) to check # @return [Boolean] +true+ if the given value will be treated as # a list of types. - def self.multiple?(type) + def multiple?(type) (type.is_a?(Array) || type.is_a?(Set)) && type.size > 1 end @@ -103,7 +88,7 @@ def self.multiple?(type) # # @param type [Class] type to check # @return [Boolean] +true+ if special routines are available - def self.special?(type) + def special?(type) SPECIAL.key? type end @@ -112,7 +97,7 @@ def self.special?(type) # # @param type [Array,Class] type to check # @return [Boolean] +true+ if the type is a supported group type - def self.group?(type) + def group?(type) GROUPS.include? type end @@ -121,7 +106,7 @@ def self.group?(type) # # @param type [Class] type to check # @return [Boolean] whether or not the type can be used as a custom type - def self.custom?(type) + def custom?(type) !primitive?(type) && !structure?(type) && !multiple?(type) && @@ -134,15 +119,98 @@ def self.custom?(type) # @param type [Array,Class] type to check # @return [Boolean] true if +type+ is a collection of a type that implements # its own +#parse+ method. - def self.collection_of_custom?(type) + def collection_of_custom?(type) (type.is_a?(Array) || type.is_a?(Set)) && type.length == 1 && (custom?(type.first) || special?(type.first)) end - def self.map_special(type) + def map_special(type) SPECIAL.fetch(type, type) end + + # Chooses the best coercer for the given type. For example, if the type + # is Integer, it will return a coercer which will be able to coerce a value + # to the integer. + # + # There are a few very special coercers which might be returned. + # + # +Grape::Types::MultipleTypeCoercer+ is a coercer which is returned when + # the given type implies values in an array with different types. + # For example, +[Integer, String]+ allows integer and string values in + # an array. + # + # +Grape::Types::CustomTypeCoercer+ is a coercer which is returned when + # a method is specified by a user with +coerce_with+ option or the user + # specifies a custom type which implements requirments of + # +Grape::Types::CustomTypeCoercer+. + # + # +Grape::Types::CustomTypeCollectionCoercer+ is a very similar to the + # previous one, but it expects an array or set of values having a custom + # type implemented by the user. + # + # There is also a group of custom types implemented by Grape, check + # +Grape::Validations::Types::SPECIAL+ to get the full list. + # + # @param type [Class] the type to which input strings + # should be coerced + # @param method [Class,#call] the coercion method to use + # @return [Object] object to be used + # for coercion and type validation + def build_coercer(type, method: nil, strict: false) + cache_instance(type, method, strict) do + create_coercer_instance(type, method, strict) + end + end + + def create_coercer_instance(type, method, strict) + # Maps a custom type provided by Grape, it doesn't map types wrapped by collections!!! + type = Types.map_special(type) + + # Use a special coercer for multiply-typed parameters. + if Types.multiple?(type) + MultipleTypeCoercer.new(type, method) + + # Use a special coercer for custom types and coercion methods. + elsif method || Types.custom?(type) + CustomTypeCoercer.new(type, method) + + # Special coercer for collections of types that implement a parse method. + # CustomTypeCoercer (above) already handles such types when an explicit coercion + # method is supplied. + elsif Types.collection_of_custom?(type) + Types::CustomTypeCollectionCoercer.new( + Types.map_special(type.first), type.is_a?(Set) + ) + else + DryTypeCoercer.coercer_instance_for(type, strict) + end + end + + def cache_instance(type, method, strict, &_block) + key = cache_key(type, method, strict) + + return @__cache[key] if @__cache.key?(key) + + instance = yield + + @__cache_write_lock.synchronize do + @__cache[key] = instance + end + + instance + end + + def 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, {}) + instance_variable_set(:@__cache_write_lock, Mutex.new) end end end diff --git a/lib/grape/validations/types/array_coercer.rb b/lib/grape/validations/types/array_coercer.rb index d3aeb2146..c6d6b106e 100644 --- a/lib/grape/validations/types/array_coercer.rb +++ b/lib/grape/validations/types/array_coercer.rb @@ -14,8 +14,6 @@ module Types # 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 diff --git a/lib/grape/validations/types/dry_type_coercer.rb b/lib/grape/validations/types/dry_type_coercer.rb index 6b772378e..97a1f8287 100644 --- a/lib/grape/validations/types/dry_type_coercer.rb +++ b/lib/grape/validations/types/dry_type_coercer.rb @@ -18,19 +18,15 @@ module Types # 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] + collection_coercers.fetch(type) do + DryTypeCoercer.collection_coercers[type] = Grape::Validations::Types.const_get("#{type.name.camelize}Coercer") + end end # Returns an instance of a coercer for a given type diff --git a/lib/grape/validations/types/set_coercer.rb b/lib/grape/validations/types/set_coercer.rb index dc76fc773..2d385c935 100644 --- a/lib/grape/validations/types/set_coercer.rb +++ b/lib/grape/validations/types/set_coercer.rb @@ -9,8 +9,6 @@ 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 diff --git a/lib/grape/validations/validators/all_or_none.rb b/lib/grape/validations/validators/all_or_none_of_validator.rb similarity index 87% rename from lib/grape/validations/validators/all_or_none.rb rename to lib/grape/validations/validators/all_or_none_of_validator.rb index 24dc4f8b6..2fe553a15 100644 --- a/lib/grape/validations/validators/all_or_none.rb +++ b/lib/grape/validations/validators/all_or_none_of_validator.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'grape/validations/validators/multiple_params_base' - module Grape module Validations module Validators diff --git a/lib/grape/validations/validators/allow_blank.rb b/lib/grape/validations/validators/allow_blank_validator.rb similarity index 100% rename from lib/grape/validations/validators/allow_blank.rb rename to lib/grape/validations/validators/allow_blank_validator.rb diff --git a/lib/grape/validations/validators/as.rb b/lib/grape/validations/validators/as_validator.rb similarity index 100% rename from lib/grape/validations/validators/as.rb rename to lib/grape/validations/validators/as_validator.rb diff --git a/lib/grape/validations/validators/at_least_one_of.rb b/lib/grape/validations/validators/at_least_one_of_validator.rb similarity index 86% rename from lib/grape/validations/validators/at_least_one_of.rb rename to lib/grape/validations/validators/at_least_one_of_validator.rb index 6fedbef46..3467e4f1d 100644 --- a/lib/grape/validations/validators/at_least_one_of.rb +++ b/lib/grape/validations/validators/at_least_one_of_validator.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'grape/validations/validators/multiple_params_base' - module Grape module Validations module Validators diff --git a/lib/grape/validations/validators/coerce.rb b/lib/grape/validations/validators/coerce_validator.rb similarity index 100% rename from lib/grape/validations/validators/coerce.rb rename to lib/grape/validations/validators/coerce_validator.rb diff --git a/lib/grape/validations/validators/default.rb b/lib/grape/validations/validators/default_validator.rb similarity index 100% rename from lib/grape/validations/validators/default.rb rename to lib/grape/validations/validators/default_validator.rb diff --git a/lib/grape/validations/validators/exactly_one_of.rb b/lib/grape/validations/validators/exactly_one_of_validator.rb similarity index 89% rename from lib/grape/validations/validators/exactly_one_of.rb rename to lib/grape/validations/validators/exactly_one_of_validator.rb index 84d6142fb..735c45701 100644 --- a/lib/grape/validations/validators/exactly_one_of.rb +++ b/lib/grape/validations/validators/exactly_one_of_validator.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'grape/validations/validators/multiple_params_base' - module Grape module Validations module Validators diff --git a/lib/grape/validations/validators/except_values.rb b/lib/grape/validations/validators/except_values_validator.rb similarity index 100% rename from lib/grape/validations/validators/except_values.rb rename to lib/grape/validations/validators/except_values_validator.rb diff --git a/lib/grape/validations/validators/mutual_exclusion.rb b/lib/grape/validations/validators/mutual_exclusion_validator.rb similarity index 86% rename from lib/grape/validations/validators/mutual_exclusion.rb rename to lib/grape/validations/validators/mutual_exclusion_validator.rb index e0f49278b..8d19da34c 100644 --- a/lib/grape/validations/validators/mutual_exclusion.rb +++ b/lib/grape/validations/validators/mutual_exclusion_validator.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'grape/validations/validators/multiple_params_base' - module Grape module Validations module Validators diff --git a/lib/grape/validations/validators/presence.rb b/lib/grape/validations/validators/presence_validator.rb similarity index 100% rename from lib/grape/validations/validators/presence.rb rename to lib/grape/validations/validators/presence_validator.rb diff --git a/lib/grape/validations/validators/regexp.rb b/lib/grape/validations/validators/regexp_validator.rb similarity index 100% rename from lib/grape/validations/validators/regexp.rb rename to lib/grape/validations/validators/regexp_validator.rb diff --git a/lib/grape/validations/validators/same_as.rb b/lib/grape/validations/validators/same_as_validator.rb similarity index 100% rename from lib/grape/validations/validators/same_as.rb rename to lib/grape/validations/validators/same_as_validator.rb diff --git a/lib/grape/validations/validators/values.rb b/lib/grape/validations/validators/values_validator.rb similarity index 100% rename from lib/grape/validations/validators/values.rb rename to lib/grape/validations/validators/values_validator.rb diff --git a/spec/grape/validations/types_spec.rb b/spec/grape/validations/types_spec.rb index d0ab42eef..402c3b6db 100644 --- a/spec/grape/validations/types_spec.rb +++ b/spec/grape/validations/types_spec.rb @@ -48,6 +48,34 @@ def self.parse; end end end + describe 'special types' do + subject { described_class::SPECIAL[type] } + + context 'when JSON' do + let(:type) { JSON } + + it { is_expected.to eq(Grape::Validations::Types::Json) } + end + + context 'when Array[JSON]' do + let(:type) { Array[JSON] } + + it { is_expected.to eq(Grape::Validations::Types::JsonArray) } + end + + context 'when File' do + let(:type) { File } + + it { is_expected.to eq(Grape::Validations::Types::File) } + end + + context 'when Rack::Multipart::UploadedFile' do + let(:type) { Rack::Multipart::UploadedFile } + + it { is_expected.to eq(Grape::Validations::Types::File) } + end + end + describe '::custom?' do it 'returns false if the type does not respond to :parse' do expect(described_class).not_to be_custom(Object) diff --git a/spec/grape/validations_spec.rb b/spec/grape/validations_spec.rb index 07b6e5fd2..489a4f4a0 100644 --- a/spec/grape/validations_spec.rb +++ b/spec/grape/validations_spec.rb @@ -1973,4 +1973,22 @@ def validate_param!(attr_name, params) end end end + + describe 'require_validator' do + subject { described_class.require_validator(short_name) } + + context 'when found' do + let(:short_name) { :presence } + + it { is_expected.to be(Grape::Validations::Validators::PresenceValidator) } + end + + context 'when not found' do + let(:short_name) { :test } + + it 'raises an error' do + expect { subject }.to raise_error(Grape::Exceptions::UnknownValidator) + end + end + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 66c56727b..1168a12e2 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -23,8 +23,6 @@ def rollback_transaction; end require file end -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 diff --git a/spec/support/eager_load.rb b/spec/support/eager_load.rb deleted file mode 100644 index c55e4f243..000000000 --- a/spec/support/eager_load.rb +++ /dev/null @@ -1,19 +0,0 @@ -# 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 From eb1b802288cc4516e15f07113e29e823bffb435a Mon Sep 17 00:00:00 2001 From: eproulx Date: Mon, 3 Jan 2022 14:26:47 +0100 Subject: [PATCH 093/304] Move require 'active_support/concern' in grape.rb --- lib/grape.rb | 1 + lib/grape/dsl/api.rb | 2 -- lib/grape/dsl/callbacks.rb | 2 -- lib/grape/dsl/configuration.rb | 2 -- lib/grape/dsl/helpers.rb | 2 -- lib/grape/dsl/inside_route.rb | 1 - lib/grape/dsl/middleware.rb | 2 -- lib/grape/dsl/parameters.rb | 2 -- lib/grape/dsl/request_response.rb | 2 -- lib/grape/dsl/routing.rb | 2 -- lib/grape/dsl/settings.rb | 2 -- lib/grape/dsl/validations.rb | 2 -- lib/grape/middleware/auth/dsl.rb | 1 - 13 files changed, 1 insertion(+), 22 deletions(-) diff --git a/lib/grape.rb b/lib/grape.rb index 5c091a537..0f0da8e1e 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -8,6 +8,7 @@ require 'rack/auth/digest/md5' require 'set' require 'active_support' +require 'active_support/concern' require 'active_support/version' require 'active_support/isolated_execution_state' if ActiveSupport::VERSION::MAJOR > 6 require 'active_support/core_ext/array/conversions' diff --git a/lib/grape/dsl/api.rb b/lib/grape/dsl/api.rb index 0543cac54..09126a5fb 100644 --- a/lib/grape/dsl/api.rb +++ b/lib/grape/dsl/api.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'active_support/concern' - module Grape module DSL module API diff --git a/lib/grape/dsl/callbacks.rb b/lib/grape/dsl/callbacks.rb index 03827684d..43d4b18dc 100644 --- a/lib/grape/dsl/callbacks.rb +++ b/lib/grape/dsl/callbacks.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'active_support/concern' - module Grape module DSL # Blocks can be executed before or after every API call, using `before`, `after`, diff --git a/lib/grape/dsl/configuration.rb b/lib/grape/dsl/configuration.rb index f33af3225..6abf75968 100644 --- a/lib/grape/dsl/configuration.rb +++ b/lib/grape/dsl/configuration.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'active_support/concern' - module Grape module DSL module Configuration diff --git a/lib/grape/dsl/helpers.rb b/lib/grape/dsl/helpers.rb index a6c4dd6ca..ef8118c0e 100644 --- a/lib/grape/dsl/helpers.rb +++ b/lib/grape/dsl/helpers.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'active_support/concern' - module Grape module DSL module Helpers diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index e2ac44888..b440d0035 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require 'active_support/concern' require 'grape/dsl/headers' module Grape diff --git a/lib/grape/dsl/middleware.rb b/lib/grape/dsl/middleware.rb index 2a8d050db..07e6f9fe7 100644 --- a/lib/grape/dsl/middleware.rb +++ b/lib/grape/dsl/middleware.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'active_support/concern' - module Grape module DSL module Middleware diff --git a/lib/grape/dsl/parameters.rb b/lib/grape/dsl/parameters.rb index 81a30b168..c52d0f1a7 100644 --- a/lib/grape/dsl/parameters.rb +++ b/lib/grape/dsl/parameters.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'active_support/concern' - module Grape module DSL # Defines DSL methods, meant to be applied to a ParamsScope, which define diff --git a/lib/grape/dsl/request_response.rb b/lib/grape/dsl/request_response.rb index 23b57b0da..122d73b43 100644 --- a/lib/grape/dsl/request_response.rb +++ b/lib/grape/dsl/request_response.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'active_support/concern' - module Grape module DSL module RequestResponse diff --git a/lib/grape/dsl/routing.rb b/lib/grape/dsl/routing.rb index cbd3a2326..5facda3dd 100644 --- a/lib/grape/dsl/routing.rb +++ b/lib/grape/dsl/routing.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'active_support/concern' - module Grape module DSL module Routing diff --git a/lib/grape/dsl/settings.rb b/lib/grape/dsl/settings.rb index 899cb42f8..e5ffb8090 100644 --- a/lib/grape/dsl/settings.rb +++ b/lib/grape/dsl/settings.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'active_support/concern' - module Grape module DSL # Keeps track of settings (implemented as key-value pairs, grouped by diff --git a/lib/grape/dsl/validations.rb b/lib/grape/dsl/validations.rb index d2d354fb1..9f6105245 100644 --- a/lib/grape/dsl/validations.rb +++ b/lib/grape/dsl/validations.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'active_support/concern' - module Grape module DSL module Validations diff --git a/lib/grape/middleware/auth/dsl.rb b/lib/grape/middleware/auth/dsl.rb index d85171fc5..eb27588b7 100644 --- a/lib/grape/middleware/auth/dsl.rb +++ b/lib/grape/middleware/auth/dsl.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require 'rack/auth/basic' -require 'active_support/concern' module Grape module Middleware From 19fe117a100a83ce858b37a84bc7947ea0d33c37 Mon Sep 17 00:00:00 2001 From: dm1try Date: Wed, 5 Jan 2022 01:26:07 +0300 Subject: [PATCH 094/304] support kwargs in shared params definition fixes #2231 --- CHANGELOG.md | 1 + lib/grape/dsl/parameters.rb | 2 +- spec/grape/validations_spec.rb | 26 ++++++++++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 697580513..4fb291f76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ #### Fixes * [#2222](https://github.com/ruby-grape/grape/pull/2222): Autoload types and validators - [@ericproulx](https://github.com/ericproulx). +* [#2232](https://github.com/ruby-grape/grape/pull/2232): Fix kwargs support in shared params definition - [@dm1try](https://github.com/dm1try). * Your contribution here. ### 1.6.2 (2021/12/30) diff --git a/lib/grape/dsl/parameters.rb b/lib/grape/dsl/parameters.rb index c52d0f1a7..3a8bad653 100644 --- a/lib/grape/dsl/parameters.rb +++ b/lib/grape/dsl/parameters.rb @@ -62,7 +62,7 @@ def use(*names) params_block = named_params.fetch(name) do raise "Params :#{name} not found!" end - instance_exec(options, ¶ms_block) + instance_exec(**options, ¶ms_block) end end alias use_scope use diff --git a/spec/grape/validations_spec.rb b/spec/grape/validations_spec.rb index 489a4f4a0..d577dce3a 100644 --- a/spec/grape/validations_spec.rb +++ b/spec/grape/validations_spec.rb @@ -1483,6 +1483,32 @@ def validate_param!(attr_name, params) end end + context 'with block and keyword argument' do + before do + subject.helpers do + params :shared_params do |type:| + optional :param, default: type + end + end + subject.format :json + subject.params do + use :shared_params, type: 'value' + end + subject.get '/shared_params' do + { + param: params[:param] + } + end + end + + it 'works' do + get '/shared_params' + + expect(last_response.status).to eq(200) + expect(last_response.body).to eq({ param: 'value' }.to_json) + end + end + context 'documentation' do it 'can be included with a hash' do documentation = { example: 'Joe' } From d52e1fcff675b79735cfcf9872eebf5c7684358e Mon Sep 17 00:00:00 2001 From: Dmitriy Nesteryuk Date: Tue, 4 Jan 2022 17:29:52 +0200 Subject: [PATCH 095/304] do not collect params in route settings --- CHANGELOG.md | 1 + benchmark/nested_params.rb | 1 + lib/grape/dsl/desc.rb | 15 --------------- lib/grape/dsl/validations.rb | 9 +-------- spec/grape/dsl/validations_spec.rb | 5 ----- 5 files changed, 3 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fb291f76..3fecadadc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * [#2222](https://github.com/ruby-grape/grape/pull/2222): Autoload types and validators - [@ericproulx](https://github.com/ericproulx). * [#2232](https://github.com/ruby-grape/grape/pull/2232): Fix kwargs support in shared params definition - [@dm1try](https://github.com/dm1try). +* [#2229](https://github.com/ruby-grape/grape/pull/2229): Do not collect params in route settings - [@dnesteryuk](https://github.com/dnesteryuk). * Your contribution here. ### 1.6.2 (2021/12/30) diff --git a/benchmark/nested_params.rb b/benchmark/nested_params.rb index 2523939b8..f7cf0798c 100644 --- a/benchmark/nested_params.rb +++ b/benchmark/nested_params.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) require 'grape' require 'benchmark/ips' diff --git a/lib/grape/dsl/desc.rb b/lib/grape/dsl/desc.rb index ab19bfd05..b606639e8 100644 --- a/lib/grape/dsl/desc.rb +++ b/lib/grape/dsl/desc.rb @@ -78,21 +78,6 @@ def desc(description, options = {}, &config_block) route_setting :description, options end - def description_field(field, value = nil) - description = route_setting(:description) - if value - description ||= route_setting(:description, {}) - description[field] = value - elsif description - description[field] - end - end - - def unset_description_field(field) - description = route_setting(:description) - description&.delete(field) - end - # Returns an object which configures itself via an instance-context DSL. def desc_container(endpoint_configuration) Module.new do diff --git a/lib/grape/dsl/validations.rb b/lib/grape/dsl/validations.rb index 9f6105245..a150ac7f3 100644 --- a/lib/grape/dsl/validations.rb +++ b/lib/grape/dsl/validations.rb @@ -30,7 +30,6 @@ def reset_validations! unset_namespace_stackable :declared_params unset_namespace_stackable :validations unset_namespace_stackable :params - unset_description_field :params end # Opens a root-level ParamsScope, defining parameter coercions and @@ -41,14 +40,8 @@ def params(&block) end def document_attribute(names, opts) - setting = description_field(:params) - setting ||= description_field(:params, {}) Array(names).each do |name| - full_name = name[:full_name].to_s - setting[full_name] ||= {} - setting[full_name].merge!(opts) - - namespace_stackable(:params, full_name => opts) + namespace_stackable(:params, name[:full_name].to_s => opts) end end end diff --git a/spec/grape/dsl/validations_spec.rb b/spec/grape/dsl/validations_spec.rb index 08b951d65..d70385b1c 100644 --- a/spec/grape/dsl/validations_spec.rb +++ b/spec/grape/dsl/validations_spec.rb @@ -36,10 +36,6 @@ class Dummy expect(subject.namespace_stackable(:params)).to eq [] end - it 'resets documentation params' do - expect(subject.route_setting(:description)[:params]).to be_nil - end - it 'does not reset documentation description' do expect(subject.route_setting(:description)[:description]).to eq 'lol' end @@ -62,7 +58,6 @@ class Dummy it 'creates a param documentation' do expect(subject.namespace_stackable(:params)).to eq(['xxx' => { foo: 'bar' }]) - expect(subject.route_setting(:description)).to eq(params: { 'xxx' => { foo: 'bar' } }) end end end From 1c65d8b62885f0e7beab659b9074a7f2affeae9e Mon Sep 17 00:00:00 2001 From: Dmitriy Nesteryuk Date: Mon, 27 Dec 2021 15:25:03 +0200 Subject: [PATCH 096/304] a setting for disabling documentation to internal APIs Our application has 3 APIs, 2 of them are internal and don't require documentations. However, there has not been any option to prevent Grape from documenting parameters for internal APIs. This change adds a `do_not_document!` setting which instructs Grape to not document parameters, thus needless objects allocation is avoided. class Api < Grape::API do_not_document! end The logic for documenting parameters was moved to a separate class, the `Grape::Validations::ParamsScope` class has to many responsibilities, it would be better to split it. --- CHANGELOG.md | 3 +- README.md | 12 ++ lib/grape/dsl/routing.rb | 4 + lib/grape/dsl/validations.rb | 6 - lib/grape/validations/attributes_doc.rb | 58 +++++++ lib/grape/validations/params_scope.rb | 52 +++--- spec/grape/api/documentation_spec.rb | 59 +++++++ spec/grape/dsl/validations_spec.rb | 10 -- spec/grape/validations/attributes_doc_spec.rb | 153 ++++++++++++++++++ spec/grape/validations/params_scope_spec.rb | 80 --------- spec/grape/validations_spec.rb | 14 -- 11 files changed, 307 insertions(+), 144 deletions(-) create mode 100644 lib/grape/validations/attributes_doc.rb create mode 100644 spec/grape/api/documentation_spec.rb create mode 100644 spec/grape/validations/attributes_doc_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fecadadc..eba1449ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ #### Features +* [#2233](https://github.com/ruby-grape/grape/pull/2233): A setting for disabling documentation to internal APIs - [@dnesteryuk](https://github.com/dnesteryuk). * Your contribution here. #### Fixes @@ -13,8 +14,6 @@ ### 1.6.2 (2021/12/30) -#### Features - #### Fixes * [#2219](https://github.com/ruby-grape/grape/pull/2219): Revert the changes for autoloading provided in 1.6.1 - [@dm1try](https://github.com/dm1try). diff --git a/README.md b/README.md index 640cf4620..0f55d2049 100644 --- a/README.md +++ b/README.md @@ -2249,6 +2249,18 @@ params do end ``` +If documentation isn't needed (for instance, it is an internal API), documentation can be disabled. + +```ruby +class API < Grape::API + do_not_document! + + # endpoints... +end +``` + +In this case, Grape won't create objects related to documentation which are retained in RAM forever. + ## Cookies You can set, get and delete your cookies very simply using `cookies` method. diff --git a/lib/grape/dsl/routing.rb b/lib/grape/dsl/routing.rb index 5facda3dd..158db99f5 100644 --- a/lib/grape/dsl/routing.rb +++ b/lib/grape/dsl/routing.rb @@ -77,6 +77,10 @@ def do_not_route_options! namespace_inheritable(:do_not_route_options, true) end + def do_not_document! + namespace_inheritable(:do_not_document, true) + end + def mount(mounts, *opts) mounts = { mounts => '/' } unless mounts.respond_to?(:each_pair) mounts.each_pair do |app, path| diff --git a/lib/grape/dsl/validations.rb b/lib/grape/dsl/validations.rb index a150ac7f3..66aff55ef 100644 --- a/lib/grape/dsl/validations.rb +++ b/lib/grape/dsl/validations.rb @@ -38,12 +38,6 @@ def reset_validations! def params(&block) Grape::Validations::ParamsScope.new(api: self, type: Hash, &block) end - - def document_attribute(names, opts) - Array(names).each do |name| - namespace_stackable(:params, name[:full_name].to_s => opts) - end - end end end end diff --git a/lib/grape/validations/attributes_doc.rb b/lib/grape/validations/attributes_doc.rb new file mode 100644 index 000000000..a04670703 --- /dev/null +++ b/lib/grape/validations/attributes_doc.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module Grape + module Validations + class ParamsScope + # Documents parameters of an endpoint. If documentation isn't needed (for instance, it is an + # internal API), the class only cleans up attributes to avoid junk in RAM. + class AttributesDoc + attr_accessor :type, :values + + # @param api [Grape::API::Instance] + # @param scope [Validations::ParamsScope] + def initialize(api, scope) + @api = api + @scope = scope + @type = type + end + + def extract_details(validations) + details[:required] = validations.key?(:presence) + + desc = validations.delete(:desc) || validations.delete(:description) + + details[:desc] = desc if desc + + documentation = validations.delete(:documentation) + + details[:documentation] = documentation if documentation + + details[:default] = validations[:default] if validations.key?(:default) + end + + def document(attrs) + return if @api.namespace_inheritable(:do_not_document) + + details[:type] = type.to_s if type + details[:values] = values if values + + documented_attrs = attrs.each_with_object({}) do |name, memo| + memo[@scope.full_name(name)] = details + end + + @api.namespace_stackable(:params, documented_attrs) + end + + def required + details[:required] + end + + protected + + def details + @details ||= {} + end + end + end + end +end diff --git a/lib/grape/validations/params_scope.rb b/lib/grape/validations/params_scope.rb index 7b57ba42c..6f6907141 100644 --- a/lib/grape/validations/params_scope.rb +++ b/lib/grape/validations/params_scope.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require_relative 'attributes_doc' + module Grape module Validations class ParamsScope @@ -31,7 +33,7 @@ def initialize(opts, &block) @api = opts[:api] @optional = opts[:optional] || false @type = opts[:type] - @group = opts[:group] || {} + @group = opts[:group] @dependent_on = opts[:dependent_on] @declared_params = [] @index = nil @@ -269,17 +271,14 @@ def configure_declared_params end def validates(attrs, validations) - doc_attrs = { required: validations.key?(:presence) } + doc = AttributesDoc.new @api, self + doc.extract_details validations coerce_type = infer_coercion(validations) - doc_attrs[:type] = coerce_type.to_s if coerce_type - - desc = validations.delete(:desc) || validations.delete(:description) - doc_attrs[:desc] = desc if desc + doc.type = coerce_type default = validations[:default] - doc_attrs[:default] = default if validations.key?(:default) if (values_hash = validations[:values]).is_a? Hash values = values_hash[:value] @@ -288,7 +287,8 @@ def validates(attrs, validations) else values = validations[:values] end - doc_attrs[:values] = values if values + + doc.values = values except_values = options_key?(:except_values, :value, validations) ? validations[:except_values][:value] : validations[:except_values] @@ -304,28 +304,22 @@ def validates(attrs, validations) # type should be compatible with values array, if both exist validate_value_coercion(coerce_type, values, except_values, excepts) - doc_attrs[:documentation] = validations.delete(:documentation) if validations.key?(:documentation) - - document_attribute(attrs, doc_attrs) + doc.document attrs opts = derive_validator_options(validations) - order_specific_validations = Set[:as] - # Validate for presence before any other validators - validates_presence(validations, attrs, doc_attrs, opts) do |validation_type| - order_specific_validations << validation_type - end + validates_presence(validations, attrs, doc, opts) # Before we run the rest of the validators, let's handle # whatever coercion so that we are working with correctly # type casted values - coerce_type validations, attrs, doc_attrs, opts + coerce_type validations, attrs, doc, opts validations.each do |type, options| - next if order_specific_validations.include?(type) + next if type == :as - validate(type, options, attrs, doc_attrs, opts) + validate(type, options, attrs, doc, opts) end end @@ -389,7 +383,7 @@ def check_coerce_with(validations) # composited from more than one +requires+/+optional+ # parameter, and needs to be run before most other # validations. - def coerce_type(validations, attrs, doc_attrs, opts) + def coerce_type(validations, attrs, doc, opts) check_coerce_with(validations) return unless validations.key?(:coerce) @@ -399,7 +393,7 @@ def coerce_type(validations, attrs, doc_attrs, opts) method: validations[:coerce_with], message: validations[:coerce_message] } - validate('coerce', coerce_options, attrs, doc_attrs, opts) + validate('coerce', coerce_options, attrs, doc, opts) validations.delete(:coerce_with) validations.delete(:coerce) validations.delete(:coerce_message) @@ -430,11 +424,11 @@ def check_incompatible_option_values(default, values, except_values, excepts) unless Array(default).none? { |def_val| excepts.include?(def_val) } end - def validate(type, options, attrs, doc_attrs, opts) + def validate(type, options, attrs, doc, opts) validator_options = { attributes: attrs, options: options, - required: doc_attrs[:required], + required: doc.required, params_scope: self, opts: opts, validator_class: Validations.require_validator(type) @@ -481,17 +475,11 @@ def derive_validator_options(validations) } end - def validates_presence(validations, attrs, doc_attrs, opts) + def validates_presence(validations, attrs, doc, opts) return unless validations.key?(:presence) && validations[:presence] - validate(:presence, validations[:presence], attrs, doc_attrs, opts) - yield :presence - yield :message if validations.key?(:message) - end - - def document_attribute(attrs, doc_attrs) - full_attrs = attrs.collect { |name| { name: name, full_name: full_name(name) } } - @api.document_attribute(full_attrs, doc_attrs) + validate(:presence, validations.delete(:presence), attrs, doc, opts) + validations.delete(:message) if validations.key?(:message) end end end diff --git a/spec/grape/api/documentation_spec.rb b/spec/grape/api/documentation_spec.rb new file mode 100644 index 000000000..f7c50fd49 --- /dev/null +++ b/spec/grape/api/documentation_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Grape::API do + subject { Class.new(described_class) } + + let(:app) { subject } + + context 'an endpoint with documentation' do + it 'documents parameters' do + subject.params do + requires 'price', type: Float, desc: 'Sales price' + end + subject.get '/' + + expect(subject.routes.first.params['price']).to eq(required: true, + type: 'Float', + desc: 'Sales price') + end + + it 'allows documentation with a hash' do + documentation = { example: 'Joe' } + + subject.params do + requires 'first_name', documentation: documentation + end + subject.get '/' + + expect(subject.routes.first.params['first_name'][:documentation]).to eq(documentation) + end + end + + context 'an endpoint without documentation' do + before do + subject.do_not_document! + + subject.params do + requires :city, type: String, desc: 'Should be ignored' + optional :postal_code, type: Integer + end + subject.post '/' do + declared(params).to_json + end + end + + it 'does not document parameters for the endpoint' do + expect(subject.routes.first.params).to eq({}) + end + + it 'still declares params internally' do + data = { city: 'Berlin', postal_code: 10_115 } + + post '/', data + + expect(last_response.body).to eq(data.to_json) + end + end +end diff --git a/spec/grape/dsl/validations_spec.rb b/spec/grape/dsl/validations_spec.rb index d70385b1c..66db7645a 100644 --- a/spec/grape/dsl/validations_spec.rb +++ b/spec/grape/dsl/validations_spec.rb @@ -50,16 +50,6 @@ class Dummy expect { subject.params { raise 'foo' } }.to raise_error RuntimeError, 'foo' end end - - describe '.document_attribute' do - before do - subject.document_attribute([full_name: 'xxx'], foo: 'bar') - end - - it 'creates a param documentation' do - expect(subject.namespace_stackable(:params)).to eq(['xxx' => { foo: 'bar' }]) - end - end end end end diff --git a/spec/grape/validations/attributes_doc_spec.rb b/spec/grape/validations/attributes_doc_spec.rb new file mode 100644 index 000000000..25bf40a3d --- /dev/null +++ b/spec/grape/validations/attributes_doc_spec.rb @@ -0,0 +1,153 @@ +# frozen_string_literal: true + +describe Grape::Validations::ParamsScope::AttributesDoc do + shared_examples 'an optional doc attribute' do |attr| + it 'does not mention it' do + expected_opts.delete(attr) + validations.delete(attr) + + expect(subject.first['nested[engine_age]']).not_to have_key(attr) + end + end + + let(:api) { Class.new(Grape::API::Instance) } + let(:scope) do + params = nil + api_instance = api + + # just to get nested params + Grape::Validations::ParamsScope.new(type: Hash, api: api) do + params = Grape::Validations::ParamsScope.new(element: 'nested', + type: Hash, + api: api_instance, + parent: self) + end + + params + end + + let(:validations) do + { + presence: true, + desc: 'Age of...', + documentation: 'Age is...', + default: 1 + } + end + + let(:doc) { described_class.new(api, scope) } + + describe '#extract_details' do + subject { doc.extract_details(validations) } + + it 'cleans up doc attrs needed for documentation only' do + subject + + expect(validations[:desc]).to be_nil + expect(validations[:documentation]).to be_nil + end + + it 'does not clean up doc attrs mandatory for validators' do + subject + + expect(validations[:presence]).not_to be_nil + expect(validations[:default]).not_to be_nil + end + + it 'tells when attributes are required' do + subject + + expect(doc.required).to be_truthy + end + end + + describe '#document' do + subject do + doc.extract_details validations + doc.document attrs + end + + let(:attrs) { %w[engine_age car_age] } + let(:valid_values) { [1, 3, 5, 8] } + + let!(:expected_opts) do + { + required: true, + desc: validations[:desc], + documentation: validations[:documentation], + default: validations[:default], + type: 'Integer', + values: valid_values + } + end + + before do + doc.type = Integer + doc.values = valid_values + end + + context 'documentation is enabled' do + subject do + super() + api.namespace_stackable(:params) + end + + it 'documents attributes' do + expect(subject.first).to eq('nested[engine_age]' => expected_opts, + 'nested[car_age]' => expected_opts) + end + + it_behaves_like 'an optional doc attribute', :default + it_behaves_like 'an optional doc attribute', :documentation + it_behaves_like 'an optional doc attribute', :desc + it_behaves_like 'an optional doc attribute', :type do + before { doc.type = nil } + end + it_behaves_like 'an optional doc attribute', :values do + before { doc.values = nil } + end + + context 'false as a default value' do + before { validations[:default] = false } + + it 'is still documented' do + doc = subject.first['nested[engine_age]'] + + expect(doc).to have_key(:default) + expect(doc[:default]).to eq(false) + end + end + + context 'nil as a default value' do + before { validations[:default] = nil } + + it 'is still documented' do + doc = subject.first['nested[engine_age]'] + + expect(doc).to have_key(:default) + expect(doc[:default]).to be_nil + end + end + + context 'the description key instead of desc' do + let!(:desc) { validations.delete(:desc) } + + before { validations[:description] = desc } + + it 'adds the given description' do + expect(subject.first['nested[engine_age]'][:desc]).to eq(desc) + end + end + end + + context 'documentation is disabled' do + before { api.namespace_inheritable :do_not_document, true } + + it 'does not document attributes' do + subject + + expect(api.namespace_stackable(:params)).to eq([]) + end + end + end +end diff --git a/spec/grape/validations/params_scope_spec.rb b/spec/grape/validations/params_scope_spec.rb index 87b9bb962..cba61b17d 100644 --- a/spec/grape/validations/params_scope_spec.rb +++ b/spec/grape/validations/params_scope_spec.rb @@ -9,86 +9,6 @@ def app subject end - context 'setting a default' do - let(:documentation) { subject.routes.first.params } - - context 'when the default value is truthy' do - before do - subject.params do - optional :int, type: Integer, default: 42 - end - subject.get - end - - it 'adds documentation about the default value' do - expect(documentation).to have_key('int') - expect(documentation['int']).to have_key(:default) - expect(documentation['int'][:default]).to eq(42) - end - end - - context 'when the default value is false' do - before do - subject.params do - optional :bool, type: Grape::API::Boolean, default: false - end - subject.get - end - - it 'adds documentation about the default value' do - expect(documentation).to have_key('bool') - expect(documentation['bool']).to have_key(:default) - expect(documentation['bool'][:default]).to eq(false) - end - end - - context 'when the default value is nil' do - before do - subject.params do - optional :object, type: Object, default: nil - end - subject.get - end - - it 'adds documentation about the default value' do - expect(documentation).to have_key('object') - expect(documentation['object']).to have_key(:default) - expect(documentation['object'][:default]).to eq(nil) - end - end - end - - context 'without a default' do - before do - subject.params do - optional :object, type: Object - end - subject.get - end - - it 'does not add documentation for the default value' do - documentation = subject.routes.first.params - expect(documentation).to have_key('object') - expect(documentation['object']).not_to have_key(:default) - end - end - - context 'setting description' do - %i[desc description].each do |description_type| - it "allows setting #{description_type}" do - subject.params do - requires :int, type: Integer, description_type => 'My very nice integer' - end - subject.get '/single' do - 'int works' - end - get '/single', int: 420 - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('int works') - end - end - end - context 'when using custom types' do module ParamsScopeSpec class CustomType diff --git a/spec/grape/validations_spec.rb b/spec/grape/validations_spec.rb index d577dce3a..1ad4585d2 100644 --- a/spec/grape/validations_spec.rb +++ b/spec/grape/validations_spec.rb @@ -1509,20 +1509,6 @@ def validate_param!(attr_name, params) end end - context 'documentation' do - it 'can be included with a hash' do - documentation = { example: 'Joe' } - - subject.params do - requires 'first_name', documentation: documentation - end - subject.get '/' do - end - - expect(subject.routes.first.params['first_name'][:documentation]).to eq(documentation) - end - end - context 'all or none' do context 'optional params' do before do From 71d3474aee466ab629c0e65c537627907d9875ab Mon Sep 17 00:00:00 2001 From: Ben Schmeckpeper Date: Mon, 10 Jan 2022 16:59:26 -0600 Subject: [PATCH 097/304] Add a test demonstrating the error when format includes non-UTF-8 characters --- spec/grape/api_spec.rb | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index 7cf73b4a4..e518917bb 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -4157,6 +4157,20 @@ def before end end + context 'with non-UTF-8 characters in specified format' do + it 'converts the characters' do + subject.format :json + subject.content_type :json, 'application/json' + subject.get '/something' do + 'foo' + end + get '/something?format=%0A%0B%BF' + expect(last_response.status).to eq(406) + message = "The requested format '\n\u000b\357\277\275' is not supported." + expect(last_response.body).to eq({ error: message }.to_json) + end + end + context 'body' do context 'false' do before do From c154f3f838e969b80830aee2c36c4ea1adf41e97 Mon Sep 17 00:00:00 2001 From: Ben Schmeckpeper Date: Mon, 10 Jan 2022 17:09:38 -0600 Subject: [PATCH 098/304] Replace invalid and undefined UTF-8 characters before dumping error message to JSON --- lib/grape/error_formatter/json.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/grape/error_formatter/json.rb b/lib/grape/error_formatter/json.rb index 770c5482d..535919234 100644 --- a/lib/grape/error_formatter/json.rb +++ b/lib/grape/error_formatter/json.rb @@ -21,9 +21,15 @@ def wrap_message(message) if message.is_a?(Exceptions::ValidationErrors) || message.is_a?(Hash) message else - { error: message } + { error: ensure_utf8(message) } end end + + def ensure_utf8(message) + return message unless message.respond_to? :encode + + message.encode('UTF-8', invalid: :replace, undef: :replace) + end end end end From 112fec24b26df87924bd32dd1d5a11d4e6cb732a Mon Sep 17 00:00:00 2001 From: Ben Schmeckpeper Date: Tue, 11 Jan 2022 11:00:50 -0600 Subject: [PATCH 099/304] Add CHANGELOG entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fecadadc..4ef599629 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * [#2222](https://github.com/ruby-grape/grape/pull/2222): Autoload types and validators - [@ericproulx](https://github.com/ericproulx). * [#2232](https://github.com/ruby-grape/grape/pull/2232): Fix kwargs support in shared params definition - [@dm1try](https://github.com/dm1try). * [#2229](https://github.com/ruby-grape/grape/pull/2229): Do not collect params in route settings - [@dnesteryuk](https://github.com/dnesteryuk). +* [#2234](https://github.com/ruby-grape/grape/pull/2234): Remove non-utf-8 characters from format before generating JSON error - [@bschmeck](https://github.com/bschmeck). * Your contribution here. ### 1.6.2 (2021/12/30) From 6b490d2d25a0cc9a2be3b567e8bb08f5cdaf39f8 Mon Sep 17 00:00:00 2001 From: Peter Goldstein Date: Fri, 14 Jan 2022 10:27:05 -0800 Subject: [PATCH 100/304] Add Ruby 3.1 to CI --- .github/workflows/test.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 353d7109e..4db5bbd23 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,6 +40,15 @@ jobs: - gemfiles/rails_6_1.gemfile experimental: [false] include: + - ruby: 3.1 + gemfile: 'gemfiles/multi_json.gemfile' + experimental: false + - ruby: 3.1 + gemfile: 'gemfiles/multi_xml.gemfile' + experimental: false + - ruby: 3.1 + gemfile: 'gemfiles/rails_7.gemfile' + experimental: false - ruby: "3.0" gemfile: 'gemfiles/multi_json.gemfile' experimental: false From b1bde4cbfcee69633aeff94128e6ebf257209dea Mon Sep 17 00:00:00 2001 From: Peter Goldstein Date: Fri, 14 Jan 2022 10:34:13 -0800 Subject: [PATCH 101/304] Add CHANGELOG.md entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81bb1ea72..fc149e1aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ #### Features * [#2233](https://github.com/ruby-grape/grape/pull/2233): A setting for disabling documentation to internal APIs - [@dnesteryuk](https://github.com/dnesteryuk). +* [#2235](https://github.com/ruby-grape/grape/pull/2235): Add Ruby 3.1 to CI - [@petergoldstein](https://github.com/petergoldstein). * Your contribution here. #### Fixes From e93d27897fbf5f360fd11064d7b3c4d8094b7c53 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Mon, 14 Feb 2022 18:18:02 +0100 Subject: [PATCH 102/304] Rename MissingGroupType and UnsupportedGroupType (#2227) * Rename MissingGroupType and UnsupportedGroupType * Add CHANGELOG.md AND UPGRADING.md entries * Quote classes * Add for more information * Add alias for MissingGroupType and UnsupportedGroupeType * Add Final newline missing. --- CHANGELOG.md | 1 + UPGRADING.md | 11 +++++++++++ lib/grape.rb | 4 ++-- lib/grape/dsl/parameters.rb | 4 ++-- lib/grape/exceptions/missing_group_type.rb | 4 +++- lib/grape/exceptions/unsupported_group_type.rb | 4 +++- lib/grape/validations/params_scope.rb | 4 ++-- .../grape/exceptions/missing_group_type_spec.rb | 15 +++++++++++++++ .../exceptions/unsupported_group_type_spec.rb | 17 +++++++++++++++++ spec/grape/validations/params_scope_spec.rb | 8 ++++---- 10 files changed, 60 insertions(+), 12 deletions(-) create mode 100644 spec/grape/exceptions/missing_group_type_spec.rb create mode 100644 spec/grape/exceptions/unsupported_group_type_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index fc149e1aa..c3c9f6347 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ * [#2232](https://github.com/ruby-grape/grape/pull/2232): Fix kwargs support in shared params definition - [@dm1try](https://github.com/dm1try). * [#2229](https://github.com/ruby-grape/grape/pull/2229): Do not collect params in route settings - [@dnesteryuk](https://github.com/dnesteryuk). * [#2234](https://github.com/ruby-grape/grape/pull/2234): Remove non-utf-8 characters from format before generating JSON error - [@bschmeck](https://github.com/bschmeck). +* [#2227](https://github.com/ruby-grape/grape/pull/2222): Rename "MissingGroupType" and "UnsupportedGroupType" exceptions - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 1.6.2 (2021/12/30) diff --git a/UPGRADING.md b/UPGRADING.md index 8dc77ea67..969fc1347 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,6 +1,17 @@ Upgrading Grape =============== +### Upgrading to >= 1.6.3 + +#### Exceptions renaming + +The following exceptions has been renamed for consistency through exceptions naming : + +* `MissingGroupTypeError` => `MissingGroupType` +* `UnsupportedGroupTypeError` => `UnsupportedGroupType` + +See [#2227](https://github.com/ruby-grape/grape/pull/2227) for more information. + ### Upgrading to >= 1.6.0 #### Parameter renaming with :as diff --git a/lib/grape.rb b/lib/grape.rb index 0f0da8e1e..7aedc07db 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -73,8 +73,8 @@ module Exceptions autoload :UnknownParameter autoload :InvalidWithOptionForRepresent autoload :IncompatibleOptionValues - autoload :MissingGroupTypeError, 'grape/exceptions/missing_group_type' - autoload :UnsupportedGroupTypeError, 'grape/exceptions/unsupported_group_type' + autoload :MissingGroupType + autoload :UnsupportedGroupType autoload :InvalidMessageBody autoload :InvalidAcceptHeader autoload :InvalidVersionHeader diff --git a/lib/grape/dsl/parameters.rb b/lib/grape/dsl/parameters.rb index 3a8bad653..31a7b34e7 100644 --- a/lib/grape/dsl/parameters.rb +++ b/lib/grape/dsl/parameters.rb @@ -148,8 +148,8 @@ def optional(*attrs, &block) # check type for optional parameter group if attrs && block - raise Grape::Exceptions::MissingGroupTypeError.new if type.nil? - raise Grape::Exceptions::UnsupportedGroupTypeError.new unless Grape::Validations::Types.group?(type) + raise Grape::Exceptions::MissingGroupType if type.nil? + raise Grape::Exceptions::UnsupportedGroupType unless Grape::Validations::Types.group?(type) end if opts[:using] diff --git a/lib/grape/exceptions/missing_group_type.rb b/lib/grape/exceptions/missing_group_type.rb index 398113ff8..d400424b4 100644 --- a/lib/grape/exceptions/missing_group_type.rb +++ b/lib/grape/exceptions/missing_group_type.rb @@ -2,10 +2,12 @@ module Grape module Exceptions - class MissingGroupTypeError < Base + class MissingGroupType < Base def initialize super(message: compose_message(:missing_group_type)) end end end end + +Grape::Exceptions::MissingGroupTypeError = Grape::Exceptions::MissingGroupType diff --git a/lib/grape/exceptions/unsupported_group_type.rb b/lib/grape/exceptions/unsupported_group_type.rb index 9cbc7aac2..3fb6160b7 100644 --- a/lib/grape/exceptions/unsupported_group_type.rb +++ b/lib/grape/exceptions/unsupported_group_type.rb @@ -2,10 +2,12 @@ module Grape module Exceptions - class UnsupportedGroupTypeError < Base + class UnsupportedGroupType < Base def initialize super(message: compose_message(:unsupported_group_type)) end end end end + +Grape::Exceptions::UnsupportedGroupTypeError = Grape::Exceptions::UnsupportedGroupType diff --git a/lib/grape/validations/params_scope.rb b/lib/grape/validations/params_scope.rb index 6f6907141..d9dbff469 100644 --- a/lib/grape/validations/params_scope.rb +++ b/lib/grape/validations/params_scope.rb @@ -208,8 +208,8 @@ def new_scope(attrs, optional = false, &block) # if required params are grouped and no type or unsupported type is provided, raise an error type = attrs[1] ? attrs[1][:type] : nil if attrs.first && !optional - raise Grape::Exceptions::MissingGroupTypeError.new if type.nil? - raise Grape::Exceptions::UnsupportedGroupTypeError.new unless Grape::Validations::Types.group?(type) + raise Grape::Exceptions::MissingGroupType if type.nil? + raise Grape::Exceptions::UnsupportedGroupType unless Grape::Validations::Types.group?(type) end self.class.new( diff --git a/spec/grape/exceptions/missing_group_type_spec.rb b/spec/grape/exceptions/missing_group_type_spec.rb new file mode 100644 index 000000000..509f823f2 --- /dev/null +++ b/spec/grape/exceptions/missing_group_type_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +RSpec.describe Grape::Exceptions::MissingGroupType do + describe '#message' do + subject { described_class.new.message } + + it { is_expected.to include 'group type is required' } + end + + describe '#alias' do + subject { described_class } + + it { is_expected.to eq(Grape::Exceptions::MissingGroupTypeError) } + end +end diff --git a/spec/grape/exceptions/unsupported_group_type_spec.rb b/spec/grape/exceptions/unsupported_group_type_spec.rb new file mode 100644 index 000000000..167755aef --- /dev/null +++ b/spec/grape/exceptions/unsupported_group_type_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +RSpec.describe Grape::Exceptions::UnsupportedGroupType do + subject { described_class.new } + + describe '#message' do + subject { described_class.new.message } + + it { is_expected.to include 'group type must be Array, Hash, JSON or Array[JSON]' } + end + + describe '#alias' do + subject { described_class } + + it { is_expected.to eq(Grape::Exceptions::UnsupportedGroupTypeError) } + end +end diff --git a/spec/grape/validations/params_scope_spec.rb b/spec/grape/validations/params_scope_spec.rb index cba61b17d..20f03f593 100644 --- a/spec/grape/validations/params_scope_spec.rb +++ b/spec/grape/validations/params_scope_spec.rb @@ -256,7 +256,7 @@ def initialize(value) requires :b end end - end.to raise_error Grape::Exceptions::MissingGroupTypeError + end.to raise_error Grape::Exceptions::MissingGroupType expect do subject.params do @@ -264,7 +264,7 @@ def initialize(value) requires :b end end - end.to raise_error Grape::Exceptions::MissingGroupTypeError + end.to raise_error Grape::Exceptions::MissingGroupType end it 'allows Hash as type' do @@ -324,7 +324,7 @@ def initialize(value) requires :b end end - end.to raise_error Grape::Exceptions::UnsupportedGroupTypeError + end.to raise_error Grape::Exceptions::UnsupportedGroupType expect do subject.params do @@ -332,7 +332,7 @@ def initialize(value) requires :b end end - end.to raise_error Grape::Exceptions::UnsupportedGroupTypeError + end.to raise_error Grape::Exceptions::UnsupportedGroupType end end From 9687a88bafe8e75547f1fe5cb19e626b429ce475 Mon Sep 17 00:00:00 2001 From: dm1try Date: Fri, 18 Feb 2022 02:24:50 +0300 Subject: [PATCH 103/304] fix a breaking change provided in 1.6.1 ref #2230 #2200 --- CHANGELOG.md | 1 + lib/grape.rb | 1 + lib/grape/validations/validators/base.rb | 7 ++++ spec/grape/api/custom_validations_spec.rb | 41 +++++++++++++++++++++++ 4 files changed, 50 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3c9f6347..daff0878c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ * [#2229](https://github.com/ruby-grape/grape/pull/2229): Do not collect params in route settings - [@dnesteryuk](https://github.com/dnesteryuk). * [#2234](https://github.com/ruby-grape/grape/pull/2234): Remove non-utf-8 characters from format before generating JSON error - [@bschmeck](https://github.com/bschmeck). * [#2227](https://github.com/ruby-grape/grape/pull/2222): Rename "MissingGroupType" and "UnsupportedGroupType" exceptions - [@ericproulx](https://github.com/ericproulx). +* [#2244](https://github.com/ruby-grape/grape/pull/2244): Fix a breaking change in `Grape::Validations` provided in 1.6.1 - [@dm1try](https://github.com/dm1try). * Your contribution here. ### 1.6.2 (2021/12/30) diff --git a/lib/grape.rb b/lib/grape.rb index 7aedc07db..57e1281f4 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -231,6 +231,7 @@ module Validations autoload :Types autoload :ParamsScope autoload :ValidatorFactory + autoload :Base, 'grape/validations/validators/base' end module Types diff --git a/lib/grape/validations/validators/base.rb b/lib/grape/validations/validators/base.rb index aaa06dc37..2c8403bbc 100644 --- a/lib/grape/validations/validators/base.rb +++ b/lib/grape/validations/validators/base.rb @@ -93,3 +93,10 @@ def fail_fast? end end end + +Grape::Validations::Base = Class.new(Grape::Validations::Validators::Base) do + def initialize(*) + super + warn '[DEPRECATION] `Grape::Validations::Base` is deprecated. Use `Grape::Validations::Validators::Base` instead.' + end +end diff --git a/spec/grape/api/custom_validations_spec.rb b/spec/grape/api/custom_validations_spec.rb index 75c8b8641..25879fc57 100644 --- a/spec/grape/api/custom_validations_spec.rb +++ b/spec/grape/api/custom_validations_spec.rb @@ -1,6 +1,47 @@ # frozen_string_literal: true describe Grape::Validations do + context 'deprecated Grape::Validations::Base' do + subject do + Class.new(Grape::API) do + params do + requires :text, validator_with_old_base: true + end + get do + end + end + end + + let(:validator_with_old_base) do + Class.new(Grape::Validations::Base) do + def validate_param!(_attr_name, _params) + true + end + end + end + + before do + described_class.register_validator('validator_with_old_base', validator_with_old_base) + allow(Warning).to receive(:warn) + end + + after do + described_class.deregister_validator('validator_with_old_base') + end + + def app + subject + end + + it 'puts a deprecation warning' do + expect(Warning).to receive(:warn) do |message| + expect(message).to include('`Grape::Validations::Base` is deprecated') + end + + get '/' + end + end + context 'using a custom length validator' do subject do Class.new(Grape::API) do From 889acfcbca1f667712ff258941eb6a236fdd893c Mon Sep 17 00:00:00 2001 From: dblock Date: Sat, 26 Feb 2022 17:34:16 -0700 Subject: [PATCH 104/304] Make rack.edge optional in CI. --- .github/workflows/test.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4db5bbd23..b5862c60d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,7 +34,6 @@ jobs: - 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 @@ -69,7 +68,10 @@ jobs: experimental: false - ruby: 2.7 gemfile: 'gemfiles/rails_edge.gemfile' - experimental: false + experimental: true + - ruby: 2.7 + gemfile: 'gemfiles/rack_edge.gemfile' + experimental: true - ruby: "ruby-head" experimental: true - ruby: "truffleruby-head" From 72dd21c52d683c554ff08a22d53d25a7087299a0 Mon Sep 17 00:00:00 2001 From: dblock Date: Sat, 26 Feb 2022 17:46:40 -0700 Subject: [PATCH 105/304] Lock RSpec at 3.9.0, receive(:hash).with(args) misbehaves. --- Gemfile | 2 +- gemfiles/multi_json.gemfile | 2 +- gemfiles/multi_xml.gemfile | 2 +- gemfiles/rack1.gemfile | 2 +- gemfiles/rack2.gemfile | 2 +- gemfiles/rack2_2.gemfile | 2 +- gemfiles/rack_edge.gemfile | 2 +- gemfiles/rails_5.gemfile | 2 +- gemfiles/rails_6.gemfile | 2 +- gemfiles/rails_6_1.gemfile | 2 +- gemfiles/rails_7.gemfile | 2 +- gemfiles/rails_edge.gemfile | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Gemfile b/Gemfile index ee9970adf..817c480f0 100644 --- a/Gemfile +++ b/Gemfile @@ -33,7 +33,7 @@ group :test do gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '~> 1.1.0' - gem 'rspec', '~> 3.0' + gem 'rspec', '~> 3.9.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'test-prof', require: false end diff --git a/gemfiles/multi_json.gemfile b/gemfiles/multi_json.gemfile index e14434a60..dd4b9d665 100644 --- a/gemfiles/multi_json.gemfile +++ b/gemfiles/multi_json.gemfile @@ -33,7 +33,7 @@ group :test do gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '~> 1.1.0' - gem 'rspec', '~> 3.0' + gem 'rspec', '~> 3.9.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'test-prof', require: false end diff --git a/gemfiles/multi_xml.gemfile b/gemfiles/multi_xml.gemfile index 6d7593f05..b9cc8d1fc 100644 --- a/gemfiles/multi_xml.gemfile +++ b/gemfiles/multi_xml.gemfile @@ -33,7 +33,7 @@ group :test do gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '~> 1.1.0' - gem 'rspec', '~> 3.0' + gem 'rspec', '~> 3.9.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'test-prof', require: false end diff --git a/gemfiles/rack1.gemfile b/gemfiles/rack1.gemfile index 688c9866a..b131bcb9c 100644 --- a/gemfiles/rack1.gemfile +++ b/gemfiles/rack1.gemfile @@ -33,7 +33,7 @@ group :test do gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '~> 1.1.0' - gem 'rspec', '~> 3.0' + gem 'rspec', '~> 3.9.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'test-prof', require: false end diff --git a/gemfiles/rack2.gemfile b/gemfiles/rack2.gemfile index d48a8bb0e..9f76c853d 100644 --- a/gemfiles/rack2.gemfile +++ b/gemfiles/rack2.gemfile @@ -33,7 +33,7 @@ group :test do gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '~> 1.1.0' - gem 'rspec', '~> 3.0' + gem 'rspec', '~> 3.9.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'test-prof', require: false end diff --git a/gemfiles/rack2_2.gemfile b/gemfiles/rack2_2.gemfile index 78a013c03..a6770c2a5 100644 --- a/gemfiles/rack2_2.gemfile +++ b/gemfiles/rack2_2.gemfile @@ -33,7 +33,7 @@ group :test do gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '~> 1.1.0' - gem 'rspec', '~> 3.0' + gem 'rspec', '~> 3.9.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'test-prof', require: false end diff --git a/gemfiles/rack_edge.gemfile b/gemfiles/rack_edge.gemfile index 91ad3bdef..c2c1a11e0 100644 --- a/gemfiles/rack_edge.gemfile +++ b/gemfiles/rack_edge.gemfile @@ -33,7 +33,7 @@ group :test do gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '~> 1.1.0' - gem 'rspec', '~> 3.0' + gem 'rspec', '~> 3.9.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'test-prof', require: false end diff --git a/gemfiles/rails_5.gemfile b/gemfiles/rails_5.gemfile index 330213fba..3dfffb200 100644 --- a/gemfiles/rails_5.gemfile +++ b/gemfiles/rails_5.gemfile @@ -33,7 +33,7 @@ group :test do gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '~> 1.1.0' - gem 'rspec', '~> 3.0' + gem 'rspec', '~> 3.9.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'test-prof', require: false end diff --git a/gemfiles/rails_6.gemfile b/gemfiles/rails_6.gemfile index 27efb6cbf..f3569fa44 100644 --- a/gemfiles/rails_6.gemfile +++ b/gemfiles/rails_6.gemfile @@ -33,7 +33,7 @@ group :test do gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '~> 1.1.0' - gem 'rspec', '~> 3.0' + gem 'rspec', '~> 3.9.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'test-prof', require: false end diff --git a/gemfiles/rails_6_1.gemfile b/gemfiles/rails_6_1.gemfile index bbb35078f..fe695232d 100644 --- a/gemfiles/rails_6_1.gemfile +++ b/gemfiles/rails_6_1.gemfile @@ -33,7 +33,7 @@ group :test do gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '~> 1.1.0' - gem 'rspec', '~> 3.0' + gem 'rspec', '~> 3.9.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'test-prof', require: false end diff --git a/gemfiles/rails_7.gemfile b/gemfiles/rails_7.gemfile index 5dd4290d9..eb4f037ac 100644 --- a/gemfiles/rails_7.gemfile +++ b/gemfiles/rails_7.gemfile @@ -33,7 +33,7 @@ group :test do gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '~> 1.1.0' - gem 'rspec', '~> 3.0' + gem 'rspec', '~> 3.9.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'test-prof', require: false end diff --git a/gemfiles/rails_edge.gemfile b/gemfiles/rails_edge.gemfile index ca650a61a..f2d8584af 100644 --- a/gemfiles/rails_edge.gemfile +++ b/gemfiles/rails_edge.gemfile @@ -33,7 +33,7 @@ group :test do gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '~> 1.1.0' - gem 'rspec', '~> 3.0' + gem 'rspec', '~> 3.9.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'test-prof', require: false end From 76fa2ffe5816c7ac50aa8ed6ad89a74cd399fca4 Mon Sep 17 00:00:00 2001 From: dblock Date: Sun, 27 Feb 2022 11:21:24 -0500 Subject: [PATCH 106/304] Upgraded to rspec 3.11.0. --- CHANGELOG.md | 1 + Gemfile | 2 +- gemfiles/multi_json.gemfile | 2 +- gemfiles/multi_xml.gemfile | 2 +- gemfiles/rack1.gemfile | 2 +- gemfiles/rack2.gemfile | 2 +- gemfiles/rack2_2.gemfile | 2 +- gemfiles/rack_edge.gemfile | 2 +- gemfiles/rails_5.gemfile | 2 +- gemfiles/rails_6.gemfile | 2 +- gemfiles/rails_6_1.gemfile | 2 +- gemfiles/rails_7.gemfile | 2 +- gemfiles/rails_edge.gemfile | 2 +- spec/grape/dsl/request_response_spec.rb | 12 ++++++------ spec/grape/dsl/routing_spec.rb | 2 +- 15 files changed, 20 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index daff0878c..fac46396c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * [#2233](https://github.com/ruby-grape/grape/pull/2233): A setting for disabling documentation to internal APIs - [@dnesteryuk](https://github.com/dnesteryuk). * [#2235](https://github.com/ruby-grape/grape/pull/2235): Add Ruby 3.1 to CI - [@petergoldstein](https://github.com/petergoldstein). +* [#2248](https://github.com/ruby-grape/grape/pull/2248): Upgraded to rspec 3.11.0 - [@dblock](https://github.com/dblock). * Your contribution here. #### Fixes diff --git a/Gemfile b/Gemfile index 817c480f0..fd454c5c1 100644 --- a/Gemfile +++ b/Gemfile @@ -33,7 +33,7 @@ group :test do gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '~> 1.1.0' - gem 'rspec', '~> 3.9.0' + gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'test-prof', require: false end diff --git a/gemfiles/multi_json.gemfile b/gemfiles/multi_json.gemfile index dd4b9d665..41d0bb577 100644 --- a/gemfiles/multi_json.gemfile +++ b/gemfiles/multi_json.gemfile @@ -33,7 +33,7 @@ group :test do gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '~> 1.1.0' - gem 'rspec', '~> 3.9.0' + gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'test-prof', require: false end diff --git a/gemfiles/multi_xml.gemfile b/gemfiles/multi_xml.gemfile index b9cc8d1fc..0326db003 100644 --- a/gemfiles/multi_xml.gemfile +++ b/gemfiles/multi_xml.gemfile @@ -33,7 +33,7 @@ group :test do gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '~> 1.1.0' - gem 'rspec', '~> 3.9.0' + gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'test-prof', require: false end diff --git a/gemfiles/rack1.gemfile b/gemfiles/rack1.gemfile index b131bcb9c..18e9ef895 100644 --- a/gemfiles/rack1.gemfile +++ b/gemfiles/rack1.gemfile @@ -33,7 +33,7 @@ group :test do gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '~> 1.1.0' - gem 'rspec', '~> 3.9.0' + gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'test-prof', require: false end diff --git a/gemfiles/rack2.gemfile b/gemfiles/rack2.gemfile index 9f76c853d..5605f6979 100644 --- a/gemfiles/rack2.gemfile +++ b/gemfiles/rack2.gemfile @@ -33,7 +33,7 @@ group :test do gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '~> 1.1.0' - gem 'rspec', '~> 3.9.0' + gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'test-prof', require: false end diff --git a/gemfiles/rack2_2.gemfile b/gemfiles/rack2_2.gemfile index a6770c2a5..f28d30eb2 100644 --- a/gemfiles/rack2_2.gemfile +++ b/gemfiles/rack2_2.gemfile @@ -33,7 +33,7 @@ group :test do gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '~> 1.1.0' - gem 'rspec', '~> 3.9.0' + gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'test-prof', require: false end diff --git a/gemfiles/rack_edge.gemfile b/gemfiles/rack_edge.gemfile index c2c1a11e0..06294aba7 100644 --- a/gemfiles/rack_edge.gemfile +++ b/gemfiles/rack_edge.gemfile @@ -33,7 +33,7 @@ group :test do gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '~> 1.1.0' - gem 'rspec', '~> 3.9.0' + gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'test-prof', require: false end diff --git a/gemfiles/rails_5.gemfile b/gemfiles/rails_5.gemfile index 3dfffb200..b28b0b140 100644 --- a/gemfiles/rails_5.gemfile +++ b/gemfiles/rails_5.gemfile @@ -33,7 +33,7 @@ group :test do gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '~> 1.1.0' - gem 'rspec', '~> 3.9.0' + gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'test-prof', require: false end diff --git a/gemfiles/rails_6.gemfile b/gemfiles/rails_6.gemfile index f3569fa44..c910592fe 100644 --- a/gemfiles/rails_6.gemfile +++ b/gemfiles/rails_6.gemfile @@ -33,7 +33,7 @@ group :test do gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '~> 1.1.0' - gem 'rspec', '~> 3.9.0' + gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'test-prof', require: false end diff --git a/gemfiles/rails_6_1.gemfile b/gemfiles/rails_6_1.gemfile index fe695232d..11b8eb07e 100644 --- a/gemfiles/rails_6_1.gemfile +++ b/gemfiles/rails_6_1.gemfile @@ -33,7 +33,7 @@ group :test do gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '~> 1.1.0' - gem 'rspec', '~> 3.9.0' + gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'test-prof', require: false end diff --git a/gemfiles/rails_7.gemfile b/gemfiles/rails_7.gemfile index eb4f037ac..411424527 100644 --- a/gemfiles/rails_7.gemfile +++ b/gemfiles/rails_7.gemfile @@ -33,7 +33,7 @@ group :test do gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '~> 1.1.0' - gem 'rspec', '~> 3.9.0' + gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'test-prof', require: false end diff --git a/gemfiles/rails_edge.gemfile b/gemfiles/rails_edge.gemfile index f2d8584af..69ab1ee2f 100644 --- a/gemfiles/rails_edge.gemfile +++ b/gemfiles/rails_edge.gemfile @@ -33,7 +33,7 @@ group :test do gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '~> 1.1.0' - gem 'rspec', '~> 3.9.0' + gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'test-prof', require: false end diff --git a/spec/grape/dsl/request_response_spec.rb b/spec/grape/dsl/request_response_spec.rb index e2fb5f798..8546ace00 100644 --- a/spec/grape/dsl/request_response_spec.rb +++ b/spec/grape/dsl/request_response_spec.rb @@ -160,34 +160,34 @@ def self.imbue(key, value) describe 'list of exceptions is passed' do it 'sets hash of exceptions as rescue handlers' do - expect(subject).to receive(:namespace_reverse_stackable).with(:rescue_handlers, StandardError => nil) + expect(subject).to receive(:namespace_reverse_stackable).with(:rescue_handlers, { StandardError => nil }) expect(subject).to receive(:namespace_stackable).with(:rescue_options, {}) subject.rescue_from StandardError end it 'rescues only base handlers if rescue_subclasses: false option is passed' do - expect(subject).to receive(:namespace_reverse_stackable).with(:base_only_rescue_handlers, StandardError => nil) - expect(subject).to receive(:namespace_stackable).with(:rescue_options, rescue_subclasses: false) + expect(subject).to receive(:namespace_reverse_stackable).with(:base_only_rescue_handlers, { StandardError => nil }) + expect(subject).to receive(:namespace_stackable).with(:rescue_options, { rescue_subclasses: false }) subject.rescue_from StandardError, rescue_subclasses: false end it 'sets given proc as rescue handler for each key in hash' do rescue_handler_proc = proc {} - expect(subject).to receive(:namespace_reverse_stackable).with(:rescue_handlers, StandardError => rescue_handler_proc) + expect(subject).to receive(:namespace_reverse_stackable).with(:rescue_handlers, { StandardError => rescue_handler_proc }) expect(subject).to receive(:namespace_stackable).with(:rescue_options, {}) subject.rescue_from StandardError, rescue_handler_proc end it 'sets given block as rescue handler for each key in hash' do rescue_handler_proc = proc {} - expect(subject).to receive(:namespace_reverse_stackable).with(:rescue_handlers, StandardError => rescue_handler_proc) + expect(subject).to receive(:namespace_reverse_stackable).with(:rescue_handlers, { StandardError => rescue_handler_proc }) expect(subject).to receive(:namespace_stackable).with(:rescue_options, {}) subject.rescue_from StandardError, &rescue_handler_proc end it 'sets a rescue handler declared through :with option for each key in hash' do with_block = -> { 'hello' } - expect(subject).to receive(:namespace_reverse_stackable).with(:rescue_handlers, StandardError => an_instance_of(Proc)) + expect(subject).to receive(:namespace_reverse_stackable).with(:rescue_handlers, { StandardError => an_instance_of(Proc) }) expect(subject).to receive(:namespace_stackable).with(:rescue_options, {}) subject.rescue_from StandardError, with: with_block end diff --git a/spec/grape/dsl/routing_spec.rb b/spec/grape/dsl/routing_spec.rb index 0695e4c45..8812b05bd 100644 --- a/spec/grape/dsl/routing_spec.rb +++ b/spec/grape/dsl/routing_spec.rb @@ -19,7 +19,7 @@ class Dummy it 'sets a version for route' do version = 'v1' expect(subject).to receive(:namespace_inheritable).with(:version, [version]) - expect(subject).to receive(:namespace_inheritable).with(:version_options, using: :path) + expect(subject).to receive(:namespace_inheritable).with(:version_options, { using: :path }) expect(subject.version(version)).to eq(version) end end From 6089469eb26e31e71c1934dba03b52be82502e70 Mon Sep 17 00:00:00 2001 From: dblock Date: Sun, 27 Feb 2022 11:32:31 -0500 Subject: [PATCH 107/304] Split CI matrix, extract edge. --- .github/workflows/edge.yml | 35 +++++++++++++++++++++++++++++++++++ .github/workflows/test.yml | 23 ----------------------- CHANGELOG.md | 1 + 3 files changed, 36 insertions(+), 23 deletions(-) create mode 100644 .github/workflows/edge.yml diff --git a/.github/workflows/edge.yml b/.github/workflows/edge.yml new file mode 100644 index 000000000..795873aba --- /dev/null +++ b/.github/workflows/edge.yml @@ -0,0 +1,35 @@ +--- +name: edge +on: + pull_request: + branches: + - "*" +jobs: + test: + strategy: + fail-fast: false + matrix: + include: + - ruby: 2.7 + gemfile: 'gemfiles/rails_edge.gemfile' + - ruby: 2.7 + gemfile: 'gemfiles/rack_edge.gemfile' + - ruby: "ruby-head" + - ruby: "truffleruby-head" + - ruby: "jruby-head" + runs-on: ubuntu-20.04 + continue-on-error: true + 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 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b5862c60d..324b2a4f1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -37,49 +37,26 @@ jobs: - gemfiles/rails_5.gemfile - gemfiles/rails_6.gemfile - gemfiles/rails_6_1.gemfile - experimental: [false] include: - ruby: 3.1 gemfile: 'gemfiles/multi_json.gemfile' - experimental: false - ruby: 3.1 gemfile: 'gemfiles/multi_xml.gemfile' - experimental: false - ruby: 3.1 gemfile: 'gemfiles/rails_7.gemfile' - experimental: false - ruby: "3.0" gemfile: 'gemfiles/multi_json.gemfile' - experimental: false - ruby: "3.0" gemfile: 'gemfiles/multi_xml.gemfile' - experimental: false - ruby: "3.0" gemfile: 'gemfiles/rails_7.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_7.gemfile' - experimental: false - - ruby: 2.7 - gemfile: 'gemfiles/rails_edge.gemfile' - experimental: true - - ruby: 2.7 - gemfile: 'gemfiles/rack_edge.gemfile' - experimental: true - - 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 }} diff --git a/CHANGELOG.md b/CHANGELOG.md index fac46396c..839a20635 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * [#2233](https://github.com/ruby-grape/grape/pull/2233): A setting for disabling documentation to internal APIs - [@dnesteryuk](https://github.com/dnesteryuk). * [#2235](https://github.com/ruby-grape/grape/pull/2235): Add Ruby 3.1 to CI - [@petergoldstein](https://github.com/petergoldstein). * [#2248](https://github.com/ruby-grape/grape/pull/2248): Upgraded to rspec 3.11.0 - [@dblock](https://github.com/dblock). +* [#2249](https://github.com/ruby-grape/grape/pull/2249): Split ci matrix, extract edge - [@dblock](https://github.com/dblock). * Your contribution here. #### Fixes From 34cff2cf2155f8d0d982b14ea446ed6acbf65078 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Mon, 28 Feb 2022 16:05:58 +0100 Subject: [PATCH 108/304] Add deprecation warning for UnsupportedGroupTypeError And MissingGroupTypeError (#2250) * Add deprecation warning for UnsupportedGroupTypeError And MissingGroupTypeError * Add CHANGELOG.md --- CHANGELOG.md | 1 + lib/grape.rb | 2 ++ lib/grape/exceptions/missing_group_type.rb | 7 ++++++- lib/grape/exceptions/unsupported_group_type.rb | 7 ++++++- spec/grape/exceptions/missing_group_type_spec.rb | 12 +++++++++--- spec/grape/exceptions/unsupported_group_type_spec.rb | 12 +++++++++--- 6 files changed, 33 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 839a20635..30032bec9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ * [#2234](https://github.com/ruby-grape/grape/pull/2234): Remove non-utf-8 characters from format before generating JSON error - [@bschmeck](https://github.com/bschmeck). * [#2227](https://github.com/ruby-grape/grape/pull/2222): Rename "MissingGroupType" and "UnsupportedGroupType" exceptions - [@ericproulx](https://github.com/ericproulx). * [#2244](https://github.com/ruby-grape/grape/pull/2244): Fix a breaking change in `Grape::Validations` provided in 1.6.1 - [@dm1try](https://github.com/dm1try). +* [#2250](https://github.com/ruby-grape/grape/pull/2250): Add deprecation warning for unsupportedgrouptypeerror and missinggrouptypeerror - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 1.6.2 (2021/12/30) diff --git a/lib/grape.rb b/lib/grape.rb index 57e1281f4..49b3ec403 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -81,6 +81,8 @@ module Exceptions autoload :MethodNotAllowed autoload :InvalidResponse autoload :EmptyMessageBody + autoload :MissingGroupTypeError, 'grape/exceptions/missing_group_type' + autoload :UnsupportedGroupTypeError, 'grape/exceptions/unsupported_group_type' end end diff --git a/lib/grape/exceptions/missing_group_type.rb b/lib/grape/exceptions/missing_group_type.rb index d400424b4..48ef996ab 100644 --- a/lib/grape/exceptions/missing_group_type.rb +++ b/lib/grape/exceptions/missing_group_type.rb @@ -10,4 +10,9 @@ def initialize end end -Grape::Exceptions::MissingGroupTypeError = Grape::Exceptions::MissingGroupType +Grape::Exceptions::MissingGroupTypeError = Class.new(Grape::Exceptions::MissingGroupType) do + def initialize(*) + super + warn '[DEPRECATION] `Grape::Exceptions::MissingGroupTypeError` is deprecated. Use `Grape::Exceptions::MissingGroupType` instead.' + end +end diff --git a/lib/grape/exceptions/unsupported_group_type.rb b/lib/grape/exceptions/unsupported_group_type.rb index 3fb6160b7..5d845aad6 100644 --- a/lib/grape/exceptions/unsupported_group_type.rb +++ b/lib/grape/exceptions/unsupported_group_type.rb @@ -10,4 +10,9 @@ def initialize end end -Grape::Exceptions::UnsupportedGroupTypeError = Grape::Exceptions::UnsupportedGroupType +Grape::Exceptions::UnsupportedGroupTypeError = Class.new(Grape::Exceptions::UnsupportedGroupType) do + def initialize(*) + super + warn '[DEPRECATION] `Grape::Exceptions::UnsupportedGroupTypeError` is deprecated. Use `Grape::Exceptions::UnsupportedGroupType` instead.' + end +end diff --git a/spec/grape/exceptions/missing_group_type_spec.rb b/spec/grape/exceptions/missing_group_type_spec.rb index 509f823f2..b6612882f 100644 --- a/spec/grape/exceptions/missing_group_type_spec.rb +++ b/spec/grape/exceptions/missing_group_type_spec.rb @@ -7,9 +7,15 @@ it { is_expected.to include 'group type is required' } end - describe '#alias' do - subject { described_class } + describe 'deprecated Grape::Exceptions::MissingGroupTypeError' do + subject { Grape::Exceptions::MissingGroupTypeError.new } - it { is_expected.to eq(Grape::Exceptions::MissingGroupTypeError) } + it 'puts a deprecation warning' do + expect(Warning).to receive(:warn) do |message| + expect(message).to include('`Grape::Exceptions::MissingGroupTypeError` is deprecated') + end + + subject + end end end diff --git a/spec/grape/exceptions/unsupported_group_type_spec.rb b/spec/grape/exceptions/unsupported_group_type_spec.rb index 167755aef..ee1451aaa 100644 --- a/spec/grape/exceptions/unsupported_group_type_spec.rb +++ b/spec/grape/exceptions/unsupported_group_type_spec.rb @@ -9,9 +9,15 @@ it { is_expected.to include 'group type must be Array, Hash, JSON or Array[JSON]' } end - describe '#alias' do - subject { described_class } + describe 'deprecated Grape::Exceptions::UnsupportedGroupTypeError' do + subject { Grape::Exceptions::UnsupportedGroupTypeError.new } - it { is_expected.to eq(Grape::Exceptions::UnsupportedGroupTypeError) } + it 'puts a deprecation warning' do + expect(Warning).to receive(:warn) do |message| + expect(message).to include('`Grape::Exceptions::UnsupportedGroupTypeError` is deprecated') + end + + subject + end end end From c72f0f2f57292b4609f421d9114bffcc1304aca8 Mon Sep 17 00:00:00 2001 From: dblock Date: Mon, 28 Feb 2022 10:08:34 -0500 Subject: [PATCH 109/304] Upgraded RuboCop to 1.25.1. --- CHANGELOG.md | 13 ++++++------ Gemfile | 4 ++-- gemfiles/multi_json.gemfile | 6 ++---- gemfiles/multi_xml.gemfile | 6 ++---- gemfiles/rack1.gemfile | 6 ++---- gemfiles/rack2.gemfile | 6 ++---- gemfiles/rack2_2.gemfile | 4 ++-- gemfiles/rack_edge.gemfile | 6 ++---- gemfiles/rails_5.gemfile | 6 ++---- gemfiles/rails_6.gemfile | 6 ++---- gemfiles/rails_6_1.gemfile | 6 ++---- gemfiles/rails_7.gemfile | 6 ++---- gemfiles/rails_edge.gemfile | 6 ++---- lib/grape/api/instance.rb | 2 +- spec/grape/api_spec.rb | 6 +++--- spec/grape/dsl/headers_spec.rb | 4 ++-- spec/grape/dsl/inside_route_spec.rb | 20 +++++++++---------- spec/grape/endpoint/declared_spec.rb | 4 ++-- spec/grape/exceptions/validation_spec.rb | 2 +- .../grape/middleware/versioner/header_spec.rb | 4 ++-- spec/grape/validations/attributes_doc_spec.rb | 2 +- .../types/primitive_coercer_spec.rb | 6 +++--- 22 files changed, 56 insertions(+), 75 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30032bec9..10544bd24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,11 @@ #### Features -* [#2233](https://github.com/ruby-grape/grape/pull/2233): A setting for disabling documentation to internal APIs - [@dnesteryuk](https://github.com/dnesteryuk). -* [#2235](https://github.com/ruby-grape/grape/pull/2235): Add Ruby 3.1 to CI - [@petergoldstein](https://github.com/petergoldstein). +* [#2233](https://github.com/ruby-grape/grape/pull/2233): Added `do_not_document!` for disabling documentation to internal APIs - [@dnesteryuk](https://github.com/dnesteryuk). +* [#2235](https://github.com/ruby-grape/grape/pull/2235): Add support for Ruby 3.1 - [@petergoldstein](https://github.com/petergoldstein). * [#2248](https://github.com/ruby-grape/grape/pull/2248): Upgraded to rspec 3.11.0 - [@dblock](https://github.com/dblock). -* [#2249](https://github.com/ruby-grape/grape/pull/2249): Split ci matrix, extract edge - [@dblock](https://github.com/dblock). +* [#2249](https://github.com/ruby-grape/grape/pull/2249): Split CI matrix, extract edge - [@dblock](https://github.com/dblock). +* [#2249](https://github.com/ruby-grape/grape/pull/2251): Upgraded to RuboCop 1.25.1 - [@dblock](https://github.com/dblock). * Your contribution here. #### Fixes @@ -13,10 +14,10 @@ * [#2222](https://github.com/ruby-grape/grape/pull/2222): Autoload types and validators - [@ericproulx](https://github.com/ericproulx). * [#2232](https://github.com/ruby-grape/grape/pull/2232): Fix kwargs support in shared params definition - [@dm1try](https://github.com/dm1try). * [#2229](https://github.com/ruby-grape/grape/pull/2229): Do not collect params in route settings - [@dnesteryuk](https://github.com/dnesteryuk). -* [#2234](https://github.com/ruby-grape/grape/pull/2234): Remove non-utf-8 characters from format before generating JSON error - [@bschmeck](https://github.com/bschmeck). -* [#2227](https://github.com/ruby-grape/grape/pull/2222): Rename "MissingGroupType" and "UnsupportedGroupType" exceptions - [@ericproulx](https://github.com/ericproulx). +* [#2234](https://github.com/ruby-grape/grape/pull/2234): Remove non-UTF8 characters from format before generating JSON error - [@bschmeck](https://github.com/bschmeck). +* [#2227](https://github.com/ruby-grape/grape/pull/2222): Rename `MissingGroupType` and `UnsupportedGroupType` exceptions - [@ericproulx](https://github.com/ericproulx). * [#2244](https://github.com/ruby-grape/grape/pull/2244): Fix a breaking change in `Grape::Validations` provided in 1.6.1 - [@dm1try](https://github.com/dm1try). -* [#2250](https://github.com/ruby-grape/grape/pull/2250): Add deprecation warning for unsupportedgrouptypeerror and missinggrouptypeerror - [@ericproulx](https://github.com/ericproulx). +* [#2250](https://github.com/ruby-grape/grape/pull/2250): Add deprecation warning for `UnsupportedGroupTypeError` and `MissingGroupTypeError` - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 1.6.2 (2021/12/30) diff --git a/Gemfile b/Gemfile index fd454c5c1..3f7f3b1b8 100644 --- a/Gemfile +++ b/Gemfile @@ -10,8 +10,8 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '~> 1.23.0' - gem 'rubocop-ast', '~> 1.14.0' + gem 'rubocop', '1.25.1' + gem 'rubocop-ast' gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false end diff --git a/gemfiles/multi_json.gemfile b/gemfiles/multi_json.gemfile index 41d0bb577..ac46d9c70 100644 --- a/gemfiles/multi_json.gemfile +++ b/gemfiles/multi_json.gemfile @@ -1,5 +1,3 @@ -# frozen_string_literal: true - # This file was generated by Appraisal source 'https://rubygems.org' @@ -10,8 +8,8 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '~> 1.23.0' - gem 'rubocop-ast', '~> 1.14.0' + gem 'rubocop', '1.25.1' + gem 'rubocop-ast' gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false end diff --git a/gemfiles/multi_xml.gemfile b/gemfiles/multi_xml.gemfile index 0326db003..e8000c52e 100644 --- a/gemfiles/multi_xml.gemfile +++ b/gemfiles/multi_xml.gemfile @@ -1,5 +1,3 @@ -# frozen_string_literal: true - # This file was generated by Appraisal source 'https://rubygems.org' @@ -10,8 +8,8 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '~> 1.23.0' - gem 'rubocop-ast', '~> 1.14.0' + gem 'rubocop', '1.25.1' + gem 'rubocop-ast' gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false end diff --git a/gemfiles/rack1.gemfile b/gemfiles/rack1.gemfile index 18e9ef895..1cf94bf23 100644 --- a/gemfiles/rack1.gemfile +++ b/gemfiles/rack1.gemfile @@ -1,5 +1,3 @@ -# frozen_string_literal: true - # This file was generated by Appraisal source 'https://rubygems.org' @@ -10,8 +8,8 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '~> 1.23.0' - gem 'rubocop-ast', '~> 1.14.0' + gem 'rubocop', '1.25.1' + gem 'rubocop-ast' gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false end diff --git a/gemfiles/rack2.gemfile b/gemfiles/rack2.gemfile index 5605f6979..7016964f9 100644 --- a/gemfiles/rack2.gemfile +++ b/gemfiles/rack2.gemfile @@ -1,5 +1,3 @@ -# frozen_string_literal: true - # This file was generated by Appraisal source 'https://rubygems.org' @@ -10,8 +8,8 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '~> 1.23.0' - gem 'rubocop-ast', '~> 1.14.0' + gem 'rubocop', '1.25.1' + gem 'rubocop-ast' gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false end diff --git a/gemfiles/rack2_2.gemfile b/gemfiles/rack2_2.gemfile index f28d30eb2..1a865a87d 100644 --- a/gemfiles/rack2_2.gemfile +++ b/gemfiles/rack2_2.gemfile @@ -10,8 +10,8 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '~> 1.23.0' - gem 'rubocop-ast', '~> 1.14.0' + gem 'rubocop', '1.25.1' + gem 'rubocop-ast' gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false end diff --git a/gemfiles/rack_edge.gemfile b/gemfiles/rack_edge.gemfile index 06294aba7..a3174e87a 100644 --- a/gemfiles/rack_edge.gemfile +++ b/gemfiles/rack_edge.gemfile @@ -1,5 +1,3 @@ -# frozen_string_literal: true - # This file was generated by Appraisal source 'https://rubygems.org' @@ -10,8 +8,8 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '~> 1.23.0' - gem 'rubocop-ast', '~> 1.14.0' + gem 'rubocop', '1.25.1' + gem 'rubocop-ast' gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false end diff --git a/gemfiles/rails_5.gemfile b/gemfiles/rails_5.gemfile index b28b0b140..e3de8efdf 100644 --- a/gemfiles/rails_5.gemfile +++ b/gemfiles/rails_5.gemfile @@ -1,5 +1,3 @@ -# frozen_string_literal: true - # This file was generated by Appraisal source 'https://rubygems.org' @@ -10,8 +8,8 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '~> 1.23.0' - gem 'rubocop-ast', '~> 1.14.0' + gem 'rubocop', '1.25.1' + gem 'rubocop-ast' gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false end diff --git a/gemfiles/rails_6.gemfile b/gemfiles/rails_6.gemfile index c910592fe..f2eb38db4 100644 --- a/gemfiles/rails_6.gemfile +++ b/gemfiles/rails_6.gemfile @@ -1,5 +1,3 @@ -# frozen_string_literal: true - # This file was generated by Appraisal source 'https://rubygems.org' @@ -10,8 +8,8 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '~> 1.23.0' - gem 'rubocop-ast', '~> 1.14.0' + gem 'rubocop', '1.25.1' + gem 'rubocop-ast' gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false end diff --git a/gemfiles/rails_6_1.gemfile b/gemfiles/rails_6_1.gemfile index 11b8eb07e..d3a52b70a 100644 --- a/gemfiles/rails_6_1.gemfile +++ b/gemfiles/rails_6_1.gemfile @@ -1,5 +1,3 @@ -# frozen_string_literal: true - # This file was generated by Appraisal source 'https://rubygems.org' @@ -10,8 +8,8 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '~> 1.23.0' - gem 'rubocop-ast', '~> 1.14.0' + gem 'rubocop', '1.25.1' + gem 'rubocop-ast' gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false end diff --git a/gemfiles/rails_7.gemfile b/gemfiles/rails_7.gemfile index 411424527..020830749 100644 --- a/gemfiles/rails_7.gemfile +++ b/gemfiles/rails_7.gemfile @@ -1,5 +1,3 @@ -# frozen_string_literal: true - # This file was generated by Appraisal source 'https://rubygems.org' @@ -10,8 +8,8 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '~> 1.23.0' - gem 'rubocop-ast', '~> 1.14.0' + gem 'rubocop', '1.25.1' + gem 'rubocop-ast' gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false end diff --git a/gemfiles/rails_edge.gemfile b/gemfiles/rails_edge.gemfile index 69ab1ee2f..ba25dba3e 100644 --- a/gemfiles/rails_edge.gemfile +++ b/gemfiles/rails_edge.gemfile @@ -1,5 +1,3 @@ -# frozen_string_literal: true - # This file was generated by Appraisal source 'https://rubygems.org' @@ -10,8 +8,8 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '~> 1.23.0' - gem 'rubocop-ast', '~> 1.14.0' + gem 'rubocop', '1.25.1' + gem 'rubocop-ast' gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false end diff --git a/lib/grape/api/instance.rb b/lib/grape/api/instance.rb index d0dd57734..a326f44c6 100644 --- a/lib/grape/api/instance.rb +++ b/lib/grape/api/instance.rb @@ -101,7 +101,7 @@ def prepare_routes # block passed in. Allows for simple 'before' setups # of settings stack pushes. def nest(*blocks, &block) - blocks.reject!(&:nil?) + blocks.compact! if blocks.any? evaluate_as_instance_with_configuration(block) if block blocks.each { |b| evaluate_as_instance_with_configuration(b) } diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index e518917bb..283dc70a6 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -1216,7 +1216,7 @@ class DummyFormatClass it 'does not set Cache-Control' do get '/foo' - expect(last_response.headers['Cache-Control']).to eq(nil) + expect(last_response.headers['Cache-Control']).to be_nil end it 'sets content type for xml' do @@ -1241,7 +1241,7 @@ class DummyFormatClass it 'returns raw data when content type binary' do image_filename = 'grape.png' - file = File.open(image_filename, 'rb', &:read) + file = File.binread(image_filename) subject.format :binary subject.get('/binary_file') { File.binread(image_filename) } get '/binary_file' @@ -1273,7 +1273,7 @@ class DummyFormatClass 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) + expect(last_response.headers['Content-Length']).to be_nil expect(last_response.headers['Cache-Control']).to eq('no-cache') expect(last_response.headers['Transfer-Encoding']).to eq('chunked') diff --git a/spec/grape/dsl/headers_spec.rb b/spec/grape/dsl/headers_spec.rb index e6b90ebd9..9ced6304f 100644 --- a/spec/grape/dsl/headers_spec.rb +++ b/spec/grape/dsl/headers_spec.rb @@ -52,8 +52,8 @@ class Dummy context 'when no headers are set' do describe '#header' do it 'returns nil' do - expect(subject.header['First Key']).to be nil - expect(subject.header('First Key')).to be nil + expect(subject.header['First Key']).to be_nil + expect(subject.header('First Key')).to be_nil end end end diff --git a/spec/grape/dsl/inside_route_spec.rb b/spec/grape/dsl/inside_route_spec.rb index 31e84a9ac..b6f4aba87 100644 --- a/spec/grape/dsl/inside_route_spec.rb +++ b/spec/grape/dsl/inside_route_spec.rb @@ -23,7 +23,7 @@ def initialize describe '#version' do it 'defaults to nil' do - expect(subject.version).to be nil + expect(subject.version).to be_nil end it 'returns env[api.version]' do @@ -165,7 +165,7 @@ def initialize end it 'returns default' do - expect(subject.content_type).to be nil + expect(subject.content_type).to be_nil end end @@ -198,7 +198,7 @@ def initialize end it 'returns default' do - expect(subject.body).to be nil + expect(subject.body).to be_nil end end @@ -313,7 +313,7 @@ def initialize end it 'returns default' do - expect(subject.sendfile).to be nil + expect(subject.sendfile).to be_nil end end @@ -360,13 +360,13 @@ def initialize it 'sets Content-Length header to nil' do subject.stream file_path - expect(subject.header['Content-Length']).to eq nil + expect(subject.header['Content-Length']).to be_nil end it 'sets Transfer-Encoding header to nil' do subject.stream file_path - expect(subject.header['Transfer-Encoding']).to eq nil + expect(subject.header['Transfer-Encoding']).to be_nil end end @@ -404,13 +404,13 @@ def initialize it 'sets Content-Length header to nil' do subject.stream stream_object - expect(subject.header['Content-Length']).to eq nil + expect(subject.header['Content-Length']).to be_nil end it 'sets Transfer-Encoding header to nil' do subject.stream stream_object - expect(subject.header['Transfer-Encoding']).to eq nil + expect(subject.header['Transfer-Encoding']).to be_nil end end @@ -424,8 +424,8 @@ def initialize end it 'returns default' do - expect(subject.stream).to be nil - expect(subject.header['Cache-Control']).to eq nil + expect(subject.stream).to be_nil + expect(subject.header['Cache-Control']).to be_nil end end diff --git a/spec/grape/endpoint/declared_spec.rb b/spec/grape/endpoint/declared_spec.rb index 5b0614f5c..f4402416d 100644 --- a/spec/grape/endpoint/declared_spec.rb +++ b/spec/grape/endpoint/declared_spec.rb @@ -247,7 +247,7 @@ def app 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 + expect(JSON.parse(last_response.body).key?(:other)).to be false end it 'stringifies if that option is passed' do @@ -520,7 +520,7 @@ def app 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 + expect(json[:declared_params_no_parent][:id]).to be_nil end end diff --git a/spec/grape/exceptions/validation_spec.rb b/spec/grape/exceptions/validation_spec.rb index 1e2515939..767941122 100644 --- a/spec/grape/exceptions/validation_spec.rb +++ b/spec/grape/exceptions/validation_spec.rb @@ -13,7 +13,7 @@ context 'when message is a String' do it 'does not store the message_key' do - expect(described_class.new(params: ['id'], message: 'presence').message_key).to eq(nil) + expect(described_class.new(params: ['id'], message: 'presence').message_key).to be_nil end end end diff --git a/spec/grape/middleware/versioner/header_spec.rb b/spec/grape/middleware/versioner/header_spec.rb index b19ea0449..af1312fc3 100644 --- a/spec/grape/middleware/versioner/header_spec.rb +++ b/spec/grape/middleware/versioner/header_spec.rb @@ -46,7 +46,7 @@ it 'is nil if not provided' do status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor') - expect(env['api.format']).to be nil + expect(env['api.format']).to be_nil expect(status).to eq(200) end @@ -64,7 +64,7 @@ it 'is nil if not provided' do status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1') - expect(env['api.format']).to be nil + expect(env['api.format']).to be_nil expect(status).to eq(200) end end diff --git a/spec/grape/validations/attributes_doc_spec.rb b/spec/grape/validations/attributes_doc_spec.rb index 25bf40a3d..a21ce3591 100644 --- a/spec/grape/validations/attributes_doc_spec.rb +++ b/spec/grape/validations/attributes_doc_spec.rb @@ -114,7 +114,7 @@ doc = subject.first['nested[engine_age]'] expect(doc).to have_key(:default) - expect(doc[:default]).to eq(false) + expect(doc[:default]).to be(false) end end diff --git a/spec/grape/validations/types/primitive_coercer_spec.rb b/spec/grape/validations/types/primitive_coercer_spec.rb index 2b64277d2..0c9b15e24 100644 --- a/spec/grape/validations/types/primitive_coercer_spec.rb +++ b/spec/grape/validations/types/primitive_coercer_spec.rb @@ -23,13 +23,13 @@ [true, 'true', 1].each do |val| it "coerces '#{val}' to true" do - expect(subject.call(val)).to eq(true) + expect(subject.call(val)).to be(true) end end [false, 'false', 0].each do |val| it "coerces '#{val}' to false" do - expect(subject.call(val)).to eq(false) + expect(subject.call(val)).to be(false) end end @@ -113,7 +113,7 @@ end it 'returns a value as it is when the given value is Boolean' do - expect(subject.call(true)).to eq(true) + expect(subject.call(true)).to be(true) end end From 862101c8b52a5f8ffec080a31eb6b98594290adc Mon Sep 17 00:00:00 2001 From: dblock Date: Mon, 28 Feb 2022 11:14:15 -0500 Subject: [PATCH 110/304] Re-add #frozen-string-literal. --- gemfiles/multi_json.gemfile | 2 ++ gemfiles/multi_xml.gemfile | 2 ++ gemfiles/rack1.gemfile | 2 ++ gemfiles/rack2.gemfile | 2 ++ gemfiles/rack_edge.gemfile | 2 ++ gemfiles/rails_5.gemfile | 2 ++ gemfiles/rails_6.gemfile | 2 ++ gemfiles/rails_6_1.gemfile | 2 ++ gemfiles/rails_7.gemfile | 2 ++ gemfiles/rails_edge.gemfile | 2 ++ 10 files changed, 20 insertions(+) diff --git a/gemfiles/multi_json.gemfile b/gemfiles/multi_json.gemfile index ac46d9c70..477484d34 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' diff --git a/gemfiles/multi_xml.gemfile b/gemfiles/multi_xml.gemfile index e8000c52e..8c3c30678 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' diff --git a/gemfiles/rack1.gemfile b/gemfiles/rack1.gemfile index 1cf94bf23..0b168c406 100644 --- a/gemfiles/rack1.gemfile +++ b/gemfiles/rack1.gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This file was generated by Appraisal source 'https://rubygems.org' diff --git a/gemfiles/rack2.gemfile b/gemfiles/rack2.gemfile index 7016964f9..bbf54cf72 100644 --- a/gemfiles/rack2.gemfile +++ b/gemfiles/rack2.gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This file was generated by Appraisal source 'https://rubygems.org' diff --git a/gemfiles/rack_edge.gemfile b/gemfiles/rack_edge.gemfile index a3174e87a..b3810e93d 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' diff --git a/gemfiles/rails_5.gemfile b/gemfiles/rails_5.gemfile index e3de8efdf..e41baad55 100644 --- a/gemfiles/rails_5.gemfile +++ b/gemfiles/rails_5.gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This file was generated by Appraisal source 'https://rubygems.org' diff --git a/gemfiles/rails_6.gemfile b/gemfiles/rails_6.gemfile index f2eb38db4..3733c92e3 100644 --- a/gemfiles/rails_6.gemfile +++ b/gemfiles/rails_6.gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This file was generated by Appraisal source 'https://rubygems.org' diff --git a/gemfiles/rails_6_1.gemfile b/gemfiles/rails_6_1.gemfile index d3a52b70a..795655e59 100644 --- a/gemfiles/rails_6_1.gemfile +++ b/gemfiles/rails_6_1.gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This file was generated by Appraisal source 'https://rubygems.org' diff --git a/gemfiles/rails_7.gemfile b/gemfiles/rails_7.gemfile index 020830749..4407f38cb 100644 --- a/gemfiles/rails_7.gemfile +++ b/gemfiles/rails_7.gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This file was generated by Appraisal source 'https://rubygems.org' diff --git a/gemfiles/rails_edge.gemfile b/gemfiles/rails_edge.gemfile index ba25dba3e..4cc0dc4fb 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' From d54e20c406139ef0b0a81ca15022ddd15e7dd87a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Glauco=20Cust=C3=B3dio?= Date: Mon, 21 Mar 2022 12:04:22 +0000 Subject: [PATCH 111/304] Use bundle add instead As per https://github.com/rubygems/rubygems/pull/5337, we can simplify the steps of adding a gem. --- README.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 0f55d2049..e5a921474 100644 --- a/README.md +++ b/README.md @@ -181,15 +181,9 @@ In 2020, we plan to use the money towards gathering Grape contributors for dinne Ruby 2.4 or newer is required. -Grape is available as a gem, to install it just install the gem: +Grape is available as a gem, to install it run: - gem install grape - -If you're using Bundler, add the gem to Gemfile. - - gem 'grape' - -Run `bundle install`. + bundle add grape ## Basic Usage From 75276be92431527791c40068fccb9bacb131d06b Mon Sep 17 00:00:00 2001 From: Ben Schmeckpeper Date: Tue, 26 Apr 2022 13:04:23 -0500 Subject: [PATCH 112/304] Handle Rack errors when too many files are uploaded (#2256) * Capture Rack's "too many multipart files" error with a spec * Create a custom error class to handle Rack's multipart error * fixup! Capture Rack's "too many multipart files" error with a spec * Use our custom error class when Rack raises a multipart limit error * Use a Payload Too Large status code for TooManyMultipartFiles errors * Restore Rack's multipart limit after testing the failure * Upadate CHANGELOG * Reword CHANGELOG entry for MultipartPartLimitError change * Backticks around classname in CHANGELOG * Change next version of Grape from 1.6.3 to 1.7.0 The introduction of the TooManyMultipartFiles changes API behavior, so bump the minor version. * Include the system's configured multipart file limit in the error message The number of allowed multipart files is a configurable value in Rack, pull that limit and include it in the generated error message. * Add a note to UPGRADING about the new TooManyMultipartFiles exception --- CHANGELOG.md | 3 ++- README.md | 2 +- UPGRADING.md | 6 +++++- lib/grape.rb | 1 + .../exceptions/too_many_multipart_files.rb | 11 ++++++++++ lib/grape/locale/en.yml | 1 + lib/grape/request.rb | 2 ++ lib/grape/version.rb | 2 +- spec/grape/endpoint_spec.rb | 21 +++++++++++++++++++ 9 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 lib/grape/exceptions/too_many_multipart_files.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 10544bd24..e360bffa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -### 1.6.3 (Next) +### 1.7.0 (Next) #### Features @@ -18,6 +18,7 @@ * [#2227](https://github.com/ruby-grape/grape/pull/2222): Rename `MissingGroupType` and `UnsupportedGroupType` exceptions - [@ericproulx](https://github.com/ericproulx). * [#2244](https://github.com/ruby-grape/grape/pull/2244): Fix a breaking change in `Grape::Validations` provided in 1.6.1 - [@dm1try](https://github.com/dm1try). * [#2250](https://github.com/ruby-grape/grape/pull/2250): Add deprecation warning for `UnsupportedGroupTypeError` and `MissingGroupTypeError` - [@ericproulx](https://github.com/ericproulx). +* [#2256](https://github.com/ruby-grape/grape/pull/2256): Raise `Grape::Exceptions::MultipartPartLimitError` from Rack when too many files are uploaded - [@bschmeck](https://github.com/bschmeck). * Your contribution here. ### 1.6.2 (2021/12/30) diff --git a/README.md b/README.md index e5a921474..fb83a68c9 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ content negotiation, versioning and much more. ## Stable Release -You're reading the documentation for the next release of Grape, which should be **1.6.3**. +You're reading the documentation for the next release of Grape, which should be **1.7.0**. Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. The current stable release is [1.6.2](https://github.com/ruby-grape/grape/blob/v1.6.2/README.md). diff --git a/UPGRADING.md b/UPGRADING.md index 969fc1347..620e1d6ec 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,7 +1,7 @@ Upgrading Grape =============== -### Upgrading to >= 1.6.3 +### Upgrading to >= 1.7.0 #### Exceptions renaming @@ -12,6 +12,10 @@ The following exceptions has been renamed for consistency through exceptions nam See [#2227](https://github.com/ruby-grape/grape/pull/2227) for more information. +#### Handling Multipart Limit Errors + +Rack supports a configurable limit on the number of files created from multipart parameters (`Rack::Utils.multipart_part_limit`) and raises an error if params are received that create too many files. If you were handling the Rack error directly, Grape now wraps that error in `Grape::Execeptions::TooManyMultipartFiles`. Additionally, Grape will return a 413 status code if the exception goes unhandled. + ### Upgrading to >= 1.6.0 #### Parameter renaming with :as diff --git a/lib/grape.rb b/lib/grape.rb index 49b3ec403..c8824eff7 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -81,6 +81,7 @@ module Exceptions autoload :MethodNotAllowed autoload :InvalidResponse autoload :EmptyMessageBody + autoload :TooManyMultipartFiles autoload :MissingGroupTypeError, 'grape/exceptions/missing_group_type' autoload :UnsupportedGroupTypeError, 'grape/exceptions/unsupported_group_type' end diff --git a/lib/grape/exceptions/too_many_multipart_files.rb b/lib/grape/exceptions/too_many_multipart_files.rb new file mode 100644 index 000000000..72deffb39 --- /dev/null +++ b/lib/grape/exceptions/too_many_multipart_files.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Grape + module Exceptions + class TooManyMultipartFiles < Base + def initialize(limit) + super(message: compose_message(:too_many_multipart_files, limit: limit), status: 413) + end + end + end +end diff --git a/lib/grape/locale/en.yml b/lib/grape/locale/en.yml index 036eb93b8..546529a3e 100644 --- a/lib/grape/locale/en.yml +++ b/lib/grape/locale/en.yml @@ -45,6 +45,7 @@ en: %{body_format} in the request's 'body' " empty_message_body: 'Empty message body supplied with %{body_format} content-type' + too_many_multipart_files: "The number of uploaded files exceeded the system's configured limit (%{limit})" invalid_accept_header: problem: 'Invalid accept header' resolution: '%{message}' diff --git a/lib/grape/request.rb b/lib/grape/request.rb index e3ed4492d..c45df9876 100644 --- a/lib/grape/request.rb +++ b/lib/grape/request.rb @@ -17,6 +17,8 @@ def params @params ||= build_params rescue EOFError raise Grape::Exceptions::EmptyMessageBody.new(content_type) + rescue Rack::Multipart::MultipartPartLimitError + raise Grape::Exceptions::TooManyMultipartFiles.new(Rack::Utils.multipart_part_limit) end def headers diff --git a/lib/grape/version.rb b/lib/grape/version.rb index af7c1a7cd..3a0817e8d 100644 --- a/lib/grape/version.rb +++ b/lib/grape/version.rb @@ -2,5 +2,5 @@ module Grape # The current version of Grape. - VERSION = '1.6.3' + VERSION = '1.7.0' end diff --git a/spec/grape/endpoint_spec.rb b/spec/grape/endpoint_spec.rb index 9fee973ca..a599abdf4 100644 --- a/spec/grape/endpoint_spec.rb +++ b/spec/grape/endpoint_spec.rb @@ -436,6 +436,27 @@ def app end end + context 'when the limit on multipart files is exceeded' do + around do |example| + limit = Rack::Utils.multipart_part_limit + Rack::Utils.multipart_part_limit = 1 + example.run + Rack::Utils.multipart_part_limit = limit + end + + it 'returns a 413 if given too many multipart files' do + subject.params do + requires :file, type: Rack::Multipart::UploadedFile + end + subject.post '/upload' do + params[:file][:filename] + end + post '/upload', { file: Rack::Test::UploadedFile.new(__FILE__, 'text/plain'), extra: Rack::Test::UploadedFile.new(__FILE__, 'text/plain') } + expect(last_response.status).to eq(413) + expect(last_response.body).to eq("The number of uploaded files exceeded the system's configured limit (1)") + end + end + it 'responds with a 415 for an unsupported content-type' do subject.format :json # subject.content_type :json, "application/json" From 5cc3226a77a3dd294b26b0c09c2707c36d50cf99 Mon Sep 17 00:00:00 2001 From: Daniel Dao Date: Mon, 6 Jun 2022 16:07:38 -0700 Subject: [PATCH 113/304] =?UTF-8?q?=F0=9F=93=9D=20=20Update=20`README.md`?= =?UTF-8?q?=20with=20`rename`-ing=20notes=20for=20`params`=20(#2259)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fb83a68c9..31c42a3f5 100644 --- a/README.md +++ b/README.md @@ -1448,7 +1448,7 @@ resource :users do end ``` -The value passed to `as` will be the key when calling `params` or `declared(params)`. +The value passed to `as` will be the key when calling `declared(params)`. ### Built-in Validators From 5b159abcf1a6aa05d8f47e68e7cf68cf218bf3ff Mon Sep 17 00:00:00 2001 From: Nicholas Duffy <3457341+duffn@users.noreply.github.com> Date: Mon, 13 Jun 2022 13:04:49 -0600 Subject: [PATCH 114/304] Update README to raise custom validation exception --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 31c42a3f5..50a74b54b 100644 --- a/README.md +++ b/README.md @@ -1740,7 +1740,7 @@ end class AlphaNumeric < Grape::Validations::Validators::Base def validate_param!(attr_name, params) unless params[attr_name] =~ /\A[[:alnum:]]+\z/ - fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: 'must consist of alpha-numeric characters' + raise Grape::Exceptions::Validation.new params: [@scope.full_name(attr_name)], message: 'must consist of alpha-numeric characters' end end end @@ -1758,7 +1758,7 @@ You can also create custom classes that take parameters. class Length < Grape::Validations::Validators::Base def validate_param!(attr_name, params) unless params[attr_name].length <= @option - fail 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 @@ -1784,7 +1784,7 @@ class Admin < Grape::Validations::Validators::Base 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 - fail 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 ``` From c6481d98a5bb61ad4eaf06e97fac11e0a577573c Mon Sep 17 00:00:00 2001 From: "Daniel (dB.) Doubrovkine" Date: Mon, 13 Jun 2022 17:50:22 -0400 Subject: [PATCH 115/304] Explicitly require bigdecimal and date. --- CHANGELOG.md | 1 + lib/grape.rb | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e360bffa1..826a87da0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ #### Fixes +* [#2263](https://github.com/ruby-grape/grape/pull/2263): Explicitly require `bigdecimal` and `date` - [@dblock](https://github.com/dblock). * [#2222](https://github.com/ruby-grape/grape/pull/2222): Autoload types and validators - [@ericproulx](https://github.com/ericproulx). * [#2232](https://github.com/ruby-grape/grape/pull/2232): Fix kwargs support in shared params definition - [@dm1try](https://github.com/dm1try). * [#2229](https://github.com/ruby-grape/grape/pull/2229): Do not collect params in route settings - [@dnesteryuk](https://github.com/dnesteryuk). diff --git a/lib/grape.rb b/lib/grape.rb index c8824eff7..0b5db09ff 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -7,6 +7,8 @@ require 'rack/auth/basic' require 'rack/auth/digest/md5' require 'set' +require 'bigdecimal' +require 'date' require 'active_support' require 'active_support/concern' require 'active_support/version' From 7ee07d14f1ea29464102cf22640d87f6a7b1f7f7 Mon Sep 17 00:00:00 2001 From: duffn <3457341+duffn@users.noreply.github.com> Date: Tue, 14 Jun 2022 12:48:01 -0600 Subject: [PATCH 116/304] Fix Coveralls code coverage (#2264) --- .coveralls.yml | 2 +- .github/workflows/edge.yml | 1 + .github/workflows/test.yml | 1 + CHANGELOG.md | 1 + 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.coveralls.yml b/.coveralls.yml index 91600595a..1157ff256 100644 --- a/.coveralls.yml +++ b/.coveralls.yml @@ -1 +1 @@ -service_name: travis-ci +service_name: github-actions diff --git a/.github/workflows/edge.yml b/.github/workflows/edge.yml index 795873aba..9f5999d75 100644 --- a/.github/workflows/edge.yml +++ b/.github/workflows/edge.yml @@ -21,6 +21,7 @@ jobs: continue-on-error: true env: BUNDLE_GEMFILE: ${{ matrix.gemfile }} + COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 324b2a4f1..be207740d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,6 +59,7 @@ jobs: runs-on: ubuntu-20.04 env: BUNDLE_GEMFILE: ${{ matrix.gemfile }} + COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} steps: - uses: actions/checkout@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 826a87da0..c320bd019 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ * [#2244](https://github.com/ruby-grape/grape/pull/2244): Fix a breaking change in `Grape::Validations` provided in 1.6.1 - [@dm1try](https://github.com/dm1try). * [#2250](https://github.com/ruby-grape/grape/pull/2250): Add deprecation warning for `UnsupportedGroupTypeError` and `MissingGroupTypeError` - [@ericproulx](https://github.com/ericproulx). * [#2256](https://github.com/ruby-grape/grape/pull/2256): Raise `Grape::Exceptions::MultipartPartLimitError` from Rack when too many files are uploaded - [@bschmeck](https://github.com/bschmeck). +* [#2264](https://github.com/ruby-grape/grape/pull/2264): Fix coveralls code coverage - [@duffn](https://github.com/duffn). * Your contribution here. ### 1.6.2 (2021/12/30) From f5dc7dc5dcbbe2cd43d4ba76cf3f8a788f698bb2 Mon Sep 17 00:00:00 2001 From: duffn <3457341+duffn@users.noreply.github.com> Date: Tue, 14 Jun 2022 20:30:31 -0600 Subject: [PATCH 117/304] Use Coveralls GitHub Action (#2266) * Use Coveralls GitHub Action * Update CHANGELOG --- .coveralls.yml | 2 +- .github/workflows/edge.yml | 18 +++++++++++++++++- .github/workflows/test.yml | 18 +++++++++++++++++- CHANGELOG.md | 2 +- Gemfile | 3 ++- gemfiles/multi_json.gemfile | 3 ++- gemfiles/multi_xml.gemfile | 3 ++- gemfiles/rack1.gemfile | 3 ++- gemfiles/rack2.gemfile | 3 ++- gemfiles/rack2_2.gemfile | 3 ++- gemfiles/rack_edge.gemfile | 3 ++- gemfiles/rails_5.gemfile | 3 ++- gemfiles/rails_6.gemfile | 3 ++- gemfiles/rails_6_1.gemfile | 3 ++- gemfiles/rails_7.gemfile | 3 ++- gemfiles/rails_edge.gemfile | 3 ++- spec/spec_helper.rb | 11 +++++++++-- 17 files changed, 69 insertions(+), 18 deletions(-) diff --git a/.coveralls.yml b/.coveralls.yml index 1157ff256..fce062d2e 100644 --- a/.coveralls.yml +++ b/.coveralls.yml @@ -1 +1 @@ -service_name: github-actions +service_name: github diff --git a/.github/workflows/edge.yml b/.github/workflows/edge.yml index 9f5999d75..57573a7b5 100644 --- a/.github/workflows/edge.yml +++ b/.github/workflows/edge.yml @@ -21,7 +21,6 @@ jobs: continue-on-error: true env: BUNDLE_GEMFILE: ${{ matrix.gemfile }} - COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} steps: - uses: actions/checkout@v2 @@ -34,3 +33,20 @@ jobs: - name: Run tests run: bundle exec rake spec + + - name: Coveralls + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + flag-name: run-${{ matrix.ruby }}-${{ matrix.gemfile }} + parallel: true + + finish: + needs: test + runs-on: ubuntu-latest + steps: + - name: Coveralls Finished + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.github_token }} + parallel-finished: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index be207740d..66336200b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,7 +59,6 @@ jobs: runs-on: ubuntu-20.04 env: BUNDLE_GEMFILE: ${{ matrix.gemfile }} - COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} steps: - uses: actions/checkout@v2 @@ -84,3 +83,20 @@ jobs: - name: Run tests (spec/integration/multi_xml) if: ${{ matrix.gemfile == 'gemfiles/multi_xml.gemfile' }} run: bundle exec rspec spec/integration/multi_xml + + - name: Coveralls + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + flag-name: run-${{ matrix.ruby }}-${{ matrix.gemfile }} + parallel: true + + finish: + needs: test + runs-on: ubuntu-latest + steps: + - name: Coveralls Finished + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.github_token }} + parallel-finished: true diff --git a/CHANGELOG.md b/CHANGELOG.md index c320bd019..df59267f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ * [#2244](https://github.com/ruby-grape/grape/pull/2244): Fix a breaking change in `Grape::Validations` provided in 1.6.1 - [@dm1try](https://github.com/dm1try). * [#2250](https://github.com/ruby-grape/grape/pull/2250): Add deprecation warning for `UnsupportedGroupTypeError` and `MissingGroupTypeError` - [@ericproulx](https://github.com/ericproulx). * [#2256](https://github.com/ruby-grape/grape/pull/2256): Raise `Grape::Exceptions::MultipartPartLimitError` from Rack when too many files are uploaded - [@bschmeck](https://github.com/bschmeck). -* [#2264](https://github.com/ruby-grape/grape/pull/2264): Fix coveralls code coverage - [@duffn](https://github.com/duffn). +* [#2266](https://github.com/ruby-grape/grape/pull/2266): Fix code coverage - [@duffn](https://github.com/duffn). * Your contribution here. ### 1.6.2 (2021/12/30) diff --git a/Gemfile b/Gemfile index 3f7f3b1b8..acc721aa0 100644 --- a/Gemfile +++ b/Gemfile @@ -27,7 +27,6 @@ end group :test do gem 'cookiejar' - gem 'coveralls_reborn' gem 'grape-entity', '~> 0.6' gem 'maruku' gem 'mime-types' @@ -35,6 +34,8 @@ group :test do gem 'rack-test', '~> 1.1.0' gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false + gem 'simplecov', '~> 0.21.2' + gem 'simplecov-lcov', '~> 0.8.0' gem 'test-prof', require: false end diff --git a/gemfiles/multi_json.gemfile b/gemfiles/multi_json.gemfile index 477484d34..2b45868af 100644 --- a/gemfiles/multi_json.gemfile +++ b/gemfiles/multi_json.gemfile @@ -27,7 +27,6 @@ end group :test do gem 'cookiejar' - gem 'coveralls_reborn' gem 'grape-entity', '~> 0.6' gem 'maruku' gem 'mime-types' @@ -35,6 +34,8 @@ group :test do gem 'rack-test', '~> 1.1.0' gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false + gem 'simplecov', '~> 0.21.2' + gem 'simplecov-lcov', '~> 0.8.0' gem 'test-prof', require: false end diff --git a/gemfiles/multi_xml.gemfile b/gemfiles/multi_xml.gemfile index 8c3c30678..8d98df33e 100644 --- a/gemfiles/multi_xml.gemfile +++ b/gemfiles/multi_xml.gemfile @@ -27,7 +27,6 @@ end group :test do gem 'cookiejar' - gem 'coveralls_reborn' gem 'grape-entity', '~> 0.6' gem 'maruku' gem 'mime-types' @@ -35,6 +34,8 @@ group :test do gem 'rack-test', '~> 1.1.0' gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false + gem 'simplecov', '~> 0.21.2' + gem 'simplecov-lcov', '~> 0.8.0' gem 'test-prof', require: false end diff --git a/gemfiles/rack1.gemfile b/gemfiles/rack1.gemfile index 0b168c406..7e2907aa3 100644 --- a/gemfiles/rack1.gemfile +++ b/gemfiles/rack1.gemfile @@ -27,7 +27,6 @@ end group :test do gem 'cookiejar' - gem 'coveralls_reborn' gem 'grape-entity', '~> 0.6' gem 'maruku' gem 'mime-types' @@ -35,6 +34,8 @@ group :test do gem 'rack-test', '~> 1.1.0' gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false + gem 'simplecov', '~> 0.21.2' + gem 'simplecov-lcov', '~> 0.8.0' gem 'test-prof', require: false end diff --git a/gemfiles/rack2.gemfile b/gemfiles/rack2.gemfile index bbf54cf72..c0141b6a3 100644 --- a/gemfiles/rack2.gemfile +++ b/gemfiles/rack2.gemfile @@ -27,7 +27,6 @@ end group :test do gem 'cookiejar' - gem 'coveralls_reborn' gem 'grape-entity', '~> 0.6' gem 'maruku' gem 'mime-types' @@ -35,6 +34,8 @@ group :test do gem 'rack-test', '~> 1.1.0' gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false + gem 'simplecov', '~> 0.21.2' + gem 'simplecov-lcov', '~> 0.8.0' gem 'test-prof', require: false end diff --git a/gemfiles/rack2_2.gemfile b/gemfiles/rack2_2.gemfile index 1a865a87d..ed24bfc6d 100644 --- a/gemfiles/rack2_2.gemfile +++ b/gemfiles/rack2_2.gemfile @@ -27,7 +27,6 @@ end group :test do gem 'cookiejar' - gem 'coveralls_reborn' gem 'grape-entity', '~> 0.6' gem 'maruku' gem 'mime-types' @@ -35,6 +34,8 @@ group :test do gem 'rack-test', '~> 1.1.0' gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false + gem 'simplecov', '~> 0.21.2' + gem 'simplecov-lcov', '~> 0.8.0' gem 'test-prof', require: false end diff --git a/gemfiles/rack_edge.gemfile b/gemfiles/rack_edge.gemfile index b3810e93d..411d05d9e 100644 --- a/gemfiles/rack_edge.gemfile +++ b/gemfiles/rack_edge.gemfile @@ -27,7 +27,6 @@ end group :test do gem 'cookiejar' - gem 'coveralls_reborn' gem 'grape-entity', '~> 0.6' gem 'maruku' gem 'mime-types' @@ -35,6 +34,8 @@ group :test do gem 'rack-test', '~> 1.1.0' gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false + gem 'simplecov', '~> 0.21.2' + gem 'simplecov-lcov', '~> 0.8.0' gem 'test-prof', require: false end diff --git a/gemfiles/rails_5.gemfile b/gemfiles/rails_5.gemfile index e41baad55..198c72175 100644 --- a/gemfiles/rails_5.gemfile +++ b/gemfiles/rails_5.gemfile @@ -27,7 +27,6 @@ end group :test do gem 'cookiejar' - gem 'coveralls_reborn' gem 'grape-entity', '~> 0.6' gem 'maruku' gem 'mime-types' @@ -35,6 +34,8 @@ group :test do gem 'rack-test', '~> 1.1.0' gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false + gem 'simplecov', '~> 0.21.2' + gem 'simplecov-lcov', '~> 0.8.0' gem 'test-prof', require: false end diff --git a/gemfiles/rails_6.gemfile b/gemfiles/rails_6.gemfile index 3733c92e3..f64eb4b5f 100644 --- a/gemfiles/rails_6.gemfile +++ b/gemfiles/rails_6.gemfile @@ -27,7 +27,6 @@ end group :test do gem 'cookiejar' - gem 'coveralls_reborn' gem 'grape-entity', '~> 0.6' gem 'maruku' gem 'mime-types' @@ -35,6 +34,8 @@ group :test do gem 'rack-test', '~> 1.1.0' gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false + gem 'simplecov', '~> 0.21.2' + gem 'simplecov-lcov', '~> 0.8.0' gem 'test-prof', require: false end diff --git a/gemfiles/rails_6_1.gemfile b/gemfiles/rails_6_1.gemfile index 795655e59..d21aee37c 100644 --- a/gemfiles/rails_6_1.gemfile +++ b/gemfiles/rails_6_1.gemfile @@ -27,7 +27,6 @@ end group :test do gem 'cookiejar' - gem 'coveralls_reborn' gem 'grape-entity', '~> 0.6' gem 'maruku' gem 'mime-types' @@ -35,6 +34,8 @@ group :test do gem 'rack-test', '~> 1.1.0' gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false + gem 'simplecov', '~> 0.21.2' + gem 'simplecov-lcov', '~> 0.8.0' gem 'test-prof', require: false end diff --git a/gemfiles/rails_7.gemfile b/gemfiles/rails_7.gemfile index 4407f38cb..3245fe8ac 100644 --- a/gemfiles/rails_7.gemfile +++ b/gemfiles/rails_7.gemfile @@ -27,7 +27,6 @@ end group :test do gem 'cookiejar' - gem 'coveralls_reborn' gem 'grape-entity', '~> 0.6' gem 'maruku' gem 'mime-types' @@ -35,6 +34,8 @@ group :test do gem 'rack-test', '~> 1.1.0' gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false + gem 'simplecov', '~> 0.21.2' + gem 'simplecov-lcov', '~> 0.8.0' gem 'test-prof', require: false end diff --git a/gemfiles/rails_edge.gemfile b/gemfiles/rails_edge.gemfile index 4cc0dc4fb..533fa931b 100644 --- a/gemfiles/rails_edge.gemfile +++ b/gemfiles/rails_edge.gemfile @@ -27,7 +27,6 @@ end group :test do gem 'cookiejar' - gem 'coveralls_reborn' gem 'grape-entity', '~> 0.6' gem 'maruku' gem 'mime-types' @@ -35,6 +34,8 @@ group :test do gem 'rack-test', '~> 1.1.0' gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false + gem 'simplecov', '~> 0.21.2' + gem 'simplecov-lcov', '~> 0.8.0' gem 'test-prof', require: false end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 1168a12e2..f09fdd356 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -41,5 +41,12 @@ def rollback_transaction; end config.example_status_persistence_file_path = '.rspec_status' end -require 'coveralls' -Coveralls.wear! +require 'simplecov' +require 'simplecov-lcov' +SimpleCov::Formatter::LcovFormatter.config do |c| + c.report_with_single_file = true + c.single_report_path = 'coverage/lcov.info' +end + +SimpleCov.formatter = SimpleCov::Formatter::LcovFormatter +SimpleCov.start From ea5da5065374215459736aff871fb02ef65ffe48 Mon Sep 17 00:00:00 2001 From: "Daniel (dB.) Doubrovkine" Date: Tue, 14 Jun 2022 22:33:26 -0400 Subject: [PATCH 118/304] Removed past plans. --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 50a74b54b..cb6fc1417 100644 --- a/README.md +++ b/README.md @@ -175,8 +175,6 @@ 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. From dc6644cc4c45549f272f151444db68d921f92bab Mon Sep 17 00:00:00 2001 From: "Daniel (dB.) Doubrovkine" Date: Tue, 14 Jun 2022 23:04:04 -0400 Subject: [PATCH 119/304] Added missing test coverage for Grape::Exceptions::MissingVendorOption. --- spec/grape/middleware/versioner/header_spec.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/spec/grape/middleware/versioner/header_spec.rb b/spec/grape/middleware/versioner/header_spec.rb index af1312fc3..29fa056ca 100644 --- a/spec/grape/middleware/versioner/header_spec.rb +++ b/spec/grape/middleware/versioner/header_spec.rb @@ -326,4 +326,20 @@ def app end end end + + context 'with missing vendor option' do + subject do + Class.new(Grape::API) do + version 'v1', using: :header + end + end + + def app + subject + end + + it 'fails' do + expect { versioned_get '/', 'v1', using: :header }.to raise_error Grape::Exceptions::MissingVendorOption + end + end end From 8b479bd43556ebc51d1c851bc3c3d6a4237ba74f Mon Sep 17 00:00:00 2001 From: Vasily Fedoseyev Date: Thu, 21 Jul 2022 18:16:10 +0300 Subject: [PATCH 120/304] Numeric is not a dry-types built-in type, fix coercion/validation --- CHANGELOG.md | 1 + lib/grape/validations/types/primitive_coercer.rb | 4 +++- spec/grape/validations/types/primitive_coercer_spec.rb | 8 ++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df59267f6..8b8f882dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * [#2248](https://github.com/ruby-grape/grape/pull/2248): Upgraded to rspec 3.11.0 - [@dblock](https://github.com/dblock). * [#2249](https://github.com/ruby-grape/grape/pull/2249): Split CI matrix, extract edge - [@dblock](https://github.com/dblock). * [#2249](https://github.com/ruby-grape/grape/pull/2251): Upgraded to RuboCop 1.25.1 - [@dblock](https://github.com/dblock). +* [#2271](https://github.com/ruby-grape/grape/pull/2271): Fixed validation regression on Numeric type introduced in 1.3 - [@vasfed](https://github.com/Vasfed). * Your contribution here. #### Fixes diff --git a/lib/grape/validations/types/primitive_coercer.rb b/lib/grape/validations/types/primitive_coercer.rb index 368ccff64..58fd77743 100644 --- a/lib/grape/validations/types/primitive_coercer.rb +++ b/lib/grape/validations/types/primitive_coercer.rb @@ -12,6 +12,7 @@ class PrimitiveCoercer < DryTypeCoercer MAPPING = { Grape::API::Boolean => DryTypes::Params::Bool, BigDecimal => DryTypes::Params::Decimal, + Numeric => DryTypes::Params::Integer | DryTypes::Params::Float | DryTypes::Params::Decimal, # unfortunately, a +Params+ scope doesn't contain String String => DryTypes::Coercible::String @@ -19,7 +20,8 @@ class PrimitiveCoercer < DryTypeCoercer STRICT_MAPPING = { Grape::API::Boolean => DryTypes::Strict::Bool, - BigDecimal => DryTypes::Strict::Decimal + BigDecimal => DryTypes::Strict::Decimal, + Numeric => DryTypes::Strict::Integer | DryTypes::Strict::Float | DryTypes::Strict::Decimal }.freeze def initialize(type, strict = false) diff --git a/spec/grape/validations/types/primitive_coercer_spec.rb b/spec/grape/validations/types/primitive_coercer_spec.rb index 0c9b15e24..11b28fcdc 100644 --- a/spec/grape/validations/types/primitive_coercer_spec.rb +++ b/spec/grape/validations/types/primitive_coercer_spec.rb @@ -64,6 +64,10 @@ it 'coerces an empty string to nil' do expect(subject.call('')).to be_nil end + + it 'accepts non-nil value' do + expect(subject.call(42)).to be_a(Integer) + end end context 'Numeric' do @@ -72,6 +76,10 @@ it 'coerces an empty string to nil' do expect(subject.call('')).to be_nil end + + it 'accepts a non-nil value' do + expect(subject.call(42)).to be_a(Numeric) # in fact Integer + end end context 'Time' do From 3b37b881c78545f0a9fbc7a1c61d9366b4b24977 Mon Sep 17 00:00:00 2001 From: "Daniel (dB.) Doubrovkine" Date: Tue, 14 Jun 2022 23:03:06 -0400 Subject: [PATCH 121/304] Standardize error message casing to lowercase. --- CHANGELOG.md | 1 + lib/grape/locale/en.yml | 18 +++++++++--------- spec/grape/endpoint_spec.rb | 4 ++-- .../invalid_versioner_option_spec.rb | 2 +- spec/grape/exceptions/missing_option_spec.rb | 2 +- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b8f882dc..01811fc15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * [#2249](https://github.com/ruby-grape/grape/pull/2249): Split CI matrix, extract edge - [@dblock](https://github.com/dblock). * [#2249](https://github.com/ruby-grape/grape/pull/2251): Upgraded to RuboCop 1.25.1 - [@dblock](https://github.com/dblock). * [#2271](https://github.com/ruby-grape/grape/pull/2271): Fixed validation regression on Numeric type introduced in 1.3 - [@vasfed](https://github.com/Vasfed). +* [#2267](https://github.com/ruby-grape/grape/pull/2267): Standardized English error messages - [@dblock](https://github.com/dblock). * Your contribution here. #### Fixes diff --git a/lib/grape/locale/en.yml b/lib/grape/locale/en.yml index 546529a3e..9377e4c4e 100644 --- a/lib/grape/locale/en.yml +++ b/lib/grape/locale/en.yml @@ -11,8 +11,8 @@ en: 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. ' + problem: 'missing :vendor option' + summary: 'when version using header, you must specify :vendor option' resolution: "eg: version 'v1', using: :header, vendor: 'twitter'" missing_mime_type: problem: 'missing mime type for %{new_format}' @@ -21,12 +21,12 @@ en: or add your own with content_type :%{new_format}, 'application/%{new_format}' " invalid_with_option_for_represent: - problem: 'You must specify an entity class in the :with option.' + problem: 'you must specify an entity class in the :with option' resolution: 'eg: represent User, :with => Entity::User' - missing_option: 'You must specify :%{option} options.' + missing_option: 'you must specify :%{option} options' invalid_formatter: 'cannot convert %{klass} to %{to_format}' invalid_versioner_option: - problem: 'Unknown :using for versioner: %{strategy}' + problem: 'unknown :using for versioner: %{strategy}' resolution: 'available strategy for :using is :path, :header, :accept_version_header, :param' unknown_validator: 'unknown validator: %{validator_type}' unknown_options: 'unknown options: %{options}' @@ -44,12 +44,12 @@ 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' - too_many_multipart_files: "The number of uploaded files exceeded the system's configured limit (%{limit})" + empty_message_body: 'empty message body supplied with %{body_format} content-type' + too_many_multipart_files: "the number of uploaded files exceeded the system's configured limit (%{limit})" invalid_accept_header: - problem: 'Invalid accept header' + problem: 'invalid accept header' resolution: '%{message}' invalid_version_header: - problem: 'Invalid version header' + problem: 'invalid version header' resolution: '%{message}' invalid_response: 'Invalid response' diff --git a/spec/grape/endpoint_spec.rb b/spec/grape/endpoint_spec.rb index a599abdf4..ec51d2edb 100644 --- a/spec/grape/endpoint_spec.rb +++ b/spec/grape/endpoint_spec.rb @@ -432,7 +432,7 @@ def app 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') + expect(last_response.body).to eq('empty message body supplied with multipart/form-data; boundary=foobar content-type') end end @@ -453,7 +453,7 @@ def app end post '/upload', { file: Rack::Test::UploadedFile.new(__FILE__, 'text/plain'), extra: Rack::Test::UploadedFile.new(__FILE__, 'text/plain') } expect(last_response.status).to eq(413) - expect(last_response.body).to eq("The number of uploaded files exceeded the system's configured limit (1)") + expect(last_response.body).to eq("the number of uploaded files exceeded the system's configured limit (1)") end end diff --git a/spec/grape/exceptions/invalid_versioner_option_spec.rb b/spec/grape/exceptions/invalid_versioner_option_spec.rb index 19fff343d..d6955bd8a 100644 --- a/spec/grape/exceptions/invalid_versioner_option_spec.rb +++ b/spec/grape/exceptions/invalid_versioner_option_spec.rb @@ -8,7 +8,7 @@ it 'contains the problem in the message' do expect(error.message).to include( - 'Unknown :using for versioner: headers' + 'unknown :using for versioner: headers' ) end end diff --git a/spec/grape/exceptions/missing_option_spec.rb b/spec/grape/exceptions/missing_option_spec.rb index 633c28a3f..3d8f03e58 100644 --- a/spec/grape/exceptions/missing_option_spec.rb +++ b/spec/grape/exceptions/missing_option_spec.rb @@ -8,7 +8,7 @@ it 'contains the problem in the message' do expect(error.message).to include( - 'You must specify :path options.' + 'you must specify :path options' ) end end From c061898b4201967f57b9ccbe73c14da167c23b97 Mon Sep 17 00:00:00 2001 From: Vasily Fedoseyev Date: Thu, 21 Jul 2022 21:51:16 +0300 Subject: [PATCH 122/304] Check for dry-types type existence and prevent top level const lookup --- CHANGELOG.md | 1 + .../validations/types/primitive_coercer.rb | 18 ++++++++++++------ .../types/primitive_coercer_spec.rb | 9 +++++++++ 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01811fc15..f1be3b1d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * [#2249](https://github.com/ruby-grape/grape/pull/2251): Upgraded to RuboCop 1.25.1 - [@dblock](https://github.com/dblock). * [#2271](https://github.com/ruby-grape/grape/pull/2271): Fixed validation regression on Numeric type introduced in 1.3 - [@vasfed](https://github.com/Vasfed). * [#2267](https://github.com/ruby-grape/grape/pull/2267): Standardized English error messages - [@dblock](https://github.com/dblock). +* [#2272](https://github.com/ruby-grape/grape/pull/2272): Added error on param init when provided type does not have `[]` coercion method, previously validation silently failed for any value - [@vasfed](https://github.com/Vasfed). * Your contribution here. #### Fixes diff --git a/lib/grape/validations/types/primitive_coercer.rb b/lib/grape/validations/types/primitive_coercer.rb index 58fd77743..e2e3f9df5 100644 --- a/lib/grape/validations/types/primitive_coercer.rb +++ b/lib/grape/validations/types/primitive_coercer.rb @@ -13,6 +13,8 @@ class PrimitiveCoercer < DryTypeCoercer Grape::API::Boolean => DryTypes::Params::Bool, BigDecimal => DryTypes::Params::Decimal, Numeric => DryTypes::Params::Integer | DryTypes::Params::Float | DryTypes::Params::Decimal, + TrueClass => DryTypes::Params::Bool.constrained(eql: true), + FalseClass => DryTypes::Params::Bool.constrained(eql: false), # unfortunately, a +Params+ scope doesn't contain String String => DryTypes::Coercible::String @@ -21,7 +23,9 @@ class PrimitiveCoercer < DryTypeCoercer STRICT_MAPPING = { Grape::API::Boolean => DryTypes::Strict::Bool, BigDecimal => DryTypes::Strict::Decimal, - Numeric => DryTypes::Strict::Integer | DryTypes::Strict::Float | DryTypes::Strict::Decimal + Numeric => DryTypes::Strict::Integer | DryTypes::Strict::Float | DryTypes::Strict::Decimal, + TrueClass => DryTypes::Strict::Bool.constrained(eql: true), + FalseClass => DryTypes::Strict::Bool.constrained(eql: false) }.freeze def initialize(type, strict = false) @@ -29,11 +33,13 @@ def initialize(type, strict = false) @type = type - @coercer = if strict - STRICT_MAPPING.fetch(type) { scope.const_get(type.name) } - else - MAPPING.fetch(type) { scope.const_get(type.name) } - end + @coercer = (strict ? STRICT_MAPPING : MAPPING).fetch(type) do + scope.const_get(type.name, false) + rescue NameError + raise ArgumentError, "type #{type} should support coercion via `[]`" unless type.respond_to?(:[]) + + type + end end def call(val) diff --git a/spec/grape/validations/types/primitive_coercer_spec.rb b/spec/grape/validations/types/primitive_coercer_spec.rb index 11b28fcdc..cc0dd1995 100644 --- a/spec/grape/validations/types/primitive_coercer_spec.rb +++ b/spec/grape/validations/types/primitive_coercer_spec.rb @@ -110,6 +110,15 @@ end end + context 'a type unknown in Dry-types' do + let(:type) { Complex } + + it 'raises error on init' do + expect(DryTypes::Params.constants).not_to include(type.name.to_sym) + expect { subject }.to raise_error(/type Complex should support coercion/) + end + end + context 'the strict mode' do let(:strict) { true } From 362724df3b4dcef0dd79281cb5c70c08752c5e5d Mon Sep 17 00:00:00 2001 From: Dhruv Paranjape Date: Sun, 31 Jul 2022 15:55:00 +0200 Subject: [PATCH 123/304] Error middleware support using rack util's symbols as status (#2274) --- CHANGELOG.md | 1 + lib/grape/middleware/error.rb | 2 +- spec/grape/middleware/error_spec.rb | 6 ++++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1be3b1d9..862a60d54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ * [#2271](https://github.com/ruby-grape/grape/pull/2271): Fixed validation regression on Numeric type introduced in 1.3 - [@vasfed](https://github.com/Vasfed). * [#2267](https://github.com/ruby-grape/grape/pull/2267): Standardized English error messages - [@dblock](https://github.com/dblock). * [#2272](https://github.com/ruby-grape/grape/pull/2272): Added error on param init when provided type does not have `[]` coercion method, previously validation silently failed for any value - [@vasfed](https://github.com/Vasfed). +* [#2274](https://github.com/ruby-grape/grape/pull/2274): Error middleware support using rack util's symbols as status - [@dhruvCW](https://github.com/dhruvCW). * Your contribution here. #### Fixes diff --git a/lib/grape/middleware/error.rb b/lib/grape/middleware/error.rb index 20f6a89f3..5a8ba3daf 100644 --- a/lib/grape/middleware/error.rb +++ b/lib/grape/middleware/error.rb @@ -72,7 +72,7 @@ def error_response(error = {}) def rack_response(message, status = options[:default_status], headers = { Grape::Http::Headers::CONTENT_TYPE => content_type }) message = ERB::Util.html_escape(message) if headers[Grape::Http::Headers::CONTENT_TYPE] == TEXT_HTML - Rack::Response.new([message], status, headers) + Rack::Response.new([message], Rack::Utils.status_code(status), headers) end def format_message(message, backtrace, original_exception = nil) diff --git a/spec/grape/middleware/error_spec.rb b/spec/grape/middleware/error_spec.rb index cdfcc82d1..d056ca7e2 100644 --- a/spec/grape/middleware/error_spec.rb +++ b/spec/grape/middleware/error_spec.rb @@ -41,6 +41,12 @@ def app expect(last_response.status).to eq(410) end + it 'sets the status code based on the rack util status code symbol' do + ErrorSpec::ErrApp.error = { status: :gone } + get '/' + expect(last_response.status).to eq(410) + end + it 'sets the error message appropriately' do ErrorSpec::ErrApp.error = { message: 'Awesome stuff.' } get '/' From edc6abef69054d61fdcd77ee7e054c8afc4f4c5e Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Thu, 11 Aug 2022 16:38:48 +0200 Subject: [PATCH 124/304] Fix exception super (#2276) * Add super(message) + spec * Fix rubocop * Add changelog * Remove message attr_read * Add message spec --- CHANGELOG.md | 1 + lib/grape/exceptions/base.rb | 4 ++-- lib/grape/exceptions/validation.rb | 4 ---- spec/grape/exceptions/base_spec.rb | 16 ++++++++++++++++ 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 862a60d54..08195c66b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ * [#2267](https://github.com/ruby-grape/grape/pull/2267): Standardized English error messages - [@dblock](https://github.com/dblock). * [#2272](https://github.com/ruby-grape/grape/pull/2272): Added error on param init when provided type does not have `[]` coercion method, previously validation silently failed for any value - [@vasfed](https://github.com/Vasfed). * [#2274](https://github.com/ruby-grape/grape/pull/2274): Error middleware support using rack util's symbols as status - [@dhruvCW](https://github.com/dhruvCW). +* [#2276](https://github.com/ruby-grape/grape/pull/2276): Fix exception super - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/lib/grape/exceptions/base.rb b/lib/grape/exceptions/base.rb index 9eeba9301..1dc1f518a 100644 --- a/lib/grape/exceptions/base.rb +++ b/lib/grape/exceptions/base.rb @@ -7,12 +7,12 @@ class Base < StandardError BASE_ATTRIBUTES_KEY = 'grape.errors.attributes' FALLBACK_LOCALE = :en - attr_reader :status, :message, :headers + attr_reader :status, :headers def initialize(status: nil, message: nil, headers: nil, **_options) @status = status - @message = message @headers = headers + super(message) end def [](index) diff --git a/lib/grape/exceptions/validation.rb b/lib/grape/exceptions/validation.rb index c2901039d..b66fc46c9 100644 --- a/lib/grape/exceptions/validation.rb +++ b/lib/grape/exceptions/validation.rb @@ -21,10 +21,6 @@ def initialize(params:, message: nil, **args) def as_json(*_args) to_s end - - def to_s - message - end end end end diff --git a/spec/grape/exceptions/base_spec.rb b/spec/grape/exceptions/base_spec.rb index 8bf949d01..2378fdf6a 100644 --- a/spec/grape/exceptions/base_spec.rb +++ b/spec/grape/exceptions/base_spec.rb @@ -1,6 +1,22 @@ # frozen_string_literal: true describe Grape::Exceptions::Base do + describe '#to_s' do + subject { described_class.new(message: message).to_s } + + let(:message) { 'a_message' } + + it { is_expected.to eq(message) } + end + + describe '#message' do + subject { described_class.new(message: message).message } + + let(:message) { 'a_message' } + + it { is_expected.to eq(message) } + end + describe '#compose_message' do subject { described_class.new.send(:compose_message, key, **attributes) } From 62562f869c04e386bed7b169df23bf1781b03314 Mon Sep 17 00:00:00 2001 From: zysend <92354009+zysend@users.noreply.github.com> Date: Fri, 21 Oct 2022 21:45:52 +0800 Subject: [PATCH 125/304] An unexpected backtick (#2284) * an unexpected backtick * spec and changelog Co-authored-by: zachary.zhang --- CHANGELOG.md | 1 + lib/grape/middleware/error.rb | 2 +- spec/grape/api_spec.rb | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08195c66b..4a158dd67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ * [#2250](https://github.com/ruby-grape/grape/pull/2250): Add deprecation warning for `UnsupportedGroupTypeError` and `MissingGroupTypeError` - [@ericproulx](https://github.com/ericproulx). * [#2256](https://github.com/ruby-grape/grape/pull/2256): Raise `Grape::Exceptions::MultipartPartLimitError` from Rack when too many files are uploaded - [@bschmeck](https://github.com/bschmeck). * [#2266](https://github.com/ruby-grape/grape/pull/2266): Fix code coverage - [@duffn](https://github.com/duffn). +* [#2284](https://github.com/ruby-grape/grape/pull/2284): Fix an unexpected backtick - [@zysend](https://github.com/zysend). * Your contribution here. ### 1.6.2 (2021/12/30) diff --git a/lib/grape/middleware/error.rb b/lib/grape/middleware/error.rb index 5a8ba3daf..3e9d8c768 100644 --- a/lib/grape/middleware/error.rb +++ b/lib/grape/middleware/error.rb @@ -121,7 +121,7 @@ def rescue_handler_for_any_class(klass) def run_rescue_handler(handler, error) if handler.instance_of?(Symbol) - raise NoMethodError, "undefined method `#{handler}'" unless respond_to?(handler) + raise NoMethodError, "undefined method '#{handler}'" unless respond_to?(handler) handler = public_method(handler) end diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index 283dc70a6..0c77fd605 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -2291,7 +2291,7 @@ def rescue_no_method_error subject.rescue_from :all, with: :not_exist_method subject.get('/rescue_method') { raise StandardError } - expect { get '/rescue_method' }.to raise_error(NoMethodError, /^undefined method `not_exist_method'/) + expect { get '/rescue_method' }.to raise_error(NoMethodError, /^undefined method 'not_exist_method'/) end it 'correctly chooses exception handler if :all handler is specified' do From 0e727c1adff75e22f3282f6e07397dbf682f5637 Mon Sep 17 00:00:00 2001 From: zysend <92354009+zysend@users.noreply.github.com> Date: Thu, 3 Nov 2022 02:01:24 +0800 Subject: [PATCH 126/304] Fix the declared with not given (#2285). --- CHANGELOG.md | 1 + README.md | 97 +++++++++++++++ lib/grape/dsl/inside_route.rb | 61 +++++----- lib/grape/dsl/parameters.rb | 4 +- lib/grape/validations/params_scope.rb | 38 +++++- spec/grape/validations/params_scope_spec.rb | 126 ++++++++++++++++++++ spec/grape/validations_spec.rb | 26 ++-- 7 files changed, 309 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a158dd67..046bb8566 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ * [#2272](https://github.com/ruby-grape/grape/pull/2272): Added error on param init when provided type does not have `[]` coercion method, previously validation silently failed for any value - [@vasfed](https://github.com/Vasfed). * [#2274](https://github.com/ruby-grape/grape/pull/2274): Error middleware support using rack util's symbols as status - [@dhruvCW](https://github.com/dhruvCW). * [#2276](https://github.com/ruby-grape/grape/pull/2276): Fix exception super - [@ericproulx](https://github.com/ericproulx). +* [#2285](https://github.com/ruby-grape/grape/pull/2285): Added :evaluate_given to declared(params) - [@zysend](https://github.com/zysend). * Your contribution here. #### Fixes diff --git a/README.md b/README.md index cb6fc1417..031ee4084 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ - [Declared](#declared) - [Include Parent Namespaces](#include-parent-namespaces) - [Include Missing](#include-missing) + - [Evaluate Given](#evaluate-given) - [Parameter Validation and Coercion](#parameter-validation-and-coercion) - [Supported Parameter Types](#supported-parameter-types) - [Integer/Fixnum and Coercions](#integerfixnum-and-coercions) @@ -1078,6 +1079,102 @@ curl -X POST -H "Content-Type: application/json" localhost:9292/users/signup -d } ```` +### Evaluate Given + +By default `declared(params)` will not evaluate `given` and return all parameters. Use `evaluate_given` to evaluate all `given` blocks and return only parameters that satisfy `given` conditions. Consider the following API: + +````ruby +format :json + +params do + optional :child_id, type: Integer + given :child_id do + requires :father_id, type: Integer + end +end + +post 'child' do + { 'declared_params' => declared(params, evaluate_given: true) } +end +```` + +**Request** + +````bash +curl -X POST -H "Content-Type: application/json" localhost:9292/child -d '{"father_id": 1}' +```` + +**Response with evaluate_given:false** + +````json +{ + "declared_params": { + "child_id": null, + "father_id": 1 + } +} +```` + +**Response with evaluate_given:true** + +````json +{ + "declared_params": { + "child_id": null + } +} +```` + +It also works on nested hashes: + +````ruby +format :json + +params do + requires :child, type: Hash do + optional :child_id, type: Integer + given :child_id do + requires :father_id, type: Integer + end + end +end + +post 'child' do + { 'declared_params' => declared(params, evaluate_given: true) } +end +```` + +**Request** + +````bash +curl -X POST -H "Content-Type: application/json" localhost:9292/child -d '{"child": {"father_id": 1}}' +```` + +**Response with evaluate_given:false** + +````json +{ + "declared_params": { + "child": { + "child_id": null, + "father_id": 1 + } + } +} +```` + +**Response with evaluate_given:true** + +````json +{ + "declared_params": { + "child": { + "child_id": null + } + } +} +```` + ## Parameter Validation and Coercion You can define validations and coercion options for your parameters using a `params` block. diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index b440d0035..c8697ee7d 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -28,7 +28,7 @@ def self.post_filter_methods(type) # has completed module PostBeforeFilter def declared(passed_params, options = {}, declared_params = nil, params_nested_path = []) - options = options.reverse_merge(include_missing: true, include_parent_namespaces: true) + options = options.reverse_merge(include_missing: true, include_parent_namespaces: true, evaluate_given: false, request_params: passed_params) declared_params ||= optioned_declared_params(**options) if passed_params.is_a?(Array) @@ -47,41 +47,46 @@ def declared_array(passed_params, options, declared_params, params_nested_path) end def declared_hash(passed_params, options, declared_params, params_nested_path) - renamed_params = route_setting(:renamed_params) || {} + declared_params.each_with_object(passed_params.class.new) do |declared_param_attr, memo| + next if options[:evaluate_given] && !declared_param_attr.meets_dependency?(options[:request_params]) - 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) + declared_hash_attr(passed_params, options, declared_param_attr.key, params_nested_path, memo) + end + end - rename_path = params_nested_path + [declared_parent_param.to_s] - renamed_param_name = renamed_params[rename_path] + def declared_hash_attr(passed_params, options, declared_param, params_nested_path, memo) + renamed_params = route_setting(:renamed_params) || {} + 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) + + rename_path = params_nested_path + [declared_parent_param.to_s] + renamed_param_name = renamed_params[rename_path] - memo_key = optioned_param_key(renamed_param_name || declared_parent_param, options) - passed_children_params = passed_params[declared_parent_param] || passed_params.class.new + memo_key = optioned_param_key(renamed_param_name || declared_parent_param, options) + passed_children_params = passed_params[declared_parent_param] || passed_params.class.new - 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 + 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 - else - # If it is not a Hash then it does not have children. - # Find its value or set it to nil. - next unless options[:include_missing] || passed_params.key?(declared_param) + end + else + # If it is not a Hash then it does not have children. + # Find its value or set it to nil. + return unless options[:include_missing] || passed_params.key?(declared_param) - rename_path = params_nested_path + [declared_param.to_s] - renamed_param_name = renamed_params[rename_path] + rename_path = params_nested_path + [declared_param.to_s] + renamed_param_name = renamed_params[rename_path] - memo_key = optioned_param_key(renamed_param_name || declared_param, options) - passed_param = passed_params[declared_param] + memo_key = optioned_param_key(renamed_param_name || declared_param, options) + passed_param = passed_params[declared_param] - params_nested_path_dup = params_nested_path.dup - params_nested_path_dup << declared_param.to_s - memo[memo_key] = passed_param || handle_passed_param(params_nested_path_dup) do - passed_param - end + 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 diff --git a/lib/grape/dsl/parameters.rb b/lib/grape/dsl/parameters.rb index 31a7b34e7..f751fe557 100644 --- a/lib/grape/dsl/parameters.rb +++ b/lib/grape/dsl/parameters.rb @@ -217,8 +217,8 @@ def 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 + @declared_params.flatten.any? do |declared_param_attr| + first_hash_key_or_param(declared_param_attr.key) == param end end end diff --git a/lib/grape/validations/params_scope.rb b/lib/grape/validations/params_scope.rb index d9dbff469..5eb28cf7f 100644 --- a/lib/grape/validations/params_scope.rb +++ b/lib/grape/validations/params_scope.rb @@ -10,6 +10,41 @@ class ParamsScope include Grape::DSL::Parameters + class Attr + attr_accessor :key, :scope + + # Open up a new ParamsScope::Attr + # @param key [Hash, Symbol] key of attr + # @param scope [Grape::Validations::ParamsScope] scope of attr + def initialize(key, scope) + @key = key + @scope = scope + end + + def meets_dependency?(request_params) + return true if scope.nil? + + scope.meets_dependency?(scope.params(request_params), request_params) + end + + # @return Array[Symbol, Hash[Symbol => Array]] declared_params with symbol instead of Attr + def self.attrs_keys(declared_params) + declared_params.map do |declared_param_attr| + attr_key(declared_param_attr) + end + end + + def self.attr_key(declared_param_attr) + return attr_key(declared_param_attr.key) if declared_param_attr.is_a?(self) + + if declared_param_attr.is_a?(Hash) + declared_param_attr.transform_values { |value| attrs_keys(value) } + else + declared_param_attr + end + end + end + # Open up a new ParamsScope, allowing parameter definitions per # Grape::DSL::Params. # @param opts [Hash] options for this scope @@ -130,13 +165,14 @@ def required? # Adds a parameter declaration to our list of validations. # @param attrs [Array] (see Grape::DSL::Parameters#requires) def push_declared_params(attrs, **opts) + opts = opts.merge(declared_params_scope: self) unless opts.key?(:declared_params_scope) if lateral? @parent.push_declared_params(attrs, **opts) else push_renamed_param(full_path + [attrs.first], opts[:as]) \ if opts && opts[:as] - @declared_params.concat attrs + @declared_params.concat(attrs.map { |attr| ::Grape::Validations::ParamsScope::Attr.new(attr, opts[:declared_params_scope]) }) end end diff --git a/spec/grape/validations/params_scope_spec.rb b/spec/grape/validations/params_scope_spec.rb index 20f03f593..ed46b9ee9 100644 --- a/spec/grape/validations/params_scope_spec.rb +++ b/spec/grape/validations/params_scope_spec.rb @@ -664,6 +664,132 @@ def initialize(value) get '/nested', bar: { a: true, c: { b: 'yes' } } expect(JSON.parse(last_response.body)).to eq('bar' => { 'a' => 'true', 'c' => { 'b' => 'yes' } }) end + + context 'when the dependent parameter is not present #declared(params)' do + context 'lateral parameter' do + before do + [true, false].each do |evaluate_given| + subject.params do + optional :a + given :a do + optional :b + end + end + subject.get("/evaluate_given_#{evaluate_given}") { declared(params, evaluate_given: evaluate_given).to_json } + end + end + + it 'evaluate_given_false' do + get '/evaluate_given_false', b: 'b' + expect(JSON.parse(last_response.body)).to eq('a' => nil, 'b' => 'b') + end + + it 'evaluate_given_true' do + get '/evaluate_given_true', b: 'b' + expect(JSON.parse(last_response.body)).to eq('a' => nil) + end + end + + context 'nested parameter' do + before do + [true, false].each do |evaluate_given| + subject.params do + optional :a, values: %w[x y] + given a: ->(a) { a == 'x' } do + optional :b, type: Hash do + optional :c + end + optional :e + end + given a: ->(a) { a == 'y' } do + optional :b, type: Hash do + optional :d + end + optional :f + end + end + subject.get("/evaluate_given_#{evaluate_given}") { declared(params, evaluate_given: evaluate_given).to_json } + end + end + + it 'evaluate_given_false' do + get '/evaluate_given_false', a: 'x' + expect(JSON.parse(last_response.body)).to eq('a' => 'x', 'b' => { 'd' => nil }, 'e' => nil, 'f' => nil) + + get '/evaluate_given_false', a: 'y' + expect(JSON.parse(last_response.body)).to eq('a' => 'y', 'b' => { 'd' => nil }, 'e' => nil, 'f' => nil) + end + + it 'evaluate_given_true' do + get '/evaluate_given_true', a: 'x' + expect(JSON.parse(last_response.body)).to eq('a' => 'x', 'b' => { 'c' => nil }, 'e' => nil) + + get '/evaluate_given_true', a: 'y' + expect(JSON.parse(last_response.body)).to eq('a' => 'y', 'b' => { 'd' => nil }, 'f' => nil) + end + end + + context 'nested given parameter' do + before do + [true, false].each do |evaluate_given| + subject.params do + optional :a, values: %w[x y] + given a: ->(a) { a == 'x' } do + optional :b, type: Hash do + optional :c + given :c do + optional :g + optional :e, type: Hash do + optional :h + end + end + end + end + given a: ->(a) { a == 'y' } do + optional :b, type: Hash do + optional :d + given :d do + optional :f + optional :e, type: Hash do + optional :i + end + end + end + end + end + subject.get("/evaluate_given_#{evaluate_given}") { declared(params, evaluate_given: evaluate_given).to_json } + end + end + + it 'evaluate_given_false' do + get '/evaluate_given_false', a: 'x' + expect(JSON.parse(last_response.body)).to eq('a' => 'x', 'b' => { 'd' => nil, 'f' => nil, 'e' => { 'i' => nil } }) + + get '/evaluate_given_false', a: 'x', b: { c: 'c' } + expect(JSON.parse(last_response.body)).to eq('a' => 'x', 'b' => { 'd' => nil, 'f' => nil, 'e' => { 'i' => nil } }) + + get '/evaluate_given_false', a: 'y' + expect(JSON.parse(last_response.body)).to eq('a' => 'y', 'b' => { 'd' => nil, 'f' => nil, 'e' => { 'i' => nil } }) + + get '/evaluate_given_false', a: 'y', b: { d: 'd' } + expect(JSON.parse(last_response.body)).to eq('a' => 'y', 'b' => { 'd' => 'd', 'f' => nil, 'e' => { 'i' => nil } }) + end + + it 'evaluate_given_true' do + get '/evaluate_given_true', a: 'x' + expect(JSON.parse(last_response.body)).to eq('a' => 'x', 'b' => { 'c' => nil }) + + get '/evaluate_given_true', a: 'x', b: { c: 'c' } + expect(JSON.parse(last_response.body)).to eq('a' => 'x', 'b' => { 'c' => 'c', 'g' => nil, 'e' => { 'h' => nil } }) + + get '/evaluate_given_true', a: 'y' + expect(JSON.parse(last_response.body)).to eq('a' => 'y', 'b' => { 'd' => nil }) + + get '/evaluate_given_true', a: 'y', b: { d: 'd' } + expect(JSON.parse(last_response.body)).to eq('a' => 'y', 'b' => { 'd' => 'd', 'f' => nil, 'e' => { 'i' => nil } }) + end + end + end end context 'default value in given block' do diff --git a/spec/grape/validations_spec.rb b/spec/grape/validations_spec.rb index 1ad4585d2..2afcecb24 100644 --- a/spec/grape/validations_spec.rb +++ b/spec/grape/validations_spec.rb @@ -43,7 +43,7 @@ def declared_params subject.params do optional :some_param end - expect(declared_params).to eq([:some_param]) + expect(Grape::Validations::ParamsScope::Attr.attrs_keys(declared_params)).to eq([:some_param]) end end @@ -63,7 +63,7 @@ def define_optional_using it 'adds entity documentation to declared params' do define_optional_using - expect(declared_params).to eq(%i[field_a field_b]) + expect(Grape::Validations::ParamsScope::Attr.attrs_keys(declared_params)).to eq(%i[field_a field_b]) end it 'works when field_a and field_b are not present' do @@ -110,7 +110,7 @@ def define_optional_using subject.params do requires :some_param end - expect(declared_params).to eq([:some_param]) + expect(Grape::Validations::ParamsScope::Attr.attrs_keys(declared_params)).to eq([:some_param]) end it 'works when required field is present but nil' do @@ -195,7 +195,7 @@ def define_requires_all it 'adds entity documentation to declared params' do define_requires_all - expect(declared_params).to eq(%i[required_field optional_field]) + expect(Grape::Validations::ParamsScope::Attr.attrs_keys(declared_params)).to eq(%i[required_field optional_field]) end it 'errors when required_field is not present' do @@ -230,7 +230,7 @@ def define_requires_none it 'adds entity documentation to declared params' do define_requires_none - expect(declared_params).to eq(%i[required_field optional_field]) + expect(Grape::Validations::ParamsScope::Attr.attrs_keys(declared_params)).to eq(%i[required_field optional_field]) end it 'errors when required_field is not present' do @@ -260,7 +260,7 @@ def define_requires_all it 'adds only the entity documentation to declared params, nothing more' do define_requires_all - expect(declared_params).to eq(%i[required_field optional_field]) + expect(Grape::Validations::ParamsScope::Attr.attrs_keys(declared_params)).to eq(%i[required_field optional_field]) end end @@ -326,7 +326,7 @@ def define_requires_none requires :key end end - expect(declared_params).to eq([items: [:key]]) + expect(Grape::Validations::ParamsScope::Attr.attrs_keys(declared_params)).to eq([items: [:key]]) end end @@ -398,7 +398,7 @@ def define_requires_none requires :key end end - expect(declared_params).to eq([items: [:key]]) + expect(Grape::Validations::ParamsScope::Attr.attrs_keys(declared_params)).to eq([items: [:key]]) end end @@ -461,7 +461,7 @@ def define_requires_none requires :key end end - expect(declared_params).to eq([items: [:key]]) + expect(Grape::Validations::ParamsScope::Attr.attrs_keys(declared_params)).to eq([items: [:key]]) end end @@ -822,7 +822,7 @@ def validate_param!(attr_name, params) requires :key end end - expect(declared_params).to eq([items: [:key]]) + expect(Grape::Validations::ParamsScope::Attr.attrs_keys(declared_params)).to eq([items: [:key]]) end end @@ -886,7 +886,7 @@ def validate_param!(attr_name, params) requires(:required_subitems, type: Array) { requires :value } end end - expect(declared_params).to eq([items: [:key, { optional_subitems: [:value] }, { required_subitems: [:value] }]]) + expect(Grape::Validations::ParamsScope::Attr.attrs_keys(declared_params)).to eq([items: [:key, { optional_subitems: [:value] }, { required_subitems: [:value] }]]) end context <<~DESC do @@ -1426,14 +1426,14 @@ def validate_param!(attr_name, params) subject.params do use :pagination end - expect(declared_params).to eq %i[page per_page] + expect(Grape::Validations::ParamsScope::Attr.attrs_keys(declared_params)).to eq %i[page per_page] end it 'by #use with multiple params' do subject.params do use :pagination, :period end - expect(declared_params).to eq %i[page per_page start_date end_date] + expect(Grape::Validations::ParamsScope::Attr.attrs_keys(declared_params)).to eq %i[page per_page start_date end_date] end end From f8aaaec8d18f754f84b611161320a159618aed5d Mon Sep 17 00:00:00 2001 From: zysend <92354009+zysend@users.noreply.github.com> Date: Fri, 25 Nov 2022 04:33:50 +0800 Subject: [PATCH 127/304] fix evaluate given in array (#2287) * fix evaluate given in array * changelog * avoid a bug that may be caused by rack-test by modifying spec * changelog, spec * meets_dependency will use params directly instead of @parent.params(request_params) when request_params is not given * create attr_meets_dependency instand of changing meets_dependency to more complex --- CHANGELOG.md | 2 +- lib/grape/dsl/inside_route.rb | 4 +- lib/grape/validations/params_scope.rb | 18 +- spec/grape/validations/params_scope_spec.rb | 189 +++++++++++++++++++- 4 files changed, 202 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 046bb8566..670f15d3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ * [#2272](https://github.com/ruby-grape/grape/pull/2272): Added error on param init when provided type does not have `[]` coercion method, previously validation silently failed for any value - [@vasfed](https://github.com/Vasfed). * [#2274](https://github.com/ruby-grape/grape/pull/2274): Error middleware support using rack util's symbols as status - [@dhruvCW](https://github.com/dhruvCW). * [#2276](https://github.com/ruby-grape/grape/pull/2276): Fix exception super - [@ericproulx](https://github.com/ericproulx). -* [#2285](https://github.com/ruby-grape/grape/pull/2285): Added :evaluate_given to declared(params) - [@zysend](https://github.com/zysend). +* [#2285](https://github.com/ruby-grape/grape/pull/2285), [#2287](https://github.com/ruby-grape/grape/pull/2287): Added :evaluate_given to declared(params) - [@zysend](https://github.com/zysend). * Your contribution here. #### Fixes diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index c8697ee7d..fb72d0898 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -28,7 +28,7 @@ def self.post_filter_methods(type) # has completed module PostBeforeFilter def declared(passed_params, options = {}, declared_params = nil, params_nested_path = []) - options = options.reverse_merge(include_missing: true, include_parent_namespaces: true, evaluate_given: false, request_params: passed_params) + options = options.reverse_merge(include_missing: true, include_parent_namespaces: true, evaluate_given: false) declared_params ||= optioned_declared_params(**options) if passed_params.is_a?(Array) @@ -48,7 +48,7 @@ def declared_array(passed_params, options, declared_params, params_nested_path) def declared_hash(passed_params, options, declared_params, params_nested_path) declared_params.each_with_object(passed_params.class.new) do |declared_param_attr, memo| - next if options[:evaluate_given] && !declared_param_attr.meets_dependency?(options[:request_params]) + next if options[:evaluate_given] && !declared_param_attr.scope.attr_meets_dependency?(passed_params) declared_hash_attr(passed_params, options, declared_param_attr.key, params_nested_path, memo) end diff --git a/lib/grape/validations/params_scope.rb b/lib/grape/validations/params_scope.rb index 5eb28cf7f..ae4d91d18 100644 --- a/lib/grape/validations/params_scope.rb +++ b/lib/grape/validations/params_scope.rb @@ -21,12 +21,6 @@ def initialize(key, scope) @scope = scope end - def meets_dependency?(request_params) - return true if scope.nil? - - scope.meets_dependency?(scope.params(request_params), request_params) - end - # @return Array[Symbol, Hash[Symbol => Array]] declared_params with symbol instead of Attr def self.attrs_keys(declared_params) declared_params.map do |declared_param_attr| @@ -101,6 +95,18 @@ def meets_dependency?(params, request_params) return params.any? { |param| meets_dependency?(param, request_params) } if params.is_a?(Array) + meets_hash_dependency?(params) + end + + def attr_meets_dependency?(params) + return true unless @dependent_on + + return false if @parent.present? && !@parent.attr_meets_dependency?(params) + + meets_hash_dependency?(params) + end + + def meets_hash_dependency?(params) # params might be anything what looks like a hash, so it must implement a `key?` method return false unless params.respond_to?(:key?) diff --git a/spec/grape/validations/params_scope_spec.rb b/spec/grape/validations/params_scope_spec.rb index ed46b9ee9..05fa9c265 100644 --- a/spec/grape/validations/params_scope_spec.rb +++ b/spec/grape/validations/params_scope_spec.rb @@ -690,7 +690,7 @@ def initialize(value) end end - context 'nested parameter' do + context 'lateral hash parameter' do before do [true, false].each do |evaluate_given| subject.params do @@ -729,7 +729,7 @@ def initialize(value) end end - context 'nested given parameter' do + context 'lateral parameter within lateral hash parameter' do before do [true, false].each do |evaluate_given| subject.params do @@ -789,6 +789,191 @@ def initialize(value) expect(JSON.parse(last_response.body)).to eq('a' => 'y', 'b' => { 'd' => 'd', 'f' => nil, 'e' => { 'i' => nil } }) end end + + context 'lateral parameter within an array param' do + before do + [true, false].each do |evaluate_given| + subject.params do + optional :array, type: Array do + optional :a + given :a do + optional :b + end + end + end + subject.post("/evaluate_given_#{evaluate_given}") do + declared(params, evaluate_given: evaluate_given).to_json + end + end + end + + it 'evaluate_given_false' do + post '/evaluate_given_false', { array: [{ b: 'b' }, { a: 'a', b: 'b' }] }.to_json, 'CONTENT_TYPE' => 'application/json' + expect(JSON.parse(last_response.body)).to eq('array' => [{ 'a' => nil, 'b' => 'b' }, { 'a' => 'a', 'b' => 'b' }]) + end + + it 'evaluate_given_true' do + post '/evaluate_given_true', { array: [{ b: 'b' }, { a: 'a', b: 'b' }] }.to_json, 'CONTENT_TYPE' => 'application/json' + expect(JSON.parse(last_response.body)).to eq('array' => [{ 'a' => nil }, { 'a' => 'a', 'b' => 'b' }]) + end + end + + context 'nested given parameter' do + before do + [true, false].each do |evaluate_given| + subject.params do + optional :a + optional :c + given :a do + given :c do + optional :b + end + end + end + subject.post("/evaluate_given_#{evaluate_given}") do + declared(params, evaluate_given: evaluate_given).to_json + end + end + end + + it 'evaluate_given_false' do + post '/evaluate_given_false', { a: 'a', b: 'b' }.to_json, 'CONTENT_TYPE' => 'application/json' + expect(JSON.parse(last_response.body)).to eq('a' => 'a', 'b' => 'b', 'c' => nil) + + post '/evaluate_given_false', { c: 'c', b: 'b' }.to_json, 'CONTENT_TYPE' => 'application/json' + expect(JSON.parse(last_response.body)).to eq('a' => nil, 'b' => 'b', 'c' => 'c') + + post '/evaluate_given_false', { a: 'a', c: 'c', b: 'b' }.to_json, 'CONTENT_TYPE' => 'application/json' + expect(JSON.parse(last_response.body)).to eq('a' => 'a', 'b' => 'b', 'c' => 'c') + end + + it 'evaluate_given_true' do + post '/evaluate_given_true', { a: 'a', b: 'b' }.to_json, 'CONTENT_TYPE' => 'application/json' + expect(JSON.parse(last_response.body)).to eq('a' => 'a', 'c' => nil) + + post '/evaluate_given_true', { c: 'c', b: 'b' }.to_json, 'CONTENT_TYPE' => 'application/json' + expect(JSON.parse(last_response.body)).to eq('a' => nil, 'c' => 'c') + + post '/evaluate_given_true', { a: 'a', c: 'c', b: 'b' }.to_json, 'CONTENT_TYPE' => 'application/json' + expect(JSON.parse(last_response.body)).to eq('a' => 'a', 'b' => 'b', 'c' => 'c') + end + end + + context 'nested given parameter within an array param' do + before do + [true, false].each do |evaluate_given| + subject.params do + optional :array, type: Array do + optional :a + optional :c + given :a do + given :c do + optional :b + end + end + end + end + subject.post("/evaluate_given_#{evaluate_given}") do + declared(params, evaluate_given: evaluate_given).to_json + end + end + end + + let :evaluate_given_params do + { + array: [ + { a: 'a', b: 'b' }, + { c: 'c', b: 'b' }, + { a: 'a', c: 'c', b: 'b' } + ] + } + end + + it 'evaluate_given_false' do + post '/evaluate_given_false', evaluate_given_params.to_json, 'CONTENT_TYPE' => 'application/json' + expect(JSON.parse(last_response.body)).to eq('array' => [{ 'a' => 'a', 'b' => 'b', 'c' => nil }, { 'a' => nil, 'b' => 'b', 'c' => 'c' }, { 'a' => 'a', 'b' => 'b', 'c' => 'c' }]) + end + + it 'evaluate_given_true' do + post '/evaluate_given_true', evaluate_given_params.to_json, 'CONTENT_TYPE' => 'application/json' + expect(JSON.parse(last_response.body)).to eq('array' => [{ 'a' => 'a', 'c' => nil }, { 'a' => nil, 'c' => 'c' }, { 'a' => 'a', 'b' => 'b', 'c' => 'c' }]) + end + end + + context 'nested given parameter within a nested given parameter within an array param' do + before do + [true, false].each do |evaluate_given| + subject.params do + optional :array, type: Array do + optional :a + optional :c + given :a do + given :c do + optional :array, type: Array do + optional :a + optional :c + given :a do + given :c do + optional :b + end + end + end + end + end + end + end + subject.post("/evaluate_given_#{evaluate_given}") do + declared(params, evaluate_given: evaluate_given).to_json + end + end + end + + let :evaluate_given_params do + { + array: [{ + a: 'a', + c: 'c', + array: [ + { a: 'a', b: 'b' }, + { c: 'c', b: 'b' }, + { a: 'a', c: 'c', b: 'b' } + ] + }] + } + end + + it 'evaluate_given_false' do + expected_response_hash = { + 'array' => [{ + 'a' => 'a', + 'c' => 'c', + 'array' => [ + { 'a' => 'a', 'b' => 'b', 'c' => nil }, + { 'a' => nil, 'c' => 'c', 'b' => 'b' }, + { 'a' => 'a', 'c' => 'c', 'b' => 'b' } + ] + }] + } + post '/evaluate_given_false', evaluate_given_params.to_json, 'CONTENT_TYPE' => 'application/json' + expect(JSON.parse(last_response.body)).to eq(expected_response_hash) + end + + it 'evaluate_given_true' do + expected_response_hash = { + 'array' => [{ + 'a' => 'a', + 'c' => 'c', + 'array' => [ + { 'a' => 'a', 'c' => nil }, + { 'a' => nil, 'c' => 'c' }, + { 'a' => 'a', 'b' => 'b', 'c' => 'c' } + ] + }] + } + post '/evaluate_given_true', evaluate_given_params.to_json, 'CONTENT_TYPE' => 'application/json' + expect(JSON.parse(last_response.body)).to eq(expected_response_hash) + end + end end end From 6ec6568fa65361a6aa3e24e70bf6ed82e44af45e Mon Sep 17 00:00:00 2001 From: "Daniel (dB.) Doubrovkine" Date: Tue, 20 Dec 2022 10:16:47 -0500 Subject: [PATCH 128/304] Rotate Danger token. --- .github/workflows/danger.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml index 040beda21..7a04b4409 100644 --- a/.github/workflows/danger.yml +++ b/.github/workflows/danger.yml @@ -15,7 +15,6 @@ jobs: bundler-cache: true - name: Run Danger run: | - # the token is public, this is ok - TOKEN='b8b19daa0ade737762c' - TOKEN+='f35edcb328642d371ce86' + # the token is public, has public_repo scope and belongs to the grape-bot user owned by @dblock, this is ok + TOKEN=$(echo -n Z2hwX2lYb0dPNXNyejYzOFJyaTV3QUxUdkNiS1dtblFwZTFuRXpmMwo= | base64 --decode) DANGER_GITHUB_API_TOKEN=$TOKEN bundle exec danger --verbose From 0922736852ce3e319fc61b505c5421e4959266b0 Mon Sep 17 00:00:00 2001 From: "Daniel (dB.) Doubrovkine" Date: Tue, 20 Dec 2022 10:23:42 -0500 Subject: [PATCH 129/304] Preparing for release, 1.7.0. --- CHANGELOG.md | 4 +--- README.md | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 670f15d3a..c53ffe1a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -### 1.7.0 (Next) +### 1.7.0 (2022/12/20) #### Features @@ -13,7 +13,6 @@ * [#2274](https://github.com/ruby-grape/grape/pull/2274): Error middleware support using rack util's symbols as status - [@dhruvCW](https://github.com/dhruvCW). * [#2276](https://github.com/ruby-grape/grape/pull/2276): Fix exception super - [@ericproulx](https://github.com/ericproulx). * [#2285](https://github.com/ruby-grape/grape/pull/2285), [#2287](https://github.com/ruby-grape/grape/pull/2287): Added :evaluate_given to declared(params) - [@zysend](https://github.com/zysend). -* Your contribution here. #### Fixes @@ -28,7 +27,6 @@ * [#2256](https://github.com/ruby-grape/grape/pull/2256): Raise `Grape::Exceptions::MultipartPartLimitError` from Rack when too many files are uploaded - [@bschmeck](https://github.com/bschmeck). * [#2266](https://github.com/ruby-grape/grape/pull/2266): Fix code coverage - [@duffn](https://github.com/duffn). * [#2284](https://github.com/ruby-grape/grape/pull/2284): Fix an unexpected backtick - [@zysend](https://github.com/zysend). -* Your contribution here. ### 1.6.2 (2021/12/30) diff --git a/README.md b/README.md index 031ee4084..d3e853d8d 100644 --- a/README.md +++ b/README.md @@ -159,9 +159,8 @@ content negotiation, versioning and much more. ## Stable Release -You're reading the documentation for the next release of Grape, which should be **1.7.0**. +You're reading the documentation for the stable release of Grape, 1.7.0. Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. -The current stable release is [1.6.2](https://github.com/ruby-grape/grape/blob/v1.6.2/README.md). ## Project Resources From 095c6e814696b4225d3f63835102f622d94fa371 Mon Sep 17 00:00:00 2001 From: "Daniel (dB.) Doubrovkine" Date: Tue, 20 Dec 2022 10:25:25 -0500 Subject: [PATCH 130/304] Preparing for next development iteration, 1.7.1. --- CHANGELOG.md | 10 ++++++++++ README.md | 3 ++- lib/grape/version.rb | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c53ffe1a2..271ce7e33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +### 1.7.1 (Next) + +#### Features + +* Your contribution here. + +#### Fixes + +* Your contribution here. + ### 1.7.0 (2022/12/20) #### Features diff --git a/README.md b/README.md index d3e853d8d..185098466 100644 --- a/README.md +++ b/README.md @@ -159,8 +159,9 @@ content negotiation, versioning and much more. ## Stable Release -You're reading the documentation for the stable release of Grape, 1.7.0. +You're reading the documentation for the next release of Grape, which should be **1.7.1**. Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. +The current stable release is [1.7.0](https://github.com/ruby-grape/grape/blob/v1.7.0/README.md). ## Project Resources diff --git a/lib/grape/version.rb b/lib/grape/version.rb index 3a0817e8d..54cc42080 100644 --- a/lib/grape/version.rb +++ b/lib/grape/version.rb @@ -2,5 +2,5 @@ module Grape # The current version of Grape. - VERSION = '1.7.0' + VERSION = '1.7.1' end From 5e8cd2a10007f28d4233eb94b689a013c39aa3f6 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Thu, 22 Dec 2022 06:15:23 +0100 Subject: [PATCH 131/304] Update rubocop to 1.41.0 (#2288) * Update rubocop to 1.40.0 * Update rubocop_todo * Rotate Danger token. * Preparing for release, 1.7.0. * Preparing for next development iteration, 1.7.1. * rubocop 1.41.0 * Drop support to ruby 2.5 Update actions/checkout to v3 * Add drop support in CHANGELOG.md * Rerun rubocop --auto-gen-config * Split CHANGELOG lines. * Attempt to trigger on all pull_request. * Increase fetch depth. Co-authored-by: Daniel (dB.) Doubrovkine --- .github/workflows/danger.yml | 10 ++- .github/workflows/edge.yml | 2 +- .github/workflows/test.yml | 5 +- .rubocop.yml | 2 +- .rubocop_todo.yml | 163 +++++++++++++++++++++++++++-------- CHANGELOG.md | 2 + Gemfile | 2 +- gemfiles/multi_json.gemfile | 2 +- gemfiles/multi_xml.gemfile | 2 +- gemfiles/rack1.gemfile | 2 +- gemfiles/rack2.gemfile | 2 +- gemfiles/rack2_2.gemfile | 2 +- gemfiles/rack_edge.gemfile | 2 +- gemfiles/rails_5.gemfile | 2 +- gemfiles/rails_6.gemfile | 2 +- gemfiles/rails_6_1.gemfile | 2 +- gemfiles/rails_7.gemfile | 2 +- gemfiles/rails_edge.gemfile | 2 +- grape.gemspec | 2 +- 19 files changed, 153 insertions(+), 57 deletions(-) diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml index 7a04b4409..380f5daa8 100644 --- a/.github/workflows/danger.yml +++ b/.github/workflows/danger.yml @@ -1,13 +1,15 @@ --- name: danger -on: - pull_request: - types: [opened, reopened, edited, synchronize] + +on: pull_request + jobs: danger: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 + with: + fetch-depth: 100 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: diff --git a/.github/workflows/edge.yml b/.github/workflows/edge.yml index 57573a7b5..9055f2f59 100644 --- a/.github/workflows/edge.yml +++ b/.github/workflows/edge.yml @@ -23,7 +23,7 @@ jobs: BUNDLE_GEMFILE: ${{ matrix.gemfile }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Ruby uses: ruby/setup-ruby@v1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 66336200b..1fb5251af 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: name: RuboCop runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: @@ -25,7 +25,6 @@ jobs: fail-fast: false matrix: ruby: - - 2.5 - 2.6 - 2.7 - "3.0" @@ -61,7 +60,7 @@ jobs: BUNDLE_GEMFILE: ${{ matrix.gemfile }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Ruby uses: ruby/setup-ruby@v1 diff --git a/.rubocop.yml b/.rubocop.yml index 86493d132..acc0555ca 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,6 +1,6 @@ AllCops: NewCops: enable - TargetRubyVersion: 2.5 + TargetRubyVersion: 2.6 SuggestExtensions: false Exclude: - vendor/**/* diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 27d504550..406fa6e66 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,21 +1,29 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2022-01-02 10:41:35 UTC using RuboCop version 1.23.0. +# on 2022-12-21 16:30:41 UTC using RuboCop version 1.41.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. # Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: Include. +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: Severity, Include. +# Include: **/*.gemspec +Gemspec/DeprecatedAttributeAssignment: + Exclude: + - 'grape.gemspec' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: Severity, Include. # Include: **/*.gemspec Gemspec/RequireMFA: Exclude: - 'grape.gemspec' # Offense count: 1 -# Configuration parameters: IgnoredMethods. +# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods. Lint/AmbiguousBlockAssociation: Exclude: - 'spec/grape/dsl/routing_spec.rb' @@ -47,40 +55,39 @@ Lint/EmptyClass: - 'spec/grape/entity_spec.rb' - 'spec/grape/middleware/stack_spec.rb' -# Offense count: 7 +# Offense count: 6 Lint/MissingSuper: Exclude: - '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: 43 -# Configuration parameters: IgnoredMethods, CountRepeatedAttributes. +# Offense count: 41 +# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods, CountRepeatedAttributes. Metrics/AbcSize: Max: 43 -# Offense count: 6 -# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. -# IgnoredMethods: refine +# Offense count: 1 +# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, AllowedMethods, AllowedPatterns, IgnoredMethods, inherit_mode. +# AllowedMethods: refine Metrics/BlockLength: - Max: 182 + Max: 27 # Offense count: 9 # Configuration parameters: CountComments, CountAsOne. Metrics/ClassLength: - Max: 298 + Max: 295 -# Offense count: 30 -# Configuration parameters: IgnoredMethods. +# Offense count: 28 +# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods. Metrics/CyclomaticComplexity: Max: 15 # Offense count: 68 -# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. +# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, AllowedMethods, AllowedPatterns, IgnoredMethods. Metrics/MethodLength: Max: 32 @@ -94,8 +101,8 @@ Metrics/ModuleLength: Metrics/ParameterLists: MaxOptionalParameters: 4 -# Offense count: 27 -# Configuration parameters: IgnoredMethods. +# Offense count: 25 +# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods. Metrics/PerceivedComplexity: Max: 15 @@ -111,7 +118,7 @@ Naming/MemoizedInstanceVariableName: # Offense count: 5 # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. -# AllowedNames: at, by, db, id, in, io, ip, of, on, os, pp, to +# AllowedNames: as, at, by, cc, db, id, if, in, io, ip, of, on, os, pp, to Naming/MethodParameterName: Exclude: - 'lib/grape/endpoint.rb' @@ -120,7 +127,7 @@ Naming/MethodParameterName: - 'spec/grape/api_spec.rb' # Offense count: 18 -# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers. +# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns. # SupportedStyles: snake_case, normalcase, non_integer # AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339 Naming/VariableNumber: @@ -147,8 +154,18 @@ RSpec/AnyInstance: - 'spec/grape/api_spec.rb' - 'spec/grape/middleware/base_spec.rb' -# Offense count: 332 -# Configuration parameters: Prefixes. +# Offense count: 5 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: be_a, be_kind_of +RSpec/ClassCheck: + Exclude: + - 'spec/grape/api_spec.rb' + - 'spec/grape/endpoint_spec.rb' + - 'spec/grape/middleware/base_spec.rb' + +# Offense count: 345 +# Configuration parameters: Prefixes, AllowedPatterns. # Prefixes: when, with, without RSpec/ContextWording: Enabled: false @@ -167,19 +184,29 @@ RSpec/DescribeClass: - 'spec/grape/validations/instance_behaivour_spec.rb' # Offense count: 3 +# This cop supports unsafe autocorrection (--autocorrect-all). RSpec/EmptyExampleGroup: Exclude: - 'spec/grape/api_spec.rb' - 'spec/grape/dsl/configuration_spec.rb' - 'spec/grape/validations/attributes_iterator_spec.rb' -# Offense count: 499 +# Offense count: 507 # Configuration parameters: CountAsOne. RSpec/ExampleLength: Max: 57 +# Offense count: 2 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: CustomTransform, IgnoredWords, DisallowedExamples. +# DisallowedExamples: works +RSpec/ExampleWording: + Exclude: + - 'spec/grape/integration/global_namespace_function_spec.rb' + - 'spec/grape/validations_spec.rb' + # Offense count: 7 -# Cop supports --auto-correct. +# This cop supports safe autocorrection (--autocorrect). RSpec/ExpectActual: Exclude: - 'spec/routing/**/*' @@ -192,7 +219,7 @@ RSpec/ExpectInHook: - 'spec/grape/api_spec.rb' - 'spec/grape/validations/validators/values_spec.rb' -# Offense count: 41 +# Offense count: 43 # Configuration parameters: Include, CustomTransform, IgnoreMethods, SpecSuffixOnly. # Include: **/*_spec*rb*, **/spec/**/* RSpec/FilePath: @@ -233,7 +260,7 @@ RSpec/MessageChain: Exclude: - 'spec/grape/middleware/formatter_spec.rb' -# Offense count: 135 +# Offense count: 138 # Configuration parameters: . # SupportedStyles: have_received, receive RSpec/MessageSpies: @@ -244,24 +271,42 @@ RSpec/MissingExampleGroupArgument: Exclude: - 'spec/grape/middleware/exception_spec.rb' -# Offense count: 755 +# Offense count: 766 RSpec/MultipleExpectations: Max: 16 -# Offense count: 32 +# Offense count: 38 # Configuration parameters: AllowSubject. RSpec/MultipleMemoizedHelpers: Max: 10 -# Offense count: 2116 -# Configuration parameters: IgnoreSharedExamples. +# Offense count: 2145 +# Configuration parameters: EnforcedStyle, IgnoreSharedExamples. +# SupportedStyles: always, named_only RSpec/NamedSubject: Enabled: false -# Offense count: 161 +# Offense count: 171 +# Configuration parameters: AllowedGroups. RSpec/NestedGroups: Max: 6 +# Offense count: 18 +# Configuration parameters: AllowedPatterns. +# AllowedPatterns: ^expect_, ^assert_ +RSpec/NoExpectationExample: + Exclude: + - 'spec/grape/api_remount_spec.rb' + - 'spec/grape/api_spec.rb' + - 'spec/grape/entity_spec.rb' + - 'spec/grape/validations_spec.rb' + +# Offense count: 6 +# This cop supports unsafe autocorrection (--autocorrect-all). +RSpec/Rails/HaveHttpStatus: + Exclude: + - 'spec/grape/api_spec.rb' + # Offense count: 12 RSpec/RepeatedDescription: Exclude: @@ -343,11 +388,28 @@ Style/CombinableLoops: - 'spec/grape/endpoint_spec.rb' # Offense count: 2 -# Configuration parameters: MaxUnannotatedPlaceholdersAllowed, IgnoredMethods. +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: MaxUnannotatedPlaceholdersAllowed, AllowedMethods, AllowedPatterns, IgnoredMethods. # SupportedStyles: annotated, template, unannotated Style/FormatStringToken: EnforcedStyle: template +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, DirectiveCapitalization, ValueCapitalization. +# SupportedStyles: snake_case, kebab_case +# SupportedCapitalizations: lowercase, uppercase +Style/MagicCommentFormat: + Exclude: + - 'lib/grape/util/cache.rb' + +# Offense count: 3 +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/MapToHash: + Exclude: + - 'lib/grape/dsl/request_response.rb' + - 'spec/grape/endpoint_spec.rb' + # Offense count: 12 # Configuration parameters: AllowedMethods. # AllowedMethods: respond_to_missing? @@ -365,9 +427,40 @@ Style/OptionalBooleanParameter: - 'lib/grape/validations/types/primitive_coercer.rb' - 'lib/grape/validations/types/set_coercer.rb' -# Offense count: 144 -# Cop supports --auto-correct. -# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. +# Offense count: 28 +# This cop supports safe autocorrection (--autocorrect). +Style/RedundantConstantBase: + Exclude: + - 'spec/grape/api/invalid_format_spec.rb' + - 'spec/grape/api_spec.rb' + - 'spec/grape/dsl/logger_spec.rb' + - 'spec/grape/endpoint/declared_spec.rb' + - 'spec/grape/endpoint_spec.rb' + - 'spec/grape/middleware/formatter_spec.rb' + - 'spec/grape/validations/validators/coerce_spec.rb' + - 'spec/grape/validations/validators/default_spec.rb' + - 'spec/integration/multi_json/json_spec.rb' + - 'spec/integration/multi_xml/xml_spec.rb' + +# Offense count: 2 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength. +# AllowedMethods: present?, blank?, presence, try, try! +Style/SafeNavigation: + Exclude: + - 'lib/grape/endpoint.rb' + +# Offense count: 3 +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/SlicingWithRange: + Exclude: + - 'lib/grape/dsl/inside_route.rb' + - 'lib/grape/request.rb' + - 'lib/grape/router/attribute_translator.rb' + +# Offense count: 168 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, IgnoredPatterns. # URISchemes: http, https Layout/LineLength: Max: 215 diff --git a/CHANGELOG.md b/CHANGELOG.md index 271ce7e33..45bbeeb62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ #### Features +* [#2288](https://github.com/ruby-grape/grape/pull/2288): Droped support for Ruby 2.5 - [@ericproulx](https://github.com/ericproulx). +* [#2288](https://github.com/ruby-grape/grape/pull/2288): Updated rubocop to 1.41.0 - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/Gemfile b/Gemfile index acc721aa0..4adf9b7e3 100644 --- a/Gemfile +++ b/Gemfile @@ -10,7 +10,7 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '1.25.1' + gem 'rubocop', '1.41.0' gem 'rubocop-ast' gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false diff --git a/gemfiles/multi_json.gemfile b/gemfiles/multi_json.gemfile index 2b45868af..11941e7ec 100644 --- a/gemfiles/multi_json.gemfile +++ b/gemfiles/multi_json.gemfile @@ -10,7 +10,7 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '1.25.1' + gem 'rubocop', '1.41.0' gem 'rubocop-ast' gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false diff --git a/gemfiles/multi_xml.gemfile b/gemfiles/multi_xml.gemfile index 8d98df33e..059f944e0 100644 --- a/gemfiles/multi_xml.gemfile +++ b/gemfiles/multi_xml.gemfile @@ -10,7 +10,7 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '1.25.1' + gem 'rubocop', '1.41.0' gem 'rubocop-ast' gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false diff --git a/gemfiles/rack1.gemfile b/gemfiles/rack1.gemfile index 7e2907aa3..f05604345 100644 --- a/gemfiles/rack1.gemfile +++ b/gemfiles/rack1.gemfile @@ -10,7 +10,7 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '1.25.1' + gem 'rubocop', '1.41.0' gem 'rubocop-ast' gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false diff --git a/gemfiles/rack2.gemfile b/gemfiles/rack2.gemfile index c0141b6a3..d364e4084 100644 --- a/gemfiles/rack2.gemfile +++ b/gemfiles/rack2.gemfile @@ -10,7 +10,7 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '1.25.1' + gem 'rubocop', '1.41.0' gem 'rubocop-ast' gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false diff --git a/gemfiles/rack2_2.gemfile b/gemfiles/rack2_2.gemfile index ed24bfc6d..cf4f1081c 100644 --- a/gemfiles/rack2_2.gemfile +++ b/gemfiles/rack2_2.gemfile @@ -10,7 +10,7 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '1.25.1' + gem 'rubocop', '1.41.0' gem 'rubocop-ast' gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false diff --git a/gemfiles/rack_edge.gemfile b/gemfiles/rack_edge.gemfile index 411d05d9e..a00083043 100644 --- a/gemfiles/rack_edge.gemfile +++ b/gemfiles/rack_edge.gemfile @@ -10,7 +10,7 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '1.25.1' + gem 'rubocop', '1.41.0' gem 'rubocop-ast' gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false diff --git a/gemfiles/rails_5.gemfile b/gemfiles/rails_5.gemfile index 198c72175..8148083aa 100644 --- a/gemfiles/rails_5.gemfile +++ b/gemfiles/rails_5.gemfile @@ -10,7 +10,7 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '1.25.1' + gem 'rubocop', '1.41.0' gem 'rubocop-ast' gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false diff --git a/gemfiles/rails_6.gemfile b/gemfiles/rails_6.gemfile index f64eb4b5f..7b088fcf7 100644 --- a/gemfiles/rails_6.gemfile +++ b/gemfiles/rails_6.gemfile @@ -10,7 +10,7 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '1.25.1' + gem 'rubocop', '1.41.0' gem 'rubocop-ast' gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false diff --git a/gemfiles/rails_6_1.gemfile b/gemfiles/rails_6_1.gemfile index d21aee37c..7962e1b59 100644 --- a/gemfiles/rails_6_1.gemfile +++ b/gemfiles/rails_6_1.gemfile @@ -10,7 +10,7 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '1.25.1' + gem 'rubocop', '1.41.0' gem 'rubocop-ast' gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false diff --git a/gemfiles/rails_7.gemfile b/gemfiles/rails_7.gemfile index 3245fe8ac..468d498d2 100644 --- a/gemfiles/rails_7.gemfile +++ b/gemfiles/rails_7.gemfile @@ -10,7 +10,7 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '1.25.1' + gem 'rubocop', '1.41.0' gem 'rubocop-ast' gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false diff --git a/gemfiles/rails_edge.gemfile b/gemfiles/rails_edge.gemfile index 533fa931b..aca8be74c 100644 --- a/gemfiles/rails_edge.gemfile +++ b/gemfiles/rails_edge.gemfile @@ -10,7 +10,7 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '1.25.1' + gem 'rubocop', '1.41.0' gem 'rubocop-ast' gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false diff --git a/grape.gemspec b/grape.gemspec index 8eb2cf2b4..b0e3a51b6 100644 --- a/grape.gemspec +++ b/grape.gemspec @@ -32,5 +32,5 @@ Gem::Specification.new do |s| s.files += Dir['lib/**/*'] s.test_files = Dir['spec/**/*'] s.require_paths = ['lib'] - s.required_ruby_version = '>= 2.5.0' + s.required_ruby_version = '>= 2.6.0' end From 02fd374717a6b063e02dfb42f72f2dc80f9e0e29 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Tue, 27 Dec 2022 16:52:33 +0100 Subject: [PATCH 132/304] Fix Rubocop offenses (#2296) * Rerun rubocop --auto-gen-config with --auto-gen-only-exclude --exclude-limit 5000 Move metrics cops in rubocop Fix Performance/CollectionLiteralInLoop Fix Performance/MethodObjectAsBlock Fix RSpec/ClassCheck Fix RSpec/EmptyExampleGroup Fix RSpec/IdenticalEqualityAssertion Fix RSpec/IteratedExpectation Fix RSpec/LetSetup Fix Style/MagicCommentFormat Fix Style/MapToHash Fix Style/SafeNavigation Fix Style/SlicingWithRange Enabled Lint/ConstantDefinitionInBlock Enabled Lint/EmptyBlock Enabled RSpec/ContextWording Enabled RSpec/FilePath Enabled RSpec/LeakyConstantDeclaration Enabled RSpec/NamedSubject * Add CHANGELOG.md * Add # rubocop:disable RSpec/IteratedExpectation because Rack::Response does not respond to :each_with_index --- .rubocop.yml | 35 +- .rubocop_todo.yml | 470 ++++++++++++------ CHANGELOG.md | 1 + lib/grape/dsl/inside_route.rb | 2 +- lib/grape/dsl/request_response.rb | 2 +- lib/grape/endpoint.rb | 4 +- lib/grape/middleware/stack.rb | 2 +- lib/grape/request.rb | 2 +- lib/grape/router/attribute_translator.rb | 2 +- lib/grape/util/cache.rb | 2 +- spec/grape/api_spec.rb | 25 +- spec/grape/dsl/configuration_spec.rb | 14 - spec/grape/endpoint_spec.rb | 10 +- spec/grape/integration/rack_spec.rb | 11 +- spec/grape/middleware/base_spec.rb | 12 +- spec/grape/middleware/formatter_spec.rb | 12 +- .../validations/attributes_iterator_spec.rb | 4 - 17 files changed, 386 insertions(+), 224 deletions(-) delete mode 100644 spec/grape/dsl/configuration_spec.rb delete mode 100644 spec/grape/validations/attributes_iterator_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index acc0555ca..089a33cf6 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -12,6 +12,9 @@ require: inherit_from: .rubocop_todo.yml +Layout/LineLength: + Max: 215 + Style/Documentation: Enabled: false @@ -21,18 +24,34 @@ Style/MultilineIfModifier: Style/RaiseArgs: Enabled: false -Style/HashEachMethods: - Enabled: true - -Style/HashTransformKeys: - Enabled: true - -Style/HashTransformValues: - Enabled: true +Metrics/AbcSize: + Max: 45 Metrics/BlockLength: + Max: 30 Exclude: - spec/**/*_spec.rb +Metrics/ClassLength: + Max: 300 + +Metrics/CyclomaticComplexity: + Max: 15 + +Metrics/ParameterLists: + MaxOptionalParameters: 4 + +Metrics/MethodLength: + Max: 32 + +Metrics/ModuleLength: + Max: 220 + +Metrics/PerceivedComplexity: + Max: 15 + RSpec/Capybara/FeatureMethods: Enabled: false + +RSpec/ExampleLength: + Max: 60 diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 406fa6e66..42e421297 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by -# `rubocop --auto-gen-config` -# on 2022-12-21 16:30:41 UTC using RuboCop version 1.41.0. +# `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 5000` +# on 2022-12-22 16:47:25 UTC using RuboCop version 1.41.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 @@ -22,7 +22,7 @@ Gemspec/RequireMFA: Exclude: - 'grape.gemspec' -# Offense count: 1 +# Offense count: 2 # Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods. Lint/AmbiguousBlockAssociation: Exclude: @@ -32,7 +32,25 @@ Lint/AmbiguousBlockAssociation: # Configuration parameters: AllowedMethods. # AllowedMethods: enums Lint/ConstantDefinitionInBlock: - Enabled: false + Exclude: + - 'spec/grape/api/defines_boolean_in_params_spec.rb' + - 'spec/grape/api/inherited_helpers_spec.rb' + - 'spec/grape/api/nested_helpers_spec.rb' + - 'spec/grape/api/patch_method_helpers_spec.rb' + - 'spec/grape/api_spec.rb' + - 'spec/grape/entity_spec.rb' + - 'spec/grape/loading_spec.rb' + - 'spec/grape/middleware/auth/strategies_spec.rb' + - 'spec/grape/middleware/base_spec.rb' + - 'spec/grape/middleware/error_spec.rb' + - 'spec/grape/middleware/formatter_spec.rb' + - 'spec/grape/middleware/stack_spec.rb' + - 'spec/grape/validations/params_scope_spec.rb' + - 'spec/grape/validations/types_spec.rb' + - 'spec/grape/validations/validators/coerce_spec.rb' + - 'spec/grape/validations/validators/except_values_spec.rb' + - 'spec/grape/validations/validators/presence_spec.rb' + - 'spec/grape/validations/validators/values_spec.rb' # Offense count: 5 # Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches. @@ -41,10 +59,29 @@ Lint/DuplicateBranch: - 'lib/grape/extensions/deep_symbolize_hash.rb' - 'spec/support/versioned_helpers.rb' -# Offense count: 72 +# Offense count: 71 # Configuration parameters: AllowComments, AllowEmptyLambdas. Lint/EmptyBlock: - Enabled: false + Exclude: + - 'spec/grape/api/custom_validations_spec.rb' + - 'spec/grape/api/recognize_path_spec.rb' + - 'spec/grape/api/required_parameters_with_invalid_method_spec.rb' + - 'spec/grape/api_spec.rb' + - 'spec/grape/dsl/routing_spec.rb' + - 'spec/grape/dsl/settings_spec.rb' + - 'spec/grape/endpoint/declared_spec.rb' + - 'spec/grape/endpoint_spec.rb' + - 'spec/grape/loading_spec.rb' + - 'spec/grape/validations/params_scope_spec.rb' + - 'spec/grape/validations/validators/all_or_none_spec.rb' + - 'spec/grape/validations/validators/at_least_one_of_spec.rb' + - 'spec/grape/validations/validators/coerce_spec.rb' + - 'spec/grape/validations/validators/exactly_one_of_spec.rb' + - 'spec/grape/validations/validators/mutual_exclusion_spec.rb' + - 'spec/grape/validations/validators/regexp_spec.rb' + - 'spec/grape/validations/validators/same_as_spec.rb' + - 'spec/grape/validations_spec.rb' + - 'spec/support/endpoint_faker.rb' # Offense count: 5 # Configuration parameters: AllowComments. @@ -65,48 +102,7 @@ Lint/MissingSuper: - 'lib/grape/router/pattern.rb' - 'lib/grape/validations/validators/base.rb' -# Offense count: 41 -# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods, CountRepeatedAttributes. -Metrics/AbcSize: - Max: 43 - -# Offense count: 1 -# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, AllowedMethods, AllowedPatterns, IgnoredMethods, inherit_mode. -# AllowedMethods: refine -Metrics/BlockLength: - Max: 27 - -# Offense count: 9 -# Configuration parameters: CountComments, CountAsOne. -Metrics/ClassLength: - Max: 295 - -# Offense count: 28 -# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods. -Metrics/CyclomaticComplexity: - Max: 15 - -# Offense count: 68 -# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, AllowedMethods, AllowedPatterns, IgnoredMethods. -Metrics/MethodLength: - Max: 32 - -# Offense count: 12 -# Configuration parameters: CountComments, CountAsOne. -Metrics/ModuleLength: - Max: 220 - -# Offense count: 1 -# Configuration parameters: Max, CountKeywordArgs. -Metrics/ParameterLists: - MaxOptionalParameters: 4 - -# Offense count: 25 -# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods. -Metrics/PerceivedComplexity: - Max: 15 - -# Offense count: 4 +# Offense count: 3 # Configuration parameters: EnforcedStyleForLeadingUnderscores. # SupportedStylesForLeadingUnderscores: disallowed, required, optional Naming/MemoizedInstanceVariableName: @@ -114,7 +110,6 @@ Naming/MemoizedInstanceVariableName: - 'lib/grape/api/instance.rb' - 'lib/grape/config.rb' - 'lib/grape/middleware/base.rb' - - 'spec/grape/integration/rack_spec.rb' # Offense count: 5 # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. @@ -136,39 +131,69 @@ Naming/VariableNumber: - 'spec/grape/exceptions/validation_errors_spec.rb' - 'spec/grape/validations_spec.rb' -# Offense count: 2 -# Configuration parameters: MinSize. -Performance/CollectionLiteralInLoop: - Exclude: - - 'spec/grape/api_spec.rb' - - 'spec/grape/middleware/formatter_spec.rb' - -# Offense count: 1 -Performance/MethodObjectAsBlock: - Exclude: - - 'lib/grape/middleware/stack.rb' - # Offense count: 4 RSpec/AnyInstance: Exclude: - 'spec/grape/api_spec.rb' - 'spec/grape/middleware/base_spec.rb' -# Offense count: 5 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: be_a, be_kind_of -RSpec/ClassCheck: - Exclude: - - 'spec/grape/api_spec.rb' - - 'spec/grape/endpoint_spec.rb' - - 'spec/grape/middleware/base_spec.rb' - # Offense count: 345 # Configuration parameters: Prefixes, AllowedPatterns. # Prefixes: when, with, without RSpec/ContextWording: - Enabled: false + Exclude: + - 'spec/grape/api/custom_validations_spec.rb' + - 'spec/grape/api/defines_boolean_in_params_spec.rb' + - 'spec/grape/api/documentation_spec.rb' + - 'spec/grape/api/inherited_helpers_spec.rb' + - 'spec/grape/api/instance_spec.rb' + - 'spec/grape/api/invalid_format_spec.rb' + - 'spec/grape/api/namespace_parameters_in_route_spec.rb' + - 'spec/grape/api/optional_parameters_in_route_spec.rb' + - 'spec/grape/api/patch_method_helpers_spec.rb' + - 'spec/grape/api/required_parameters_in_route_spec.rb' + - 'spec/grape/api/required_parameters_with_invalid_method_spec.rb' + - 'spec/grape/api/routes_with_requirements_spec.rb' + - 'spec/grape/api_remount_spec.rb' + - 'spec/grape/api_spec.rb' + - 'spec/grape/dsl/helpers_spec.rb' + - 'spec/grape/dsl/inside_route_spec.rb' + - 'spec/grape/endpoint_spec.rb' + - 'spec/grape/entity_spec.rb' + - 'spec/grape/exceptions/body_parse_errors_spec.rb' + - 'spec/grape/exceptions/invalid_accept_header_spec.rb' + - 'spec/grape/exceptions/validation_errors_spec.rb' + - 'spec/grape/extensions/param_builders/hashie/mash_spec.rb' + - 'spec/grape/middleware/auth/strategies_spec.rb' + - 'spec/grape/middleware/base_spec.rb' + - 'spec/grape/middleware/exception_spec.rb' + - 'spec/grape/middleware/formatter_spec.rb' + - 'spec/grape/middleware/globals_spec.rb' + - 'spec/grape/middleware/stack_spec.rb' + - 'spec/grape/middleware/versioner/accept_version_header_spec.rb' + - 'spec/grape/middleware/versioner/header_spec.rb' + - 'spec/grape/path_spec.rb' + - 'spec/grape/util/inheritable_values_spec.rb' + - 'spec/grape/util/reverse_stackable_values_spec.rb' + - 'spec/grape/util/stackable_values_spec.rb' + - 'spec/grape/validations/attributes_doc_spec.rb' + - 'spec/grape/validations/params_scope_spec.rb' + - 'spec/grape/validations/single_attribute_iterator_spec.rb' + - 'spec/grape/validations/types/array_coercer_spec.rb' + - 'spec/grape/validations/types/primitive_coercer_spec.rb' + - 'spec/grape/validations/types/set_coercer_spec.rb' + - 'spec/grape/validations/validators/all_or_none_spec.rb' + - 'spec/grape/validations/validators/allow_blank_spec.rb' + - 'spec/grape/validations/validators/at_least_one_of_spec.rb' + - 'spec/grape/validations/validators/coerce_spec.rb' + - 'spec/grape/validations/validators/default_spec.rb' + - 'spec/grape/validations/validators/exactly_one_of_spec.rb' + - 'spec/grape/validations/validators/mutual_exclusion_spec.rb' + - 'spec/grape/validations/validators/regexp_spec.rb' + - 'spec/grape/validations/validators/same_as_spec.rb' + - 'spec/grape/validations/validators/values_spec.rb' + - 'spec/grape/validations_spec.rb' + - 'spec/shared/versioning_examples.rb' # Offense count: 3 # Configuration parameters: IgnoredMetadata. @@ -183,19 +208,6 @@ RSpec/DescribeClass: - 'spec/grape/named_api_spec.rb' - 'spec/grape/validations/instance_behaivour_spec.rb' -# Offense count: 3 -# This cop supports unsafe autocorrection (--autocorrect-all). -RSpec/EmptyExampleGroup: - Exclude: - - 'spec/grape/api_spec.rb' - - 'spec/grape/dsl/configuration_spec.rb' - - 'spec/grape/validations/attributes_iterator_spec.rb' - -# Offense count: 507 -# Configuration parameters: CountAsOne. -RSpec/ExampleLength: - Max: 57 - # Offense count: 2 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: CustomTransform, IgnoredWords, DisallowedExamples. @@ -223,12 +235,50 @@ RSpec/ExpectInHook: # Configuration parameters: Include, CustomTransform, IgnoreMethods, SpecSuffixOnly. # Include: **/*_spec*rb*, **/spec/**/* RSpec/FilePath: - Enabled: false - -# Offense count: 2 -RSpec/IdenticalEqualityAssertion: Exclude: - - 'spec/grape/middleware/base_spec.rb' + - 'spec/grape/api/custom_validations_spec.rb' + - 'spec/grape/api/deeply_included_options_spec.rb' + - 'spec/grape/api/defines_boolean_in_params_spec.rb' + - 'spec/grape/api/documentation_spec.rb' + - 'spec/grape/api/inherited_helpers_spec.rb' + - 'spec/grape/api/invalid_format_spec.rb' + - 'spec/grape/api/namespace_parameters_in_route_spec.rb' + - 'spec/grape/api/nested_helpers_spec.rb' + - 'spec/grape/api/optional_parameters_in_route_spec.rb' + - 'spec/grape/api/parameters_modification_spec.rb' + - 'spec/grape/api/patch_method_helpers_spec.rb' + - 'spec/grape/api/recognize_path_spec.rb' + - 'spec/grape/api/required_parameters_in_route_spec.rb' + - 'spec/grape/api/required_parameters_with_invalid_method_spec.rb' + - 'spec/grape/api/routes_with_requirements_spec.rb' + - 'spec/grape/api/shared_helpers_exactly_one_of_spec.rb' + - 'spec/grape/api/shared_helpers_spec.rb' + - 'spec/grape/dsl/inside_route_spec.rb' + - 'spec/grape/endpoint/declared_spec.rb' + - 'spec/grape/exceptions/body_parse_errors_spec.rb' + - 'spec/grape/extensions/param_builders/hash_spec.rb' + - 'spec/grape/extensions/param_builders/hash_with_indifferent_access_spec.rb' + - 'spec/grape/extensions/param_builders/hashie/mash_spec.rb' + - 'spec/grape/integration/global_namespace_function_spec.rb' + - 'spec/grape/integration/rack_sendfile_spec.rb' + - 'spec/grape/loading_spec.rb' + - 'spec/grape/middleware/exception_spec.rb' + - 'spec/grape/validations/attributes_doc_spec.rb' + - 'spec/grape/validations/validators/all_or_none_spec.rb' + - 'spec/grape/validations/validators/allow_blank_spec.rb' + - 'spec/grape/validations/validators/at_least_one_of_spec.rb' + - 'spec/grape/validations/validators/coerce_spec.rb' + - 'spec/grape/validations/validators/default_spec.rb' + - 'spec/grape/validations/validators/exactly_one_of_spec.rb' + - 'spec/grape/validations/validators/except_values_spec.rb' + - 'spec/grape/validations/validators/mutual_exclusion_spec.rb' + - 'spec/grape/validations/validators/presence_spec.rb' + - 'spec/grape/validations/validators/regexp_spec.rb' + - 'spec/grape/validations/validators/same_as_spec.rb' + - 'spec/grape/validations/validators/values_spec.rb' + - 'spec/integration/eager_load/eager_load_spec.rb' + - 'spec/integration/multi_json/json_spec.rb' + - 'spec/integration/multi_xml/xml_spec.rb' # Offense count: 38 # Configuration parameters: AssignmentOnly. @@ -241,26 +291,35 @@ RSpec/InstanceVariable: - 'spec/grape/middleware/versioner/header_spec.rb' - 'spec/grape/validations/validators/except_values_spec.rb' -# Offense count: 4 -RSpec/IteratedExpectation: - Exclude: - - 'spec/grape/middleware/formatter_spec.rb' - # Offense count: 84 RSpec/LeakyConstantDeclaration: - Enabled: false - -# Offense count: 1 -RSpec/LetSetup: Exclude: - - 'spec/grape/integration/rack_spec.rb' + - 'spec/grape/api/defines_boolean_in_params_spec.rb' + - 'spec/grape/api/inherited_helpers_spec.rb' + - 'spec/grape/api/nested_helpers_spec.rb' + - 'spec/grape/api/patch_method_helpers_spec.rb' + - 'spec/grape/api_spec.rb' + - 'spec/grape/entity_spec.rb' + - 'spec/grape/loading_spec.rb' + - 'spec/grape/middleware/auth/strategies_spec.rb' + - 'spec/grape/middleware/base_spec.rb' + - 'spec/grape/middleware/error_spec.rb' + - 'spec/grape/middleware/exception_spec.rb' + - 'spec/grape/middleware/formatter_spec.rb' + - 'spec/grape/middleware/stack_spec.rb' + - 'spec/grape/validations/params_scope_spec.rb' + - 'spec/grape/validations/types_spec.rb' + - 'spec/grape/validations/validators/coerce_spec.rb' + - 'spec/grape/validations/validators/except_values_spec.rb' + - 'spec/grape/validations/validators/presence_spec.rb' + - 'spec/grape/validations/validators/values_spec.rb' # Offense count: 2 RSpec/MessageChain: Exclude: - 'spec/grape/middleware/formatter_spec.rb' -# Offense count: 138 +# Offense count: 139 # Configuration parameters: . # SupportedStyles: have_received, receive RSpec/MessageSpies: @@ -271,25 +330,177 @@ RSpec/MissingExampleGroupArgument: Exclude: - 'spec/grape/middleware/exception_spec.rb' -# Offense count: 766 +# Offense count: 767 +# Configuration parameters: Max. RSpec/MultipleExpectations: - Max: 16 + Exclude: + - 'spec/grape/api/custom_validations_spec.rb' + - 'spec/grape/api/deeply_included_options_spec.rb' + - 'spec/grape/api/defines_boolean_in_params_spec.rb' + - 'spec/grape/api/invalid_format_spec.rb' + - 'spec/grape/api/namespace_parameters_in_route_spec.rb' + - 'spec/grape/api/optional_parameters_in_route_spec.rb' + - 'spec/grape/api/parameters_modification_spec.rb' + - 'spec/grape/api/patch_method_helpers_spec.rb' + - 'spec/grape/api/required_parameters_in_route_spec.rb' + - 'spec/grape/api/routes_with_requirements_spec.rb' + - 'spec/grape/api/shared_helpers_exactly_one_of_spec.rb' + - 'spec/grape/api/shared_helpers_spec.rb' + - 'spec/grape/api_remount_spec.rb' + - 'spec/grape/api_spec.rb' + - 'spec/grape/dsl/desc_spec.rb' + - 'spec/grape/dsl/headers_spec.rb' + - 'spec/grape/dsl/helpers_spec.rb' + - 'spec/grape/dsl/inside_route_spec.rb' + - 'spec/grape/dsl/parameters_spec.rb' + - 'spec/grape/dsl/request_response_spec.rb' + - 'spec/grape/dsl/routing_spec.rb' + - 'spec/grape/dsl/settings_spec.rb' + - 'spec/grape/endpoint/declared_spec.rb' + - 'spec/grape/endpoint_spec.rb' + - 'spec/grape/entity_spec.rb' + - 'spec/grape/exceptions/body_parse_errors_spec.rb' + - 'spec/grape/exceptions/invalid_accept_header_spec.rb' + - 'spec/grape/exceptions/missing_group_type_spec.rb' + - 'spec/grape/exceptions/unsupported_group_type_spec.rb' + - 'spec/grape/exceptions/validation_errors_spec.rb' + - 'spec/grape/extensions/param_builders/hash_spec.rb' + - 'spec/grape/extensions/param_builders/hash_with_indifferent_access_spec.rb' + - 'spec/grape/extensions/param_builders/hashie/mash_spec.rb' + - 'spec/grape/middleware/auth/base_spec.rb' + - 'spec/grape/middleware/auth/dsl_spec.rb' + - 'spec/grape/middleware/base_spec.rb' + - 'spec/grape/middleware/exception_spec.rb' + - 'spec/grape/middleware/formatter_spec.rb' + - 'spec/grape/middleware/stack_spec.rb' + - 'spec/grape/middleware/versioner/accept_version_header_spec.rb' + - 'spec/grape/middleware/versioner/header_spec.rb' + - 'spec/grape/middleware/versioner/param_spec.rb' + - 'spec/grape/presenters/presenter_spec.rb' + - 'spec/grape/util/inheritable_setting_spec.rb' + - 'spec/grape/util/reverse_stackable_values_spec.rb' + - 'spec/grape/util/stackable_values_spec.rb' + - 'spec/grape/validations/attributes_doc_spec.rb' + - 'spec/grape/validations/instance_behaivour_spec.rb' + - 'spec/grape/validations/params_scope_spec.rb' + - 'spec/grape/validations/types/array_coercer_spec.rb' + - 'spec/grape/validations/types/primitive_coercer_spec.rb' + - 'spec/grape/validations/types/set_coercer_spec.rb' + - 'spec/grape/validations/types_spec.rb' + - 'spec/grape/validations/validators/all_or_none_spec.rb' + - 'spec/grape/validations/validators/allow_blank_spec.rb' + - 'spec/grape/validations/validators/at_least_one_of_spec.rb' + - 'spec/grape/validations/validators/coerce_spec.rb' + - 'spec/grape/validations/validators/default_spec.rb' + - 'spec/grape/validations/validators/exactly_one_of_spec.rb' + - 'spec/grape/validations/validators/except_values_spec.rb' + - 'spec/grape/validations/validators/mutual_exclusion_spec.rb' + - 'spec/grape/validations/validators/presence_spec.rb' + - 'spec/grape/validations/validators/regexp_spec.rb' + - 'spec/grape/validations/validators/same_as_spec.rb' + - 'spec/grape/validations/validators/values_spec.rb' + - 'spec/grape/validations_spec.rb' + - 'spec/shared/versioning_examples.rb' # Offense count: 38 -# Configuration parameters: AllowSubject. +# Configuration parameters: AllowSubject, Max. RSpec/MultipleMemoizedHelpers: - Max: 10 + Exclude: + - 'spec/grape/middleware/exception_spec.rb' + - 'spec/grape/request_spec.rb' + - 'spec/grape/validations/attributes_doc_spec.rb' -# Offense count: 2145 +# Offense count: 2143 # Configuration parameters: EnforcedStyle, IgnoreSharedExamples. # SupportedStyles: always, named_only RSpec/NamedSubject: - Enabled: false + Exclude: + - 'spec/grape/api/defines_boolean_in_params_spec.rb' + - 'spec/grape/api/documentation_spec.rb' + - 'spec/grape/api/invalid_format_spec.rb' + - 'spec/grape/api/namespace_parameters_in_route_spec.rb' + - 'spec/grape/api/optional_parameters_in_route_spec.rb' + - 'spec/grape/api/parameters_modification_spec.rb' + - 'spec/grape/api/recognize_path_spec.rb' + - 'spec/grape/api/required_parameters_in_route_spec.rb' + - 'spec/grape/api/required_parameters_with_invalid_method_spec.rb' + - 'spec/grape/api/routes_with_requirements_spec.rb' + - 'spec/grape/api_spec.rb' + - 'spec/grape/dsl/callbacks_spec.rb' + - 'spec/grape/dsl/desc_spec.rb' + - 'spec/grape/dsl/headers_spec.rb' + - 'spec/grape/dsl/helpers_spec.rb' + - 'spec/grape/dsl/inside_route_spec.rb' + - 'spec/grape/dsl/logger_spec.rb' + - 'spec/grape/dsl/middleware_spec.rb' + - 'spec/grape/dsl/parameters_spec.rb' + - 'spec/grape/dsl/request_response_spec.rb' + - 'spec/grape/dsl/routing_spec.rb' + - 'spec/grape/dsl/settings_spec.rb' + - 'spec/grape/dsl/validations_spec.rb' + - 'spec/grape/endpoint/declared_spec.rb' + - 'spec/grape/endpoint_spec.rb' + - 'spec/grape/entity_spec.rb' + - 'spec/grape/exceptions/base_spec.rb' + - 'spec/grape/exceptions/body_parse_errors_spec.rb' + - 'spec/grape/exceptions/invalid_accept_header_spec.rb' + - 'spec/grape/exceptions/missing_group_type_spec.rb' + - 'spec/grape/exceptions/unsupported_group_type_spec.rb' + - 'spec/grape/exceptions/validation_errors_spec.rb' + - 'spec/grape/extensions/param_builders/hash_spec.rb' + - 'spec/grape/extensions/param_builders/hash_with_indifferent_access_spec.rb' + - 'spec/grape/extensions/param_builders/hashie/mash_spec.rb' + - 'spec/grape/integration/rack_sendfile_spec.rb' + - 'spec/grape/middleware/auth/dsl_spec.rb' + - 'spec/grape/middleware/base_spec.rb' + - 'spec/grape/middleware/formatter_spec.rb' + - 'spec/grape/middleware/globals_spec.rb' + - 'spec/grape/middleware/stack_spec.rb' + - 'spec/grape/middleware/versioner/accept_version_header_spec.rb' + - 'spec/grape/middleware/versioner/header_spec.rb' + - 'spec/grape/middleware/versioner/param_spec.rb' + - 'spec/grape/middleware/versioner/path_spec.rb' + - 'spec/grape/parser_spec.rb' + - 'spec/grape/presenters/presenter_spec.rb' + - 'spec/grape/util/inheritable_setting_spec.rb' + - 'spec/grape/util/inheritable_values_spec.rb' + - 'spec/grape/util/reverse_stackable_values_spec.rb' + - 'spec/grape/util/stackable_values_spec.rb' + - 'spec/grape/util/strict_hash_configuration_spec.rb' + - 'spec/grape/validations/attributes_doc_spec.rb' + - 'spec/grape/validations/params_scope_spec.rb' + - 'spec/grape/validations/types/array_coercer_spec.rb' + - 'spec/grape/validations/types/primitive_coercer_spec.rb' + - 'spec/grape/validations/types/set_coercer_spec.rb' + - 'spec/grape/validations/validators/coerce_spec.rb' + - 'spec/grape/validations/validators/default_spec.rb' + - 'spec/grape/validations/validators/presence_spec.rb' + - 'spec/grape/validations_spec.rb' # Offense count: 171 -# Configuration parameters: AllowedGroups. +# Configuration parameters: Max, AllowedGroups. RSpec/NestedGroups: - Max: 6 + Exclude: + - 'spec/grape/api_remount_spec.rb' + - 'spec/grape/api_spec.rb' + - 'spec/grape/dsl/headers_spec.rb' + - 'spec/grape/dsl/inside_route_spec.rb' + - 'spec/grape/endpoint_spec.rb' + - 'spec/grape/exceptions/base_spec.rb' + - 'spec/grape/exceptions/invalid_accept_header_spec.rb' + - 'spec/grape/middleware/formatter_spec.rb' + - 'spec/grape/presenters/presenter_spec.rb' + - 'spec/grape/validations/attributes_doc_spec.rb' + - 'spec/grape/validations/params_scope_spec.rb' + - 'spec/grape/validations/single_attribute_iterator_spec.rb' + - 'spec/grape/validations/types/primitive_coercer_spec.rb' + - 'spec/grape/validations/validators/all_or_none_spec.rb' + - 'spec/grape/validations/validators/allow_blank_spec.rb' + - 'spec/grape/validations/validators/at_least_one_of_spec.rb' + - 'spec/grape/validations/validators/coerce_spec.rb' + - 'spec/grape/validations/validators/exactly_one_of_spec.rb' + - 'spec/grape/validations/validators/mutual_exclusion_spec.rb' + - 'spec/grape/validations_spec.rb' # Offense count: 18 # Configuration parameters: AllowedPatterns. @@ -337,7 +548,7 @@ RSpec/ScatteredSetup: - 'spec/grape/util/inheritable_setting_spec.rb' - 'spec/grape/validations_spec.rb' -# Offense count: 9 +# Offense count: 10 RSpec/StubbedMock: Exclude: - 'spec/grape/api_spec.rb' @@ -394,22 +605,6 @@ Style/CombinableLoops: Style/FormatStringToken: EnforcedStyle: template -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, DirectiveCapitalization, ValueCapitalization. -# SupportedStyles: snake_case, kebab_case -# SupportedCapitalizations: lowercase, uppercase -Style/MagicCommentFormat: - Exclude: - - 'lib/grape/util/cache.rb' - -# Offense count: 3 -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/MapToHash: - Exclude: - - 'lib/grape/dsl/request_response.rb' - - 'spec/grape/endpoint_spec.rb' - # Offense count: 12 # Configuration parameters: AllowedMethods. # AllowedMethods: respond_to_missing? @@ -441,26 +636,3 @@ Style/RedundantConstantBase: - 'spec/grape/validations/validators/default_spec.rb' - 'spec/integration/multi_json/json_spec.rb' - 'spec/integration/multi_xml/xml_spec.rb' - -# Offense count: 2 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength. -# AllowedMethods: present?, blank?, presence, try, try! -Style/SafeNavigation: - Exclude: - - 'lib/grape/endpoint.rb' - -# Offense count: 3 -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/SlicingWithRange: - Exclude: - - 'lib/grape/dsl/inside_route.rb' - - 'lib/grape/request.rb' - - 'lib/grape/router/attribute_translator.rb' - -# Offense count: 168 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, IgnoredPatterns. -# URISchemes: http, https -Layout/LineLength: - Max: 215 diff --git a/CHANGELOG.md b/CHANGELOG.md index 45bbeeb62..b2aa48174 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * [#2288](https://github.com/ruby-grape/grape/pull/2288): Droped support for Ruby 2.5 - [@ericproulx](https://github.com/ericproulx). * [#2288](https://github.com/ruby-grape/grape/pull/2288): Updated rubocop to 1.41.0 - [@ericproulx](https://github.com/ericproulx). +* [#2296](https://github.com/ruby-grape/grape/pull/2296): Fix cops and enables some - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index fb72d0898..a54967ead 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -95,7 +95,7 @@ def handle_passed_param(params_nested_path, has_passed_children = false, &_block return yield if has_passed_children key = params_nested_path[0] - key += "[#{params_nested_path[1..-1].join('][')}]" if params_nested_path.size > 1 + key += "[#{params_nested_path[1..].join('][')}]" if params_nested_path.size > 1 route_options_params = options[:route_options][:params] || {} type = route_options_params.dig(key, :type) diff --git a/lib/grape/dsl/request_response.rb b/lib/grape/dsl/request_response.rb index 122d73b43..6129574fd 100644 --- a/lib/grape/dsl/request_response.rb +++ b/lib/grape/dsl/request_response.rb @@ -125,7 +125,7 @@ def rescue_from(*args, &block) :base_only_rescue_handlers end - namespace_reverse_stackable handler_type, args.map { |arg| [arg, handler] }.to_h + namespace_reverse_stackable(handler_type, args.to_h { |arg| [arg, handler] }) end namespace_stackable(:rescue_options, options) diff --git a/lib/grape/endpoint.rb b/lib/grape/endpoint.rb index 35f5a3fc2..d0eaee5cd 100644 --- a/lib/grape/endpoint.rb +++ b/lib/grape/endpoint.rb @@ -299,7 +299,7 @@ def build_stack(helpers) if namespace_inheritable(:version) stack.use Grape::Middleware::Versioner.using(namespace_inheritable(:version_options)[:using]), - versions: namespace_inheritable(:version) ? namespace_inheritable(:version).flatten : nil, + versions: namespace_inheritable(:version)&.flatten, version_options: namespace_inheritable(:version_options), prefix: namespace_inheritable(:root_prefix), mount_path: namespace_stackable(:mount_path).first @@ -325,7 +325,7 @@ def build_helpers private :build_stack, :build_helpers def execute - @block ? @block.call(self) : nil + @block&.call(self) end def helpers diff --git a/lib/grape/middleware/stack.rb b/lib/grape/middleware/stack.rb index 9492448a4..2e143ac4f 100644 --- a/lib/grape/middleware/stack.rb +++ b/lib/grape/middleware/stack.rb @@ -95,7 +95,7 @@ def merge_with(middleware_specs) # @return [Rack::Builder] the builder object with our middlewares applied def build(builder = Rack::Builder.new) - others.shift(others.size).each(&method(:merge_with)) + others.shift(others.size).each { |m| merge_with(m) } middlewares.each do |m| m.use_in(builder) end diff --git a/lib/grape/request.rb b/lib/grape/request.rb index c45df9876..10fd28cc6 100644 --- a/lib/grape/request.rb +++ b/lib/grape/request.rb @@ -47,7 +47,7 @@ def build_headers end def transform_header(header) - -header[5..-1].split('_').each(&:capitalize!).join('-') + -header[5..].split('_').each(&:capitalize!).join('-') end end end diff --git a/lib/grape/router/attribute_translator.rb b/lib/grape/router/attribute_translator.rb index 93ba4bdcd..8264e2196 100644 --- a/lib/grape/router/attribute_translator.rb +++ b/lib/grape/router/attribute_translator.rb @@ -39,7 +39,7 @@ def to_h def method_missing(method_name, *args) if setter?(method_name[-1]) - attributes[method_name[0..-1]] = *args + attributes[method_name[0..]] = *args else attributes[method_name] end diff --git a/lib/grape/util/cache.rb b/lib/grape/util/cache.rb index 3f51148f7..b58f43240 100644 --- a/lib/grape/util/cache.rb +++ b/lib/grape/util/cache.rb @@ -1,4 +1,4 @@ -# frozen_String_literal: true +# frozen_string_literal: true require 'singleton' require 'forwardable' diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index 0c77fd605..008a42026 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -103,22 +103,6 @@ def app } end end - - # Behavior as defined by rfc2616 when no header is defined - # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html - describe 'no specified accept header' do - # subject.version 'v1', using: :header - # subject.get '/hello' do - # 'hello' - # end - - # it 'routes' do - # get '/hello' - # last_response.status.should eql 200 - # end - end - - # pending 'routes if any media type is allowed' end describe '.version using accept_version_header' do @@ -448,9 +432,10 @@ class DummyFormatClass expect(last_response.body).to eql 'hiya' end + objects = ['string', :symbol, 1, -1.1, {}, [], true, false, nil].freeze %i[put post].each do |verb| context verb.to_s do - ['string', :symbol, 1, -1.1, {}, [], true, false, nil].each do |object| + objects.each do |object| it "allows a(n) #{object.class} json object in params" do subject.format :json subject.send(verb) do @@ -1601,7 +1586,7 @@ def call(env) subject.get(:hello) { 'Hello, world.' } get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever') - expect(basic_auth_context).to be_a_kind_of(Grape::Endpoint) + expect(basic_auth_context).to be_a(Grape::Endpoint) end it 'has access to helper methods' do @@ -3067,7 +3052,7 @@ def static expect(route.description).to eq('first method') expect(route.route_foo).to be_nil expect(route.params).to eq({}) - expect(route.options).to be_a_kind_of(Hash) + expect(route.options).to be_a(Hash) end it 'has params which does not include format and version as named captures' do @@ -3725,7 +3710,7 @@ def my_method it 'sets the instance' do expect(subject.instance).to be_nil subject.compile - expect(subject.instance).to be_kind_of(subject.base_instance) + expect(subject.instance).to be_a(subject.base_instance) end end diff --git a/spec/grape/dsl/configuration_spec.rb b/spec/grape/dsl/configuration_spec.rb deleted file mode 100644 index 9265fdc41..000000000 --- a/spec/grape/dsl/configuration_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -module Grape - module DSL - module ConfigurationSpec - class Dummy - include Grape::DSL::Configuration - end - end - describe Configuration do - subject { Class.new(ConfigurationSpec::Dummy) } - end - end -end diff --git a/spec/grape/endpoint_spec.rb b/spec/grape/endpoint_spec.rb index ec51d2edb..158aa6c97 100644 --- a/spec/grape/endpoint_spec.rb +++ b/spec/grape/endpoint_spec.rb @@ -75,7 +75,7 @@ def app it 'sets itself in the env upon call' do subject.get('/') { 'Hello world.' } get '/' - expect(last_request.env['api.endpoint']).to be_kind_of(described_class) + expect(last_request.env['api.endpoint']).to be_a(described_class) end describe '#status' do @@ -213,10 +213,10 @@ def app end get '/test', {}, 'HTTP_COOKIE' => 'delete_this_cookie=1; and_this=2' expect(last_response.body).to eq('3') - cookies = last_response.headers['Set-Cookie'].split("\n").map do |set_cookie| + cookies = last_response.headers['Set-Cookie'].split("\n").to_h do |set_cookie| cookie = CookieJar::Cookie.from_set_cookie 'http://localhost/test', set_cookie [cookie.name, cookie] - end.to_h + end expect(cookies.size).to eq(2) %w[and_this delete_this_cookie].each do |cookie_name| cookie = cookies[cookie_name] @@ -237,10 +237,10 @@ def app end get('/test', {}, 'HTTP_COOKIE' => 'delete_this_cookie=1; and_this=2') expect(last_response.body).to eq('3') - cookies = last_response.headers['Set-Cookie'].split("\n").map do |set_cookie| + cookies = last_response.headers['Set-Cookie'].split("\n").to_h do |set_cookie| cookie = CookieJar::Cookie.from_set_cookie 'http://localhost/test', set_cookie [cookie.name, cookie] - end.to_h + end expect(cookies.size).to eq(2) %w[and_this delete_this_cookie].each do |cookie_name| cookie = cookies[cookie_name] diff --git a/spec/grape/integration/rack_spec.rb b/spec/grape/integration/rack_spec.rb index 83dbf7c59..fc0aca682 100644 --- a/spec/grape/integration/rack_spec.rb +++ b/spec/grape/integration/rack_spec.rb @@ -27,19 +27,20 @@ end context 'when the app is mounted' do - def app - @main_app ||= Class.new(Grape::API) do + let(:ping_mount) do + Class.new(Grape::API) do get 'ping' end end - let!(:base) do - app_to_mount = app - Class.new(Grape::API) do + let(:app) do + app_to_mount = ping_mount + app = Class.new(Grape::API) do namespace 'namespace' do mount app_to_mount end end + Rack::Builder.new(app) end it 'finds the app on the namespace' do diff --git a/spec/grape/middleware/base_spec.rb b/spec/grape/middleware/base_spec.rb index ee733b745..cbba974e1 100644 --- a/spec/grape/middleware/base_spec.rb +++ b/spec/grape/middleware/base_spec.rb @@ -70,18 +70,18 @@ it 'is able to access the response' do subject.call({}) - expect(subject.response).to be_kind_of(Rack::Response) + expect(subject.response).to be_a(Rack::Response) end describe '#response' do subject do - puts described_class described_class.new(response) end before { subject.call({}) } context 'when Array' do + let(:rack_response) { Rack::Response.new('test', 204, abc: 1) } let(:response) { ->(_) { [204, { abc: 1 }, 'test'] } } it 'status' do @@ -97,12 +97,14 @@ end it 'returns the memoized Rack::Response instance' do - expect(subject.response).to be(subject.response) + allow(Rack::Response).to receive(:new).and_return(rack_response) + expect(subject.response).to eq(rack_response) end end context 'when Rack::Response' do - let(:response) { ->(_) { Rack::Response.new('test', 204, abc: 1) } } + let(:rack_response) { Rack::Response.new('test', 204, abc: 1) } + let(:response) { ->(_) { rack_response } } it 'status' do expect(subject.response.status).to eq(204) @@ -117,7 +119,7 @@ end it 'returns the memoized Rack::Response instance' do - expect(subject.response).to be(subject.response) + expect(subject.response).to eq(rack_response) end end end diff --git a/spec/grape/middleware/formatter_spec.rb b/spec/grape/middleware/formatter_spec.rb index 6310d2d04..b4ae0ec05 100644 --- a/spec/grape/middleware/formatter_spec.rb +++ b/spec/grape/middleware/formatter_spec.rb @@ -13,7 +13,7 @@ it 'looks at the bodies for possibly serializable data' do _, _, bodies = *subject.call('PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/json') - bodies.each { |b| expect(b).to eq(::Grape::Json.dump(body)) } + bodies.each { |b| expect(b).to eq(::Grape::Json.dump(body)) } # rubocop:disable RSpec/IteratedExpectation end context 'default format' do @@ -26,7 +26,7 @@ def to_json(*_args) end end - subject.call('PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/json').to_a.last.each { |b| expect(b).to eq('"bar"') } + subject.call('PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/json').to_a.last.each { |b| expect(b).to eq('"bar"') } # rubocop:disable RSpec/IteratedExpectation end end @@ -40,7 +40,7 @@ def to_json(*_args) end end - subject.call('PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/vnd.api+json').to_a.last.each { |b| expect(b).to eq('{"foos":[{"bar":"baz"}] }') } + subject.call('PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/vnd.api+json').to_a.last.each { |b| expect(b).to eq('{"foos":[{"bar":"baz"}] }') } # rubocop:disable RSpec/IteratedExpectation end end @@ -53,8 +53,7 @@ def to_xml '' end end - - subject.call('PATH_INFO' => '/somewhere.xml', 'HTTP_ACCEPT' => 'application/json').to_a.last.each { |b| expect(b).to eq('') } + subject.call('PATH_INFO' => '/somewhere.xml', 'HTTP_ACCEPT' => 'application/json').to_a.last.each { |b| expect(b).to eq('') } # rubocop:disable RSpec/IteratedExpectation end end end @@ -243,6 +242,7 @@ def to_xml end context 'input' do + content_types = ['application/json', 'application/json; charset=utf-8'].freeze %w[POST PATCH PUT DELETE].each do |method| context 'when body is not nil or empty' do context 'when Content-Type is supported' do @@ -320,7 +320,7 @@ def to_xml end end - ['application/json', 'application/json; charset=utf-8'].each do |content_type| + content_types.each do |content_type| context content_type do it "parses the body from #{method} and copies values into rack.request.form_hash" do io = StringIO.new('{"is_boolean":true,"string":"thing"}') diff --git a/spec/grape/validations/attributes_iterator_spec.rb b/spec/grape/validations/attributes_iterator_spec.rb deleted file mode 100644 index 40286c840..000000000 --- a/spec/grape/validations/attributes_iterator_spec.rb +++ /dev/null @@ -1,4 +0,0 @@ -# frozen_string_literal: true - -describe Grape::Validations::AttributesIterator do -end From b51665dc04577d480efbfc2d94c24bf3fd131b05 Mon Sep 17 00:00:00 2001 From: dm1try Date: Thu, 29 Dec 2022 22:58:26 +0100 Subject: [PATCH 133/304] fix, do not use kwargs for empty args for backward compatibility with the previous implemention where options param behaves as an empty hash by default see #2295 --- CHANGELOG.md | 1 + lib/grape/dsl/parameters.rb | 7 ++++++- spec/grape/validations_spec.rb | 23 +++++++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2aa48174..2d8ca05b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ #### Fixes +* [#2299](https://github.com/ruby-grape/grape/pull/2299): Fix, do not use kwargs for empty args - [@dm1try](https://github.com/dm1try). * Your contribution here. ### 1.7.0 (2022/12/20) diff --git a/lib/grape/dsl/parameters.rb b/lib/grape/dsl/parameters.rb index f751fe557..561662417 100644 --- a/lib/grape/dsl/parameters.rb +++ b/lib/grape/dsl/parameters.rb @@ -62,7 +62,12 @@ def use(*names) params_block = named_params.fetch(name) do raise "Params :#{name} not found!" end - instance_exec(**options, ¶ms_block) + + if options.empty? + instance_exec(options, ¶ms_block) + else + instance_exec(**options, ¶ms_block) + end end end alias use_scope use diff --git a/spec/grape/validations_spec.rb b/spec/grape/validations_spec.rb index 2afcecb24..0a3423254 100644 --- a/spec/grape/validations_spec.rb +++ b/spec/grape/validations_spec.rb @@ -1509,6 +1509,29 @@ def validate_param!(attr_name, params) end end + context 'with block and empty args' do + before do + subject.helpers do + params :shared_params do |empty_args| + optional :param, default: empty_args[:some] + end + end + subject.format :json + subject.params do + use :shared_params + end + subject.get '/shared_params' do + :ok + end + end + + it 'works' do + get '/shared_params' + + expect(last_response.status).to eq(200) + end + end + context 'all or none' do context 'optional params' do before do From 0ec178823295680ab420a85fbf53c712b0a16768 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Thu, 5 Jan 2023 14:01:40 +0100 Subject: [PATCH 134/304] Rack < 3 (#2302) * Add rack3.gemfile Update rack-test and limit rack < 3 Update multiple gemfiles * Add CHANGELOG.md Fix #2300 --- .github/workflows/edge.yml | 10 ++++---- CHANGELOG.md | 1 + Gemfile | 2 +- gemfiles/multi_json.gemfile | 2 +- gemfiles/multi_xml.gemfile | 2 +- gemfiles/rack1.gemfile | 2 +- gemfiles/rack2.gemfile | 2 +- gemfiles/rack2_2.gemfile | 2 +- gemfiles/rack3.gemfile | 46 +++++++++++++++++++++++++++++++++++++ gemfiles/rack_edge.gemfile | 2 +- gemfiles/rails_5.gemfile | 2 +- gemfiles/rails_6.gemfile | 2 +- gemfiles/rails_6_1.gemfile | 2 +- gemfiles/rails_7.gemfile | 2 +- gemfiles/rails_edge.gemfile | 2 +- grape.gemspec | 2 +- spec/grape/endpoint_spec.rb | 5 ++-- 17 files changed, 68 insertions(+), 20 deletions(-) create mode 100644 gemfiles/rack3.gemfile diff --git a/.github/workflows/edge.yml b/.github/workflows/edge.yml index 9055f2f59..3089415f5 100644 --- a/.github/workflows/edge.yml +++ b/.github/workflows/edge.yml @@ -1,9 +1,6 @@ --- name: edge -on: - pull_request: - branches: - - "*" +on: pull_request jobs: test: strategy: @@ -14,10 +11,12 @@ jobs: gemfile: 'gemfiles/rails_edge.gemfile' - ruby: 2.7 gemfile: 'gemfiles/rack_edge.gemfile' + - ruby: 2.7 + gemfile: 'gemfiles/rack3.gemfile' - ruby: "ruby-head" - ruby: "truffleruby-head" - ruby: "jruby-head" - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest continue-on-error: true env: BUNDLE_GEMFILE: ${{ matrix.gemfile }} @@ -30,6 +29,7 @@ jobs: with: ruby-version: ${{ matrix.ruby }} bundler-cache: true + rubygems: latest - name: Run tests run: bundle exec rake spec diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d8ca05b0..172c84328 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * [#2288](https://github.com/ruby-grape/grape/pull/2288): Droped support for Ruby 2.5 - [@ericproulx](https://github.com/ericproulx). * [#2288](https://github.com/ruby-grape/grape/pull/2288): Updated rubocop to 1.41.0 - [@ericproulx](https://github.com/ericproulx). * [#2296](https://github.com/ruby-grape/grape/pull/2296): Fix cops and enables some - [@ericproulx](https://github.com/ericproulx). +* [#2302](https://github.com/ruby-grape/grape/pull/2302): Rack < 3 and update rack-test - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/Gemfile b/Gemfile index 4adf9b7e3..7e7aef6a3 100644 --- a/Gemfile +++ b/Gemfile @@ -31,7 +31,7 @@ group :test do gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' - gem 'rack-test', '~> 1.1.0' + gem 'rack-test' gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'simplecov', '~> 0.21.2' diff --git a/gemfiles/multi_json.gemfile b/gemfiles/multi_json.gemfile index 11941e7ec..f306bc020 100644 --- a/gemfiles/multi_json.gemfile +++ b/gemfiles/multi_json.gemfile @@ -31,7 +31,7 @@ group :test do gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' - gem 'rack-test', '~> 1.1.0' + gem 'rack-test' gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'simplecov', '~> 0.21.2' diff --git a/gemfiles/multi_xml.gemfile b/gemfiles/multi_xml.gemfile index 059f944e0..f43c7b8c2 100644 --- a/gemfiles/multi_xml.gemfile +++ b/gemfiles/multi_xml.gemfile @@ -31,7 +31,7 @@ group :test do gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' - gem 'rack-test', '~> 1.1.0' + gem 'rack-test' gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'simplecov', '~> 0.21.2' diff --git a/gemfiles/rack1.gemfile b/gemfiles/rack1.gemfile index f05604345..647416e59 100644 --- a/gemfiles/rack1.gemfile +++ b/gemfiles/rack1.gemfile @@ -31,7 +31,7 @@ group :test do gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' - gem 'rack-test', '~> 1.1.0' + gem 'rack-test' gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'simplecov', '~> 0.21.2' diff --git a/gemfiles/rack2.gemfile b/gemfiles/rack2.gemfile index d364e4084..5d30e7930 100644 --- a/gemfiles/rack2.gemfile +++ b/gemfiles/rack2.gemfile @@ -31,7 +31,7 @@ group :test do gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' - gem 'rack-test', '~> 1.1.0' + gem 'rack-test' gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'simplecov', '~> 0.21.2' diff --git a/gemfiles/rack2_2.gemfile b/gemfiles/rack2_2.gemfile index cf4f1081c..31ef181af 100644 --- a/gemfiles/rack2_2.gemfile +++ b/gemfiles/rack2_2.gemfile @@ -31,7 +31,7 @@ group :test do gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' - gem 'rack-test', '~> 1.1.0' + gem 'rack-test' gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'simplecov', '~> 0.21.2' diff --git a/gemfiles/rack3.gemfile b/gemfiles/rack3.gemfile new file mode 100644 index 000000000..b3ff65fc2 --- /dev/null +++ b/gemfiles/rack3.gemfile @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +# This file was generated by Appraisal + +source 'https://rubygems.org' + +gem 'rack', '~> 3' + +group :development, :test do + gem 'bundler' + gem 'hashie' + gem 'rake' + gem 'rubocop', '1.41.0' + gem 'rubocop-ast' + gem 'rubocop-performance', require: false + gem 'rubocop-rspec', 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 'grape-entity', '~> 0.6' + gem 'maruku' + gem 'mime-types' + gem 'rack-jsonp', require: 'rack/jsonp' + gem 'rack-test' + gem 'rspec', '~> 3.11.0' + gem 'ruby-grape-danger', '~> 0.2.0', require: false + gem 'simplecov', '~> 0.21.2' + gem 'simplecov-lcov', '~> 0.8.0' + gem 'test-prof', require: false +end + +platforms :jruby do + gem 'racc' +end + +gemspec path: '../' diff --git a/gemfiles/rack_edge.gemfile b/gemfiles/rack_edge.gemfile index a00083043..137848ded 100644 --- a/gemfiles/rack_edge.gemfile +++ b/gemfiles/rack_edge.gemfile @@ -31,7 +31,7 @@ group :test do gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' - gem 'rack-test', '~> 1.1.0' + gem 'rack-test' gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'simplecov', '~> 0.21.2' diff --git a/gemfiles/rails_5.gemfile b/gemfiles/rails_5.gemfile index 8148083aa..75cc1ebe9 100644 --- a/gemfiles/rails_5.gemfile +++ b/gemfiles/rails_5.gemfile @@ -31,7 +31,7 @@ group :test do gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' - gem 'rack-test', '~> 1.1.0' + gem 'rack-test' gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'simplecov', '~> 0.21.2' diff --git a/gemfiles/rails_6.gemfile b/gemfiles/rails_6.gemfile index 7b088fcf7..a07b203b5 100644 --- a/gemfiles/rails_6.gemfile +++ b/gemfiles/rails_6.gemfile @@ -31,7 +31,7 @@ group :test do gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' - gem 'rack-test', '~> 1.1.0' + gem 'rack-test' gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'simplecov', '~> 0.21.2' diff --git a/gemfiles/rails_6_1.gemfile b/gemfiles/rails_6_1.gemfile index 7962e1b59..2f62e7130 100644 --- a/gemfiles/rails_6_1.gemfile +++ b/gemfiles/rails_6_1.gemfile @@ -31,7 +31,7 @@ group :test do gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' - gem 'rack-test', '~> 1.1.0' + gem 'rack-test' gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'simplecov', '~> 0.21.2' diff --git a/gemfiles/rails_7.gemfile b/gemfiles/rails_7.gemfile index 468d498d2..1c3e639f4 100644 --- a/gemfiles/rails_7.gemfile +++ b/gemfiles/rails_7.gemfile @@ -31,7 +31,7 @@ group :test do gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' - gem 'rack-test', '~> 1.1.0' + gem 'rack-test' gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'simplecov', '~> 0.21.2' diff --git a/gemfiles/rails_edge.gemfile b/gemfiles/rails_edge.gemfile index aca8be74c..50d8260d0 100644 --- a/gemfiles/rails_edge.gemfile +++ b/gemfiles/rails_edge.gemfile @@ -31,7 +31,7 @@ group :test do gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' - gem 'rack-test', '~> 1.1.0' + gem 'rack-test' gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'simplecov', '~> 0.21.2' diff --git a/grape.gemspec b/grape.gemspec index b0e3a51b6..a2b312a75 100644 --- a/grape.gemspec +++ b/grape.gemspec @@ -24,7 +24,7 @@ Gem::Specification.new do |s| 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', '< 3' s.add_runtime_dependency 'rack-accept' s.files = %w[CHANGELOG.md CONTRIBUTING.md README.md grape.png UPGRADING.md LICENSE] diff --git a/spec/grape/endpoint_spec.rb b/spec/grape/endpoint_spec.rb index 158aa6c97..5b563434c 100644 --- a/spec/grape/endpoint_spec.rb +++ b/spec/grape/endpoint_spec.rb @@ -140,7 +140,8 @@ def app get '/headers' expect(JSON.parse(last_response.body)).to eq( 'Host' => 'example.org', - 'Cookie' => '' + 'Cookie' => '', + 'Version' => 'HTTP/1.0' ) end @@ -432,7 +433,7 @@ def app 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') + expect(last_response.body).to eq('file is invalid') end end From c8b755deb89ca3594c74398216a9911985ae43df Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sun, 8 Jan 2023 02:48:07 +0100 Subject: [PATCH 135/304] Rack: Add >= 1.3.0 like before (#2303) * Add >= 1.3.0 like before * Add CHANGELOG.md --- CHANGELOG.md | 1 + grape.gemspec | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 172c84328..372511dbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * [#2288](https://github.com/ruby-grape/grape/pull/2288): Updated rubocop to 1.41.0 - [@ericproulx](https://github.com/ericproulx). * [#2296](https://github.com/ruby-grape/grape/pull/2296): Fix cops and enables some - [@ericproulx](https://github.com/ericproulx). * [#2302](https://github.com/ruby-grape/grape/pull/2302): Rack < 3 and update rack-test - [@ericproulx](https://github.com/ericproulx). +* [#2303](https://github.com/ruby-grape/grape/pull/2302): Rack >= 1.3.0 - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/grape.gemspec b/grape.gemspec index a2b312a75..bee87fbc6 100644 --- a/grape.gemspec +++ b/grape.gemspec @@ -24,7 +24,7 @@ Gem::Specification.new do |s| 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', '< 3' + s.add_runtime_dependency 'rack', '>= 1.3.0', '< 3' s.add_runtime_dependency 'rack-accept' s.files = %w[CHANGELOG.md CONTRIBUTING.md README.md grape.png UPGRADING.md LICENSE] From cdeaa1329654cca8d074c4b2c091f0ef66a4dbbc Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Mon, 9 Jan 2023 03:17:27 +0100 Subject: [PATCH 136/304] Revisit GitHub workflows (#2301) * Rename gemfiles to follow standard * Use ubuntu-latest Refactor ruby-matrix on test and edge Add rubygems: latest Add 3.2 * Fix typo * Remove rack_3_0.gemfile * Exclude ruby 2.6 for rails_7_0 * Add entry CHANGELOG.md * Remove spaces in arrays Add rubygems in danger.yml * Include [Rails 5.2, Ruby 2.6] Exclude [Rails 7.0, Ruby 2.6] * Fix test.yml * Only exclude * Lock rails_7_0 to not include 7.1 Lock rack_3_0 to not include 3.1 Remove rack_2_2 in favor of 2_0 to test Rename rack3 to rack_3_0 Testing workflow include/exclude * testing ruby matrix '2.7', '3.0', '3.1', '3.2' testing gemfile rack_2_0, rails_6_0, rails_6_1, rails_7_0 include 2.6 for rails_5_2 include 2.7 for rack_1_0, multi_json, multi_xml * Fix integration and eager_load * Try fixing test.yml * Danger now on 2.7 Remove matrix os in edge Add rubygems: latest to rubocop --- .github/workflows/danger.yml | 6 +- .github/workflows/edge.yml | 15 +--- .github/workflows/test.yml | 72 +++++++------------ CHANGELOG.md | 1 + gemfiles/{rack1.gemfile => rack_1_0.gemfile} | 0 .../{rack2_2.gemfile => rack_2_0.gemfile} | 2 +- gemfiles/{rack2.gemfile => rack_3_0.gemfile} | 2 +- .../{rails_5.gemfile => rails_5_2.gemfile} | 0 .../{rails_6.gemfile => rails_6_0.gemfile} | 0 gemfiles/rails_7.gemfile | 46 ------------ gemfiles/{rack3.gemfile => rails_7_0.gemfile} | 2 +- 11 files changed, 36 insertions(+), 110 deletions(-) rename gemfiles/{rack1.gemfile => rack_1_0.gemfile} (100%) rename gemfiles/{rack2_2.gemfile => rack_2_0.gemfile} (97%) rename gemfiles/{rack2.gemfile => rack_3_0.gemfile} (97%) rename gemfiles/{rails_5.gemfile => rails_5_2.gemfile} (100%) rename gemfiles/{rails_6.gemfile => rails_6_0.gemfile} (100%) delete mode 100644 gemfiles/rails_7.gemfile rename gemfiles/{rack3.gemfile => rails_7_0.gemfile} (97%) diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml index 380f5daa8..a8d1ba96d 100644 --- a/.github/workflows/danger.yml +++ b/.github/workflows/danger.yml @@ -1,11 +1,10 @@ --- name: danger - on: pull_request jobs: danger: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: @@ -13,8 +12,9 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: 2.6 + ruby-version: 2.7 bundler-cache: true + rubygems: latest - name: Run Danger run: | # the token is public, has public_repo scope and belongs to the grape-bot user owned by @dblock, this is ok diff --git a/.github/workflows/edge.yml b/.github/workflows/edge.yml index 3089415f5..d670b2a98 100644 --- a/.github/workflows/edge.yml +++ b/.github/workflows/edge.yml @@ -6,21 +6,12 @@ jobs: strategy: fail-fast: false matrix: - include: - - ruby: 2.7 - gemfile: 'gemfiles/rails_edge.gemfile' - - ruby: 2.7 - gemfile: 'gemfiles/rack_edge.gemfile' - - ruby: 2.7 - gemfile: 'gemfiles/rack3.gemfile' - - ruby: "ruby-head" - - ruby: "truffleruby-head" - - ruby: "jruby-head" + ruby: ['2.7', '3.0', '3.1', '3.2', ruby-head, truffleruby-head, jruby-head] + gemfile: [rails_edge, rack_edge, rack_3_0] runs-on: ubuntu-latest continue-on-error: true env: - BUNDLE_GEMFILE: ${{ matrix.gemfile }} - + BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1fb5251af..910a8cc6a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,64 +1,42 @@ ---- name: test -on: - push: - branches: - - "*" - pull_request: - branches: - - "*" + +on: [push, pull_request] + jobs: lint: name: RuboCop - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: 2.7 bundler-cache: true + rubygems: latest + - name: Run RuboCop run: bundle exec rubocop + test: strategy: fail-fast: false matrix: - ruby: - - 2.6 - - 2.7 - - "3.0" - gemfile: - - Gemfile - - gemfiles/rack1.gemfile - - gemfiles/rack2.gemfile - - gemfiles/rack2_2.gemfile - - gemfiles/rails_5.gemfile - - gemfiles/rails_6.gemfile - - gemfiles/rails_6_1.gemfile + ruby: ['2.7', '3.0', '3.1', '3.2'] + gemfile: [rack_2_0, rails_6_0, rails_6_1, rails_7_0] include: - - ruby: 3.1 - gemfile: 'gemfiles/multi_json.gemfile' - - ruby: 3.1 - gemfile: 'gemfiles/multi_xml.gemfile' - - ruby: 3.1 - gemfile: 'gemfiles/rails_7.gemfile' - - ruby: "3.0" - gemfile: 'gemfiles/multi_json.gemfile' - - ruby: "3.0" - gemfile: 'gemfiles/multi_xml.gemfile' - - ruby: "3.0" - gemfile: 'gemfiles/rails_7.gemfile' - - ruby: 2.7 - gemfile: 'gemfiles/multi_json.gemfile' - - ruby: 2.7 - gemfile: 'gemfiles/multi_xml.gemfile' - - ruby: 2.7 - gemfile: 'gemfiles/rails_7.gemfile' - runs-on: ubuntu-20.04 + - ruby: '2.6' + gemfile: rails_5_2 + - ruby: '2.7' + gemfile: rack_1_0 + - ruby: '2.7' + gemfile: multi_json + - ruby: '2.7' + gemfile: multi_xml + runs-on: ubuntu-latest env: - BUNDLE_GEMFILE: ${{ matrix.gemfile }} - + BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile steps: - uses: actions/checkout@v3 @@ -67,20 +45,22 @@ jobs: with: ruby-version: ${{ matrix.ruby }} bundler-cache: true + rubygems: latest - name: Run tests run: bundle exec rake spec - name: Run tests (spec/integration/eager_load) - if: ${{ matrix.gemfile == 'Gemfile' }} + # rack_2_0.gemfile is equals to Gemfile + if: ${{ matrix.gemfile == 'rack_2_0' }} run: bundle exec rspec spec/integration/eager_load - name: Run tests (spec/integration/multi_json) - if: ${{ matrix.gemfile == 'gemfiles/multi_json.gemfile' }} + if: ${{ matrix.gemfile == 'multi_json' }} run: bundle exec rspec spec/integration/multi_json - name: Run tests (spec/integration/multi_xml) - if: ${{ matrix.gemfile == 'gemfiles/multi_xml.gemfile' }} + if: ${{ matrix.gemfile == 'multi_xml' }} run: bundle exec rspec spec/integration/multi_xml - name: Coveralls @@ -98,4 +78,4 @@ jobs: uses: coverallsapp/github-action@master with: github-token: ${{ secrets.github_token }} - parallel-finished: true + parallel-finished: true \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 372511dbe..e57ddc7d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * [#2296](https://github.com/ruby-grape/grape/pull/2296): Fix cops and enables some - [@ericproulx](https://github.com/ericproulx). * [#2302](https://github.com/ruby-grape/grape/pull/2302): Rack < 3 and update rack-test - [@ericproulx](https://github.com/ericproulx). * [#2303](https://github.com/ruby-grape/grape/pull/2302): Rack >= 1.3.0 - [@ericproulx](https://github.com/ericproulx). +* [#2301](https://github.com/ruby-grape/grape/pull/2301): Revisit GH workflows - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/gemfiles/rack1.gemfile b/gemfiles/rack_1_0.gemfile similarity index 100% rename from gemfiles/rack1.gemfile rename to gemfiles/rack_1_0.gemfile diff --git a/gemfiles/rack2_2.gemfile b/gemfiles/rack_2_0.gemfile similarity index 97% rename from gemfiles/rack2_2.gemfile rename to gemfiles/rack_2_0.gemfile index 31ef181af..c12cd1fce 100644 --- a/gemfiles/rack2_2.gemfile +++ b/gemfiles/rack_2_0.gemfile @@ -4,7 +4,7 @@ source 'https://rubygems.org' -gem 'rack', '~> 2.2' +gem 'rack', '~> 2.0' group :development, :test do gem 'bundler' diff --git a/gemfiles/rack2.gemfile b/gemfiles/rack_3_0.gemfile similarity index 97% rename from gemfiles/rack2.gemfile rename to gemfiles/rack_3_0.gemfile index 5d30e7930..b4d53aee8 100644 --- a/gemfiles/rack2.gemfile +++ b/gemfiles/rack_3_0.gemfile @@ -4,7 +4,7 @@ source 'https://rubygems.org' -gem 'rack', '~> 2.0.0' +gem 'rack', '~> 3.0.0' group :development, :test do gem 'bundler' diff --git a/gemfiles/rails_5.gemfile b/gemfiles/rails_5_2.gemfile similarity index 100% rename from gemfiles/rails_5.gemfile rename to gemfiles/rails_5_2.gemfile diff --git a/gemfiles/rails_6.gemfile b/gemfiles/rails_6_0.gemfile similarity index 100% rename from gemfiles/rails_6.gemfile rename to gemfiles/rails_6_0.gemfile diff --git a/gemfiles/rails_7.gemfile b/gemfiles/rails_7.gemfile deleted file mode 100644 index 1c3e639f4..000000000 --- a/gemfiles/rails_7.gemfile +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -# This file was generated by Appraisal - -source 'https://rubygems.org' - -gem 'rails', '~> 7.0' - -group :development, :test do - gem 'bundler' - gem 'hashie' - gem 'rake' - gem 'rubocop', '1.41.0' - gem 'rubocop-ast' - gem 'rubocop-performance', require: false - gem 'rubocop-rspec', 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 'grape-entity', '~> 0.6' - gem 'maruku' - gem 'mime-types' - gem 'rack-jsonp', require: 'rack/jsonp' - gem 'rack-test' - gem 'rspec', '~> 3.11.0' - gem 'ruby-grape-danger', '~> 0.2.0', require: false - gem 'simplecov', '~> 0.21.2' - gem 'simplecov-lcov', '~> 0.8.0' - gem 'test-prof', require: false -end - -platforms :jruby do - gem 'racc' -end - -gemspec path: '../' diff --git a/gemfiles/rack3.gemfile b/gemfiles/rails_7_0.gemfile similarity index 97% rename from gemfiles/rack3.gemfile rename to gemfiles/rails_7_0.gemfile index b3ff65fc2..ce8b6256d 100644 --- a/gemfiles/rack3.gemfile +++ b/gemfiles/rails_7_0.gemfile @@ -4,7 +4,7 @@ source 'https://rubygems.org' -gem 'rack', '~> 3' +gem 'rails', '~> 7.0.0' group :development, :test do gem 'bundler' From 3e865af49a27d1c391809fcae37a334b33c9519a Mon Sep 17 00:00:00 2001 From: Alexander Kurakin Date: Tue, 10 Jan 2023 14:58:41 +0300 Subject: [PATCH 137/304] Grape::Exceptions::Base#initialize: call super earlier --- lib/grape/exceptions/base.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/grape/exceptions/base.rb b/lib/grape/exceptions/base.rb index 1dc1f518a..31848a94f 100644 --- a/lib/grape/exceptions/base.rb +++ b/lib/grape/exceptions/base.rb @@ -10,9 +10,10 @@ class Base < StandardError attr_reader :status, :headers def initialize(status: nil, message: nil, headers: nil, **_options) + super(message) + @status = status @headers = headers - super(message) end def [](index) From 8c2f3b8d5fc8be11410e59727cb4b0759fc71c46 Mon Sep 17 00:00:00 2001 From: Radin Reth Date: Thu, 16 Feb 2023 01:53:49 +0700 Subject: [PATCH 138/304] Update README.md (#2306) Wrong typo `API::Enitities::Status` --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 185098466..694b87a07 100644 --- a/README.md +++ b/README.md @@ -539,12 +539,12 @@ end class V1 < Grape::API version 'v1' - mount BasicAPI, with: { entity: mounted { configuration[:entity] || API::Enitities::Status } } + mount BasicAPI, with: { entity: mounted { configuration[:entity] || API::Entities::Status } } end class V2 < Grape::API version 'v2' - mount BasicAPI, with: { entity: mounted { configuration[:entity] || API::Enitities::V2::Status } } + mount BasicAPI, with: { entity: mounted { configuration[:entity] || API::Entities::V2::Status } } end ``` From fc89d001eba2fed39461800353959b72923ebff2 Mon Sep 17 00:00:00 2001 From: Nathan Fixler Date: Wed, 22 Feb 2023 17:48:35 -0800 Subject: [PATCH 139/304] Move convenience class into new file This commit moves the `Grape::Types::InvalidValue` class definition into a separate file so that it can be lazy loaded in projects that use zeitwerk file naming conventions. In environments that do not eager load, referencing the `Grape::Validations::Types::InvalidValue` class before `Grape::Types::InvalidValue` causes a load error due to the fact that both classes were defined in the same file. --- CHANGELOG.md | 1 + lib/grape.rb | 8 ++++++++ lib/grape/types/invalid_value.rb | 8 ++++++++ lib/grape/validations/types/invalid_value.rb | 7 ------- 4 files changed, 17 insertions(+), 7 deletions(-) create mode 100644 lib/grape/types/invalid_value.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index e57ddc7d6..946d59e12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ #### Fixes * [#2299](https://github.com/ruby-grape/grape/pull/2299): Fix, do not use kwargs for empty args - [@dm1try](https://github.com/dm1try). +* [#2307](https://github.com/ruby-grape/grape/pull/2307): Fixed autoloading of InvalidValue - [@fixlr](https://github.com/fixlr). * Your contribution here. ### 1.7.0 (2022/12/20) diff --git a/lib/grape.rb b/lib/grape.rb index 0b5db09ff..f51a2e8b0 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -277,6 +277,14 @@ module Validators end end end + + module Types + extend ::ActiveSupport::Autoload + + eager_autoload do + autoload :InvalidValue + end + end end require 'grape/config' diff --git a/lib/grape/types/invalid_value.rb b/lib/grape/types/invalid_value.rb new file mode 100644 index 000000000..ae356daa1 --- /dev/null +++ b/lib/grape/types/invalid_value.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# 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/invalid_value.rb b/lib/grape/validations/types/invalid_value.rb index 5c566a642..9744a285f 100644 --- a/lib/grape/validations/types/invalid_value.rb +++ b/lib/grape/validations/types/invalid_value.rb @@ -15,10 +15,3 @@ def initialize(message = nil) 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 From c6d2c8c8f6d1b26502620738cc75850adb4e38d6 Mon Sep 17 00:00:00 2001 From: Masatoshi Iwasaki Date: Thu, 16 Mar 2023 17:49:06 +0900 Subject: [PATCH 140/304] Add how to empty the body with status codes other than 204 --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 694b87a07..e6f8b13e2 100644 --- a/README.md +++ b/README.md @@ -3341,6 +3341,17 @@ end Use `body false` to return `204 No Content` without any data or content-type. +If you want to empty the body with an HTTP status code other than `204 No Content`, you can override the status code after specifying `body false` as follows + +```ruby +class API < Grape::API + get '/' do + body false + status 304 + end +end +``` + You can also set the response to a file with `sendfile`. This works with the [Rack::Sendfile](https://www.rubydoc.info/gems/rack/Rack/Sendfile) middleware to optimally send the file through your web server software. From cdb8316584d1bbb6e839ae726acb593b7264e61c Mon Sep 17 00:00:00 2001 From: duffn <3457341+duffn@users.noreply.github.com> Date: Mon, 20 Mar 2023 09:44:08 -0600 Subject: [PATCH 141/304] Fix tests by pinning rack-test to < 2.1 (#2311) * Pin rack-test to < 2.1 * Fix Rubocop violation * Update CHANGELOG --- CHANGELOG.md | 1 + Gemfile | 2 +- gemfiles/multi_json.gemfile | 2 +- gemfiles/multi_xml.gemfile | 2 +- gemfiles/rack_1_0.gemfile | 2 +- gemfiles/rack_2_0.gemfile | 2 +- gemfiles/rack_3_0.gemfile | 2 +- gemfiles/rack_edge.gemfile | 2 +- gemfiles/rails_5_2.gemfile | 2 +- gemfiles/rails_6_0.gemfile | 2 +- gemfiles/rails_6_1.gemfile | 2 +- gemfiles/rails_7_0.gemfile | 2 +- gemfiles/rails_edge.gemfile | 2 +- spec/grape/validations_spec.rb | 12 ++++++------ 14 files changed, 19 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 946d59e12..64fc42ac0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * [#2302](https://github.com/ruby-grape/grape/pull/2302): Rack < 3 and update rack-test - [@ericproulx](https://github.com/ericproulx). * [#2303](https://github.com/ruby-grape/grape/pull/2302): Rack >= 1.3.0 - [@ericproulx](https://github.com/ericproulx). * [#2301](https://github.com/ruby-grape/grape/pull/2301): Revisit GH workflows - [@ericproulx](https://github.com/ericproulx). +* [#2311](https://github.com/ruby-grape/grape/pull/2311): Fix tests by pinning rack-test to < 2.1 - [@duffn](https://github.com/duffn). * Your contribution here. #### Fixes diff --git a/Gemfile b/Gemfile index 7e7aef6a3..def07ccb8 100644 --- a/Gemfile +++ b/Gemfile @@ -31,7 +31,7 @@ group :test do gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' - gem 'rack-test' + gem 'rack-test', '< 2.1' gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'simplecov', '~> 0.21.2' diff --git a/gemfiles/multi_json.gemfile b/gemfiles/multi_json.gemfile index f306bc020..21297849b 100644 --- a/gemfiles/multi_json.gemfile +++ b/gemfiles/multi_json.gemfile @@ -31,7 +31,7 @@ group :test do gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' - gem 'rack-test' + gem 'rack-test', '< 2.1' gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'simplecov', '~> 0.21.2' diff --git a/gemfiles/multi_xml.gemfile b/gemfiles/multi_xml.gemfile index f43c7b8c2..646279818 100644 --- a/gemfiles/multi_xml.gemfile +++ b/gemfiles/multi_xml.gemfile @@ -31,7 +31,7 @@ group :test do gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' - gem 'rack-test' + gem 'rack-test', '< 2.1' gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'simplecov', '~> 0.21.2' diff --git a/gemfiles/rack_1_0.gemfile b/gemfiles/rack_1_0.gemfile index 647416e59..e084f894f 100644 --- a/gemfiles/rack_1_0.gemfile +++ b/gemfiles/rack_1_0.gemfile @@ -31,7 +31,7 @@ group :test do gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' - gem 'rack-test' + gem 'rack-test', '< 2.1' gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'simplecov', '~> 0.21.2' diff --git a/gemfiles/rack_2_0.gemfile b/gemfiles/rack_2_0.gemfile index c12cd1fce..228c407ed 100644 --- a/gemfiles/rack_2_0.gemfile +++ b/gemfiles/rack_2_0.gemfile @@ -31,7 +31,7 @@ group :test do gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' - gem 'rack-test' + gem 'rack-test', '< 2.1' gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'simplecov', '~> 0.21.2' diff --git a/gemfiles/rack_3_0.gemfile b/gemfiles/rack_3_0.gemfile index b4d53aee8..61498f7b3 100644 --- a/gemfiles/rack_3_0.gemfile +++ b/gemfiles/rack_3_0.gemfile @@ -31,7 +31,7 @@ group :test do gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' - gem 'rack-test' + gem 'rack-test', '< 2.1' gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'simplecov', '~> 0.21.2' diff --git a/gemfiles/rack_edge.gemfile b/gemfiles/rack_edge.gemfile index 137848ded..aaa3b4671 100644 --- a/gemfiles/rack_edge.gemfile +++ b/gemfiles/rack_edge.gemfile @@ -31,7 +31,7 @@ group :test do gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' - gem 'rack-test' + gem 'rack-test', '< 2.1' gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'simplecov', '~> 0.21.2' diff --git a/gemfiles/rails_5_2.gemfile b/gemfiles/rails_5_2.gemfile index 75cc1ebe9..61ef8e192 100644 --- a/gemfiles/rails_5_2.gemfile +++ b/gemfiles/rails_5_2.gemfile @@ -31,7 +31,7 @@ group :test do gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' - gem 'rack-test' + gem 'rack-test', '< 2.1' gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'simplecov', '~> 0.21.2' diff --git a/gemfiles/rails_6_0.gemfile b/gemfiles/rails_6_0.gemfile index a07b203b5..b4e3d1d56 100644 --- a/gemfiles/rails_6_0.gemfile +++ b/gemfiles/rails_6_0.gemfile @@ -31,7 +31,7 @@ group :test do gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' - gem 'rack-test' + gem 'rack-test', '< 2.1' gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'simplecov', '~> 0.21.2' diff --git a/gemfiles/rails_6_1.gemfile b/gemfiles/rails_6_1.gemfile index 2f62e7130..673ec317d 100644 --- a/gemfiles/rails_6_1.gemfile +++ b/gemfiles/rails_6_1.gemfile @@ -31,7 +31,7 @@ group :test do gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' - gem 'rack-test' + gem 'rack-test', '< 2.1' gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'simplecov', '~> 0.21.2' diff --git a/gemfiles/rails_7_0.gemfile b/gemfiles/rails_7_0.gemfile index ce8b6256d..c9260e467 100644 --- a/gemfiles/rails_7_0.gemfile +++ b/gemfiles/rails_7_0.gemfile @@ -31,7 +31,7 @@ group :test do gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' - gem 'rack-test' + gem 'rack-test', '< 2.1' gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'simplecov', '~> 0.21.2' diff --git a/gemfiles/rails_edge.gemfile b/gemfiles/rails_edge.gemfile index 50d8260d0..3614288d1 100644 --- a/gemfiles/rails_edge.gemfile +++ b/gemfiles/rails_edge.gemfile @@ -31,7 +31,7 @@ group :test do gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' - gem 'rack-test' + gem 'rack-test', '< 2.1' gem 'rspec', '~> 3.11.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'simplecov', '~> 0.21.2' diff --git a/spec/grape/validations_spec.rb b/spec/grape/validations_spec.rb index 0a3423254..7e2ae9bc9 100644 --- a/spec/grape/validations_spec.rb +++ b/spec/grape/validations_spec.rb @@ -1071,12 +1071,12 @@ def validate_param!(attr_name, params) } # 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.body.split(', ')).to contain_exactly( + 'top[3][top_id] is empty', + 'top[2][middle_1][0][middle_1_id] is empty', + 'top[1][middle_1][1][middle_2][0][middle_2_id] is empty', + 'top[0][middle_1][1][middle_2][1][bottom][0][bottom_id] is empty' + ) expect(last_response.status).to eq(400) end end From d6b26aff05ffcaf8c5822ee864aa9d6d8ade7ba3 Mon Sep 17 00:00:00 2001 From: duffn <3457341+duffn@users.noreply.github.com> Date: Mon, 20 Mar 2023 09:50:43 -0600 Subject: [PATCH 142/304] Fix YARD docs markdown rendering (#2310) * Fix YARD doc markdown rendering * Fix Rubocop violation * Fix contain_exactly test --------- Co-authored-by: Daniel (dB.) Doubrovkine --- .gitignore | 1 + .yardopts | 1 + CHANGELOG.md | 1 + CONTRIBUTING.md | 2 ++ 4 files changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 9e152a5d4..f9212a64a 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ dist Gemfile.lock gemfiles/*.lock tmp +.yardoc ## Rubinius .rbx diff --git a/.yardopts b/.yardopts index 51701cd72..0e82f4d97 100644 --- a/.yardopts +++ b/.yardopts @@ -1 +1,2 @@ --asset grape.png +--markup markdown diff --git a/CHANGELOG.md b/CHANGELOG.md index 64fc42ac0..d3eadea17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * [#2303](https://github.com/ruby-grape/grape/pull/2302): Rack >= 1.3.0 - [@ericproulx](https://github.com/ericproulx). * [#2301](https://github.com/ruby-grape/grape/pull/2301): Revisit GH workflows - [@ericproulx](https://github.com/ericproulx). * [#2311](https://github.com/ruby-grape/grape/pull/2311): Fix tests by pinning rack-test to < 2.1 - [@duffn](https://github.com/duffn). +* [#2310](https://github.com/ruby-grape/grape/pull/2310): Fix YARD docs markdown rendering - [@duffn](https://github.com/duffn). * Your contribution here. #### Fixes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7cde59694..5405c652a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -58,6 +58,8 @@ Make sure that `bundle exec rake` completes without errors. Document any external behavior in the [README](README.md). +You should also document code as necessary, using current code as examples. This project uses [YARD](https://yardoc.org/). You can run and preview the docs locally by [installing `yard`](https://yardoc.org/), running `yard server --reload` and view the docs at http://localhost:8808. + #### Update Changelog Add a line to [CHANGELOG](CHANGELOG.md) under *Next Release*. Make it look like every other line, including your name and link to your Github account. From aacbe2703300782d8f513df529d38a525d60c986 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sun, 16 Apr 2023 20:33:11 +0200 Subject: [PATCH 143/304] Update rspec version from `3.11.0` to `< 4` (#2315) * Change rspec version from `3.11.0` to `< 4` * Add CHANGELOG.md entry --- CHANGELOG.md | 1 + Gemfile | 2 +- gemfiles/multi_json.gemfile | 2 +- gemfiles/multi_xml.gemfile | 2 +- gemfiles/rack_1_0.gemfile | 2 +- gemfiles/rack_2_0.gemfile | 2 +- gemfiles/rack_3_0.gemfile | 2 +- gemfiles/rack_edge.gemfile | 2 +- gemfiles/rails_5_2.gemfile | 2 +- gemfiles/rails_6_0.gemfile | 2 +- gemfiles/rails_6_1.gemfile | 2 +- gemfiles/rails_7_0.gemfile | 2 +- gemfiles/rails_edge.gemfile | 2 +- 13 files changed, 13 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3eadea17..27316bf33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ * [#2299](https://github.com/ruby-grape/grape/pull/2299): Fix, do not use kwargs for empty args - [@dm1try](https://github.com/dm1try). * [#2307](https://github.com/ruby-grape/grape/pull/2307): Fixed autoloading of InvalidValue - [@fixlr](https://github.com/fixlr). +* [#23015](https://github.com/ruby-grape/grape/pull/2315): Update rspec - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 1.7.0 (2022/12/20) diff --git a/Gemfile b/Gemfile index def07ccb8..04fbe2dce 100644 --- a/Gemfile +++ b/Gemfile @@ -32,7 +32,7 @@ group :test do gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' - gem 'rspec', '~> 3.11.0' + gem 'rspec', '< 4' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'simplecov', '~> 0.21.2' gem 'simplecov-lcov', '~> 0.8.0' diff --git a/gemfiles/multi_json.gemfile b/gemfiles/multi_json.gemfile index 21297849b..571e0f7ea 100644 --- a/gemfiles/multi_json.gemfile +++ b/gemfiles/multi_json.gemfile @@ -32,7 +32,7 @@ group :test do gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' - gem 'rspec', '~> 3.11.0' + gem 'rspec', '< 4' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'simplecov', '~> 0.21.2' gem 'simplecov-lcov', '~> 0.8.0' diff --git a/gemfiles/multi_xml.gemfile b/gemfiles/multi_xml.gemfile index 646279818..a91918f2c 100644 --- a/gemfiles/multi_xml.gemfile +++ b/gemfiles/multi_xml.gemfile @@ -32,7 +32,7 @@ group :test do gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' - gem 'rspec', '~> 3.11.0' + gem 'rspec', '< 4' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'simplecov', '~> 0.21.2' gem 'simplecov-lcov', '~> 0.8.0' diff --git a/gemfiles/rack_1_0.gemfile b/gemfiles/rack_1_0.gemfile index e084f894f..85f666bcb 100644 --- a/gemfiles/rack_1_0.gemfile +++ b/gemfiles/rack_1_0.gemfile @@ -32,7 +32,7 @@ group :test do gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' - gem 'rspec', '~> 3.11.0' + gem 'rspec', '< 4' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'simplecov', '~> 0.21.2' gem 'simplecov-lcov', '~> 0.8.0' diff --git a/gemfiles/rack_2_0.gemfile b/gemfiles/rack_2_0.gemfile index 228c407ed..fc6a14a9c 100644 --- a/gemfiles/rack_2_0.gemfile +++ b/gemfiles/rack_2_0.gemfile @@ -32,7 +32,7 @@ group :test do gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' - gem 'rspec', '~> 3.11.0' + gem 'rspec', '< 4' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'simplecov', '~> 0.21.2' gem 'simplecov-lcov', '~> 0.8.0' diff --git a/gemfiles/rack_3_0.gemfile b/gemfiles/rack_3_0.gemfile index 61498f7b3..d3f24250a 100644 --- a/gemfiles/rack_3_0.gemfile +++ b/gemfiles/rack_3_0.gemfile @@ -32,7 +32,7 @@ group :test do gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' - gem 'rspec', '~> 3.11.0' + gem 'rspec', '< 4' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'simplecov', '~> 0.21.2' gem 'simplecov-lcov', '~> 0.8.0' diff --git a/gemfiles/rack_edge.gemfile b/gemfiles/rack_edge.gemfile index aaa3b4671..072089645 100644 --- a/gemfiles/rack_edge.gemfile +++ b/gemfiles/rack_edge.gemfile @@ -32,7 +32,7 @@ group :test do gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' - gem 'rspec', '~> 3.11.0' + gem 'rspec', '< 4' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'simplecov', '~> 0.21.2' gem 'simplecov-lcov', '~> 0.8.0' diff --git a/gemfiles/rails_5_2.gemfile b/gemfiles/rails_5_2.gemfile index 61ef8e192..5fa2e2955 100644 --- a/gemfiles/rails_5_2.gemfile +++ b/gemfiles/rails_5_2.gemfile @@ -32,7 +32,7 @@ group :test do gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' - gem 'rspec', '~> 3.11.0' + gem 'rspec', '< 4' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'simplecov', '~> 0.21.2' gem 'simplecov-lcov', '~> 0.8.0' diff --git a/gemfiles/rails_6_0.gemfile b/gemfiles/rails_6_0.gemfile index b4e3d1d56..27d0b5d59 100644 --- a/gemfiles/rails_6_0.gemfile +++ b/gemfiles/rails_6_0.gemfile @@ -32,7 +32,7 @@ group :test do gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' - gem 'rspec', '~> 3.11.0' + gem 'rspec', '< 4' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'simplecov', '~> 0.21.2' gem 'simplecov-lcov', '~> 0.8.0' diff --git a/gemfiles/rails_6_1.gemfile b/gemfiles/rails_6_1.gemfile index 673ec317d..55bc916cd 100644 --- a/gemfiles/rails_6_1.gemfile +++ b/gemfiles/rails_6_1.gemfile @@ -32,7 +32,7 @@ group :test do gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' - gem 'rspec', '~> 3.11.0' + gem 'rspec', '< 4' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'simplecov', '~> 0.21.2' gem 'simplecov-lcov', '~> 0.8.0' diff --git a/gemfiles/rails_7_0.gemfile b/gemfiles/rails_7_0.gemfile index c9260e467..b7cfc5cbf 100644 --- a/gemfiles/rails_7_0.gemfile +++ b/gemfiles/rails_7_0.gemfile @@ -32,7 +32,7 @@ group :test do gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' - gem 'rspec', '~> 3.11.0' + gem 'rspec', '< 4' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'simplecov', '~> 0.21.2' gem 'simplecov-lcov', '~> 0.8.0' diff --git a/gemfiles/rails_edge.gemfile b/gemfiles/rails_edge.gemfile index 3614288d1..5bc6138fc 100644 --- a/gemfiles/rails_edge.gemfile +++ b/gemfiles/rails_edge.gemfile @@ -32,7 +32,7 @@ group :test do gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' - gem 'rspec', '~> 3.11.0' + gem 'rspec', '< 4' gem 'ruby-grape-danger', '~> 0.2.0', require: false gem 'simplecov', '~> 0.21.2' gem 'simplecov-lcov', '~> 0.8.0' From a76650e2f5df928be397891b8509ada2f0aa5d99 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sun, 16 Apr 2023 20:33:52 +0200 Subject: [PATCH 144/304] Remove maruku and rubocop-ast as dependencies (#2317) * Remove maruku and rubocop-ast as dependencies * Add CHANGELOG.md entry --- CHANGELOG.md | 1 + Gemfile | 2 -- gemfiles/multi_json.gemfile | 2 -- gemfiles/multi_xml.gemfile | 2 -- gemfiles/rack_1_0.gemfile | 2 -- gemfiles/rack_2_0.gemfile | 2 -- gemfiles/rack_3_0.gemfile | 2 -- gemfiles/rack_edge.gemfile | 2 -- gemfiles/rails_5_2.gemfile | 2 -- gemfiles/rails_6_0.gemfile | 2 -- gemfiles/rails_6_1.gemfile | 2 -- gemfiles/rails_7_0.gemfile | 1 - gemfiles/rails_edge.gemfile | 2 -- 13 files changed, 1 insertion(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27316bf33..7bb69f21e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ * [#2301](https://github.com/ruby-grape/grape/pull/2301): Revisit GH workflows - [@ericproulx](https://github.com/ericproulx). * [#2311](https://github.com/ruby-grape/grape/pull/2311): Fix tests by pinning rack-test to < 2.1 - [@duffn](https://github.com/duffn). * [#2310](https://github.com/ruby-grape/grape/pull/2310): Fix YARD docs markdown rendering - [@duffn](https://github.com/duffn). +* [#2317](https://github.com/ruby-grape/grape/pull/2317): Remove maruku and rubocop-ast as direct development/testing dependencies - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/Gemfile b/Gemfile index 04fbe2dce..2981e6a3e 100644 --- a/Gemfile +++ b/Gemfile @@ -11,7 +11,6 @@ group :development, :test do gem 'hashie' gem 'rake' gem 'rubocop', '1.41.0' - gem 'rubocop-ast' gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false end @@ -28,7 +27,6 @@ end group :test do gem 'cookiejar' gem 'grape-entity', '~> 0.6' - gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' diff --git a/gemfiles/multi_json.gemfile b/gemfiles/multi_json.gemfile index 571e0f7ea..876d54ef9 100644 --- a/gemfiles/multi_json.gemfile +++ b/gemfiles/multi_json.gemfile @@ -11,7 +11,6 @@ group :development, :test do gem 'hashie' gem 'rake' gem 'rubocop', '1.41.0' - gem 'rubocop-ast' gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false end @@ -28,7 +27,6 @@ end group :test do gem 'cookiejar' gem 'grape-entity', '~> 0.6' - gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' diff --git a/gemfiles/multi_xml.gemfile b/gemfiles/multi_xml.gemfile index a91918f2c..d70611d47 100644 --- a/gemfiles/multi_xml.gemfile +++ b/gemfiles/multi_xml.gemfile @@ -11,7 +11,6 @@ group :development, :test do gem 'hashie' gem 'rake' gem 'rubocop', '1.41.0' - gem 'rubocop-ast' gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false end @@ -28,7 +27,6 @@ end group :test do gem 'cookiejar' gem 'grape-entity', '~> 0.6' - gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' diff --git a/gemfiles/rack_1_0.gemfile b/gemfiles/rack_1_0.gemfile index 85f666bcb..17489b8fc 100644 --- a/gemfiles/rack_1_0.gemfile +++ b/gemfiles/rack_1_0.gemfile @@ -11,7 +11,6 @@ group :development, :test do gem 'hashie' gem 'rake' gem 'rubocop', '1.41.0' - gem 'rubocop-ast' gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false end @@ -28,7 +27,6 @@ end group :test do gem 'cookiejar' gem 'grape-entity', '~> 0.6' - gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' diff --git a/gemfiles/rack_2_0.gemfile b/gemfiles/rack_2_0.gemfile index fc6a14a9c..c07c4cbc3 100644 --- a/gemfiles/rack_2_0.gemfile +++ b/gemfiles/rack_2_0.gemfile @@ -11,7 +11,6 @@ group :development, :test do gem 'hashie' gem 'rake' gem 'rubocop', '1.41.0' - gem 'rubocop-ast' gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false end @@ -28,7 +27,6 @@ end group :test do gem 'cookiejar' gem 'grape-entity', '~> 0.6' - gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' diff --git a/gemfiles/rack_3_0.gemfile b/gemfiles/rack_3_0.gemfile index d3f24250a..e1d2dfe6a 100644 --- a/gemfiles/rack_3_0.gemfile +++ b/gemfiles/rack_3_0.gemfile @@ -11,7 +11,6 @@ group :development, :test do gem 'hashie' gem 'rake' gem 'rubocop', '1.41.0' - gem 'rubocop-ast' gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false end @@ -28,7 +27,6 @@ end group :test do gem 'cookiejar' gem 'grape-entity', '~> 0.6' - gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' diff --git a/gemfiles/rack_edge.gemfile b/gemfiles/rack_edge.gemfile index 072089645..cb70d5937 100644 --- a/gemfiles/rack_edge.gemfile +++ b/gemfiles/rack_edge.gemfile @@ -11,7 +11,6 @@ group :development, :test do gem 'hashie' gem 'rake' gem 'rubocop', '1.41.0' - gem 'rubocop-ast' gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false end @@ -28,7 +27,6 @@ end group :test do gem 'cookiejar' gem 'grape-entity', '~> 0.6' - gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' diff --git a/gemfiles/rails_5_2.gemfile b/gemfiles/rails_5_2.gemfile index 5fa2e2955..4ea6fae34 100644 --- a/gemfiles/rails_5_2.gemfile +++ b/gemfiles/rails_5_2.gemfile @@ -11,7 +11,6 @@ group :development, :test do gem 'hashie' gem 'rake' gem 'rubocop', '1.41.0' - gem 'rubocop-ast' gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false end @@ -28,7 +27,6 @@ end group :test do gem 'cookiejar' gem 'grape-entity', '~> 0.6' - gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' diff --git a/gemfiles/rails_6_0.gemfile b/gemfiles/rails_6_0.gemfile index 27d0b5d59..ea09728f2 100644 --- a/gemfiles/rails_6_0.gemfile +++ b/gemfiles/rails_6_0.gemfile @@ -11,7 +11,6 @@ group :development, :test do gem 'hashie' gem 'rake' gem 'rubocop', '1.41.0' - gem 'rubocop-ast' gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false end @@ -28,7 +27,6 @@ end group :test do gem 'cookiejar' gem 'grape-entity', '~> 0.6' - gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' diff --git a/gemfiles/rails_6_1.gemfile b/gemfiles/rails_6_1.gemfile index 55bc916cd..1d6da7682 100644 --- a/gemfiles/rails_6_1.gemfile +++ b/gemfiles/rails_6_1.gemfile @@ -11,7 +11,6 @@ group :development, :test do gem 'hashie' gem 'rake' gem 'rubocop', '1.41.0' - gem 'rubocop-ast' gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false end @@ -28,7 +27,6 @@ end group :test do gem 'cookiejar' gem 'grape-entity', '~> 0.6' - gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' diff --git a/gemfiles/rails_7_0.gemfile b/gemfiles/rails_7_0.gemfile index b7cfc5cbf..8001ef025 100644 --- a/gemfiles/rails_7_0.gemfile +++ b/gemfiles/rails_7_0.gemfile @@ -28,7 +28,6 @@ end group :test do gem 'cookiejar' gem 'grape-entity', '~> 0.6' - gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' diff --git a/gemfiles/rails_edge.gemfile b/gemfiles/rails_edge.gemfile index 5bc6138fc..c09a63113 100644 --- a/gemfiles/rails_edge.gemfile +++ b/gemfiles/rails_edge.gemfile @@ -11,7 +11,6 @@ group :development, :test do gem 'hashie' gem 'rake' gem 'rubocop', '1.41.0' - gem 'rubocop-ast' gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false end @@ -28,7 +27,6 @@ end group :test do gem 'cookiejar' gem 'grape-entity', '~> 0.6' - gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' From 3b6f3db8ad9bb233a5fda6fc96290b85dcc1785c Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sun, 23 Apr 2023 17:40:06 -0400 Subject: [PATCH 145/304] Update rubocop, rubocop-performance and rubocop-rspec (#2319) * Update rubocop, rubocop-performance and rubocop-rspec Fix version to all. * Add CHANGELOG * Fix rails_7_0.gemfile --- .rubocop_todo.yml | 54 +++++++++++++++---- CHANGELOG.md | 3 +- Gemfile | 6 +-- gemfiles/multi_json.gemfile | 6 +-- gemfiles/multi_xml.gemfile | 6 +-- gemfiles/rack_1_0.gemfile | 6 +-- gemfiles/rack_2_0.gemfile | 6 +-- gemfiles/rack_3_0.gemfile | 6 +-- gemfiles/rack_edge.gemfile | 6 +-- gemfiles/rails_5_2.gemfile | 6 +-- gemfiles/rails_6_0.gemfile | 6 +-- gemfiles/rails_6_1.gemfile | 6 +-- gemfiles/rails_7_0.gemfile | 7 ++- gemfiles/rails_edge.gemfile | 6 +-- .../exceptions/invalid_accept_header_spec.rb | 3 ++ 15 files changed, 84 insertions(+), 49 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 42e421297..a79c3557a 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 5000` -# on 2022-12-22 16:47:25 UTC using RuboCop version 1.41.0. +# on 2023-04-23 13:05:19 UTC using RuboCop version 1.50.2. # 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 @@ -22,8 +22,8 @@ Gemspec/RequireMFA: Exclude: - 'grape.gemspec' -# Offense count: 2 -# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods. +# Offense count: 1 +# Configuration parameters: AllowedMethods, AllowedPatterns. Lint/AmbiguousBlockAssociation: Exclude: - 'spec/grape/dsl/routing_spec.rb' @@ -124,7 +124,7 @@ Naming/MethodParameterName: # Offense count: 18 # Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns. # SupportedStyles: snake_case, normalcase, non_integer -# AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339 +# AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64 Naming/VariableNumber: Exclude: - 'spec/grape/dsl/settings_spec.rb' @@ -137,7 +137,7 @@ RSpec/AnyInstance: - 'spec/grape/api_spec.rb' - 'spec/grape/middleware/base_spec.rb' -# Offense count: 345 +# Offense count: 347 # Configuration parameters: Prefixes, AllowedPatterns. # Prefixes: when, with, without RSpec/ContextWording: @@ -208,7 +208,7 @@ RSpec/DescribeClass: - 'spec/grape/named_api_spec.rb' - 'spec/grape/validations/instance_behaivour_spec.rb' -# Offense count: 2 +# Offense count: 3 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: CustomTransform, IgnoredWords, DisallowedExamples. # DisallowedExamples: works @@ -280,6 +280,16 @@ RSpec/FilePath: - 'spec/integration/multi_json/json_spec.rb' - 'spec/integration/multi_xml/xml_spec.rb' +# Offense count: 12 +# Configuration parameters: Max. +RSpec/IndexedLet: + Exclude: + - 'spec/grape/exceptions/validation_errors_spec.rb' + - 'spec/grape/middleware/versioner/header_spec.rb' + - 'spec/grape/middleware/versioner/param_spec.rb' + - 'spec/grape/presenters/presenter_spec.rb' + - 'spec/shared/versioning_examples.rb' + # Offense count: 38 # Configuration parameters: AssignmentOnly. RSpec/InstanceVariable: @@ -319,7 +329,7 @@ RSpec/MessageChain: Exclude: - 'spec/grape/middleware/formatter_spec.rb' -# Offense count: 139 +# Offense count: 138 # Configuration parameters: . # SupportedStyles: have_received, receive RSpec/MessageSpies: @@ -330,7 +340,7 @@ RSpec/MissingExampleGroupArgument: Exclude: - 'spec/grape/middleware/exception_spec.rb' -# Offense count: 767 +# Offense count: 766 # Configuration parameters: Max. RSpec/MultipleExpectations: Exclude: @@ -410,7 +420,7 @@ RSpec/MultipleMemoizedHelpers: - 'spec/grape/request_spec.rb' - 'spec/grape/validations/attributes_doc_spec.rb' -# Offense count: 2143 +# Offense count: 2147 # Configuration parameters: EnforcedStyle, IgnoreSharedExamples. # SupportedStyles: always, named_only RSpec/NamedSubject: @@ -543,12 +553,13 @@ RSpec/RepeatedExampleGroupDescription: - 'spec/grape/validations/validators/values_spec.rb' # Offense count: 6 +# This cop supports safe autocorrection (--autocorrect). RSpec/ScatteredSetup: Exclude: - 'spec/grape/util/inheritable_setting_spec.rb' - 'spec/grape/validations_spec.rb' -# Offense count: 10 +# Offense count: 9 RSpec/StubbedMock: Exclude: - 'spec/grape/api_spec.rb' @@ -600,7 +611,7 @@ Style/CombinableLoops: # Offense count: 2 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: MaxUnannotatedPlaceholdersAllowed, AllowedMethods, AllowedPatterns, IgnoredMethods. +# Configuration parameters: MaxUnannotatedPlaceholdersAllowed, AllowedMethods, AllowedPatterns. # SupportedStyles: annotated, template, unannotated Style/FormatStringToken: EnforcedStyle: template @@ -636,3 +647,24 @@ Style/RedundantConstantBase: - 'spec/grape/validations/validators/default_spec.rb' - 'spec/integration/multi_json/json_spec.rb' - 'spec/integration/multi_xml/xml_spec.rb' + +# Offense count: 2 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowAsExpressionSeparator. +Style/Semicolon: + Exclude: + - 'spec/grape/api_spec.rb' + +# Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: forbid_for_all_comparison_operators, forbid_for_equality_operators_only, require_for_all_comparison_operators, require_for_equality_operators_only +Style/YodaCondition: + Exclude: + - 'lib/grape/api.rb' + +# Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/ZeroLengthPredicate: + Exclude: + - 'lib/grape/validations/validators/exactly_one_of_validator.rb' diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bb69f21e..ce9824235 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,8 @@ * [#2299](https://github.com/ruby-grape/grape/pull/2299): Fix, do not use kwargs for empty args - [@dm1try](https://github.com/dm1try). * [#2307](https://github.com/ruby-grape/grape/pull/2307): Fixed autoloading of InvalidValue - [@fixlr](https://github.com/fixlr). -* [#23015](https://github.com/ruby-grape/grape/pull/2315): Update rspec - [@ericproulx](https://github.com/ericproulx). +* [#2315](https://github.com/ruby-grape/grape/pull/2315): Update rspec - [@ericproulx](https://github.com/ericproulx). +* [#2319](https://github.com/ruby-grape/grape/pull/2319): Update rubocop - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 1.7.0 (2022/12/20) diff --git a/Gemfile b/Gemfile index 2981e6a3e..7bce1e598 100644 --- a/Gemfile +++ b/Gemfile @@ -10,9 +10,9 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '1.41.0' - gem 'rubocop-performance', require: false - gem 'rubocop-rspec', require: false + gem 'rubocop', '1.50.2', require: false + gem 'rubocop-performance', '1.17.1', require: false + gem 'rubocop-rspec', '2.20.0', require: false end group :development do diff --git a/gemfiles/multi_json.gemfile b/gemfiles/multi_json.gemfile index 876d54ef9..84d5e1638 100644 --- a/gemfiles/multi_json.gemfile +++ b/gemfiles/multi_json.gemfile @@ -10,9 +10,9 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '1.41.0' - gem 'rubocop-performance', require: false - gem 'rubocop-rspec', require: false + gem 'rubocop', '1.50.2', require: false + gem 'rubocop-performance', '1.17.1', require: false + gem 'rubocop-rspec', '2.20.0', require: false end group :development do diff --git a/gemfiles/multi_xml.gemfile b/gemfiles/multi_xml.gemfile index d70611d47..eebdc1b08 100644 --- a/gemfiles/multi_xml.gemfile +++ b/gemfiles/multi_xml.gemfile @@ -10,9 +10,9 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '1.41.0' - gem 'rubocop-performance', require: false - gem 'rubocop-rspec', require: false + gem 'rubocop', '1.50.2', require: false + gem 'rubocop-performance', '1.17.1', require: false + gem 'rubocop-rspec', '2.20.0', require: false end group :development do diff --git a/gemfiles/rack_1_0.gemfile b/gemfiles/rack_1_0.gemfile index 17489b8fc..862236c80 100644 --- a/gemfiles/rack_1_0.gemfile +++ b/gemfiles/rack_1_0.gemfile @@ -10,9 +10,9 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '1.41.0' - gem 'rubocop-performance', require: false - gem 'rubocop-rspec', require: false + gem 'rubocop', '1.50.2', require: false + gem 'rubocop-performance', '1.17.1', require: false + gem 'rubocop-rspec', '2.20.0', require: false end group :development do diff --git a/gemfiles/rack_2_0.gemfile b/gemfiles/rack_2_0.gemfile index c07c4cbc3..e75b1699c 100644 --- a/gemfiles/rack_2_0.gemfile +++ b/gemfiles/rack_2_0.gemfile @@ -10,9 +10,9 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '1.41.0' - gem 'rubocop-performance', require: false - gem 'rubocop-rspec', require: false + gem 'rubocop', '1.50.2', require: false + gem 'rubocop-performance', '1.17.1', require: false + gem 'rubocop-rspec', '2.20.0', require: false end group :development do diff --git a/gemfiles/rack_3_0.gemfile b/gemfiles/rack_3_0.gemfile index e1d2dfe6a..2da2241a9 100644 --- a/gemfiles/rack_3_0.gemfile +++ b/gemfiles/rack_3_0.gemfile @@ -10,9 +10,9 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '1.41.0' - gem 'rubocop-performance', require: false - gem 'rubocop-rspec', require: false + gem 'rubocop', '1.50.2', require: false + gem 'rubocop-performance', '1.17.1', require: false + gem 'rubocop-rspec', '2.20.0', require: false end group :development do diff --git a/gemfiles/rack_edge.gemfile b/gemfiles/rack_edge.gemfile index cb70d5937..5d9c93ae0 100644 --- a/gemfiles/rack_edge.gemfile +++ b/gemfiles/rack_edge.gemfile @@ -10,9 +10,9 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '1.41.0' - gem 'rubocop-performance', require: false - gem 'rubocop-rspec', require: false + gem 'rubocop', '1.50.2', require: false + gem 'rubocop-performance', '1.17.1', require: false + gem 'rubocop-rspec', '2.20.0', require: false end group :development do diff --git a/gemfiles/rails_5_2.gemfile b/gemfiles/rails_5_2.gemfile index 4ea6fae34..3ff997974 100644 --- a/gemfiles/rails_5_2.gemfile +++ b/gemfiles/rails_5_2.gemfile @@ -10,9 +10,9 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '1.41.0' - gem 'rubocop-performance', require: false - gem 'rubocop-rspec', require: false + gem 'rubocop', '1.50.2', require: false + gem 'rubocop-performance', '1.17.1', require: false + gem 'rubocop-rspec', '2.20.0', require: false end group :development do diff --git a/gemfiles/rails_6_0.gemfile b/gemfiles/rails_6_0.gemfile index ea09728f2..e0f12b62f 100644 --- a/gemfiles/rails_6_0.gemfile +++ b/gemfiles/rails_6_0.gemfile @@ -10,9 +10,9 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '1.41.0' - gem 'rubocop-performance', require: false - gem 'rubocop-rspec', require: false + gem 'rubocop', '1.50.2', require: false + gem 'rubocop-performance', '1.17.1', require: false + gem 'rubocop-rspec', '2.20.0', require: false end group :development do diff --git a/gemfiles/rails_6_1.gemfile b/gemfiles/rails_6_1.gemfile index 1d6da7682..10a8c9eef 100644 --- a/gemfiles/rails_6_1.gemfile +++ b/gemfiles/rails_6_1.gemfile @@ -10,9 +10,9 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '1.41.0' - gem 'rubocop-performance', require: false - gem 'rubocop-rspec', require: false + gem 'rubocop', '1.50.2', require: false + gem 'rubocop-performance', '1.17.1', require: false + gem 'rubocop-rspec', '2.20.0', require: false end group :development do diff --git a/gemfiles/rails_7_0.gemfile b/gemfiles/rails_7_0.gemfile index 8001ef025..97b71f0e7 100644 --- a/gemfiles/rails_7_0.gemfile +++ b/gemfiles/rails_7_0.gemfile @@ -10,10 +10,9 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '1.41.0' - gem 'rubocop-ast' - gem 'rubocop-performance', require: false - gem 'rubocop-rspec', require: false + gem 'rubocop', '1.50.2', require: false + gem 'rubocop-performance', '1.17.1', require: false + gem 'rubocop-rspec', '2.20.0', require: false end group :development do diff --git a/gemfiles/rails_edge.gemfile b/gemfiles/rails_edge.gemfile index c09a63113..cc01d9f97 100644 --- a/gemfiles/rails_edge.gemfile +++ b/gemfiles/rails_edge.gemfile @@ -10,9 +10,9 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '1.41.0' - gem 'rubocop-performance', require: false - gem 'rubocop-rspec', require: false + gem 'rubocop', '1.50.2', require: false + gem 'rubocop-performance', '1.17.1', require: false + gem 'rubocop-rspec', '2.20.0', require: false end group :development do diff --git a/spec/grape/exceptions/invalid_accept_header_spec.rb b/spec/grape/exceptions/invalid_accept_header_spec.rb index 574222259..5017807cb 100644 --- a/spec/grape/exceptions/invalid_accept_header_spec.rb +++ b/spec/grape/exceptions/invalid_accept_header_spec.rb @@ -10,11 +10,13 @@ expect(last_response.body).to eq('beer received') end end + shared_examples_for 'a cascaded request' do it 'does not find a matching route' do expect(last_response.status).to eq 404 end end + shared_examples_for 'a not-cascaded request' do it 'does not include the X-Cascade=pass header' do expect(last_response.headers['X-Cascade']).to be_nil @@ -24,6 +26,7 @@ expect(last_response.status).to eq 406 end end + shared_examples_for 'a rescued request' do it 'does not include the X-Cascade=pass header' do expect(last_response.headers['X-Cascade']).to be_nil From 4efe0d642fef8a59a033f60244c179c54c2839fe Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sun, 23 Apr 2023 17:41:02 -0400 Subject: [PATCH 146/304] Introducing Docker to Grape (#2292) --- .dockerignore | 50 ++++++++++++++++++++++++++++++++++++++++++++ CHANGELOG.md | 1 + CONTRIBUTING.md | 28 +++++++++++++++++++++++++ docker-compose.yml | 17 +++++++++++++++ docker/Dockerfile | 15 +++++++++++++ docker/entrypoint.sh | 16 ++++++++++++++ 6 files changed, 127 insertions(+) create mode 100644 .dockerignore create mode 100644 docker-compose.yml create mode 100644 docker/Dockerfile create mode 100644 docker/entrypoint.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..33bb1035c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,50 @@ +## MAC OS +.DS_Store +.com.apple.timemachine.supported + +## TEXTMATE +*.tmproj +tmtags + +## EMACS +*~ +\#* +.\#* + +## REDCAR +.redcar + +## VIM +*.swp +*.swo + +## RUBYMINE +.idea + +## PROJECT::GENERAL +coverage +doc +pkg +.rvmrc +.ruby-version +.ruby-gemset +.rspec_status +.bundle +.byebug_history +dist +Gemfile.lock +gemfiles/*.lock +tmp +.yardoc + +## Rubinius +.rbx + +## Bundler binstubs +bin + +## ripper-tags and gem-ctags +tags + +## PROJECT::SPECIFIC +.project \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index ce9824235..6d93ea868 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ * [#2311](https://github.com/ruby-grape/grape/pull/2311): Fix tests by pinning rack-test to < 2.1 - [@duffn](https://github.com/duffn). * [#2310](https://github.com/ruby-grape/grape/pull/2310): Fix YARD docs markdown rendering - [@duffn](https://github.com/duffn). * [#2317](https://github.com/ruby-grape/grape/pull/2317): Remove maruku and rubocop-ast as direct development/testing dependencies - [@ericproulx](https://github.com/ericproulx). +* [#2292](https://github.com/ruby-grape/grape/pull/2292): Introduce Docker to local development - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5405c652a..4f5e326d5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,6 +23,34 @@ git pull upstream master git checkout -b my-feature-branch ``` +### Docker + +If you're familiar with [Docker](https://www.docker.com/), you can run everything through the following command: + +``` +docker-compose run --rm --build grape +``` + +About the execution process: + - displays Ruby, Rubygems, Bundle and Gemfile version when starting: + ``` + ruby 3.2.2 (2023-03-30 revision e51014f9c0) [x86_64-linux-musl] + rubygems 3.4.12 + Bundler version 2.4.1 (2022-12-24 commit f3175f033c) + Running default Gemfile + ``` + - keeps the gems to the latest possible version + - executes under `bundle exec` + +Here are some examples: + +- running all specs `docker-compose run --rm --build grape rspec` +- running rspec on a specific file `docker-compose run --rm --build grape rspec spec/:file_path` +- running task `docker-compose run --rm --build grape rake ` +- running rubocop `docker-compose run --rm --build grape rubocop` +- running all specs on a specific ruby version (e.g 2.7.7) `RUBY_VERSION=2.7.7 docker-compose run --rm --build grape rspec` +- running specs on a specific gemfile (e.g rails_7_0.gemfile) `docker-compose run -e GEMFILE=rails_7_0 --rm --build grape rspec` + #### Bundle Install and Test Ensure that you can build the project and run tests. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..0f83ee017 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +version: '3' + +volumes: + gems: + +services: + grape: + build: + context: . + dockerfile: docker/Dockerfile + args: + - RUBY_VERSION=${RUBY_VERSION:-3} + stdin_open: true + tty: true + volumes: + - .:/var/grape + - gems:/usr/local/bundle \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 000000000..bf5c75088 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,15 @@ +ARG RUBY_VERSION +FROM ruby:$RUBY_VERSION-alpine + +ENV BUNDLE_PATH /usr/local/bundle/gems +ENV LIB_PATH /var/grape + +RUN apk add --update --no-cache make gcc git libc-dev && \ + gem update --system && gem install bundler + +WORKDIR $LIB_PATH + +COPY /docker/entrypoint.sh /usr/local/bin/docker-entrypoint.sh +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +ENTRYPOINT ["docker-entrypoint.sh"] \ No newline at end of file diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 000000000..b7674f276 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +set -e + +# Useful information +echo -e "$(ruby --version)\nrubygems $(gem --version)\n$(bundle version)" +if [ -z "${GEMFILE}" ] +then + echo "Running default Gemfile" +else + export BUNDLE_GEMFILE="./gemfiles/${GEMFILE}.gemfile" + echo "Running gemfile: ${GEMFILE}" +fi + +# Keep gems in the latest possible state +(bundle check || bundle install) && bundle update && bundle exec ${@} From a2f5a8c351191cd00f32edcca0ad4cdebb0344da Mon Sep 17 00:00:00 2001 From: Dhruv Paranjape Date: Sat, 6 May 2023 15:00:51 +0200 Subject: [PATCH 147/304] fix using endless ranges for values parameter. --- CHANGELOG.md | 1 + README.md | 9 +++++++++ lib/grape/validations/params_scope.rb | 2 +- .../validations/validators/values_spec.rb | 19 +++++++++++++++++++ 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d93ea868..0f58a4e65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ * [#2307](https://github.com/ruby-grape/grape/pull/2307): Fixed autoloading of InvalidValue - [@fixlr](https://github.com/fixlr). * [#2315](https://github.com/ruby-grape/grape/pull/2315): Update rspec - [@ericproulx](https://github.com/ericproulx). * [#2319](https://github.com/ruby-grape/grape/pull/2319): Update rubocop - [@ericproulx](https://github.com/ericproulx). +* [#2323](https://github.com/ruby-grape/grape/pull/2323): Fix using endless ranges for values parameter - [@dhruvCW](https://github.com/dhruvCW). * Your contribution here. ### 1.7.0 (2022/12/20) diff --git a/README.md b/README.md index e6f8b13e2..9b36602b0 100644 --- a/README.md +++ b/README.md @@ -1587,6 +1587,15 @@ params do end ``` +Note endless ranges are also supported but they require that the type be provided. + +```ruby +params do + requires :minimum, type: Integer, values: 10.. + optional :maximum, type: Integer, values: ..10 +end +``` + Note that *both* range endpoints have to be a `#kind_of?` your `:type` option (if you don't supply the `:type` option, it will be guessed to be equal to the class of the range's first endpoint). So the following is invalid: ```ruby diff --git a/lib/grape/validations/params_scope.rb b/lib/grape/validations/params_scope.rb index ae4d91d18..952ae475a 100644 --- a/lib/grape/validations/params_scope.rb +++ b/lib/grape/validations/params_scope.rb @@ -485,7 +485,7 @@ 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 + value_types = values.is_a?(Range) ? [values.begin, values.end].compact : values value_types = value_types.map { |type| Grape::API::Boolean.build(type) } if coerce_type == Grape::API::Boolean raise Grape::Exceptions::IncompatibleOptionValues.new(:type, coerce_type, :values, values) unless value_types.all?(coerce_type) end diff --git a/spec/grape/validations/validators/values_spec.rb b/spec/grape/validations/validators/values_spec.rb index efa1b90d2..640c84336 100644 --- a/spec/grape/validations/validators/values_spec.rb +++ b/spec/grape/validations/validators/values_spec.rb @@ -114,6 +114,13 @@ def include?(value) { type: params[:type] } end + params do + optional :type, type: Integer, values: 1.. + end + get '/endless' do + { type: params[:type] } + end + params do requires :type, values: ->(v) { ValuesModel.include? v } end @@ -374,6 +381,18 @@ def include?(value) expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end + it 'validates against values in an endless range' do + get('/endless', type: 10) + expect(last_response.status).to eq 200 + expect(last_response.body).to eq({ type: 10 }.to_json) + end + + it 'does not allow an invalid value for a parameter using an endless range' do + get('/endless', type: 0) + expect(last_response.status).to eq 400 + expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) + end + it 'does not allow non-numeric string value for int value using lambda' do get('/lambda_int_val', number: 'foo') expect(last_response.status).to eq 400 From aafcbe7bdd7d7cd664d753d245d46627d854ecd4 Mon Sep 17 00:00:00 2001 From: "Daniel (dB.) Doubrovkine" Date: Mon, 8 May 2023 22:08:41 -0400 Subject: [PATCH 148/304] Make edge workflows only run on demand. --- .github/workflows/edge.yml | 2 +- CHANGELOG.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/edge.yml b/.github/workflows/edge.yml index d670b2a98..fe962dbff 100644 --- a/.github/workflows/edge.yml +++ b/.github/workflows/edge.yml @@ -1,6 +1,6 @@ --- name: edge -on: pull_request +on: workflow_dispatch jobs: test: strategy: diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f58a4e65..3d4fe79db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ * [#2310](https://github.com/ruby-grape/grape/pull/2310): Fix YARD docs markdown rendering - [@duffn](https://github.com/duffn). * [#2317](https://github.com/ruby-grape/grape/pull/2317): Remove maruku and rubocop-ast as direct development/testing dependencies - [@ericproulx](https://github.com/ericproulx). * [#2292](https://github.com/ruby-grape/grape/pull/2292): Introduce Docker to local development - [@ericproulx](https://github.com/ericproulx). +* [#2325](https://github.com/ruby-grape/grape/pull/2325): Change edge test workflows only run on demand - [@dblock](https://github.com/dblock). * Your contribution here. #### Fixes From 0e5c824462313d451046dd53067b15d4686b1f54 Mon Sep 17 00:00:00 2001 From: "Daniel (dB.) Doubrovkine" Date: Mon, 8 May 2023 22:18:37 -0400 Subject: [PATCH 149/304] Skip endless range tests with ActiveSupport < 6. --- README.md | 2 +- spec/grape/validations/validators/values_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9b36602b0..9f223264e 100644 --- a/README.md +++ b/README.md @@ -1587,7 +1587,7 @@ params do end ``` -Note endless ranges are also supported but they require that the type be provided. +Note endless ranges are also supported with ActiveSupport >= 6.0, but they require that the type be provided. ```ruby params do diff --git a/spec/grape/validations/validators/values_spec.rb b/spec/grape/validations/validators/values_spec.rb index 640c84336..43b8ea698 100644 --- a/spec/grape/validations/validators/values_spec.rb +++ b/spec/grape/validations/validators/values_spec.rb @@ -381,13 +381,13 @@ def include?(value) expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end - it 'validates against values in an endless range' do + it 'validates against values in an endless range', if: ActiveSupport::VERSION::MAJOR >= 6 do get('/endless', type: 10) expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: 10 }.to_json) end - it 'does not allow an invalid value for a parameter using an endless range' do + it 'does not allow an invalid value for a parameter using an endless range', if: ActiveSupport::VERSION::MAJOR >= 6 do get('/endless', type: 0) expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) From b2c001d4e3b631a97da01dae1c2df9328305f8fa Mon Sep 17 00:00:00 2001 From: Dhruv Paranjape Date: Mon, 8 May 2023 18:05:18 +0200 Subject: [PATCH 150/304] expose default in the description dsl. --- CHANGELOG.md | 1 + README.md | 6 ++++-- lib/grape/dsl/desc.rb | 3 ++- spec/grape/dsl/desc_spec.rb | 2 ++ 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d4fe79db..d0719bb79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ * [#2317](https://github.com/ruby-grape/grape/pull/2317): Remove maruku and rubocop-ast as direct development/testing dependencies - [@ericproulx](https://github.com/ericproulx). * [#2292](https://github.com/ruby-grape/grape/pull/2292): Introduce Docker to local development - [@ericproulx](https://github.com/ericproulx). * [#2325](https://github.com/ruby-grape/grape/pull/2325): Change edge test workflows only run on demand - [@dblock](https://github.com/dblock). +* [#2324](https://github.com/ruby-grape/grape/pull/2324): Expose default in the description dsl - [@dhruvCW](https://github.com/dhruvCW). * Your contribution here. #### Fixes diff --git a/README.md b/README.md index 9f223264e..33f84902b 100644 --- a/README.md +++ b/README.md @@ -639,6 +639,7 @@ desc 'Returns your public timeline.' do params API::Entities::Status.documentation success API::Entities::Entity failure [[401, 'Unauthorized', 'Entities::Error']] + default { code: 500, message: 'InvalidRequest', model: Entities::Error } named 'My named route' headers XAuthToken: { description: 'Validates your identity', @@ -663,8 +664,9 @@ end * `detail`: A more enhanced description * `params`: Define parameters directly from an `Entity` -* `success`: (former entity) The `Entity` to be used to present by default this route -* `failure`: (former http_codes) A definition of the used failure HTTP Codes and Entities +* `success`: (former entity) The `Entity` to be used to present the success response for this route. +* `failure`: (former http_codes) A definition of the used failure HTTP Codes and Entities. +* `default`: The definition and `Entity` used to present the default response for this route. * `named`: A helper to give a route a name and find it with this name in the documentation Hash * `headers`: A definition of the used Headers * Other options can be found in [grape-swagger][grape-swagger] diff --git a/lib/grape/dsl/desc.rb b/lib/grape/dsl/desc.rb index b606639e8..4da478690 100644 --- a/lib/grape/dsl/desc.rb +++ b/lib/grape/dsl/desc.rb @@ -98,7 +98,8 @@ def desc_container(endpoint_configuration) :produces, :consumes, :security, - :tags + :tags, + :default ) config_context.define_singleton_method(:configuration) do endpoint_configuration diff --git a/spec/grape/dsl/desc_spec.rb b/spec/grape/dsl/desc_spec.rb index 8212add8f..718e3a9fb 100644 --- a/spec/grape/dsl/desc_spec.rb +++ b/spec/grape/dsl/desc_spec.rb @@ -26,6 +26,7 @@ class Dummy detail: 'more details', params: { first: :param }, entity: Object, + default: { code: 400, message: 'Invalid' }, http_codes: [[401, 'Unauthorized', 'Entities::Error']], named: 'My named route', body_name: 'My body name', @@ -54,6 +55,7 @@ class Dummy detail 'more details' params(first: :param) success Object + default code: 400, message: 'Invalid' failure [[401, 'Unauthorized', 'Entities::Error']] named 'My named route' body_name 'My body name' From 9d121a32830f7c36a979fb0ca09c04bbb87bb462 Mon Sep 17 00:00:00 2001 From: eproulx Date: Sun, 14 May 2023 22:10:52 +0200 Subject: [PATCH 151/304] Preparing for release, 1.7.1 --- CHANGELOG.md | 4 +--- README.md | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0719bb79..359590467 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -### 1.7.1 (Next) +### 1.7.1 (2023/05/14) #### Features @@ -14,7 +14,6 @@ * [#2292](https://github.com/ruby-grape/grape/pull/2292): Introduce Docker to local development - [@ericproulx](https://github.com/ericproulx). * [#2325](https://github.com/ruby-grape/grape/pull/2325): Change edge test workflows only run on demand - [@dblock](https://github.com/dblock). * [#2324](https://github.com/ruby-grape/grape/pull/2324): Expose default in the description dsl - [@dhruvCW](https://github.com/dhruvCW). -* Your contribution here. #### Fixes @@ -23,7 +22,6 @@ * [#2315](https://github.com/ruby-grape/grape/pull/2315): Update rspec - [@ericproulx](https://github.com/ericproulx). * [#2319](https://github.com/ruby-grape/grape/pull/2319): Update rubocop - [@ericproulx](https://github.com/ericproulx). * [#2323](https://github.com/ruby-grape/grape/pull/2323): Fix using endless ranges for values parameter - [@dhruvCW](https://github.com/dhruvCW). -* Your contribution here. ### 1.7.0 (2022/12/20) diff --git a/README.md b/README.md index 33f84902b..a476b9a1d 100644 --- a/README.md +++ b/README.md @@ -159,9 +159,8 @@ content negotiation, versioning and much more. ## Stable Release -You're reading the documentation for the next release of Grape, which should be **1.7.1**. +You're reading the documentation for the stable release of Grape, 1.7.1. Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. -The current stable release is [1.7.0](https://github.com/ruby-grape/grape/blob/v1.7.0/README.md). ## Project Resources From ccb13ceb07d3432a557d67fd8a262a6d38204ef9 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sun, 14 May 2023 22:26:09 +0200 Subject: [PATCH 152/304] Preparing for next development iteration, 1.7.2. --- CHANGELOG.md | 12 +++++++++++- README.md | 4 +++- lib/grape/version.rb | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 359590467..99d0d957c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,14 @@ -### 1.7.1 (2023/05/14) +### 1.7.2 (Next) + +#### Features + +* Your contribution here. + +#### Fixes + +* Your contribution here. + +## 1.7.1 (2023/05/14) #### Features diff --git a/README.md b/README.md index a476b9a1d..8ff988925 100644 --- a/README.md +++ b/README.md @@ -159,8 +159,10 @@ content negotiation, versioning and much more. ## Stable Release -You're reading the documentation for the stable release of Grape, 1.7.1. +You're reading the documentation for the next release of Grape, which should be **1.7.2**. Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. +The current stable release is [1.7.1](https://github.com/ruby-grape/grape/blob/v1.7.1/README.md). + ## Project Resources diff --git a/lib/grape/version.rb b/lib/grape/version.rb index 54cc42080..6b84a9611 100644 --- a/lib/grape/version.rb +++ b/lib/grape/version.rb @@ -2,5 +2,5 @@ module Grape # The current version of Grape. - VERSION = '1.7.1' + VERSION = '1.7.2' end From 280e5b3aab35cd1bdf4d689d12d1fb19fe23d556 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Mon, 15 May 2023 22:43:48 +0200 Subject: [PATCH 153/304] Use Active support functions (#2326) * Replace deep_mergeable_hash.rb and deep_symbolize_hash.rb by ActiveSupport functions Use each_with_object instead of each/inject etc ... * Replace if env[...] by env.key? Use deep_dup instead of dup for rack_params * Use presence instead of ternary Replace unless present? by if blank? * Replace !include? by exclude? * Use ActiveSupport duplicable? * Fix rubocop and update CHANGELOG * Add minimal version for ActiveSupport >=5 * Remove extra * in CHANGELOG * Next version 1.8.0 --- CHANGELOG.md | 3 +- README.md | 2 +- grape.gemspec | 2 +- lib/grape.rb | 4 +-- lib/grape/content_types.rb | 10 ++---- lib/grape/dsl/inside_route.rb | 4 +-- lib/grape/dsl/settings.rb | 8 ++--- lib/grape/endpoint.rb | 7 ++-- lib/grape/error_formatter/base.rb | 2 +- lib/grape/exceptions/base.rb | 4 +-- lib/grape/exceptions/validation_errors.rb | 7 +--- .../hash_with_indifferent_access.rb | 6 ++-- lib/grape/extensions/deep_mergeable_hash.rb | 21 ------------ lib/grape/extensions/deep_symbolize_hash.rb | 32 ------------------- lib/grape/extensions/hash.rb | 11 +++---- lib/grape/extensions/hashie/mash.rb | 6 ++-- lib/grape/formatter/serializable_hash.rb | 14 ++++---- lib/grape/middleware/auth/base.rb | 2 +- lib/grape/middleware/formatter.rb | 2 +- lib/grape/middleware/versioner/header.rb | 30 +++++++---------- lib/grape/util/lazy_value.rb | 14 ++------ lib/grape/util/strict_hash_configuration.rb | 7 ++-- lib/grape/validations/params_scope.rb | 2 +- .../validations/types/custom_type_coercer.rb | 18 ++--------- lib/grape/validations/validators/base.rb | 2 +- .../validators/default_validator.rb | 22 ++----------- lib/grape/version.rb | 2 +- spec/grape/api_spec.rb | 2 +- 28 files changed, 63 insertions(+), 183 deletions(-) delete mode 100644 lib/grape/extensions/deep_mergeable_hash.rb delete mode 100644 lib/grape/extensions/deep_symbolize_hash.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 99d0d957c..b862edf6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ -### 1.7.2 (Next) +### 1.8.0 (Next) #### Features +* [#2326](https://github.com/ruby-grape/grape/pull/2326): Use ActiveSupport extensions - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/README.md b/README.md index 8ff988925..a334a72a8 100644 --- a/README.md +++ b/README.md @@ -159,7 +159,7 @@ content negotiation, versioning and much more. ## Stable Release -You're reading the documentation for the next release of Grape, which should be **1.7.2**. +You're reading the documentation for the next release of Grape, which should be **1.8.0**. Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. The current stable release is [1.7.1](https://github.com/ruby-grape/grape/blob/v1.7.1/README.md). diff --git a/grape.gemspec b/grape.gemspec index bee87fbc6..e79e1c0ed 100644 --- a/grape.gemspec +++ b/grape.gemspec @@ -20,7 +20,7 @@ Gem::Specification.new do |s| 'source_code_uri' => "https://github.com/ruby-grape/grape/tree/v#{s.version}" } - s.add_runtime_dependency 'activesupport' + s.add_runtime_dependency 'activesupport', '>= 5' s.add_runtime_dependency 'builder' s.add_runtime_dependency 'dry-types', '>= 1.1' s.add_runtime_dependency 'mustermann-grape', '~> 1.0.0' diff --git a/lib/grape.rb b/lib/grape.rb index f51a2e8b0..ed309e16f 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -20,9 +20,11 @@ require 'active_support/core_ext/hash/deep_merge' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/hash/indifferent_access' +require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/hash/reverse_merge' require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/object/duplicable' require 'active_support/dependencies/autoload' require 'active_support/notifications' require 'i18n' @@ -92,8 +94,6 @@ module Exceptions module Extensions extend ::ActiveSupport::Autoload eager_autoload do - autoload :DeepMergeableHash - autoload :DeepSymbolizeHash autoload :Hash end module ActiveSupport diff --git a/lib/grape/content_types.rb b/lib/grape/content_types.rb index 2c19f9731..c6f295154 100644 --- a/lib/grape/content_types.rb +++ b/lib/grape/content_types.rb @@ -17,17 +17,11 @@ module ContentTypes class << self def content_types_for_settings(settings) - return if settings.blank? - - settings.each_with_object({}) { |value, result| result.merge!(value) } + settings&.inject(:merge!) end def content_types_for(from_settings) - if from_settings.present? - from_settings - else - Grape::ContentTypes::CONTENT_TYPES.merge(default_elements) - end + from_settings.presence || Grape::ContentTypes::CONTENT_TYPES.merge(default_elements) end end end diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index a54967ead..e2ee006c7 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -103,7 +103,7 @@ def handle_passed_param(params_nested_path, has_passed_children = false, &_block if type == 'Hash' && !has_children {} - elsif type == 'Array' || (type&.start_with?('[') && !type&.include?(',')) + elsif type == 'Array' || (type&.start_with?('[') && type&.exclude?(',')) [] elsif type == 'Set' || type&.start_with?('# value) - end - end end end end diff --git a/lib/grape/validations/validators/base.rb b/lib/grape/validations/validators/base.rb index 2c8403bbc..86813bf9b 100644 --- a/lib/grape/validations/validators/base.rb +++ b/lib/grape/validations/validators/base.rb @@ -71,7 +71,7 @@ def self.convert_to_short_name(klass) end def self.inherited(klass) - return unless klass.name.present? + return if klass.name.blank? Validations.register_validator(convert_to_short_name(klass), klass) end diff --git a/lib/grape/validations/validators/default_validator.rb b/lib/grape/validations/validators/default_validator.rb index 8ed593675..4058d7b1d 100644 --- a/lib/grape/validations/validators/default_validator.rb +++ b/lib/grape/validations/validators/default_validator.rb @@ -12,10 +12,10 @@ def initialize(attrs, options, required, scope, **opts) def validate_param!(attr_name, params) params[attr_name] = if @default.is_a? Proc @default.call - elsif @default.frozen? || !duplicatable?(@default) + elsif @default.frozen? || !@default.duplicable? @default else - duplicate(@default) + @default.dup end end @@ -27,24 +27,6 @@ def validate!(params) validate_param!(attr_name, resource_params) if resource_params.is_a?(Hash) && resource_params[attr_name].nil? end end - - private - - # return true if we might be able to dup this object - def duplicatable?(obj) - !obj.nil? && - obj != true && - obj != false && - !obj.is_a?(Symbol) && - !obj.is_a?(Numeric) - end - - # make a best effort to dup the object - def duplicate(obj) - obj.dup - rescue TypeError - obj - end end end end diff --git a/lib/grape/version.rb b/lib/grape/version.rb index 6b84a9611..64b7c4785 100644 --- a/lib/grape/version.rb +++ b/lib/grape/version.rb @@ -2,5 +2,5 @@ module Grape # The current version of Grape. - VERSION = '1.7.2' + VERSION = '1.8.0' end diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index 008a42026..44dc83153 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -3311,7 +3311,7 @@ def static it 'is able to cascade' do subject.mount lambda { |env| headers = {} - headers['X-Cascade'] == 'pass' unless env['PATH_INFO'].include?('boo') + headers['X-Cascade'] == 'pass' if env['PATH_INFO'].exclude?('boo') [200, headers, ['Farfegnugen']] } => '/' From 96e2faf1cb6165eab50209fe045f15784b211d59 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Tue, 16 May 2023 18:53:06 +0200 Subject: [PATCH 154/304] Use ActiveSupport::Deprecation (#2327) * Replace warn [DEPRECATION] to ActiveSupport::Deprecation.warn Use ActiveSupport::Deprecation::DeprecatedConstantProxy for exceptions with deprecation Replace Validator::Base initialize by inherited * Add shared example deprecated_class and update specs accordingly Fix rubocop * Changelog entry --- CHANGELOG.md | 1 + lib/grape.rb | 1 + lib/grape/dsl/desc.rb | 2 +- lib/grape/dsl/inside_route.rb | 6 +- lib/grape/exceptions/missing_group_type.rb | 7 +- .../exceptions/unsupported_group_type.rb | 7 +- lib/grape/router/route.rb | 4 +- lib/grape/validations/validators/base.rb | 4 +- .../validators/values_validator.rb | 6 +- spec/grape/api/custom_validations_spec.rb | 71 ++------ spec/grape/dsl/desc_spec.rb | 171 +++++++++--------- spec/grape/dsl/inside_route_spec.rb | 16 +- .../exceptions/missing_group_type_spec.rb | 14 +- .../exceptions/unsupported_group_type_spec.rb | 14 +- spec/shared/deprecated_class_examples.rb | 16 ++ 15 files changed, 143 insertions(+), 197 deletions(-) create mode 100644 spec/shared/deprecated_class_examples.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index b862edf6a..e3e890a33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ #### Features * [#2326](https://github.com/ruby-grape/grape/pull/2326): Use ActiveSupport extensions - [@ericproulx](https://github.com/ericproulx). +* [#2327](https://github.com/ruby-grape/grape/pull/2327): Use ActiveSupport deprecation - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/lib/grape.rb b/lib/grape.rb index ed309e16f..db1e1d06d 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -26,6 +26,7 @@ require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/duplicable' require 'active_support/dependencies/autoload' +require 'active_support/deprecation' require 'active_support/notifications' require 'i18n' diff --git a/lib/grape/dsl/desc.rb b/lib/grape/dsl/desc.rb index 4da478690..64d5eed91 100644 --- a/lib/grape/dsl/desc.rb +++ b/lib/grape/dsl/desc.rb @@ -68,7 +68,7 @@ def desc(description, options = {}, &config_block) end config_class.configure(&config_block) - warn '[DEPRECATION] Passing a options hash and a block to `desc` is deprecated. Move all hash options to block.' unless options.empty? + ActiveSupport::Deprecation.warn('Passing a options hash and a block to `desc` is deprecated. Move all hash options to block.') if options.any? options = config_class.settings else options = options.merge(description: description) diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index e2ee006c7..429988872 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -280,13 +280,13 @@ def return_no_content # Deprecated method to send files to the client. Use `sendfile` or `stream` def file(value = nil) if value.is_a?(String) - warn '[DEPRECATION] Use sendfile or stream to send files.' + ActiveSupport::Deprecation.warn('Use sendfile or stream to send files.') sendfile(value) elsif !value.is_a?(NilClass) - warn '[DEPRECATION] Use stream to use a Stream object.' + ActiveSupport::Deprecation.warn('Use stream to use a Stream object.') stream(value) else - warn '[DEPRECATION] Use sendfile or stream to send files.' + ActiveSupport::Deprecation.warn('Use sendfile or stream to send files.') sendfile end end diff --git a/lib/grape/exceptions/missing_group_type.rb b/lib/grape/exceptions/missing_group_type.rb index 48ef996ab..1f104321f 100644 --- a/lib/grape/exceptions/missing_group_type.rb +++ b/lib/grape/exceptions/missing_group_type.rb @@ -10,9 +10,4 @@ def initialize end end -Grape::Exceptions::MissingGroupTypeError = Class.new(Grape::Exceptions::MissingGroupType) do - def initialize(*) - super - warn '[DEPRECATION] `Grape::Exceptions::MissingGroupTypeError` is deprecated. Use `Grape::Exceptions::MissingGroupType` instead.' - end -end +Grape::Exceptions::MissingGroupTypeError = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Grape::Exceptions::MissingGroupTypeError', 'Grape::Exceptions::MissingGroupType') diff --git a/lib/grape/exceptions/unsupported_group_type.rb b/lib/grape/exceptions/unsupported_group_type.rb index 5d845aad6..da45abafd 100644 --- a/lib/grape/exceptions/unsupported_group_type.rb +++ b/lib/grape/exceptions/unsupported_group_type.rb @@ -10,9 +10,4 @@ def initialize end end -Grape::Exceptions::UnsupportedGroupTypeError = Class.new(Grape::Exceptions::UnsupportedGroupType) do - def initialize(*) - super - warn '[DEPRECATION] `Grape::Exceptions::UnsupportedGroupTypeError` is deprecated. Use `Grape::Exceptions::UnsupportedGroupType` instead.' - end -end +Grape::Exceptions::UnsupportedGroupTypeError = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Grape::Exceptions::UnsupportedGroupTypeError', 'Grape::Exceptions::UnsupportedGroupType') diff --git a/lib/grape/router/route.rb b/lib/grape/router/route.rb index 3ced9ca2a..7bbfe6a0c 100644 --- a/lib/grape/router/route.rb +++ b/lib/grape/router/route.rb @@ -84,9 +84,7 @@ 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 <<~WARNING - #{path}:#{line}: The route_xxx methods such as route_#{name} have been deprecated, please use #{expected}. - WARNING + ActiveSupport::Deprecation.warn("#{path}:#{line}: The route_xxx methods such as route_#{name} have been deprecated, please use #{expected}.") end end end diff --git a/lib/grape/validations/validators/base.rb b/lib/grape/validations/validators/base.rb index 86813bf9b..b3bcfc50c 100644 --- a/lib/grape/validations/validators/base.rb +++ b/lib/grape/validations/validators/base.rb @@ -95,8 +95,8 @@ def fail_fast? end Grape::Validations::Base = Class.new(Grape::Validations::Validators::Base) do - def initialize(*) + def self.inherited(*) + ActiveSupport::Deprecation.warn 'Grape::Validations::Base is deprecated! Use Grape::Validations::Validators::Base instead.' super - warn '[DEPRECATION] `Grape::Validations::Base` is deprecated. Use `Grape::Validations::Validators::Base` instead.' end end diff --git a/lib/grape/validations/validators/values_validator.rb b/lib/grape/validations/validators/values_validator.rb index b8b05fb6b..0fba1a91c 100644 --- a/lib/grape/validations/validators/values_validator.rb +++ b/lib/grape/validations/validators/values_validator.rb @@ -10,13 +10,11 @@ def initialize(attrs, options, required, scope, **opts) @values = options[:value] @proc = options[:proc] - warn '[DEPRECATION] The values validator except option is deprecated. ' \ - 'Use the except validator instead.' if @excepts + ActiveSupport::Deprecation.warn('The values validator except option is deprecated. Use the except validator instead.') if @excepts raise ArgumentError, 'proc must be a Proc' if @proc && !@proc.is_a?(Proc) - warn '[DEPRECATION] The values validator proc option is deprecated. ' \ - 'The lambda expression can now be assigned directly to values.' if @proc + ActiveSupport::Deprecation.warn('The values validator proc option is deprecated. The lambda expression can now be assigned directly to values.') if @proc else @excepts = nil @values = nil diff --git a/spec/grape/api/custom_validations_spec.rb b/spec/grape/api/custom_validations_spec.rb index 25879fc57..bfb821de8 100644 --- a/spec/grape/api/custom_validations_spec.rb +++ b/spec/grape/api/custom_validations_spec.rb @@ -1,48 +1,17 @@ # frozen_string_literal: true -describe Grape::Validations do - context 'deprecated Grape::Validations::Base' do - subject do - Class.new(Grape::API) do - params do - requires :text, validator_with_old_base: true - end - get do - end - end - end - - let(:validator_with_old_base) do - Class.new(Grape::Validations::Base) do - def validate_param!(_attr_name, _params) - true - end - end - end - - before do - described_class.register_validator('validator_with_old_base', validator_with_old_base) - allow(Warning).to receive(:warn) - end +require 'shared/deprecated_class_examples' - after do - described_class.deregister_validator('validator_with_old_base') - end - - def app - subject +describe Grape::Validations do + describe 'Grape::Validations::Base' do + let(:deprecated_class) do + Class.new(Grape::Validations::Base) end - it 'puts a deprecation warning' do - expect(Warning).to receive(:warn) do |message| - expect(message).to include('`Grape::Validations::Base` is deprecated') - end - - get '/' - end + it_behaves_like 'deprecated class' end - context 'using a custom length validator' do + describe 'using a custom length validator' do subject do Class.new(Grape::API) do params do @@ -64,6 +33,7 @@ def validate_param!(attr_name, params) end end end + let(:app) { Rack::Builder.new(subject) } before do described_class.register_validator('default_length', default_length_validator) @@ -73,10 +43,6 @@ def validate_param!(attr_name, params) described_class.deregister_validator('default_length') end - def app - subject - end - it 'under 140 characters' do get '/', text: 'abc' expect(last_response.status).to eq 200 @@ -96,7 +62,7 @@ def app end end - context 'using a custom body-only validator' do + describe 'using a custom body-only validator' do subject do Class.new(Grape::API) do params do @@ -115,6 +81,7 @@ def validate(request) end end end + let(:app) { Rack::Builder.new(subject) } before do described_class.register_validator('in_body', in_body_validator) @@ -124,10 +91,6 @@ def validate(request) described_class.deregister_validator('in_body') end - def app - subject - end - it 'allows field in body' do get '/', text: 'abc' expect(last_response.status).to eq 200 @@ -141,7 +104,7 @@ def app end end - context 'using a custom validator with message_key' do + describe 'using a custom validator with message_key' do subject do Class.new(Grape::API) do params do @@ -160,6 +123,7 @@ def validate_param!(attr_name, _params) end end end + let(:app) { Rack::Builder.new(subject) } before do described_class.register_validator('with_message_key', message_key_validator) @@ -169,10 +133,6 @@ def validate_param!(attr_name, _params) described_class.deregister_validator('with_message_key') end - def app - subject - end - it 'fails with message' do get '/', text: 'foobar' expect(last_response.status).to eq 400 @@ -180,7 +140,7 @@ def app end end - context 'using a custom request/param validator' do + describe 'using a custom request/param validator' do subject do Class.new(Grape::API) do params do @@ -208,6 +168,7 @@ def validate(request) end end end + let(:app) { Rack::Builder.new(subject) } before do described_class.register_validator('admin', admin_validator) @@ -217,10 +178,6 @@ def validate(request) described_class.deregister_validator('admin') end - def app - subject - end - it 'fail when non-admin user sets an admin field' do get '/', admin_field: 'tester', non_admin_field: 'toaster' expect(last_response.status).to eq 400 diff --git a/spec/grape/dsl/desc_spec.rb b/spec/grape/dsl/desc_spec.rb index 718e3a9fb..c2acde910 100644 --- a/spec/grape/dsl/desc_spec.rb +++ b/spec/grape/dsl/desc_spec.rb @@ -1,101 +1,98 @@ # frozen_string_literal: true -module Grape - module DSL - module DescSpec - class Dummy - extend Grape::DSL::Desc - end +describe Grape::DSL::Desc do + subject { dummy_class } + + let(:dummy_class) do + Class.new do + extend Grape::DSL::Desc end - describe Desc do - subject { Class.new(DescSpec::Dummy) } + end - describe '.desc' do - it 'sets a description' do - desc_text = 'The description' - options = { message: 'none' } - subject.desc desc_text, options - expect(subject.namespace_setting(:description)).to eq(options.merge(description: desc_text)) - expect(subject.route_setting(:description)).to eq(options.merge(description: desc_text)) - end + describe '.desc' do + it 'sets a description' do + desc_text = 'The description' + options = { message: 'none' } + subject.desc desc_text, options + expect(subject.namespace_setting(:description)).to eq(options.merge(description: desc_text)) + expect(subject.route_setting(:description)).to eq(options.merge(description: desc_text)) + end - it 'can be set with a block' do - expected_options = { - summary: 'summary', - description: 'The description', - detail: 'more details', - params: { first: :param }, - entity: Object, - default: { code: 400, message: 'Invalid' }, - http_codes: [[401, 'Unauthorized', 'Entities::Error']], - named: 'My named route', - body_name: 'My body name', - headers: [ - XAuthToken: { - description: 'Valdates your identity', - required: true - }, - XOptionalHeader: { - description: 'Not really needed', - required: false - } - ], - hidden: false, - deprecated: false, - is_array: true, - nickname: 'nickname', - produces: %w[array of mime_types], - consumes: %w[array of mime_types], - tags: %w[tag1 tag2], - security: %w[array of security schemes] + it 'can be set with a block' do + expected_options = { + summary: 'summary', + description: 'The description', + detail: 'more details', + params: { first: :param }, + entity: Object, + default: { code: 400, message: 'Invalid' }, + http_codes: [[401, 'Unauthorized', 'Entities::Error']], + named: 'My named route', + body_name: 'My body name', + headers: [ + XAuthToken: { + description: 'Valdates your identity', + required: true + }, + XOptionalHeader: { + description: 'Not really needed', + required: false } + ], + hidden: false, + deprecated: false, + is_array: true, + nickname: 'nickname', + produces: %w[array of mime_types], + consumes: %w[array of mime_types], + tags: %w[tag1 tag2], + security: %w[array of security schemes] + } - subject.desc 'The description' do - summary 'summary' - detail 'more details' - params(first: :param) - success Object - default code: 400, message: 'Invalid' - failure [[401, 'Unauthorized', 'Entities::Error']] - named 'My named route' - body_name 'My body name' - headers [ - XAuthToken: { - description: 'Valdates your identity', - required: true - }, - XOptionalHeader: { - description: 'Not really needed', - required: false - } - ] - hidden false - deprecated false - is_array true - nickname 'nickname' - produces %w[array of mime_types] - consumes %w[array of mime_types] - tags %w[tag1 tag2] - security %w[array of security schemes] - end + subject.desc 'The description' do + summary 'summary' + detail 'more details' + params(first: :param) + success Object + default code: 400, message: 'Invalid' + failure [[401, 'Unauthorized', 'Entities::Error']] + named 'My named route' + body_name 'My body name' + headers [ + XAuthToken: { + description: 'Valdates your identity', + required: true + }, + XOptionalHeader: { + description: 'Not really needed', + required: false + } + ] + hidden false + deprecated false + is_array true + nickname 'nickname' + produces %w[array of mime_types] + consumes %w[array of mime_types] + tags %w[tag1 tag2] + security %w[array of security schemes] + end - expect(subject.namespace_setting(:description)).to eq(expected_options) - expect(subject.route_setting(:description)).to eq(expected_options) - end + expect(subject.namespace_setting(:description)).to eq(expected_options) + expect(subject.route_setting(:description)).to eq(expected_options) + end - it 'can be set with options and a block' do - expect(subject).to receive(:warn).with('[DEPRECATION] Passing a options hash and a block to `desc` is deprecated. Move all hash options to block.') + it 'can be set with options and a block' do + expect(ActiveSupport::Deprecation).to receive(:warn).with('Passing a options hash and a block to `desc` is deprecated. Move all hash options to block.') - desc_text = 'The description' - detail_text = 'more details' - options = { message: 'none' } - subject.desc desc_text, options do - detail detail_text - end - expect(subject.namespace_setting(:description)).to eq(description: desc_text, detail: detail_text) - expect(subject.route_setting(:description)).to eq(description: desc_text, detail: detail_text) - end + desc_text = 'The description' + detail_text = 'more details' + options = { message: 'none' } + subject.desc desc_text, options do + detail detail_text end + expect(subject.namespace_setting(:description)).to eq(description: desc_text, detail: detail_text) + expect(subject.route_setting(:description)).to eq(description: desc_text, detail: detail_text) end end end diff --git a/spec/grape/dsl/inside_route_spec.rb b/spec/grape/dsl/inside_route_spec.rb index b6f4aba87..e9a529b51 100644 --- a/spec/grape/dsl/inside_route_spec.rb +++ b/spec/grape/dsl/inside_route_spec.rb @@ -203,16 +203,12 @@ 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' } it 'emits a warning that this method is deprecated' do - expect(subject).to receive(:warn).with(/Use sendfile or stream/) + expect(ActiveSupport::Deprecation).to receive(:warn).with(/Use sendfile or stream/) subject.file file_path end @@ -228,7 +224,7 @@ def initialize 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/) + expect(ActiveSupport::Deprecation).to receive(:warn).with(/Use stream to use a Stream object/) subject.file file_object end @@ -243,7 +239,7 @@ def initialize describe 'get' do it 'emits a warning that this method is deprecated' do - expect(subject).to receive(:warn).with(/Use sendfile or stream/) + expect(ActiveSupport::Deprecation).to receive(:warn).with(/Use sendfile or stream/) subject.file end @@ -273,7 +269,7 @@ def initialize end it 'sends no deprecation warnings' do - expect(subject).not_to receive(:warn) + expect(ActiveSupport::Deprecation).not_to receive(:warn) subject.sendfile file_path end @@ -334,7 +330,7 @@ def initialize end it 'emits no deprecation warnings' do - expect(subject).not_to receive(:warn) + expect(ActiveSupport::Deprecation).not_to receive(:warn) subject.stream file_path end @@ -384,7 +380,7 @@ def initialize end it 'emits no deprecation warnings' do - expect(subject).not_to receive(:warn) + expect(ActiveSupport::Deprecation).not_to receive(:warn) subject.stream stream_object end diff --git a/spec/grape/exceptions/missing_group_type_spec.rb b/spec/grape/exceptions/missing_group_type_spec.rb index b6612882f..7a6ca5269 100644 --- a/spec/grape/exceptions/missing_group_type_spec.rb +++ b/spec/grape/exceptions/missing_group_type_spec.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'shared/deprecated_class_examples' + RSpec.describe Grape::Exceptions::MissingGroupType do describe '#message' do subject { described_class.new.message } @@ -7,15 +9,9 @@ it { is_expected.to include 'group type is required' } end - describe 'deprecated Grape::Exceptions::MissingGroupTypeError' do - subject { Grape::Exceptions::MissingGroupTypeError.new } - - it 'puts a deprecation warning' do - expect(Warning).to receive(:warn) do |message| - expect(message).to include('`Grape::Exceptions::MissingGroupTypeError` is deprecated') - end + describe 'Grape::Exceptions::MissingGroupTypeError' do + let(:deprecated_class) { Grape::Exceptions::MissingGroupTypeError } - subject - end + it_behaves_like 'deprecated class' end end diff --git a/spec/grape/exceptions/unsupported_group_type_spec.rb b/spec/grape/exceptions/unsupported_group_type_spec.rb index ee1451aaa..b6282ab81 100644 --- a/spec/grape/exceptions/unsupported_group_type_spec.rb +++ b/spec/grape/exceptions/unsupported_group_type_spec.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'shared/deprecated_class_examples' + RSpec.describe Grape::Exceptions::UnsupportedGroupType do subject { described_class.new } @@ -9,15 +11,9 @@ it { is_expected.to include 'group type must be Array, Hash, JSON or Array[JSON]' } end - describe 'deprecated Grape::Exceptions::UnsupportedGroupTypeError' do - subject { Grape::Exceptions::UnsupportedGroupTypeError.new } - - it 'puts a deprecation warning' do - expect(Warning).to receive(:warn) do |message| - expect(message).to include('`Grape::Exceptions::UnsupportedGroupTypeError` is deprecated') - end + describe 'Grape::Exceptions::UnsupportedGroupTypeError' do + let(:deprecated_class) { Grape::Exceptions::UnsupportedGroupTypeError } - subject - end + it_behaves_like 'deprecated class' end end diff --git a/spec/shared/deprecated_class_examples.rb b/spec/shared/deprecated_class_examples.rb new file mode 100644 index 000000000..93a4f120c --- /dev/null +++ b/spec/shared/deprecated_class_examples.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'deprecated class' do + subject { deprecated_class.new } + + around do |example| + old_deprec_behavior = ActiveSupport::Deprecation.behavior + ActiveSupport::Deprecation.behavior = :raise + example.run + ActiveSupport::Deprecation.behavior = old_deprec_behavior + end + + it 'raises an ActiveSupport::DeprecationException' do + expect { subject }.to raise_error(ActiveSupport::DeprecationException) + end +end From 82f3497bd9f9351efe41e7e13404d062efb5aae3 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Fri, 19 May 2023 16:22:04 +0200 Subject: [PATCH 155/304] ActiveSupport inflector methods (#2330) * Replace convert_to_short_name by ActiveSupport inflector methods * Use delete_suffix! * Changelog entry --- CHANGELOG.md | 1 + lib/grape.rb | 1 + lib/grape/validations/validators/base.rb | 12 ++---------- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3e890a33..bda4f0321 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * [#2326](https://github.com/ruby-grape/grape/pull/2326): Use ActiveSupport extensions - [@ericproulx](https://github.com/ericproulx). * [#2327](https://github.com/ruby-grape/grape/pull/2327): Use ActiveSupport deprecation - [@ericproulx](https://github.com/ericproulx). +* [#2330](https://github.com/ruby-grape/grape/pull/2330): Use ActiveSupport inflector - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/lib/grape.rb b/lib/grape.rb index db1e1d06d..32bb70058 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -27,6 +27,7 @@ require 'active_support/core_ext/object/duplicable' require 'active_support/dependencies/autoload' require 'active_support/deprecation' +require 'active_support/inflector' require 'active_support/notifications' require 'i18n' diff --git a/lib/grape/validations/validators/base.rb b/lib/grape/validations/validators/base.rb index b3bcfc50c..c720d793c 100644 --- a/lib/grape/validations/validators/base.rb +++ b/lib/grape/validations/validators/base.rb @@ -61,19 +61,11 @@ def validate!(params) raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors.any? end - def self.convert_to_short_name(klass) - ret = klass.name.gsub(/::/, '/') - 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 - def self.inherited(klass) return if klass.name.blank? - Validations.register_validator(convert_to_short_name(klass), klass) + short_validator_name = klass.name.demodulize.underscore.delete_suffix!('_validator') + Validations.register_validator(short_validator_name, klass) end def message(default_key = nil) From 4f273057cf4665a265d1c07f9c39dcbada856091 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Tue, 16 May 2023 07:33:31 +0200 Subject: [PATCH 156/304] Don't cache `Class.instance_methods` Fix: https://github.com/ruby-grape/grape/issues/2258 Some gems or app code may define methods on `Class` after `grape` is loaaded but before `override_all_methods!` is called. As such it's better to check `Class.method_defined?` when doing the override. --- CHANGELOG.md | 1 + lib/grape/api.rb | 4 ++-- spec/grape/api_spec.rb | 9 +++++++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b862edf6a..4138fed86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ #### Fixes +* [#2328](https://github.com/ruby-grape/grape/pull/2328): Don't cache Class.instance_methods - [@byroot](https://github.com/byroot). * Your contribution here. ## 1.7.1 (2023/05/14) diff --git a/lib/grape/api.rb b/lib/grape/api.rb index e597f257b..1fc057bfb 100644 --- a/lib/grape/api.rb +++ b/lib/grape/api.rb @@ -8,7 +8,7 @@ module Grape # should subclass this class in order to build an API. class API # Class methods that we want to call on the API rather than on the API object - NON_OVERRIDABLE = (Class.new.methods + %i[call call! configuration compile! inherited]).freeze + NON_OVERRIDABLE = %i[call call! configuration compile! inherited].freeze class Boolean def self.build(val) @@ -50,7 +50,7 @@ def initial_setup(base_instance_parent) # 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| + (base_instance.methods - Class.methods - NON_OVERRIDABLE).each do |method_override| define_singleton_method(method_override) do |*args, &block| add_setup(method_override, *args, &block) end diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index 44dc83153..c02e0b6e8 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -4226,6 +4226,15 @@ def self.inherited(child_api) end end + it 'does not override methods inherited from Class' do + Class.define_method(:test_method) {} + subclass = Class.new(described_class) + expect(subclass).not_to receive(:add_setup) + subclass.test_method + ensure + Class.remove_method(:test_method) + end + context 'overriding via composition' do module Inherited def inherited(api) From ccd2330faeeffa8011722df8ef8a8da3ff424d9b Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sat, 20 May 2023 20:11:37 +0200 Subject: [PATCH 157/304] run_validators memory optmization (#2331) * Yield Validator instead of map all of them * Changelog entry --- CHANGELOG.md | 1 + lib/grape/endpoint.rb | 26 ++++++++++++++------------ 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5ce64ac6..1ca6b3661 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * [#2326](https://github.com/ruby-grape/grape/pull/2326): Use ActiveSupport extensions - [@ericproulx](https://github.com/ericproulx). * [#2327](https://github.com/ruby-grape/grape/pull/2327): Use ActiveSupport deprecation - [@ericproulx](https://github.com/ericproulx). * [#2330](https://github.com/ruby-grape/grape/pull/2330): Use ActiveSupport inflector - [@ericproulx](https://github.com/ericproulx). +* [#2331](https://github.com/ruby-grape/grape/pull/2331): Memory optimization when running validators - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/lib/grape/endpoint.rb b/lib/grape/endpoint.rb index 0f2fc83d2..9ddb91b78 100644 --- a/lib/grape/endpoint.rb +++ b/lib/grape/endpoint.rb @@ -317,8 +317,8 @@ def build_stack(helpers) end def build_helpers - helpers = namespace_stackable(:helpers) || [] - Module.new { helpers.each { |mod_to_include| include mod_to_include } } + helpers = namespace_stackable(:helpers) + Module.new { helpers&.each { |mod_to_include| include mod_to_include } } end private :build_stack, :build_helpers @@ -344,11 +344,9 @@ def lazy_initialize! end end - def run_validators(validator_factories, request) + def run_validators(validators, request) validation_errors = [] - 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| validator.validate(request) @@ -366,34 +364,38 @@ def run_validators(validator_factories, request) def run_filters(filters, type = :other) ActiveSupport::Notifications.instrument('endpoint_run_filters.grape', endpoint: self, filters: filters, type: type) do - (filters || []).each { |filter| instance_eval(&filter) } + filters&.each { |filter| instance_eval(&filter) } end post_extension = DSL::InsideRoute.post_filter_methods(type) extend post_extension if post_extension end def befores - namespace_stackable(:befores) || [] + namespace_stackable(:befores) end def before_validations - namespace_stackable(:before_validations) || [] + namespace_stackable(:before_validations) end def after_validations - namespace_stackable(:after_validations) || [] + namespace_stackable(:after_validations) end def afters - namespace_stackable(:afters) || [] + namespace_stackable(:afters) end def finallies - namespace_stackable(:finallies) || [] + namespace_stackable(:finallies) end def validations - route_setting(:saved_validations) || [] + return enum_for(:validations) unless block_given? + + route_setting(:saved_validations)&.each do |saved_validation| + yield Grape::Validations::ValidatorFactory.create_validator(**saved_validation) + end end def options? From 3b7901c1e14edddf06718cc3da9b37f6f53975cc Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sun, 28 May 2023 16:09:53 +0200 Subject: [PATCH 158/304] Replace Grape::Config by ActiveSupport::Configurable (#2332) * Replace Grape::Config by ActiveSupport::Configurable * Changelog entry * Regen rubocop * Add spec for default config Refactor request_spec which was build_params_with instead of the actual config * Fix rubocop --- .rubocop_todo.yml | 30 ++++++++++-------------------- CHANGELOG.md | 1 + lib/grape.rb | 8 +++++++- lib/grape/config.rb | 34 ---------------------------------- spec/grape/config_spec.rb | 17 ----------------- spec/grape/grape_spec.rb | 9 +++++++++ spec/grape/request_spec.rb | 18 ++++-------------- 7 files changed, 31 insertions(+), 86 deletions(-) delete mode 100644 lib/grape/config.rb delete mode 100644 spec/grape/config_spec.rb create mode 100644 spec/grape/grape_spec.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index a79c3557a..753aee1f7 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 5000` -# on 2023-04-23 13:05:19 UTC using RuboCop version 1.50.2. +# on 2023-05-27 20:47:15 UTC using RuboCop version 1.50.2. # 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 @@ -52,18 +52,16 @@ Lint/ConstantDefinitionInBlock: - 'spec/grape/validations/validators/presence_spec.rb' - 'spec/grape/validations/validators/values_spec.rb' -# Offense count: 5 +# Offense count: 3 # Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches. Lint/DuplicateBranch: Exclude: - - 'lib/grape/extensions/deep_symbolize_hash.rb' - 'spec/support/versioned_helpers.rb' # Offense count: 71 # Configuration parameters: AllowComments, AllowEmptyLambdas. Lint/EmptyBlock: Exclude: - - 'spec/grape/api/custom_validations_spec.rb' - 'spec/grape/api/recognize_path_spec.rb' - 'spec/grape/api/required_parameters_with_invalid_method_spec.rb' - 'spec/grape/api_spec.rb' @@ -102,13 +100,12 @@ Lint/MissingSuper: - 'lib/grape/router/pattern.rb' - 'lib/grape/validations/validators/base.rb' -# Offense count: 3 +# Offense count: 2 # Configuration parameters: EnforcedStyleForLeadingUnderscores. # SupportedStylesForLeadingUnderscores: disallowed, required, optional Naming/MemoizedInstanceVariableName: Exclude: - 'lib/grape/api/instance.rb' - - 'lib/grape/config.rb' - 'lib/grape/middleware/base.rb' # Offense count: 5 @@ -137,12 +134,11 @@ RSpec/AnyInstance: - 'spec/grape/api_spec.rb' - 'spec/grape/middleware/base_spec.rb' -# Offense count: 347 +# Offense count: 342 # Configuration parameters: Prefixes, AllowedPatterns. # Prefixes: when, with, without RSpec/ContextWording: Exclude: - - 'spec/grape/api/custom_validations_spec.rb' - 'spec/grape/api/defines_boolean_in_params_spec.rb' - 'spec/grape/api/documentation_spec.rb' - 'spec/grape/api/inherited_helpers_spec.rb' @@ -195,7 +191,7 @@ RSpec/ContextWording: - 'spec/grape/validations_spec.rb' - 'spec/shared/versioning_examples.rb' -# Offense count: 3 +# Offense count: 2 # Configuration parameters: IgnoredMetadata. RSpec/DescribeClass: Exclude: @@ -204,7 +200,6 @@ RSpec/DescribeClass: - '**/spec/routing/**/*' - '**/spec/system/**/*' - '**/spec/views/**/*' - - 'spec/grape/config_spec.rb' - 'spec/grape/named_api_spec.rb' - 'spec/grape/validations/instance_behaivour_spec.rb' @@ -329,7 +324,7 @@ RSpec/MessageChain: Exclude: - 'spec/grape/middleware/formatter_spec.rb' -# Offense count: 138 +# Offense count: 136 # Configuration parameters: . # SupportedStyles: have_received, receive RSpec/MessageSpies: @@ -340,7 +335,7 @@ RSpec/MissingExampleGroupArgument: Exclude: - 'spec/grape/middleware/exception_spec.rb' -# Offense count: 766 +# Offense count: 765 # Configuration parameters: Max. RSpec/MultipleExpectations: Exclude: @@ -371,8 +366,6 @@ RSpec/MultipleExpectations: - 'spec/grape/entity_spec.rb' - 'spec/grape/exceptions/body_parse_errors_spec.rb' - 'spec/grape/exceptions/invalid_accept_header_spec.rb' - - 'spec/grape/exceptions/missing_group_type_spec.rb' - - 'spec/grape/exceptions/unsupported_group_type_spec.rb' - 'spec/grape/exceptions/validation_errors_spec.rb' - 'spec/grape/extensions/param_builders/hash_spec.rb' - 'spec/grape/extensions/param_builders/hash_with_indifferent_access_spec.rb' @@ -420,7 +413,7 @@ RSpec/MultipleMemoizedHelpers: - 'spec/grape/request_spec.rb' - 'spec/grape/validations/attributes_doc_spec.rb' -# Offense count: 2147 +# Offense count: 2137 # Configuration parameters: EnforcedStyle, IgnoreSharedExamples. # SupportedStyles: always, named_only RSpec/NamedSubject: @@ -454,8 +447,6 @@ RSpec/NamedSubject: - 'spec/grape/exceptions/base_spec.rb' - 'spec/grape/exceptions/body_parse_errors_spec.rb' - 'spec/grape/exceptions/invalid_accept_header_spec.rb' - - 'spec/grape/exceptions/missing_group_type_spec.rb' - - 'spec/grape/exceptions/unsupported_group_type_spec.rb' - 'spec/grape/exceptions/validation_errors_spec.rb' - 'spec/grape/extensions/param_builders/hash_spec.rb' - 'spec/grape/extensions/param_builders/hash_with_indifferent_access_spec.rb' @@ -487,7 +478,7 @@ RSpec/NamedSubject: - 'spec/grape/validations/validators/presence_spec.rb' - 'spec/grape/validations_spec.rb' -# Offense count: 171 +# Offense count: 174 # Configuration parameters: Max, AllowedGroups. RSpec/NestedGroups: Exclude: @@ -568,12 +559,11 @@ RSpec/StubbedMock: - 'spec/grape/middleware/formatter_spec.rb' - 'spec/grape/parser_spec.rb' -# Offense count: 122 +# Offense count: 114 RSpec/SubjectStub: Exclude: - 'spec/grape/api_spec.rb' - 'spec/grape/dsl/callbacks_spec.rb' - - 'spec/grape/dsl/desc_spec.rb' - 'spec/grape/dsl/helpers_spec.rb' - 'spec/grape/dsl/inside_route_spec.rb' - 'spec/grape/dsl/middleware_spec.rb' diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ca6b3661..344022f76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * [#2327](https://github.com/ruby-grape/grape/pull/2327): Use ActiveSupport deprecation - [@ericproulx](https://github.com/ericproulx). * [#2330](https://github.com/ruby-grape/grape/pull/2330): Use ActiveSupport inflector - [@ericproulx](https://github.com/ericproulx). * [#2331](https://github.com/ruby-grape/grape/pull/2331): Memory optimization when running validators - [@ericproulx](https://github.com/ericproulx). +* [#2332](https://github.com/ruby-grape/grape/pull/2332): Use ActiveSupport configurable - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/lib/grape.rb b/lib/grape.rb index 32bb70058..39f3905c0 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -11,6 +11,7 @@ require 'date' require 'active_support' require 'active_support/concern' +require 'active_support/configurable' require 'active_support/version' require 'active_support/isolated_execution_state' if ActiveSupport::VERSION::MAJOR > 6 require 'active_support/core_ext/array/conversions' @@ -34,6 +35,7 @@ I18n.load_path << File.expand_path('grape/locale/en.yml', __dir__) module Grape + include ActiveSupport::Configurable extend ::ActiveSupport::Autoload eager_autoload do @@ -287,9 +289,13 @@ module Types autoload :InvalidValue end end + + configure do |config| + config.param_builder = Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder + config.compile_methods! + end end -require 'grape/config' require 'grape/content_types' require 'grape/util/lazy_value' diff --git a/lib/grape/config.rb b/lib/grape/config.rb deleted file mode 100644 index 6d76c06b1..000000000 --- a/lib/grape/config.rb +++ /dev/null @@ -1,34 +0,0 @@ -# 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/spec/grape/config_spec.rb b/spec/grape/config_spec.rb deleted file mode 100644 index c09090cd7..000000000 --- a/spec/grape/config_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -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/grape_spec.rb b/spec/grape/grape_spec.rb new file mode 100644 index 000000000..021aa861d --- /dev/null +++ b/spec/grape/grape_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +RSpec.describe Grape do + describe '.config' do + subject { described_class.config } + + it { is_expected.to eq(param_builder: Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder) } + end +end diff --git a/spec/grape/request_spec.rb b/spec/grape/request_spec.rb index 0c48cf2b2..6a3f1948f 100644 --- a/spec/grape/request_spec.rb +++ b/spec/grape/request_spec.rb @@ -62,29 +62,19 @@ module Grape end end - describe 'when the param_builder is set to Hashie' do + describe 'when the build_params_with is set to Hashie' do subject(:request_params) { described_class.new(env, **opts).params } - before do - Grape.configure do |config| - config.param_builder = Grape::Extensions::Hashie::Mash::ParamBuilder - end - end - - after do - Grape.config.reset - end - context 'when the API does not include a specific param builder' do let(:opts) { {} } - it { is_expected.to be_a(Hashie::Mash) } + it { is_expected.to be_a(Hash) } end context 'when the API includes a specific param builder' do - let(:opts) { { build_params_with: Grape::Extensions::Hash::ParamBuilder } } + let(:opts) { { build_params_with: Grape::Extensions::Hashie::Mash::ParamBuilder } } - it { is_expected.to be_a(Hash) } + it { is_expected.to be_a(Hashie::Mash) } end end From d1dfdccca242e00d4d88064a013f92e89a76dbd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Marques?= <64037198+TheDevJoao@users.noreply.github.com> Date: Mon, 5 Jun 2023 14:50:44 -0300 Subject: [PATCH 159/304] Feature: Allows procs with arity 1 to validate and use custom messages (#2333) * refactor: validate_param! method - now allows a proc to validate and use custom messages - adds @proc_message instance variable - creates the skip_validation? method to pass rubocops cyclomatic error * Adds changelog entry * Adds PR # to the changelog * Properly formats the changelog line * Places the changelog line in the correct order * Adds default changelog message for future use * Edits the changelog with a better description * refactor: allows procs with an arity of 1 * Adds a test for when arity is > 1 --- CHANGELOG.md | 1 + .../validators/values_validator.rb | 13 ++++++- .../validations/validators/values_spec.rb | 37 +++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 344022f76..eaf331629 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * [#2330](https://github.com/ruby-grape/grape/pull/2330): Use ActiveSupport inflector - [@ericproulx](https://github.com/ericproulx). * [#2331](https://github.com/ruby-grape/grape/pull/2331): Memory optimization when running validators - [@ericproulx](https://github.com/ericproulx). * [#2332](https://github.com/ruby-grape/grape/pull/2332): Use ActiveSupport configurable - [@ericproulx](https://github.com/ericproulx). +* [#2333](https://github.com/ruby-grape/grape/pull/2333): Use custom messages in parameter validation with arity 1 - [@thedevjoao](https://github.com/TheDevJoao). * Your contribution here. #### Fixes diff --git a/lib/grape/validations/validators/values_validator.rb b/lib/grape/validations/validators/values_validator.rb index 0fba1a91c..2b7aca76e 100644 --- a/lib/grape/validations/validators/values_validator.rb +++ b/lib/grape/validations/validators/values_validator.rb @@ -43,7 +43,7 @@ def validate_param!(attr_name, params) unless check_values(param_array, attr_name) raise validation_exception(attr_name, message(:values)) \ - if @proc && !param_array.all? { |param| @proc.call(param) } + if @proc && !validate_proc(@proc, param_array) end private @@ -68,6 +68,17 @@ def check_excepts(param_array) param_array.none? { |param| excepts.include?(param) } end + def validate_proc(proc, param_array) + case proc.arity + when 0 + param_array.all? { |_param| proc.call } + when 1 + param_array.all? { |param| proc.call(param) } + else + raise ArgumentError, 'proc arity must be 0 or 1' + end + end + def except_message options = instance_variable_get(:@option) options_key?(:except_message) ? options[:except_message] : message(:except_values) diff --git a/spec/grape/validations/validators/values_spec.rb b/spec/grape/validations/validators/values_spec.rb index 43b8ea698..d8ef17c95 100644 --- a/spec/grape/validations/validators/values_spec.rb +++ b/spec/grape/validations/validators/values_spec.rb @@ -30,6 +30,10 @@ def add_except(except) def include?(value) values.include?(value) end + + def even?(value) + value.to_i.even? + end end end end @@ -241,6 +245,18 @@ def include?(value) end get '/proc/message' + params do + requires :number, values: { value: ->(v) { ValuesModel.even? v }, message: 'must be even' } + end + get '/proc/custom_message' do + { message: 'success' } + end + + params do + requires :input_one, :input_two, values: { value: ->(v1, v2) { v1 + v2 > 10 } } + end + get '/proc/arity2' + params do optional :name, type: String, values: %w[a b], allow_blank: true end @@ -692,5 +708,26 @@ def app expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type failed check' }.to_json) end + + context 'when proc has an arity of 1' do + it 'accepts a valid value' do + get '/proc/custom_message', number: 4 + expect(last_response.status).to eq 200 + expect(last_response.body).to eq({ message: 'success' }.to_json) + end + + it 'rejects an invalid value' do + get '/proc/custom_message', number: 5 + expect(last_response.status).to eq 400 + expect(last_response.body).to eq({ error: 'number must be even' }.to_json) + end + end + + context 'when arity is > 1' do + it 'returns an error status code' do + get '/proc/arity2', input_one: 2, input_two: 3 + expect(last_response.status).to eq 400 + end + end end end From 1147658d9136da0c23b2f6c4516b46536c5322b9 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sun, 11 Jun 2023 15:46:34 +0200 Subject: [PATCH 160/304] Fix custom validator not ending with _validator (#2337) * Remove ! Add spec * Changelog entry * Update Changelog's entry Update spec comment Add spec for anonymous class * Fix cop --- CHANGELOG.md | 1 + lib/grape/validations/validators/base.rb | 2 +- .../grape/validations/validators/base_spec.rb | 38 +++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 spec/grape/validations/validators/base_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index eaf331629..23a01a211 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ #### Fixes * [#2328](https://github.com/ruby-grape/grape/pull/2328): Don't cache Class.instance_methods - [@byroot](https://github.com/byroot). +* [#2337](https://github.com/ruby-grape/grape/pull/2337): Fix: allow custom validators that do not end with _validator - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ## 1.7.1 (2023/05/14) diff --git a/lib/grape/validations/validators/base.rb b/lib/grape/validations/validators/base.rb index c720d793c..cfa49155e 100644 --- a/lib/grape/validations/validators/base.rb +++ b/lib/grape/validations/validators/base.rb @@ -64,7 +64,7 @@ def validate!(params) def self.inherited(klass) return if klass.name.blank? - short_validator_name = klass.name.demodulize.underscore.delete_suffix!('_validator') + short_validator_name = klass.name.demodulize.underscore.delete_suffix('_validator') Validations.register_validator(short_validator_name, klass) end diff --git a/spec/grape/validations/validators/base_spec.rb b/spec/grape/validations/validators/base_spec.rb new file mode 100644 index 000000000..9f1ff9760 --- /dev/null +++ b/spec/grape/validations/validators/base_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +RSpec.describe Grape::Validations::Validators::Base do + describe '#inherited' do + context 'when validator is anonymous' do + subject(:custom_validator) { Class.new(described_class) } + + it 'does not register the validator' do + expect(Grape::Validations).not_to receive(:register_validator) + custom_validator + end + end + + # Anonymous class does not have a name and class A < B would leak. + # Simulates inherited callback + context "when validator's underscored name does not end with _validator" do + subject(:custom_validator) { described_class.inherited(TestModule::CustomValidatorABC) } + + before { stub_const('TestModule::CustomValidatorABC', Class.new) } + + it 'registers the custom validator with a short name' do + expect(Grape::Validations).to receive(:register_validator).with('custom_validator_abc', TestModule::CustomValidatorABC) + custom_validator + end + end + + context "when validator's underscored name ends with _validator" do + subject(:custom_validator) { described_class.inherited(TestModule::CustomValidator) } + + before { stub_const('TestModule::CustomValidator', Class.new) } + + it 'registers the custom validator with short name not ending with validator' do + expect(Grape::Validations).to receive(:register_validator).with('custom', TestModule::CustomValidator) + custom_validator + end + end + end +end From dd741b93b4841562866405654b7bce21a6b5ac21 Mon Sep 17 00:00:00 2001 From: Nicolas Klein Date: Thu, 22 Jun 2023 14:25:08 +0100 Subject: [PATCH 161/304] [ISSUE-2321] Updates documentation on re-mounted configuration for params (#2339) * [ISSUE-2321] Updates documentation on re-mounted configuration for params * Adds changelog * Extra space on changelog * More updates to changelog --- CHANGELOG.md | 1 + README.md | 6 +++--- spec/grape/api_remount_spec.rb | 36 ++++++++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23a01a211..110e13f74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ #### Fixes +* [#2339](https://github.com/ruby-grape/grape/pull/2339): Documentation and specs for remountable configuration in params - [@myxoh](https://github.com/myxoh). * [#2328](https://github.com/ruby-grape/grape/pull/2328): Don't cache Class.instance_methods - [@byroot](https://github.com/byroot). * [#2337](https://github.com/ruby-grape/grape/pull/2337): Fix: allow custom validators that do not end with _validator - [@ericproulx](https://github.com/ericproulx). * Your contribution here. diff --git a/README.md b/README.md index a334a72a8..b00d2c62b 100644 --- a/README.md +++ b/README.md @@ -526,15 +526,15 @@ end ```ruby class BasicAPI < Grape::API desc 'Statuses index' do - params: mounted { configuration[:entity] || API::Entities::Status }.documentation + params: (configuration[:entity] || API::Entities::Status).documentation end params do - requires :all, using: mounted { configuration[:entity] || API::Entities::Status }.documentation + requires :all, using: (configuration[:entity] || API::Entities::Status).documentation end get '/statuses' do statuses = Status.all type = current_user.admin? ? :full : :default - present statuses, with: mounted { configuration[:entity] || API::Entities::Status }, type: type + present statuses, with: (configuration[:entity] || API::Entities::Status), type: type end end diff --git a/spec/grape/api_remount_spec.rb b/spec/grape/api_remount_spec.rb index 9fea1c3f0..40b550515 100644 --- a/spec/grape/api_remount_spec.rb +++ b/spec/grape/api_remount_spec.rb @@ -147,6 +147,42 @@ def app end end + context 'when the params are configured via a configuration' do + subject(:a_remounted_api) do + Class.new(described_class) do + params do + requires configuration[:required_attr_name], type: String + end + + get(mounted { configuration[:endpoint] }) do + status 200 + end + end + end + + context 'when the configured param is my_attr' do + it 'requires the configured params' do + root_api.mount a_remounted_api, with: { + required_attr_name: 'my_attr', + endpoint: 'test' + } + get 'test?another_attr=1' + expect(last_response.status).to eq 400 + get 'test?my_attr=1' + expect(last_response.status).to eq 200 + + root_api.mount a_remounted_api, with: { + required_attr_name: 'another_attr', + endpoint: 'test_b' + } + get 'test_b?another_attr=1' + expect(last_response.status).to eq 200 + get 'test_b?my_attr=1' + expect(last_response.status).to eq 400 + end + end + end + context 'when executing a standard block within a `mounted` block with all dynamic params' do subject(:a_remounted_api) do Class.new(described_class) do From db7000b4ae1b09b5d31891fc91f1607d75419611 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Tue, 27 Jun 2023 01:48:40 +0200 Subject: [PATCH 162/304] Stop yielding skip value (#2341) * Not yielding when skipping Adjust specs * Remove begin in rescue blocks * CHANGELOG entry * Fix typo --- CHANGELOG.md | 1 + .../validations/multiple_attributes_iterator.rb | 2 +- .../validations/single_attribute_iterator.rb | 4 +++- lib/grape/validations/validators/base.rb | 11 ++++------- .../validators/multiple_params_base.rb | 12 ++++-------- .../multiple_attributes_iterator_spec.rb | 14 ++++++-------- .../single_attribute_iterator_spec.rb | 17 ++++++++--------- 7 files changed, 27 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 110e13f74..dc7d8f01a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * [#2331](https://github.com/ruby-grape/grape/pull/2331): Memory optimization when running validators - [@ericproulx](https://github.com/ericproulx). * [#2332](https://github.com/ruby-grape/grape/pull/2332): Use ActiveSupport configurable - [@ericproulx](https://github.com/ericproulx). * [#2333](https://github.com/ruby-grape/grape/pull/2333): Use custom messages in parameter validation with arity 1 - [@thedevjoao](https://github.com/TheDevJoao). +* [#2341](https://github.com/ruby-grape/grape/pull/2341): Stop yielding skip value - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/lib/grape/validations/multiple_attributes_iterator.rb b/lib/grape/validations/multiple_attributes_iterator.rb index d9ef7264b..e5621bcaa 100644 --- a/lib/grape/validations/multiple_attributes_iterator.rb +++ b/lib/grape/validations/multiple_attributes_iterator.rb @@ -6,7 +6,7 @@ class MultipleAttributesIterator < AttributesIterator private def yield_attributes(resource_params, _attrs) - yield resource_params, skip?(resource_params) + yield resource_params unless skip?(resource_params) end end end diff --git a/lib/grape/validations/single_attribute_iterator.rb b/lib/grape/validations/single_attribute_iterator.rb index 7fd3c3f47..218f4037b 100644 --- a/lib/grape/validations/single_attribute_iterator.rb +++ b/lib/grape/validations/single_attribute_iterator.rb @@ -6,8 +6,10 @@ class SingleAttributeIterator < AttributesIterator private def yield_attributes(val, attrs) + return if skip?(val) + attrs.each do |attr_name| - yield val, attr_name, empty?(val), skip?(val) + yield val, attr_name, empty?(val) end end diff --git a/lib/grape/validations/validators/base.rb b/lib/grape/validations/validators/base.rb index cfa49155e..ae0f0f48e 100644 --- a/lib/grape/validations/validators/base.rb +++ b/lib/grape/validations/validators/base.rb @@ -46,16 +46,13 @@ def validate!(params) # there may be more than one error per field array_errors = [] - attributes.each do |val, attr_name, empty_val, skip_value| - next if skip_value + attributes.each do |val, attr_name, empty_val| next if !@scope.required? && empty_val next unless @scope.meets_dependency?(val, params) - begin - validate_param!(attr_name, val) if @required || (val.respond_to?(:key?) && val.key?(attr_name)) - rescue Grape::Exceptions::Validation => e - array_errors << e - end + validate_param!(attr_name, val) if @required || (val.respond_to?(:key?) && val.key?(attr_name)) + rescue Grape::Exceptions::Validation => e + array_errors << e end raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors.any? diff --git a/lib/grape/validations/validators/multiple_params_base.rb b/lib/grape/validations/validators/multiple_params_base.rb index c0b02ac50..29df27720 100644 --- a/lib/grape/validations/validators/multiple_params_base.rb +++ b/lib/grape/validations/validators/multiple_params_base.rb @@ -8,14 +8,10 @@ def validate!(params) attributes = MultipleAttributesIterator.new(self, @scope, params) array_errors = [] - 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 + attributes.each do |resource_params| + validate_params!(resource_params) + rescue Grape::Exceptions::Validation => e + array_errors << e end raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors.any? diff --git a/spec/grape/validations/multiple_attributes_iterator_spec.rb b/spec/grape/validations/multiple_attributes_iterator_spec.rb index 5e6322785..14988b981 100644 --- a/spec/grape/validations/multiple_attributes_iterator_spec.rb +++ b/spec/grape/validations/multiple_attributes_iterator_spec.rb @@ -12,8 +12,8 @@ { 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) + it 'yields the whole params hash without the list of attrs' do + expect { |b| iterator.each(&b) }.to yield_with_args(params) end end @@ -23,17 +23,15 @@ 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]) + expect { |b| iterator.each(&b) }.to yield_successive_args(params[0], params[1]) end end context 'when params is empty optional placeholder' do - let(:params) do - [Grape::DSL::Parameters::EmptyOptionalValue, { first: 'string2', second: 'string2' }] - end + let(:params) { [Grape::DSL::Parameters::EmptyOptionalValue] } - 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]) + it 'does not yield it' do + expect { |b| iterator.each(&b) }.to yield_successive_args end end end diff --git a/spec/grape/validations/single_attribute_iterator_spec.rb b/spec/grape/validations/single_attribute_iterator_spec.rb index 1962a8e31..2cde615e3 100644 --- a/spec/grape/validations/single_attribute_iterator_spec.rb +++ b/spec/grape/validations/single_attribute_iterator_spec.rb @@ -14,7 +14,7 @@ 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]) + .to yield_successive_args([params, :first, false], [params, :second, false]) end end @@ -25,8 +25,8 @@ 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] + [params[0], :first, false], [params[0], :second, false], + [params[1], :first, false], [params[1], :second, false] ) end @@ -35,9 +35,9 @@ 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] + [params[0], :first, true], [params[0], :second, true], + [params[1], :first, true], [params[1], :second, true], + [params[2], :first, false], [params[2], :second, false] ) end end @@ -45,10 +45,9 @@ context 'when missing optional value' do let(:params) { [Grape::DSL::Parameters::EmptyOptionalValue, 10] } - it 'marks params with skipped values' do + it 'does not yield 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] + [params[1], :first, false], [params[1], :second, false] ) end end From 96ac079e4f0e6fda30cb323490c93b9c85709a68 Mon Sep 17 00:00:00 2001 From: Michael Scrivo <275524+mscrivo@users.noreply.github.com> Date: Mon, 3 Jul 2023 19:08:57 -0400 Subject: [PATCH 163/304] Allow specifying a handler for grape_exceptions (#2342) * Allow specifying a handler for grape_exceptions This allows you to customize the format of the error response for grape exceptions: For example, you could do something like this: ```rb rescue_from :grape_exceptions do |e| error!({ errors: [{ code: 'Error', message: e.message.squish }] }, e.status) end ``` which would render like this: ``` { "errors": [ { "code": "Error", "message": "Problem: message body does not match declared format Resolution: when specifying application/json as content-type, you must pass valid application/json in the request's 'body'" } ] } ``` * match format * Fix issue caught by tests * Update README & CHANGELOG * Add spec to demonstrate usage * Fix rubocop offenses * Fix rubocop offences --- .rubocop_todo.yml | 23 ++++++----- CHANGELOG.md | 1 + README.md | 8 ++++ lib/grape/dsl/request_response.rb | 3 +- lib/grape/endpoint.rb | 3 +- lib/grape/middleware/error.rb | 2 +- spec/grape/dsl/request_response_spec.rb | 23 ++++++++++- .../exceptions/body_parse_errors_spec.rb | 40 +++++++++++++++++++ 8 files changed, 89 insertions(+), 14 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 753aee1f7..91136d8cd 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 5000` -# on 2023-05-27 20:47:15 UTC using RuboCop version 1.50.2. +# on 2023-07-01 15:43:53 UTC using RuboCop version 1.50.2. # 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 @@ -100,6 +100,12 @@ Lint/MissingSuper: - 'lib/grape/router/pattern.rb' - 'lib/grape/validations/validators/base.rb' +# Offense count: 1 +# Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns. +Metrics/MethodLength: + Exclude: + - 'lib/grape/endpoint.rb' + # Offense count: 2 # Configuration parameters: EnforcedStyleForLeadingUnderscores. # SupportedStylesForLeadingUnderscores: disallowed, required, optional @@ -134,7 +140,7 @@ RSpec/AnyInstance: - 'spec/grape/api_spec.rb' - 'spec/grape/middleware/base_spec.rb' -# Offense count: 342 +# Offense count: 343 # Configuration parameters: Prefixes, AllowedPatterns. # Prefixes: when, with, without RSpec/ContextWording: @@ -324,7 +330,7 @@ RSpec/MessageChain: Exclude: - 'spec/grape/middleware/formatter_spec.rb' -# Offense count: 136 +# Offense count: 147 # Configuration parameters: . # SupportedStyles: have_received, receive RSpec/MessageSpies: @@ -335,7 +341,7 @@ RSpec/MissingExampleGroupArgument: Exclude: - 'spec/grape/middleware/exception_spec.rb' -# Offense count: 765 +# Offense count: 772 # Configuration parameters: Max. RSpec/MultipleExpectations: Exclude: @@ -413,7 +419,7 @@ RSpec/MultipleMemoizedHelpers: - 'spec/grape/request_spec.rb' - 'spec/grape/validations/attributes_doc_spec.rb' -# Offense count: 2137 +# Offense count: 2150 # Configuration parameters: EnforcedStyle, IgnoreSharedExamples. # SupportedStyles: always, named_only RSpec/NamedSubject: @@ -478,7 +484,7 @@ RSpec/NamedSubject: - 'spec/grape/validations/validators/presence_spec.rb' - 'spec/grape/validations_spec.rb' -# Offense count: 174 +# Offense count: 173 # Configuration parameters: Max, AllowedGroups. RSpec/NestedGroups: Exclude: @@ -527,11 +533,10 @@ RSpec/RepeatedDescription: - 'spec/grape/validations/validators/allow_blank_spec.rb' - 'spec/grape/validations/validators/values_spec.rb' -# Offense count: 10 +# Offense count: 8 RSpec/RepeatedExample: Exclude: - 'spec/grape/api_spec.rb' - - 'spec/grape/dsl/request_response_spec.rb' - 'spec/grape/middleware/versioner/accept_version_header_spec.rb' - 'spec/grape/validations/validators/allow_blank_spec.rb' @@ -559,7 +564,7 @@ RSpec/StubbedMock: - 'spec/grape/middleware/formatter_spec.rb' - 'spec/grape/parser_spec.rb' -# Offense count: 114 +# Offense count: 122 RSpec/SubjectStub: Exclude: - 'spec/grape/api_spec.rb' diff --git a/CHANGELOG.md b/CHANGELOG.md index dc7d8f01a..fc5b2f64d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * [#2332](https://github.com/ruby-grape/grape/pull/2332): Use ActiveSupport configurable - [@ericproulx](https://github.com/ericproulx). * [#2333](https://github.com/ruby-grape/grape/pull/2333): Use custom messages in parameter validation with arity 1 - [@thedevjoao](https://github.com/TheDevJoao). * [#2341](https://github.com/ruby-grape/grape/pull/2341): Stop yielding skip value - [@ericproulx](https://github.com/ericproulx). +* [#2342](https://github.com/ruby-grape/grape/pull/2342): Allow specifying a handler for grape_exceptions - [@mscrivo](https://github.com/mscrivo). * Your contribution here. #### Fixes diff --git a/README.md b/README.md index b00d2c62b..9e7c370fb 100644 --- a/README.md +++ b/README.md @@ -2626,6 +2626,14 @@ class Twitter::API < Grape::API end ``` +If you want to customize the shape of grape exceptions returned to the user, to match your `:all` handler for example, you can pass a block to `rescue_from :grape_exceptions`. + +```ruby +rescue_from :grape_exceptions do |e| + error!(e, e.status) +end +``` + You can also rescue specific exceptions. ```ruby diff --git a/lib/grape/dsl/request_response.rb b/lib/grape/dsl/request_response.rb index 6129574fd..7e23c9956 100644 --- a/lib/grape/dsl/request_response.rb +++ b/lib/grape/dsl/request_response.rb @@ -112,10 +112,11 @@ def rescue_from(*args, &block) if args.include?(:all) namespace_inheritable(:rescue_all, true) - namespace_inheritable :all_rescue_handler, handler + namespace_inheritable(:all_rescue_handler, handler) elsif args.include?(:grape_exceptions) namespace_inheritable(:rescue_all, true) namespace_inheritable(:rescue_grape_exceptions, true) + namespace_inheritable(:grape_exceptions_rescue_handler, handler) else handler_type = case options[:rescue_subclasses] diff --git a/lib/grape/endpoint.rb b/lib/grape/endpoint.rb index 9ddb91b78..8dfe915bc 100644 --- a/lib/grape/endpoint.rb +++ b/lib/grape/endpoint.rb @@ -292,7 +292,8 @@ def build_stack(helpers) rescue_options: namespace_stackable_with_hash(:rescue_options) || {}, rescue_handlers: namespace_reverse_stackable_with_hash(:rescue_handlers) || {}, base_only_rescue_handlers: namespace_stackable_with_hash(:base_only_rescue_handlers) || {}, - all_rescue_handler: namespace_inheritable(:all_rescue_handler) + all_rescue_handler: namespace_inheritable(:all_rescue_handler), + grape_exceptions_rescue_handler: namespace_inheritable(:grape_exceptions_rescue_handler) stack.concat namespace_stackable(:middleware) diff --git a/lib/grape/middleware/error.rb b/lib/grape/middleware/error.rb index 3e9d8c768..259d610eb 100644 --- a/lib/grape/middleware/error.rb +++ b/lib/grape/middleware/error.rb @@ -109,7 +109,7 @@ def rescue_handler_for_grape_exception(klass) return :error_response if klass == Grape::Exceptions::InvalidVersionHeader return unless options[:rescue_grape_exceptions] || !options[:rescue_all] - :error_response + options[:grape_exceptions_rescue_handler] || :error_response end def rescue_handler_for_any_class(klass) diff --git a/spec/grape/dsl/request_response_spec.rb b/spec/grape/dsl/request_response_spec.rb index 8546ace00..832ceafcf 100644 --- a/spec/grape/dsl/request_response_spec.rb +++ b/spec/grape/dsl/request_response_spec.rb @@ -148,13 +148,32 @@ def self.imbue(key, value) it 'sets rescue all to true' do expect(subject).to receive(:namespace_inheritable).with(:rescue_all, true) expect(subject).to receive(:namespace_inheritable).with(:rescue_grape_exceptions, true) + expect(subject).to receive(:namespace_inheritable).with(:grape_exceptions_rescue_handler, nil) subject.rescue_from :grape_exceptions end - it 'sets rescue_grape_exceptions to true' do + it 'sets given proc as rescue handler' do + rescue_handler_proc = proc {} expect(subject).to receive(:namespace_inheritable).with(:rescue_all, true) expect(subject).to receive(:namespace_inheritable).with(:rescue_grape_exceptions, true) - subject.rescue_from :grape_exceptions + expect(subject).to receive(:namespace_inheritable).with(:grape_exceptions_rescue_handler, rescue_handler_proc) + subject.rescue_from :grape_exceptions, rescue_handler_proc + end + + it 'sets given block as rescue handler' do + rescue_handler_proc = proc {} + expect(subject).to receive(:namespace_inheritable).with(:rescue_all, true) + expect(subject).to receive(:namespace_inheritable).with(:rescue_grape_exceptions, true) + expect(subject).to receive(:namespace_inheritable).with(:grape_exceptions_rescue_handler, rescue_handler_proc) + subject.rescue_from :grape_exceptions, &rescue_handler_proc + end + + it 'sets a rescue handler declared through :with option' do + with_block = -> { 'hello' } + expect(subject).to receive(:namespace_inheritable).with(:rescue_all, true) + expect(subject).to receive(:namespace_inheritable).with(:rescue_grape_exceptions, true) + expect(subject).to receive(:namespace_inheritable).with(:grape_exceptions_rescue_handler, an_instance_of(Proc)) + subject.rescue_from :grape_exceptions, with: with_block end end diff --git a/spec/grape/exceptions/body_parse_errors_spec.rb b/spec/grape/exceptions/body_parse_errors_spec.rb index 285f3e81b..7017400f6 100644 --- a/spec/grape/exceptions/body_parse_errors_spec.rb +++ b/spec/grape/exceptions/body_parse_errors_spec.rb @@ -91,6 +91,46 @@ def app end end + context 'api with rescue_from :grape_exceptions handler with block' do + subject { Class.new(Grape::API) } + + before do + subject.rescue_from :grape_exceptions do |e| + rack_response "Custom Error Contents, Original Message: #{e.message}", 400 + end + + subject.params do + requires :beer + end + + subject.post '/beer' do + 'beer received' + end + end + + def app + subject + end + + context 'with content_type json' do + it 'returns body parsing error message' do + post '/beer', 'test', 'CONTENT_TYPE' => 'application/json' + expect(last_response.status).to eq 400 + expect(last_response.body).to include 'message body does not match declared format' + expect(last_response.body).to include 'Custom Error Contents, Original Message' + end + end + + context 'with content_type xml' do + it 'returns body parsing error message' do + post '/beer', 'test', 'CONTENT_TYPE' => 'application/xml' + expect(last_response.status).to eq 400 + expect(last_response.body).to include 'message body does not match declared format' + expect(last_response.body).to include 'Custom Error Contents, Original Message' + end + end + end + context 'api without a rescue handler' do subject { Class.new(Grape::API) } From 8e1488d6aeae52c693c2b611d9d42091523ea670 Mon Sep 17 00:00:00 2001 From: Michael Scrivo <275524+mscrivo@users.noreply.github.com> Date: Tue, 4 Jul 2023 12:44:46 -0400 Subject: [PATCH 164/304] Fix unknown validator exception when using requires/optional with Entity (#2338) * Fix crash when using requires/optional with Entity when that entity specifies keywords that grape interprets as custom validators, ie: is_array, param_type, etc. This PR changes it so that it skips looking for a custom validator for those special documentation keywords. This allows you to do something like: ```rb desc 'Create some entity', entity: SomeEntity, params: SomeEntity.documentation params do requires :none, except: %i[field1 field2], using: SomeEntity.documentation end post do ... end ``` and SomeEntity can specify documentation like: ```rb class SomeEntity < Grape::Entity expose :id, documentation: { type: Integer, format: 'int64', desc: 'ID' } expose :name, documentation: { type: String, desc: 'Name', require: true, param_type: 'body' } expose :array_field, documentation: { type: String, desc: 'Array Field', require: true, param_type: 'bold' } end ``` and it won't crash on startup and give you correct documentation and param validation to boot. Open questions: 1. Is this an exhaustive list of keywords? 2. Is there a better way to maintain/get the list of keywords? 3. Is there a better approach altogeher? * Cleanup code and add to existing tests that fail without these changes. * Update after merge with master * PR feedback --- .rubocop_todo.yml | 2 +- CHANGELOG.md | 1 + lib/grape/validations/params_scope.rb | 8 +++++++- spec/grape/validations_spec.rb | 13 +++++++------ 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 91136d8cd..51d321919 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 5000` -# on 2023-07-01 15:43:53 UTC using RuboCop version 1.50.2. +# on 2023-07-04 00:22:04 UTC using RuboCop version 1.50.2. # 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index fc5b2f64d..e59b3bea9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ * [#2333](https://github.com/ruby-grape/grape/pull/2333): Use custom messages in parameter validation with arity 1 - [@thedevjoao](https://github.com/TheDevJoao). * [#2341](https://github.com/ruby-grape/grape/pull/2341): Stop yielding skip value - [@ericproulx](https://github.com/ericproulx). * [#2342](https://github.com/ruby-grape/grape/pull/2342): Allow specifying a handler for grape_exceptions - [@mscrivo](https://github.com/mscrivo). +* [#2338](https://github.com/ruby-grape/grape/pull/2338): Fix unknown validator when using requires/optional with entity - [@mscrivo](https://github.com/mscrivo). * Your contribution here. #### Fixes diff --git a/lib/grape/validations/params_scope.rb b/lib/grape/validations/params_scope.rb index a74cef20c..b419d6514 100644 --- a/lib/grape/validations/params_scope.rb +++ b/lib/grape/validations/params_scope.rb @@ -10,6 +10,11 @@ class ParamsScope include Grape::DSL::Parameters + # There are a number of documentation options on entities that don't have + # corresponding validators. Since there is nowhere that enumerates them all, + # we maintain a list of them here and skip looking up validators for them. + RESERVED_DOCUMENTATION_KEYWORDS = %i[as required param_type is_array format example].freeze + class Attr attr_accessor :key, :scope @@ -359,7 +364,8 @@ def validates(attrs, validations) coerce_type validations, attrs, doc, opts validations.each do |type, options| - next if type == :as + # Don't try to look up validators for documentation params that don't have one. + next if RESERVED_DOCUMENTATION_KEYWORDS.include?(type) validate(type, options, attrs, doc, opts) end diff --git a/spec/grape/validations_spec.rb b/spec/grape/validations_spec.rb index 7e2ae9bc9..58ab8a579 100644 --- a/spec/grape/validations_spec.rb +++ b/spec/grape/validations_spec.rb @@ -179,11 +179,12 @@ def define_optional_using context 'requires :all using Grape::Entity documentation' do def define_requires_all documentation = { - required_field: { type: String }, - optional_field: { type: String } + required_field: { type: String, required: true, param_type: 'query' }, + optional_field: { type: String }, + optional_array_field: { type: Array[String], is_array: true } } subject.params do - requires :all, except: :optional_field, using: documentation + requires :all, except: %i[optional_field optional_array_field], using: documentation end end before do @@ -195,7 +196,7 @@ def define_requires_all it 'adds entity documentation to declared params' do define_requires_all - expect(Grape::Validations::ParamsScope::Attr.attrs_keys(declared_params)).to eq(%i[required_field optional_field]) + expect(Grape::Validations::ParamsScope::Attr.attrs_keys(declared_params)).to eq(%i[required_field optional_field optional_array_field]) end it 'errors when required_field is not present' do @@ -214,8 +215,8 @@ def define_requires_all context 'requires :none using Grape::Entity documentation' do def define_requires_none documentation = { - required_field: { type: String }, - optional_field: { type: String } + required_field: { type: String, example: 'Foo' }, + optional_field: { type: Integer, format: 'int64' } } subject.params do requires :none, except: :required_field, using: documentation From df3b3c8a546dd7195262719c55be4fc688efb5fa Mon Sep 17 00:00:00 2001 From: Keith Barrette <896780+kbarrette@users.noreply.github.com> Date: Mon, 21 Aug 2023 13:25:09 -0400 Subject: [PATCH 165/304] Adjust test expectations to conform to rack 3 (#2346) * Adjust test expectations to conform to rack 3 --- .github/workflows/test.yml | 2 +- Appraisals | 4 ++++ CHANGELOG.md | 1 + grape.gemspec | 2 +- spec/grape/endpoint_spec.rb | 16 ++++++++-------- spec/grape/middleware/formatter_spec.rb | 2 +- 6 files changed, 16 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 910a8cc6a..d79b49f95 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,7 +24,7 @@ jobs: fail-fast: false matrix: ruby: ['2.7', '3.0', '3.1', '3.2'] - gemfile: [rack_2_0, rails_6_0, rails_6_1, rails_7_0] + gemfile: [rack_2_0, rack_3_0, rails_6_0, rails_6_1, rails_7_0] include: - ruby: '2.6' gemfile: rails_5_2 diff --git a/Appraisals b/Appraisals index 4968ae7c3..a9d68682d 100644 --- a/Appraisals +++ b/Appraisals @@ -39,3 +39,7 @@ end appraise 'rack2' do gem 'rack', '~> 2.0.0' end + +appraise 'rack3' do + gem 'rack', '~> 3.0.0' +end diff --git a/CHANGELOG.md b/CHANGELOG.md index e59b3bea9..fc5bc570e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ * [#2339](https://github.com/ruby-grape/grape/pull/2339): Documentation and specs for remountable configuration in params - [@myxoh](https://github.com/myxoh). * [#2328](https://github.com/ruby-grape/grape/pull/2328): Don't cache Class.instance_methods - [@byroot](https://github.com/byroot). * [#2337](https://github.com/ruby-grape/grape/pull/2337): Fix: allow custom validators that do not end with _validator - [@ericproulx](https://github.com/ericproulx). +* [#2346](https://github.com/ruby-grape/grape/pull/2346): Adjust test expectations to conform to rack 3 - [@kbarrette](https://github.com/kbarrette). * Your contribution here. ## 1.7.1 (2023/05/14) diff --git a/grape.gemspec b/grape.gemspec index e79e1c0ed..a5a56bc0a 100644 --- a/grape.gemspec +++ b/grape.gemspec @@ -24,7 +24,7 @@ Gem::Specification.new do |s| 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', '< 3' + s.add_runtime_dependency 'rack', '>= 1.3.0' s.add_runtime_dependency 'rack-accept' s.files = %w[CHANGELOG.md CONTRIBUTING.md README.md grape.png UPGRADING.md LICENSE] diff --git a/spec/grape/endpoint_spec.rb b/spec/grape/endpoint_spec.rb index 5b563434c..6eb37227f 100644 --- a/spec/grape/endpoint_spec.rb +++ b/spec/grape/endpoint_spec.rb @@ -138,10 +138,9 @@ def app it 'includes request headers' do get '/headers' - expect(JSON.parse(last_response.body)).to eq( + expect(JSON.parse(last_response.body)).to include( 'Host' => 'example.org', - 'Cookie' => '', - 'Version' => 'HTTP/1.0' + 'Cookie' => '' ) end @@ -174,7 +173,7 @@ def app get('/get/cookies') - expect(last_response.headers['Set-Cookie'].split("\n").sort).to eql [ + expect(Array(last_response.headers['Set-Cookie']).flat_map { |h| h.split("\n") }.sort).to eql [ 'cookie3=symbol', 'cookie4=secret+code+here', 'my-awesome-cookie1=is+cool', @@ -199,8 +198,9 @@ def app end get('/username', {}, 'HTTP_COOKIE' => 'username=user; sandbox=false') expect(last_response.body).to eq('user_test') - expect(last_response.headers['Set-Cookie']).to match(/username=user_test/) - expect(last_response.headers['Set-Cookie']).to match(/sandbox=true/) + cookies = Array(last_response.headers['Set-Cookie']).flat_map { |h| h.split("\n") } + expect(cookies.first).to match(/username=user_test/) + expect(cookies.second).to match(/sandbox=true/) end it 'deletes cookie' do @@ -214,7 +214,7 @@ def app end get '/test', {}, 'HTTP_COOKIE' => 'delete_this_cookie=1; and_this=2' expect(last_response.body).to eq('3') - cookies = last_response.headers['Set-Cookie'].split("\n").to_h do |set_cookie| + cookies = Array(last_response.headers['Set-Cookie']).flat_map { |h| h.split("\n") }.to_h do |set_cookie| cookie = CookieJar::Cookie.from_set_cookie 'http://localhost/test', set_cookie [cookie.name, cookie] end @@ -238,7 +238,7 @@ def app end get('/test', {}, 'HTTP_COOKIE' => 'delete_this_cookie=1; and_this=2') expect(last_response.body).to eq('3') - cookies = last_response.headers['Set-Cookie'].split("\n").to_h do |set_cookie| + cookies = Array(last_response.headers['Set-Cookie']).flat_map { |h| h.split("\n") }.to_h do |set_cookie| cookie = CookieJar::Cookie.from_set_cookie 'http://localhost/test', set_cookie [cookie.name, cookie] end diff --git a/spec/grape/middleware/formatter_spec.rb b/spec/grape/middleware/formatter_spec.rb index b4ae0ec05..29f49f88b 100644 --- a/spec/grape/middleware/formatter_spec.rb +++ b/spec/grape/middleware/formatter_spec.rb @@ -405,7 +405,7 @@ def to_xml env = { 'PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/json' } status, headers, body = subject.call(env) expect(status).to be == 200 - expect(headers).to be == { 'Content-Type' => 'application/json' } + expect(headers.transform_keys(&:downcase)).to be == { 'content-type' => 'application/json' } expect(read_chunks(body)).to be == ['data'] end end From 34224ac8c1967513a260e4e0f99fddf7d9db4deb Mon Sep 17 00:00:00 2001 From: "Daniel (dB.) Doubrovkine" Date: Wed, 30 Aug 2023 19:25:39 -0400 Subject: [PATCH 166/304] Replaced remaining references to Travis CI. --- CONTRIBUTING.md | 2 +- RELEASING.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4f5e326d5..084f91228 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -145,7 +145,7 @@ git push origin my-feature-branch -f #### Check on Your Pull Request -Go back to your pull request after a few minutes and see whether it passed muster with Travis-CI. Everything should look green, otherwise fix issues and amend your commit as described above. +Go back to your pull request after a few minutes and see whether it passed muster with CI. Everything should look green, otherwise fix issues and amend your commit as described above. #### Be Patient diff --git a/RELEASING.md b/RELEASING.md index 1d40997ce..71240674e 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -12,7 +12,7 @@ bundle install rake ``` -Check that the last build succeeded in [Travis CI](https://travis-ci.org/ruby-grape/grape) for all supported platforms. +Double-check that the [last build succeeded](https://github.com/ruby-grape/grape/actions) for all supported platforms. Those with r/w permissions to the [master Grape repository](https://github.com/ruby-grape/grape) generally have large Grape-based projects. Point one to Grape HEAD and run all your API tests to catch any obvious regressions. From ed8edafe510dafd74c253b9b06f4c436bb7e7f66 Mon Sep 17 00:00:00 2001 From: "Daniel (dB.) Doubrovkine" Date: Wed, 30 Aug 2023 19:28:10 -0400 Subject: [PATCH 167/304] Remove the section that says to increment the version number as part of the release. --- RELEASING.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index 71240674e..d16d6ffa4 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -20,11 +20,6 @@ Those with r/w permissions to the [master Grape repository](https://github.com/r gem grape, github: 'ruby-grape/grape' ``` -Increment the version, modify [lib/grape/version.rb](lib/grape/version.rb). - -* Increment the third number if the release has bug fixes and/or very minor features, only (eg. change `0.5.1` to `0.5.2`). -* Increment the second number if the release contains major features or breaking API changes (eg. change `0.5.1` to `0.6.0`). - Modify the "Stable Release" section in [README.md](README.md). Change the text to reflect that this is going to be the documentation for a stable release. Remove references to the previous release of Grape. Keep the file open, you'll have to undo this change after the release. ``` From ef9164c7cff50851a7192f1fc56f5f76ddaad4b0 Mon Sep 17 00:00:00 2001 From: "Daniel (dB.) Doubrovkine" Date: Wed, 30 Aug 2023 19:28:49 -0400 Subject: [PATCH 168/304] Preparing for release, 1.8.0. --- CHANGELOG.md | 4 +--- README.md | 4 +--- RELEASING.md | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc5bc570e..e573c4210 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -### 1.8.0 (Next) +### 1.8.0 (2023/08/30) #### Features @@ -11,7 +11,6 @@ * [#2341](https://github.com/ruby-grape/grape/pull/2341): Stop yielding skip value - [@ericproulx](https://github.com/ericproulx). * [#2342](https://github.com/ruby-grape/grape/pull/2342): Allow specifying a handler for grape_exceptions - [@mscrivo](https://github.com/mscrivo). * [#2338](https://github.com/ruby-grape/grape/pull/2338): Fix unknown validator when using requires/optional with entity - [@mscrivo](https://github.com/mscrivo). -* Your contribution here. #### Fixes @@ -19,7 +18,6 @@ * [#2328](https://github.com/ruby-grape/grape/pull/2328): Don't cache Class.instance_methods - [@byroot](https://github.com/byroot). * [#2337](https://github.com/ruby-grape/grape/pull/2337): Fix: allow custom validators that do not end with _validator - [@ericproulx](https://github.com/ericproulx). * [#2346](https://github.com/ruby-grape/grape/pull/2346): Adjust test expectations to conform to rack 3 - [@kbarrette](https://github.com/kbarrette). -* Your contribution here. ## 1.7.1 (2023/05/14) diff --git a/README.md b/README.md index 9e7c370fb..f4c437be7 100644 --- a/README.md +++ b/README.md @@ -159,10 +159,8 @@ content negotiation, versioning and much more. ## Stable Release -You're reading the documentation for the next release of Grape, which should be **1.8.0**. +You're reading the documentation for the stable release of Grape, **1.8.0**. Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. -The current stable release is [1.7.1](https://github.com/ruby-grape/grape/blob/v1.7.1/README.md). - ## Project Resources diff --git a/RELEASING.md b/RELEASING.md index d16d6ffa4..9774d9f52 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -39,7 +39,7 @@ Remove the line with "Your contribution here.", since there will be no more cont Commit your changes. ``` -git add README.md CHANGELOG.md lib/grape/version.rb +git add README.md CHANGELOG.md git commit -m "Preparing for release, 0.6.0." git push origin master ``` From d39031567b913834d9e586ac8a2733ecf3166057 Mon Sep 17 00:00:00 2001 From: "Daniel (dB.) Doubrovkine" Date: Wed, 30 Aug 2023 19:32:20 -0400 Subject: [PATCH 169/304] Preparing for next developer iteration, 1.8.1. --- CHANGELOG.md | 10 ++++++++++ README.md | 4 +++- lib/grape/version.rb | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e573c4210..8d550bc29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +### 1.8.1 (Next) + +#### Features + +* Your contribution here. + +#### Fixes + +* Your contribution here. + ### 1.8.0 (2023/08/30) #### Features diff --git a/README.md b/README.md index f4c437be7..b37f12b9f 100644 --- a/README.md +++ b/README.md @@ -159,8 +159,10 @@ content negotiation, versioning and much more. ## Stable Release -You're reading the documentation for the stable release of Grape, **1.8.0**. +You're reading the documentation for the next release of Grape, which should be **1.8.1**. Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. +The current stable release is [1.8.0](https://github.com/ruby-grape/grape/blob/v1.8.0/README.md). + ## Project Resources diff --git a/lib/grape/version.rb b/lib/grape/version.rb index 64b7c4785..d0632f9df 100644 --- a/lib/grape/version.rb +++ b/lib/grape/version.rb @@ -2,5 +2,5 @@ module Grape # The current version of Grape. - VERSION = '1.8.0' + VERSION = '1.8.1' end From 4e2a2ff4774b9f741a01178c0b670a5bda999563 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Thu, 12 Oct 2023 22:35:19 +0200 Subject: [PATCH 170/304] Add Rails 7.1 in test suite + compatibility (#2353) * Add Grape.deprecator since using ActiveSupport::Deprecation is deprecated https://edgeguides.rubyonrails.org/7_1_release_notes.html#active-support-deprecations Add railtie for Rails 7.1 app to include Grape.deprecator * Add railtie_spec.rb Add CHANGELOG.md * Fix rubocop * Update CHANGELOG.md * Added Rails 7.1 section in README.md --- .github/workflows/test.yml | 4 +- CHANGELOG.md | 1 + README.md | 7 ++- gemfiles/rails_7_1.gemfile | 45 +++++++++++++++++++ lib/grape.rb | 9 +++- lib/grape/dsl/desc.rb | 2 +- lib/grape/dsl/inside_route.rb | 6 +-- lib/grape/exceptions/missing_group_type.rb | 2 +- .../exceptions/unsupported_group_type.rb | 2 +- lib/grape/railtie.rb | 9 ++++ lib/grape/router/route.rb | 2 +- lib/grape/validations/validators/base.rb | 2 +- .../validators/values_validator.rb | 4 +- spec/config/spec_test_prof.rb | 9 ++++ spec/grape/dsl/desc_spec.rb | 2 +- spec/grape/dsl/inside_route_spec.rb | 12 ++--- spec/grape/railtie_spec.rb | 20 +++++++++ spec/shared/deprecated_class_examples.rb | 6 +-- spec/spec_helper.rb | 19 +++----- 19 files changed, 126 insertions(+), 37 deletions(-) create mode 100644 gemfiles/rails_7_1.gemfile create mode 100644 lib/grape/railtie.rb create mode 100644 spec/config/spec_test_prof.rb create mode 100644 spec/grape/railtie_spec.rb diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d79b49f95..40bd96b67 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,7 +24,7 @@ jobs: fail-fast: false matrix: ruby: ['2.7', '3.0', '3.1', '3.2'] - gemfile: [rack_2_0, rack_3_0, rails_6_0, rails_6_1, rails_7_0] + gemfile: [rack_2_0, rack_3_0, rails_6_0, rails_6_1, rails_7_0, rails_7_1] include: - ruby: '2.6' gemfile: rails_5_2 @@ -78,4 +78,4 @@ jobs: uses: coverallsapp/github-action@master with: github-token: ${{ secrets.github_token }} - parallel-finished: true \ No newline at end of file + parallel-finished: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d550bc29..f8dc826bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ #### Features +* [#2353](https://github.com/ruby-grape/grape/pull/2353): Added Rails 7.1 support - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/README.md b/README.md index b37f12b9f..b0fe3375f 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ - [Grape for Enterprise](#grape-for-enterprise) - [Installation](#installation) - [Basic Usage](#basic-usage) +- [Rails 7.1](#rails-71) - [Mounting](#mounting) - [All](#all) - [Rack](#rack) @@ -179,7 +180,7 @@ The maintainers of Grape are working with Tidelift to deliver commercial support ## Installation -Ruby 2.4 or newer is required. +Ruby 2.6 or newer is required. Grape is available as a gem, to install it run: @@ -268,6 +269,10 @@ module Twitter end ``` +## Rails 7.1 + +Grape's [deprecator](https://api.rubyonrails.org/v7.1.0/classes/ActiveSupport/Deprecation.html) will be added to your application's deprecators [automatically](lib/grape/railtie.rb) as `:grape`, so that your application's configuration can be applied to it. + ## Mounting ### All diff --git a/gemfiles/rails_7_1.gemfile b/gemfiles/rails_7_1.gemfile new file mode 100644 index 000000000..01fbd7900 --- /dev/null +++ b/gemfiles/rails_7_1.gemfile @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +# This file was generated by Appraisal + +source 'https://rubygems.org' + +gem 'rails', '~> 7.1.0' +gem 'tzinfo-data', require: false + +group :development, :test do + gem 'bundler' + gem 'hashie' + gem 'rake' + gem 'rubocop', '1.50.2', require: false + gem 'rubocop-performance', '1.17.1', require: false + gem 'rubocop-rspec', '2.20.0', 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 'grape-entity', '~> 0.6' + gem 'mime-types' + gem 'rack-jsonp', require: 'rack/jsonp' + gem 'rack-test', '< 2.1' + gem 'rspec', '< 4' + gem 'ruby-grape-danger', '~> 0.2.0', require: false + gem 'simplecov', '~> 0.21.2' + gem 'simplecov-lcov', '~> 0.8.0' + gem 'test-prof', require: false +end + +platforms :jruby do + gem 'racc' +end + +gemspec path: '../' diff --git a/lib/grape.rb b/lib/grape.rb index 39f3905c0..680bea819 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -38,6 +38,10 @@ module Grape include ActiveSupport::Configurable extend ::ActiveSupport::Autoload + def self.deprecator + @deprecator ||= ActiveSupport::Deprecation.new('2.0', 'Grape') + end + eager_autoload do autoload :API autoload :Endpoint @@ -301,5 +305,8 @@ module Types require 'grape/util/lazy_value' require 'grape/util/lazy_block' require 'grape/util/endpoint_configuration' - require 'grape/version' + +# https://api.rubyonrails.org/classes/ActiveSupport/Deprecation.html +# adding Grape.deprecator to Rails App if any +require 'grape/railtie' if defined?(Rails::Railtie) && ActiveSupport.gem_version >= Gem::Version.new('7.1') diff --git a/lib/grape/dsl/desc.rb b/lib/grape/dsl/desc.rb index 64d5eed91..e1611bf7f 100644 --- a/lib/grape/dsl/desc.rb +++ b/lib/grape/dsl/desc.rb @@ -68,7 +68,7 @@ def desc(description, options = {}, &config_block) end config_class.configure(&config_block) - ActiveSupport::Deprecation.warn('Passing a options hash and a block to `desc` is deprecated. Move all hash options to block.') if options.any? + Grape.deprecator.warn('Passing a options hash and a block to `desc` is deprecated. Move all hash options to block.') if options.any? options = config_class.settings else options = options.merge(description: description) diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index 429988872..7023d69dd 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -280,13 +280,13 @@ def return_no_content # Deprecated method to send files to the client. Use `sendfile` or `stream` def file(value = nil) if value.is_a?(String) - ActiveSupport::Deprecation.warn('Use sendfile or stream to send files.') + Grape.deprecator.warn('Use sendfile or stream to send files.') sendfile(value) elsif !value.is_a?(NilClass) - ActiveSupport::Deprecation.warn('Use stream to use a Stream object.') + Grape.deprecator.warn('Use stream to use a Stream object.') stream(value) else - ActiveSupport::Deprecation.warn('Use sendfile or stream to send files.') + Grape.deprecator.warn('Use sendfile or stream to send files.') sendfile end end diff --git a/lib/grape/exceptions/missing_group_type.rb b/lib/grape/exceptions/missing_group_type.rb index 1f104321f..9a7d3180a 100644 --- a/lib/grape/exceptions/missing_group_type.rb +++ b/lib/grape/exceptions/missing_group_type.rb @@ -10,4 +10,4 @@ def initialize end end -Grape::Exceptions::MissingGroupTypeError = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Grape::Exceptions::MissingGroupTypeError', 'Grape::Exceptions::MissingGroupType') +Grape::Exceptions::MissingGroupTypeError = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Grape::Exceptions::MissingGroupTypeError', 'Grape::Exceptions::MissingGroupType', Grape.deprecator) diff --git a/lib/grape/exceptions/unsupported_group_type.rb b/lib/grape/exceptions/unsupported_group_type.rb index da45abafd..4c5e6396a 100644 --- a/lib/grape/exceptions/unsupported_group_type.rb +++ b/lib/grape/exceptions/unsupported_group_type.rb @@ -10,4 +10,4 @@ def initialize end end -Grape::Exceptions::UnsupportedGroupTypeError = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Grape::Exceptions::UnsupportedGroupTypeError', 'Grape::Exceptions::UnsupportedGroupType') +Grape::Exceptions::UnsupportedGroupTypeError = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Grape::Exceptions::UnsupportedGroupTypeError', 'Grape::Exceptions::UnsupportedGroupType', Grape.deprecator) diff --git a/lib/grape/railtie.rb b/lib/grape/railtie.rb new file mode 100644 index 000000000..42eb3442c --- /dev/null +++ b/lib/grape/railtie.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Grape + class Railtie < ::Rails::Railtie + initializer 'grape.deprecator' do |app| + app.deprecators[:grape] = Grape.deprecator + end + end +end diff --git a/lib/grape/router/route.rb b/lib/grape/router/route.rb index 7bbfe6a0c..d5600df1c 100644 --- a/lib/grape/router/route.rb +++ b/lib/grape/router/route.rb @@ -84,7 +84,7 @@ 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 - ActiveSupport::Deprecation.warn("#{path}:#{line}: The route_xxx methods such as route_#{name} have been deprecated, please use #{expected}.") + Grape.deprecator.warn("#{path}:#{line}: The route_xxx methods such as route_#{name} have been deprecated, please use #{expected}.") end end end diff --git a/lib/grape/validations/validators/base.rb b/lib/grape/validations/validators/base.rb index ae0f0f48e..c76eb4b39 100644 --- a/lib/grape/validations/validators/base.rb +++ b/lib/grape/validations/validators/base.rb @@ -85,7 +85,7 @@ def fail_fast? Grape::Validations::Base = Class.new(Grape::Validations::Validators::Base) do def self.inherited(*) - ActiveSupport::Deprecation.warn 'Grape::Validations::Base is deprecated! Use Grape::Validations::Validators::Base instead.' + Grape.deprecator.warn 'Grape::Validations::Base is deprecated! Use Grape::Validations::Validators::Base instead.' super end end diff --git a/lib/grape/validations/validators/values_validator.rb b/lib/grape/validations/validators/values_validator.rb index 2b7aca76e..3be7d609b 100644 --- a/lib/grape/validations/validators/values_validator.rb +++ b/lib/grape/validations/validators/values_validator.rb @@ -10,11 +10,11 @@ def initialize(attrs, options, required, scope, **opts) @values = options[:value] @proc = options[:proc] - ActiveSupport::Deprecation.warn('The values validator except option is deprecated. Use the except validator instead.') if @excepts + Grape.deprecator.warn('The values validator except option is deprecated. Use the except validator instead.') if @excepts raise ArgumentError, 'proc must be a Proc' if @proc && !@proc.is_a?(Proc) - ActiveSupport::Deprecation.warn('The values validator proc option is deprecated. The lambda expression can now be assigned directly to values.') if @proc + Grape.deprecator.warn('The values validator proc option is deprecated. The lambda expression can now be assigned directly to values.') if @proc else @excepts = nil @values = nil diff --git a/spec/config/spec_test_prof.rb b/spec/config/spec_test_prof.rb new file mode 100644 index 000000000..e5259e110 --- /dev/null +++ b/spec/config/spec_test_prof.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'test_prof/recipes/rspec/let_it_be' + +TestProf::BeforeAll.adapter = Class.new do + def begin_transaction; end + + def rollback_transaction; end +end.new diff --git a/spec/grape/dsl/desc_spec.rb b/spec/grape/dsl/desc_spec.rb index c2acde910..fa3ae7f9e 100644 --- a/spec/grape/dsl/desc_spec.rb +++ b/spec/grape/dsl/desc_spec.rb @@ -83,7 +83,7 @@ end it 'can be set with options and a block' do - expect(ActiveSupport::Deprecation).to receive(:warn).with('Passing a options hash and a block to `desc` is deprecated. Move all hash options to block.') + expect(Grape.deprecator).to receive(:warn).with('Passing a options hash and a block to `desc` is deprecated. Move all hash options to block.') desc_text = 'The description' detail_text = 'more details' diff --git a/spec/grape/dsl/inside_route_spec.rb b/spec/grape/dsl/inside_route_spec.rb index e9a529b51..9547af46b 100644 --- a/spec/grape/dsl/inside_route_spec.rb +++ b/spec/grape/dsl/inside_route_spec.rb @@ -208,7 +208,7 @@ def initialize let(:file_path) { '/some/file/path' } it 'emits a warning that this method is deprecated' do - expect(ActiveSupport::Deprecation).to receive(:warn).with(/Use sendfile or stream/) + expect(Grape.deprecator).to receive(:warn).with(/Use sendfile or stream/) subject.file file_path end @@ -224,7 +224,7 @@ def initialize let(:file_object) { double('StreamerObject', each: nil) } it 'emits a warning that this method is deprecated' do - expect(ActiveSupport::Deprecation).to receive(:warn).with(/Use stream to use a Stream object/) + expect(Grape.deprecator).to receive(:warn).with(/Use stream to use a Stream object/) subject.file file_object end @@ -239,7 +239,7 @@ def initialize describe 'get' do it 'emits a warning that this method is deprecated' do - expect(ActiveSupport::Deprecation).to receive(:warn).with(/Use sendfile or stream/) + expect(Grape.deprecator).to receive(:warn).with(/Use sendfile or stream/) subject.file end @@ -269,7 +269,7 @@ def initialize end it 'sends no deprecation warnings' do - expect(ActiveSupport::Deprecation).not_to receive(:warn) + expect(Grape.deprecator).not_to receive(:warn) subject.sendfile file_path end @@ -330,7 +330,7 @@ def initialize end it 'emits no deprecation warnings' do - expect(ActiveSupport::Deprecation).not_to receive(:warn) + expect(Grape.deprecator).not_to receive(:warn) subject.stream file_path end @@ -380,7 +380,7 @@ def initialize end it 'emits no deprecation warnings' do - expect(ActiveSupport::Deprecation).not_to receive(:warn) + expect(Grape.deprecator).not_to receive(:warn) subject.stream stream_object end diff --git a/spec/grape/railtie_spec.rb b/spec/grape/railtie_spec.rb new file mode 100644 index 000000000..4a9555108 --- /dev/null +++ b/spec/grape/railtie_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +if defined?(Rails::Railtie) && ActiveSupport.gem_version >= Gem::Version.new('7.1') + describe Grape::Railtie do + describe '.railtie' do + subject { test_app.deprecators[:grape] } + + let(:test_app) do + Class.new(Rails::Application) do + config.eager_load = false + config.load_defaults 7.1 + end + end + + before { test_app.initialize! } + + it { is_expected.to be(Grape.deprecator) } + end + end +end diff --git a/spec/shared/deprecated_class_examples.rb b/spec/shared/deprecated_class_examples.rb index 93a4f120c..7909798ea 100644 --- a/spec/shared/deprecated_class_examples.rb +++ b/spec/shared/deprecated_class_examples.rb @@ -4,10 +4,10 @@ subject { deprecated_class.new } around do |example| - old_deprec_behavior = ActiveSupport::Deprecation.behavior - ActiveSupport::Deprecation.behavior = :raise + old_deprec_behavior = Grape.deprecator.behavior + Grape.deprecator.behavior = :raise example.run - ActiveSupport::Deprecation.behavior = old_deprec_behavior + Grape.deprecator.behavior = old_deprec_behavior end it 'raises an ActiveSupport::DeprecationException' do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f09fdd356..1a2581ae7 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -4,23 +4,16 @@ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'support')) -require 'grape' -require 'test_prof/recipes/rspec/let_it_be' - -class NullAdapter - def begin_transaction; end - - def rollback_transaction; end -end - -TestProf::BeforeAll.adapter = NullAdapter.new - require 'rubygems' require 'bundler' Bundler.require :default, :test -Dir["#{File.dirname(__FILE__)}/support/*.rb"].sort.each do |file| - require file +require 'grape' + +%w[config support].each do |dir| + Dir["#{File.dirname(__FILE__)}/#{dir}/**/*.rb"].sort.each do |file| + require file + end end # The default value for this setting is true in a standard Rails app, From 51b081cef9d4d91cb75f80dd33222805612c7b95 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sat, 21 Oct 2023 16:20:41 +0200 Subject: [PATCH 171/304] Reduce gem size by removing test_files (spec) (#2360) * Remove test_files Refactor files * Add CHANGELOG --- .rubocop_todo.yml | 8 -------- CHANGELOG.md | 1 + grape.gemspec | 5 +---- 3 files changed, 2 insertions(+), 12 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 51d321919..b2ba49521 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -6,14 +6,6 @@ # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Severity, Include. -# Include: **/*.gemspec -Gemspec/DeprecatedAttributeAssignment: - Exclude: - - 'grape.gemspec' - # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: Severity, Include. diff --git a/CHANGELOG.md b/CHANGELOG.md index f8dc826bc..4e1200ea8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ #### Features * [#2353](https://github.com/ruby-grape/grape/pull/2353): Added Rails 7.1 support - [@ericproulx](https://github.com/ericproulx). +* [#2360](https://github.com/ruby-grape/grape/pull/2360): Reduce gem size by removing specs - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/grape.gemspec b/grape.gemspec index a5a56bc0a..9e53ddea7 100644 --- a/grape.gemspec +++ b/grape.gemspec @@ -27,10 +27,7 @@ Gem::Specification.new do |s| s.add_runtime_dependency 'rack', '>= 1.3.0' s.add_runtime_dependency 'rack-accept' - 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.files = Dir['lib/**/*', 'CHANGELOG.md', 'CONTRIBUTING.md', 'README.md', 'grape.png', 'UPGRADING.md', 'LICENSE', 'grape.gemspec'] s.require_paths = ['lib'] s.required_ruby_version = '>= 2.6.0' end From 2f62365d4c526fe436019478a29d9c0982bc6596 Mon Sep 17 00:00:00 2001 From: Stuart Chinery <163900+schinery@users.noreply.github.com> Date: Tue, 24 Oct 2023 14:22:09 +0100 Subject: [PATCH 172/304] Set response headers based on Rack version (#2355) * Set response headers based on Rack version * Use Rack.release instead of Rack::RELEASE * Bumped to 1.9.0 and updated UPGRADING.md * Added .rack3? method * Removed headers_helper * Updated README and UPGRADING --- .github/workflows/test.yml | 10 ++++ .rubocop_todo.yml | 2 + CHANGELOG.md | 5 +- README.md | 7 +-- UPGRADING.md | 29 +++++++++++ lib/grape.rb | 4 ++ lib/grape/dsl/inside_route.rb | 8 +-- lib/grape/endpoint.rb | 2 +- lib/grape/http/headers.rb | 20 ++++++- lib/grape/request.rb | 10 +++- lib/grape/version.rb | 2 +- spec/grape/api/custom_validations_spec.rb | 12 +++-- spec/grape/api_spec.rb | 52 +++++++++---------- spec/grape/dsl/inside_route_spec.rb | 44 ++++++++-------- spec/grape/endpoint_spec.rb | 8 +-- .../exceptions/invalid_accept_header_spec.rb | 4 +- .../versioner/accept_version_header_spec.rb | 6 +-- .../grape/middleware/versioner/header_spec.rb | 12 ++--- spec/grape/request_spec.rb | 10 +++- spec/integration/rack/v2/headers_spec.rb | 11 ++++ spec/integration/rack/v3/headers_spec.rb | 11 ++++ 21 files changed, 187 insertions(+), 82 deletions(-) create mode 100644 spec/integration/rack/v2/headers_spec.rb create mode 100644 spec/integration/rack/v3/headers_spec.rb diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 40bd96b67..150f58840 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -63,6 +63,16 @@ jobs: if: ${{ matrix.gemfile == 'multi_xml' }} run: bundle exec rspec spec/integration/multi_xml + - name: Run tests (spec/integration/rack/v2) + # rack_2_0.gemfile is equals to Gemfile + if: ${{ matrix.gemfile == 'rack_2_0' }} + run: bundle exec rspec spec/integration/rack/v2 + + - name: Run tests (spec/integration/rack/v3) + # rack_2_0.gemfile is equals to Gemfile + if: ${{ matrix.gemfile == 'rack_3_0' }} + run: bundle exec rspec spec/integration/rack/v3 + - name: Coveralls uses: coverallsapp/github-action@master with: diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index b2ba49521..57e7a83bd 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -272,6 +272,8 @@ RSpec/FilePath: - 'spec/integration/eager_load/eager_load_spec.rb' - 'spec/integration/multi_json/json_spec.rb' - 'spec/integration/multi_xml/xml_spec.rb' + - 'spec/integration/rack/v2/headers_spec.rb' + - 'spec/integration/rack/v3/headers_spec.rb' # Offense count: 12 # Configuration parameters: Max. diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e1200ea8..54906d7d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,12 @@ -### 1.8.1 (Next) +### 1.9.0 (Next) #### Features * [#2353](https://github.com/ruby-grape/grape/pull/2353): Added Rails 7.1 support - [@ericproulx](https://github.com/ericproulx). +* [#2355](https://github.com/ruby-grape/grape/pull/2355): Set response headers based on Rack version - [@schinery](https://github.com/schinery). * [#2360](https://github.com/ruby-grape/grape/pull/2360): Reduce gem size by removing specs - [@ericproulx](https://github.com/ericproulx). * Your contribution here. - + #### Fixes * Your contribution here. diff --git a/README.md b/README.md index b0fe3375f..0e28d24c1 100644 --- a/README.md +++ b/README.md @@ -160,7 +160,7 @@ content negotiation, versioning and much more. ## Stable Release -You're reading the documentation for the next release of Grape, which should be **1.8.1**. +You're reading the documentation for the next release of Grape, which should be **1.9.0**. Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. The current stable release is [1.8.0](https://github.com/ruby-grape/grape/blob/v1.8.0/README.md). @@ -2130,8 +2130,9 @@ 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_'. +- In the `header` helper names will be coerced into a downcased kebab case as `secret-password` if using Rack 3. +- In the `header` helper names will be coerced into a capitalized kebab case as `Secret-PassWord` if using Rack < 3. +- In the `env` collection they appear in all uppercase, in snake case, and prefixed with 'HTTP_' as `HTTP_SECRET_PASSWORD` The header name will have been normalized per HTTP standards defined in [RFC2616 Section 4.2](https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2) regardless of what is being sent by a client. diff --git a/UPGRADING.md b/UPGRADING.md index 620e1d6ec..c2f856c8f 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,6 +1,35 @@ Upgrading Grape =============== +### Upgrading to >= 1.9.0 + +#### Headers + +As per [rack/rack#1592](https://github.com/rack/rack/issues/1592) Rack 3.0 is enforcing the HTTP/2 semantics, and thus treats all headers as lowercase. Starting with Grape 1.9.0, headers will be cased based on what version of Rack you are using. + +Given this request: + +```shell +curl -H "Content-Type: application/json" -H "Secret-Password: foo" ... +``` + +If you are using Rack 3 in your application then the headers will be set to: + +```ruby +{ "content-type" => "application/json", "secret-password" => "foo"} +``` + +This means if you are checking for header values in your application, you would need to change your code to use downcased keys. + +```ruby +get do + # This would use headers['Secret-Password'] in Rack < 3 + error!('Unauthorized', 401) unless headers['secret-password'] == 'swordfish' +end +``` + +See [#2355](https://github.com/ruby-grape/grape/pull/2355) for more information. + ### Upgrading to >= 1.7.0 #### Exceptions renaming diff --git a/lib/grape.rb b/lib/grape.rb index 680bea819..f4ec5701f 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -42,6 +42,10 @@ def self.deprecator @deprecator ||= ActiveSupport::Deprecation.new('2.0', 'Grape') end + def self.rack3? + Gem::Version.new(::Rack.release) >= Gem::Version.new('3') + end + eager_autoload do autoload :API autoload :Endpoint diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index 7023d69dd..e2bc9169a 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -185,7 +185,7 @@ def redirect(url, permanent: false, body: nil, **_options) status 302 body_message ||= "This resource has been moved temporarily to #{url}." end - header 'Location', url + header Grape::Http::Headers::LOCATION, url content_type 'text/plain' body body_message end @@ -328,9 +328,9 @@ def sendfile(value = nil) 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) + header Grape::Http::Headers::CONTENT_LENGTH, nil + header Grape::Http::Headers::TRANSFER_ENCODING, nil + header Grape::Http::Headers::CACHE_CONTROL, 'no-cache' # Skips ETag generation (reading the response up front) if value.is_a?(String) file_body = Grape::ServeStream::FileBody.new(value) @stream = Grape::ServeStream::StreamResponse.new(file_body) diff --git a/lib/grape/endpoint.rb b/lib/grape/endpoint.rb index 8dfe915bc..663a3e8f1 100644 --- a/lib/grape/endpoint.rb +++ b/lib/grape/endpoint.rb @@ -250,7 +250,7 @@ def run 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 + header Grape::Http::Headers::ALLOW, allowed_methods response_object = '' status 204 else diff --git a/lib/grape/http/headers.rb b/lib/grape/http/headers.rb index 564d97ff8..442f6b65c 100644 --- a/lib/grape/http/headers.rb +++ b/lib/grape/http/headers.rb @@ -10,7 +10,24 @@ module Headers PATH_INFO = 'PATH_INFO' REQUEST_METHOD = 'REQUEST_METHOD' QUERY_STRING = 'QUERY_STRING' - CONTENT_TYPE = 'Content-Type' + + if Grape.rack3? + ALLOW = 'allow' + CACHE_CONTROL = 'cache-control' + CONTENT_LENGTH = 'content-length' + CONTENT_TYPE = 'content-type' + LOCATION = 'location' + TRANSFER_ENCODING = 'transfer-encoding' + X_CASCADE = 'x-cascade' + else + ALLOW = 'Allow' + CACHE_CONTROL = 'Cache-Control' + CONTENT_LENGTH = 'Content-Length' + CONTENT_TYPE = 'Content-Type' + LOCATION = 'Location' + TRANSFER_ENCODING = 'Transfer-Encoding' + X_CASCADE = 'X-Cascade' + end GET = 'GET' POST = 'POST' @@ -24,7 +41,6 @@ module Headers 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' diff --git a/lib/grape/request.rb b/lib/grape/request.rb index 10fd28cc6..f522ea923 100644 --- a/lib/grape/request.rb +++ b/lib/grape/request.rb @@ -46,8 +46,14 @@ def build_headers end end - def transform_header(header) - -header[5..].split('_').each(&:capitalize!).join('-') + if Grape.rack3? + def transform_header(header) + -header[5..].tr('_', '-').downcase + end + else + def transform_header(header) + -header[5..].split('_').map(&:capitalize).join('-') + end end end end diff --git a/lib/grape/version.rb b/lib/grape/version.rb index d0632f9df..9b8501a1e 100644 --- a/lib/grape/version.rb +++ b/lib/grape/version.rb @@ -2,5 +2,5 @@ module Grape # The current version of Grape. - VERSION = '1.8.1' + VERSION = '1.9.0' end diff --git a/spec/grape/api/custom_validations_spec.rb b/spec/grape/api/custom_validations_spec.rb index bfb821de8..121e44f91 100644 --- a/spec/grape/api/custom_validations_spec.rb +++ b/spec/grape/api/custom_validations_spec.rb @@ -162,13 +162,19 @@ def validate(request) return unless request.params.key? @attrs.first # check if admin flag is set to true return unless @option + # check if user is admin or not # as an example get a token from request and check if it's admin or not - raise Grape::Exceptions::Validation.new(params: @attrs, message: 'Can not set Admin only field.') unless request.headers['X-Access-Token'] == 'admin' + raise Grape::Exceptions::Validation.new(params: @attrs, message: 'Can not set Admin only field.') unless request.headers[access_header] == 'admin' + end + + def access_header + Grape.rack3? ? 'x-access-token' : 'X-Access-Token' end end end let(:app) { Rack::Builder.new(subject) } + let(:x_access_token_header) { Grape.rack3? ? 'x-access-token' : 'X-Access-Token' } before do described_class.register_validator('admin', admin_validator) @@ -197,14 +203,14 @@ def validate(request) end it 'does not fail when we send admin fields and we are admin' do - header 'X-Access-Token', 'admin' + header x_access_token_header, 'admin' get '/', admin_field: 'tester', non_admin_field: 'toaster', admin_false_field: 'test' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'bacon' end it 'fails when we send admin fields and we are not admin' do - header 'X-Access-Token', 'user' + header x_access_token_header, 'user' get '/', admin_field: 'tester', non_admin_field: 'toaster', admin_false_field: 'test' expect(last_response.status).to eq 400 expect(last_response.body).to include 'Can not set Admin only field.' diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index c02e0b6e8..5e31f51cf 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -689,7 +689,7 @@ class DummyFormatClass 'example' end put '/example' - expect(last_response.headers['Content-Type']).to eql 'text/plain' + expect(last_response.headers[Grape::Http::Headers::CONTENT_TYPE]).to eql 'text/plain' end describe 'adds an OPTIONS route that' do @@ -1196,32 +1196,32 @@ class DummyFormatClass it 'sets content type for txt format' do get '/foo' - expect(last_response.headers['Content-Type']).to eq('text/plain') + expect(last_response.headers[Grape::Http::Headers::CONTENT_TYPE]).to eq('text/plain') end it 'does not set Cache-Control' do get '/foo' - expect(last_response.headers['Cache-Control']).to be_nil + expect(last_response.headers[Grape::Http::Headers::CACHE_CONTROL]).to be_nil end it 'sets content type for xml' do get '/foo.xml' - expect(last_response.headers['Content-Type']).to eq('application/xml') + expect(last_response.headers[Grape::Http::Headers::CONTENT_TYPE]).to eq('application/xml') end it 'sets content type for json' do get '/foo.json' - expect(last_response.headers['Content-Type']).to eq('application/json') + expect(last_response.headers[Grape::Http::Headers::CONTENT_TYPE]).to eq('application/json') end it 'sets content type for serializable hash format' do get '/foo.serializable_hash' - expect(last_response.headers['Content-Type']).to eq('application/json') + expect(last_response.headers[Grape::Http::Headers::CONTENT_TYPE]).to eq('application/json') end it 'sets content type for binary format' do get '/foo.binary' - expect(last_response.headers['Content-Type']).to eq('application/octet-stream') + expect(last_response.headers[Grape::Http::Headers::CONTENT_TYPE]).to eq('application/octet-stream') end it 'returns raw data when content type binary' do @@ -1230,7 +1230,7 @@ class DummyFormatClass subject.format :binary subject.get('/binary_file') { File.binread(image_filename) } get '/binary_file' - expect(last_response.headers['Content-Type']).to eq('application/octet-stream') + expect(last_response.headers[Grape::Http::Headers::CONTENT_TYPE]).to eq('application/octet-stream') expect(last_response.body).to eq(file) end @@ -1242,8 +1242,8 @@ class DummyFormatClass subject.get('/file') { file test_file } get '/file' - expect(last_response.headers['Content-Length']).to eq('25') - expect(last_response.headers['Content-Type']).to eq('text/plain') + expect(last_response.headers[Grape::Http::Headers::CONTENT_LENGTH]).to eq('25') + expect(last_response.headers[Grape::Http::Headers::CONTENT_TYPE]).to eq('text/plain') expect(last_response.body).to eq(file_content) end @@ -1257,10 +1257,10 @@ class DummyFormatClass subject.get('/stream') { stream test_stream } 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 be_nil - expect(last_response.headers['Cache-Control']).to eq('no-cache') - expect(last_response.headers['Transfer-Encoding']).to eq('chunked') + expect(last_response.headers[Grape::Http::Headers::CONTENT_TYPE]).to eq('text/plain') + expect(last_response.headers[Grape::Http::Headers::CONTENT_LENGTH]).to be_nil + expect(last_response.headers[Grape::Http::Headers::CACHE_CONTROL]).to eq('no-cache') + expect(last_response.headers[Grape::Http::Headers::TRANSFER_ENCODING]).to eq('chunked') expect(last_response.body).to eq("c\r\nThis is some\r\nd\r\n file content\r\n0\r\n\r\n") end @@ -1268,7 +1268,7 @@ class DummyFormatClass it 'sets content type for error' do subject.get('/error') { error!('error in plain text', 500) } get '/error' - expect(last_response.headers['Content-Type']).to eql 'text/plain' + expect(last_response.headers[Grape::Http::Headers::CONTENT_TYPE]).to eql 'text/plain' end it 'sets content type for json error' do @@ -1276,7 +1276,7 @@ class DummyFormatClass subject.get('/error') { error!('error in json', 500) } get '/error.json' expect(last_response.status).to be 500 - expect(last_response.headers['Content-Type']).to eql 'application/json' + expect(last_response.headers[Grape::Http::Headers::CONTENT_TYPE]).to eql 'application/json' end it 'sets content type for xml error' do @@ -1284,7 +1284,7 @@ class DummyFormatClass subject.get('/error') { error!('error in xml', 500) } get '/error' expect(last_response.status).to be 500 - expect(last_response.headers['Content-Type']).to eql 'application/xml' + expect(last_response.headers[Grape::Http::Headers::CONTENT_TYPE]).to eql 'application/xml' end it 'includes extension in format' do @@ -1314,12 +1314,12 @@ class DummyFormatClass it 'sets content type' do get '/custom.custom' - expect(last_response.headers['Content-Type']).to eql 'application/custom' + expect(last_response.headers[Grape::Http::Headers::CONTENT_TYPE]).to eql 'application/custom' end it 'sets content type for error' do get '/error.custom' - expect(last_response.headers['Content-Type']).to eql 'application/custom' + expect(last_response.headers[Grape::Http::Headers::CONTENT_TYPE]).to eql 'application/custom' end end @@ -1339,7 +1339,7 @@ class DummyFormatClass image_filename = 'grape.png' post url, file: Rack::Test::UploadedFile.new(image_filename, 'image/png', true) expect(last_response.status).to eq(201) - expect(last_response.headers['Content-Type']).to eq('image/png') + expect(last_response.headers[Grape::Http::Headers::CONTENT_TYPE]).to eq('image/png') expect(last_response.headers['Content-Disposition']).to eq("attachment; filename*=UTF-8''grape.png") File.open(image_filename, 'rb') do |io| expect(last_response.body).to eq io.read @@ -1351,7 +1351,7 @@ class DummyFormatClass filename = __FILE__ post '/attachment.rb', file: Rack::Test::UploadedFile.new(filename, 'application/x-ruby', true) expect(last_response.status).to eq(201) - expect(last_response.headers['Content-Type']).to eq('application/x-ruby') + expect(last_response.headers[Grape::Http::Headers::CONTENT_TYPE]).to eq('application/x-ruby') expect(last_response.headers['Content-Disposition']).to eq("attachment; filename*=UTF-8''api_spec.rb") File.open(filename, 'rb') do |io| expect(last_response.body).to eq io.read @@ -3311,7 +3311,7 @@ def static it 'is able to cascade' do subject.mount lambda { |env| headers = {} - headers['X-Cascade'] == 'pass' if env['PATH_INFO'].exclude?('boo') + headers[Grape::Http::Headers::X_CASCADE] == 'pass' if env['PATH_INFO'].exclude?('boo') [200, headers, ['Farfegnugen']] } => '/' @@ -4081,14 +4081,14 @@ def before subject.version 'v1', using: :path, cascade: true get '/v1/hello' expect(last_response.status).to eq(404) - expect(last_response.headers['X-Cascade']).to eq('pass') + expect(last_response.headers[Grape::Http::Headers::X_CASCADE]).to eq('pass') end it 'does not cascade' do subject.version 'v2', using: :path, cascade: false get '/v2/hello' expect(last_response.status).to eq(404) - expect(last_response.headers.keys).not_to include 'X-Cascade' + expect(last_response.headers.keys).not_to include Grape::Http::Headers::X_CASCADE end end @@ -4097,14 +4097,14 @@ def before subject.cascade true get '/hello' expect(last_response.status).to eq(404) - expect(last_response.headers['X-Cascade']).to eq('pass') + expect(last_response.headers[Grape::Http::Headers::X_CASCADE]).to eq('pass') end it 'does not cascade' do subject.cascade false get '/hello' expect(last_response.status).to eq(404) - expect(last_response.headers.keys).not_to include 'X-Cascade' + expect(last_response.headers.keys).not_to include Grape::Http::Headers::X_CASCADE end end end diff --git a/spec/grape/dsl/inside_route_spec.rb b/spec/grape/dsl/inside_route_spec.rb index 9547af46b..6dff2d15c 100644 --- a/spec/grape/dsl/inside_route_spec.rb +++ b/spec/grape/dsl/inside_route_spec.rb @@ -73,7 +73,7 @@ def initialize end it 'sets location header' do - expect(subject.header['Location']).to eq '/' + expect(subject.header[Grape::Http::Headers::LOCATION]).to eq '/' end end @@ -87,7 +87,7 @@ def initialize end it 'sets location header' do - expect(subject.header['Location']).to eq '/' + expect(subject.header[Grape::Http::Headers::LOCATION]).to eq '/' end end end @@ -263,9 +263,9 @@ def initialize end before do - subject.header 'Cache-Control', 'cache' - subject.header 'Content-Length', 123 - subject.header 'Transfer-Encoding', 'base64' + subject.header Grape::Http::Headers::CACHE_CONTROL, 'cache' + subject.header Grape::Http::Headers::CONTENT_LENGTH, 123 + subject.header Grape::Http::Headers::TRANSFER_ENCODING, 'base64' end it 'sends no deprecation warnings' do @@ -283,19 +283,19 @@ def initialize it 'does not change the Cache-Control header' do subject.sendfile file_path - expect(subject.header['Cache-Control']).to eq 'cache' + expect(subject.header[Grape::Http::Headers::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 + expect(subject.header[Grape::Http::Headers::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' + expect(subject.header[Grape::Http::Headers::TRANSFER_ENCODING]).to eq 'base64' end end @@ -324,9 +324,9 @@ def initialize end before do - subject.header 'Cache-Control', 'cache' - subject.header 'Content-Length', 123 - subject.header 'Transfer-Encoding', 'base64' + subject.header Grape::Http::Headers::CACHE_CONTROL, 'cache' + subject.header Grape::Http::Headers::CONTENT_LENGTH, 123 + subject.header Grape::Http::Headers::TRANSFER_ENCODING, 'base64' end it 'emits no deprecation warnings' do @@ -344,25 +344,25 @@ def initialize it 'sets Cache-Control header to no-cache' do subject.stream file_path - expect(subject.header['Cache-Control']).to eq 'no-cache' + expect(subject.header[Grape::Http::Headers::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' + expect(subject.header[Grape::Http::Headers::CACHE_CONTROL]).to eq 'cache' end it 'sets Content-Length header to nil' do subject.stream file_path - expect(subject.header['Content-Length']).to be_nil + expect(subject.header[Grape::Http::Headers::CONTENT_LENGTH]).to be_nil end it 'sets Transfer-Encoding header to nil' do subject.stream file_path - expect(subject.header['Transfer-Encoding']).to be_nil + expect(subject.header[Grape::Http::Headers::TRANSFER_ENCODING]).to be_nil end end @@ -374,9 +374,9 @@ def initialize end before do - subject.header 'Cache-Control', 'cache' - subject.header 'Content-Length', 123 - subject.header 'Transfer-Encoding', 'base64' + subject.header Grape::Http::Headers::CACHE_CONTROL, 'cache' + subject.header Grape::Http::Headers::CONTENT_LENGTH, 123 + subject.header Grape::Http::Headers::TRANSFER_ENCODING, 'base64' end it 'emits no deprecation warnings' do @@ -394,19 +394,19 @@ def initialize it 'sets Cache-Control header to no-cache' do subject.stream stream_object - expect(subject.header['Cache-Control']).to eq 'no-cache' + expect(subject.header[Grape::Http::Headers::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 be_nil + expect(subject.header[Grape::Http::Headers::CONTENT_LENGTH]).to be_nil end it 'sets Transfer-Encoding header to nil' do subject.stream stream_object - expect(subject.header['Transfer-Encoding']).to be_nil + expect(subject.header[Grape::Http::Headers::TRANSFER_ENCODING]).to be_nil end end @@ -421,7 +421,7 @@ def initialize it 'returns default' do expect(subject.stream).to be_nil - expect(subject.header['Cache-Control']).to be_nil + expect(subject.header[Grape::Http::Headers::CACHE_CONTROL]).to be_nil end end diff --git a/spec/grape/endpoint_spec.rb b/spec/grape/endpoint_spec.rb index 6eb37227f..e81fbc806 100644 --- a/spec/grape/endpoint_spec.rb +++ b/spec/grape/endpoint_spec.rb @@ -146,14 +146,16 @@ def app it 'includes additional request headers' do get '/headers', nil, 'HTTP_X_GRAPE_CLIENT' => '1' - expect(JSON.parse(last_response.body)['X-Grape-Client']).to eq('1') + x_grape_client_header = Grape.rack3? ? 'x-grape-client' : 'X-Grape-Client' + expect(JSON.parse(last_response.body)[x_grape_client_header]).to eq('1') end it 'includes headers passed as symbols' do env = Rack::MockRequest.env_for('/headers') env[:HTTP_SYMBOL_HEADER] = 'Goliath passes symbols' body = read_chunks(subject.call(env)[2]).join - expect(JSON.parse(body)['Symbol-Header']).to eq('Goliath passes symbols') + symbol_header = Grape.rack3? ? 'symbol-header' : 'Symbol-Header' + expect(JSON.parse(body)[symbol_header]).to eq('Goliath passes symbols') end end @@ -497,7 +499,7 @@ def app end it 'responses with given content type in headers' do - expect(last_response.headers['Content-Type']).to eq 'application/json; charset=utf-8' + expect(last_response.headers[Grape::Http::Headers::CONTENT_TYPE]).to eq 'application/json; charset=utf-8' end end diff --git a/spec/grape/exceptions/invalid_accept_header_spec.rb b/spec/grape/exceptions/invalid_accept_header_spec.rb index 5017807cb..ca4aec5ec 100644 --- a/spec/grape/exceptions/invalid_accept_header_spec.rb +++ b/spec/grape/exceptions/invalid_accept_header_spec.rb @@ -19,7 +19,7 @@ shared_examples_for 'a not-cascaded request' do it 'does not include the X-Cascade=pass header' do - expect(last_response.headers['X-Cascade']).to be_nil + expect(last_response.headers[Grape::Http::Headers::X_CASCADE]).to be_nil end it 'does not accept the request' do @@ -29,7 +29,7 @@ shared_examples_for 'a rescued request' do it 'does not include the X-Cascade=pass header' do - expect(last_response.headers['X-Cascade']).to be_nil + expect(last_response.headers[Grape::Http::Headers::X_CASCADE]).to be_nil end it 'does show rescue handler processing' do diff --git a/spec/grape/middleware/versioner/accept_version_header_spec.rb b/spec/grape/middleware/versioner/accept_version_header_spec.rb index a67c26f24..c2a66f215 100644 --- a/spec/grape/middleware/versioner/accept_version_header_spec.rb +++ b/spec/grape/middleware/versioner/accept_version_header_spec.rb @@ -36,7 +36,7 @@ end.to throw_symbol( :error, status: 406, - headers: { 'X-Cascade' => 'pass' }, + headers: { Grape::Http::Headers::X_CASCADE => 'pass' }, message: 'The requested version is not supported.' ) end @@ -65,7 +65,7 @@ end.to throw_symbol( :error, status: 406, - headers: { 'X-Cascade' => 'pass' }, + headers: { Grape::Http::Headers::X_CASCADE => 'pass' }, message: 'Accept-Version header must be set.' ) end @@ -76,7 +76,7 @@ end.to throw_symbol( :error, status: 406, - headers: { 'X-Cascade' => 'pass' }, + headers: { Grape::Http::Headers::X_CASCADE => 'pass' }, message: 'Accept-Version header must be set.' ) end diff --git a/spec/grape/middleware/versioner/header_spec.rb b/spec/grape/middleware/versioner/header_spec.rb index 29fa056ca..ec116a741 100644 --- a/spec/grape/middleware/versioner/header_spec.rb +++ b/spec/grape/middleware/versioner/header_spec.rb @@ -88,7 +88,7 @@ expect { subject.call('HTTP_ACCEPT' => 'application/vnd.othervendor+json').last } .to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) - expect(exception.headers).to eql('X-Cascade' => 'pass') + expect(exception.headers).to eql(Grape::Http::Headers::X_CASCADE => 'pass') expect(exception.status).to be 406 expect(exception.message).to include 'API vendor not found' end @@ -115,7 +115,7 @@ expect { subject.call('HTTP_ACCEPT' => 'application/vnd.othervendor-v1+json').last } .to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) - expect(exception.headers).to eql('X-Cascade' => 'pass') + expect(exception.headers).to eql(Grape::Http::Headers::X_CASCADE => 'pass') expect(exception.status).to be 406 expect(exception.message).to include('API vendor not found') end @@ -143,7 +143,7 @@ it 'fails with 406 Not Acceptable if version is invalid' do expect { subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v2+json').last }.to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidVersionHeader) - expect(exception.headers).to eql('X-Cascade' => 'pass') + expect(exception.headers).to eql(Grape::Http::Headers::X_CASCADE => 'pass') expect(exception.status).to be 406 expect(exception.message).to include('API version not found') end @@ -176,7 +176,7 @@ it 'fails with 406 Not Acceptable if header is not set' do expect { subject.call({}).last }.to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) - expect(exception.headers).to eql('X-Cascade' => 'pass') + expect(exception.headers).to eql(Grape::Http::Headers::X_CASCADE => 'pass') expect(exception.status).to be 406 expect(exception.message).to include('Accept header must be set.') end @@ -185,7 +185,7 @@ it 'fails with 406 Not Acceptable if header is empty' do expect { subject.call('HTTP_ACCEPT' => '').last }.to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) - expect(exception.headers).to eql('X-Cascade' => 'pass') + expect(exception.headers).to eql(Grape::Http::Headers::X_CASCADE => 'pass') expect(exception.status).to be 406 expect(exception.message).to include('Accept header must be set.') end @@ -262,7 +262,7 @@ it 'fails with another version' do expect { subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v3+json') }.to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidVersionHeader) - expect(exception.headers).to eql('X-Cascade' => 'pass') + expect(exception.headers).to eql(Grape::Http::Headers::X_CASCADE => 'pass') expect(exception.status).to be 406 expect(exception.message).to include('API version not found') end diff --git a/spec/grape/request_spec.rb b/spec/grape/request_spec.rb index 6a3f1948f..3c0c9e2de 100644 --- a/spec/grape/request_spec.rb +++ b/spec/grape/request_spec.rb @@ -89,9 +89,12 @@ module Grape 'HTTP_X_GRAPE_IS_COOL' => 'yeah' } end + let(:x_grape_is_cool_header) do + Grape.rack3? ? 'x-grape-is-cool' : 'X-Grape-Is-Cool' + end it 'cuts HTTP_ prefix and capitalizes header name words' do - expect(request.headers).to eq('X-Grape-Is-Cool' => 'yeah') + expect(request.headers).to eq(x_grape_is_cool_header => 'yeah') end end @@ -116,9 +119,12 @@ module Grape let(:env) do default_env.merge(request_headers) end + let(:grape_likes_symbolic_header) do + Grape.rack3? ? 'grape-likes-symbolic' : 'Grape-Likes-Symbolic' + end it 'converts them to string' do - expect(request.headers).to eq('Grape-Likes-Symbolic' => 'it is true') + expect(request.headers).to eq(grape_likes_symbolic_header => 'it is true') end end end diff --git a/spec/integration/rack/v2/headers_spec.rb b/spec/integration/rack/v2/headers_spec.rb new file mode 100644 index 000000000..63c8e4572 --- /dev/null +++ b/spec/integration/rack/v2/headers_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +describe Grape::Http::Headers do + it { expect(described_class::ALLOW).to eq('Allow') } + it { expect(described_class::CACHE_CONTROL).to eq('Cache-Control') } + it { expect(described_class::CONTENT_LENGTH).to eq('Content-Length') } + it { expect(described_class::CONTENT_TYPE).to eq('Content-Type') } + it { expect(described_class::LOCATION).to eq('Location') } + it { expect(described_class::TRANSFER_ENCODING).to eq('Transfer-Encoding') } + it { expect(described_class::X_CASCADE).to eq('X-Cascade') } +end diff --git a/spec/integration/rack/v3/headers_spec.rb b/spec/integration/rack/v3/headers_spec.rb new file mode 100644 index 000000000..3f5375e0c --- /dev/null +++ b/spec/integration/rack/v3/headers_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +describe Grape::Http::Headers do + it { expect(described_class::ALLOW).to eq('allow') } + it { expect(described_class::CACHE_CONTROL).to eq('cache-control') } + it { expect(described_class::CONTENT_LENGTH).to eq('content-length') } + it { expect(described_class::CONTENT_TYPE).to eq('content-type') } + it { expect(described_class::LOCATION).to eq('location') } + it { expect(described_class::TRANSFER_ENCODING).to eq('transfer-encoding') } + it { expect(described_class::X_CASCADE).to eq('x-cascade') } +end From de76b5cbaf19723c93fa007368f65ca773d14779 Mon Sep 17 00:00:00 2001 From: Stuart Chinery <163900+schinery@users.noreply.github.com> Date: Wed, 25 Oct 2023 13:38:46 +0100 Subject: [PATCH 173/304] Additional changes for setting Rack versioned headers (#2362) * Updated UPGRADING notes about Rack 3 * Changed .rack3? to .lowercase_headers? * Use Rack constants where available * Fixed integration specs * Re-added TRANSFER_ENCODING as not in Rack 1 --- UPGRADING.md | 2 +- lib/grape.rb | 4 +-- lib/grape/dsl/inside_route.rb | 8 ++--- lib/grape/http/headers.rb | 8 +---- lib/grape/middleware/error.rb | 8 ++--- lib/grape/middleware/formatter.rb | 6 ++-- lib/grape/request.rb | 2 +- spec/grape/api/custom_validations_spec.rb | 4 +-- spec/grape/api_spec.rb | 40 +++++++++++------------ spec/grape/dsl/inside_route_spec.rb | 28 ++++++++-------- spec/grape/endpoint_spec.rb | 6 ++-- spec/grape/request_spec.rb | 4 +-- spec/integration/rack/v2/headers_spec.rb | 3 -- spec/integration/rack/v3/headers_spec.rb | 3 -- 14 files changed, 57 insertions(+), 69 deletions(-) diff --git a/UPGRADING.md b/UPGRADING.md index c2f856c8f..b799dc4c0 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -5,7 +5,7 @@ Upgrading Grape #### Headers -As per [rack/rack#1592](https://github.com/rack/rack/issues/1592) Rack 3.0 is enforcing the HTTP/2 semantics, and thus treats all headers as lowercase. Starting with Grape 1.9.0, headers will be cased based on what version of Rack you are using. +As per [rack/rack#1592](https://github.com/rack/rack/issues/1592) Rack 3 is following the HTTP/2+ semantics which require header names to be lower case. To avoid compatibility issues, starting with Grape 1.9.0, headers will be cased based on what version of Rack you are using. Given this request: diff --git a/lib/grape.rb b/lib/grape.rb index f4ec5701f..cb36ebd1d 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -42,8 +42,8 @@ def self.deprecator @deprecator ||= ActiveSupport::Deprecation.new('2.0', 'Grape') end - def self.rack3? - Gem::Version.new(::Rack.release) >= Gem::Version.new('3') + def self.lowercase_headers? + Rack::CONTENT_TYPE == 'content-type' end eager_autoload do diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index e2bc9169a..bcfd3e84c 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -224,9 +224,9 @@ def status(status = nil) # Set response content-type def content_type(val = nil) if val - header(Grape::Http::Headers::CONTENT_TYPE, val) + header(Rack::CONTENT_TYPE, val) else - header[Grape::Http::Headers::CONTENT_TYPE] + header[Rack::CONTENT_TYPE] end end @@ -328,9 +328,9 @@ def sendfile(value = nil) def stream(value = nil) return if value.nil? && @stream.nil? - header Grape::Http::Headers::CONTENT_LENGTH, nil + header Rack::CONTENT_LENGTH, nil header Grape::Http::Headers::TRANSFER_ENCODING, nil - header Grape::Http::Headers::CACHE_CONTROL, 'no-cache' # Skips ETag generation (reading the response up front) + header Rack::CACHE_CONTROL, 'no-cache' # Skips ETag generation (reading the response up front) if value.is_a?(String) file_body = Grape::ServeStream::FileBody.new(value) @stream = Grape::ServeStream::StreamResponse.new(file_body) diff --git a/lib/grape/http/headers.rb b/lib/grape/http/headers.rb index 442f6b65c..a7e5984f7 100644 --- a/lib/grape/http/headers.rb +++ b/lib/grape/http/headers.rb @@ -11,19 +11,13 @@ module Headers REQUEST_METHOD = 'REQUEST_METHOD' QUERY_STRING = 'QUERY_STRING' - if Grape.rack3? + if Grape.lowercase_headers? ALLOW = 'allow' - CACHE_CONTROL = 'cache-control' - CONTENT_LENGTH = 'content-length' - CONTENT_TYPE = 'content-type' LOCATION = 'location' TRANSFER_ENCODING = 'transfer-encoding' X_CASCADE = 'x-cascade' else ALLOW = 'Allow' - CACHE_CONTROL = 'Cache-Control' - CONTENT_LENGTH = 'Content-Length' - CONTENT_TYPE = 'Content-Type' LOCATION = 'Location' TRANSFER_ENCODING = 'Transfer-Encoding' X_CASCADE = 'X-Cascade' diff --git a/lib/grape/middleware/error.rb b/lib/grape/middleware/error.rb index 259d610eb..05fc0312f 100644 --- a/lib/grape/middleware/error.rb +++ b/lib/grape/middleware/error.rb @@ -51,7 +51,7 @@ def call!(env) end def error!(message, status = options[:default_status], headers = {}, backtrace = [], original_exception = nil) - headers = headers.reverse_merge(Grape::Http::Headers::CONTENT_TYPE => content_type) + headers = headers.reverse_merge(Rack::CONTENT_TYPE => content_type) rack_response(format_message(message, backtrace, original_exception), status, headers) end @@ -63,15 +63,15 @@ def default_rescue_handler(e) def error_response(error = {}) status = error[:status] || options[:default_status] message = error[:message] || options[:default_message] - headers = { Grape::Http::Headers::CONTENT_TYPE => content_type } + headers = { Rack::CONTENT_TYPE => content_type } headers.merge!(error[:headers]) if error[:headers].is_a?(Hash) 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 }) - message = ERB::Util.html_escape(message) if headers[Grape::Http::Headers::CONTENT_TYPE] == TEXT_HTML + def rack_response(message, status = options[:default_status], headers = { Rack::CONTENT_TYPE => content_type }) + message = ERB::Util.html_escape(message) if headers[Rack::CONTENT_TYPE] == TEXT_HTML Rack::Response.new([message], Rack::Utils.status_code(status), headers) end diff --git a/lib/grape/middleware/formatter.rb b/lib/grape/middleware/formatter.rb index 06d9a4931..0f8e7cdb3 100644 --- a/lib/grape/middleware/formatter.rb +++ b/lib/grape/middleware/formatter.rb @@ -54,7 +54,7 @@ def build_formatted_response(status, headers, bodies) end def fetch_formatter(headers, options) - api_format = mime_types[headers[Grape::Http::Headers::CONTENT_TYPE]] || env[Grape::Env::API_FORMAT] + api_format = mime_types[headers[Rack::CONTENT_TYPE]] || env[Grape::Env::API_FORMAT] Grape::Formatter.formatter_for(api_format, **options) end @@ -63,10 +63,10 @@ def fetch_formatter(headers, options) # @param headers [Hash] # @return [Hash] def ensure_content_type(headers) - if headers[Grape::Http::Headers::CONTENT_TYPE] + if headers[Rack::CONTENT_TYPE] headers else - headers.merge(Grape::Http::Headers::CONTENT_TYPE => content_type_for(env[Grape::Env::API_FORMAT])) + headers.merge(Rack::CONTENT_TYPE => content_type_for(env[Grape::Env::API_FORMAT])) end end diff --git a/lib/grape/request.rb b/lib/grape/request.rb index f522ea923..c907f0b4a 100644 --- a/lib/grape/request.rb +++ b/lib/grape/request.rb @@ -46,7 +46,7 @@ def build_headers end end - if Grape.rack3? + if Grape.lowercase_headers? def transform_header(header) -header[5..].tr('_', '-').downcase end diff --git a/spec/grape/api/custom_validations_spec.rb b/spec/grape/api/custom_validations_spec.rb index 121e44f91..a6b7a629c 100644 --- a/spec/grape/api/custom_validations_spec.rb +++ b/spec/grape/api/custom_validations_spec.rb @@ -169,12 +169,12 @@ def validate(request) end def access_header - Grape.rack3? ? 'x-access-token' : 'X-Access-Token' + Grape.lowercase_headers? ? 'x-access-token' : 'X-Access-Token' end end end let(:app) { Rack::Builder.new(subject) } - let(:x_access_token_header) { Grape.rack3? ? 'x-access-token' : 'X-Access-Token' } + let(:x_access_token_header) { Grape.lowercase_headers? ? 'x-access-token' : 'X-Access-Token' } before do described_class.register_validator('admin', admin_validator) diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index 5e31f51cf..161b561cb 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -689,7 +689,7 @@ class DummyFormatClass 'example' end put '/example' - expect(last_response.headers[Grape::Http::Headers::CONTENT_TYPE]).to eql 'text/plain' + expect(last_response.headers[Rack::CONTENT_TYPE]).to eql 'text/plain' end describe 'adds an OPTIONS route that' do @@ -1196,32 +1196,32 @@ class DummyFormatClass it 'sets content type for txt format' do get '/foo' - expect(last_response.headers[Grape::Http::Headers::CONTENT_TYPE]).to eq('text/plain') + expect(last_response.headers[Rack::CONTENT_TYPE]).to eq('text/plain') end it 'does not set Cache-Control' do get '/foo' - expect(last_response.headers[Grape::Http::Headers::CACHE_CONTROL]).to be_nil + expect(last_response.headers[Rack::CACHE_CONTROL]).to be_nil end it 'sets content type for xml' do get '/foo.xml' - expect(last_response.headers[Grape::Http::Headers::CONTENT_TYPE]).to eq('application/xml') + expect(last_response.headers[Rack::CONTENT_TYPE]).to eq('application/xml') end it 'sets content type for json' do get '/foo.json' - expect(last_response.headers[Grape::Http::Headers::CONTENT_TYPE]).to eq('application/json') + expect(last_response.headers[Rack::CONTENT_TYPE]).to eq('application/json') end it 'sets content type for serializable hash format' do get '/foo.serializable_hash' - expect(last_response.headers[Grape::Http::Headers::CONTENT_TYPE]).to eq('application/json') + expect(last_response.headers[Rack::CONTENT_TYPE]).to eq('application/json') end it 'sets content type for binary format' do get '/foo.binary' - expect(last_response.headers[Grape::Http::Headers::CONTENT_TYPE]).to eq('application/octet-stream') + expect(last_response.headers[Rack::CONTENT_TYPE]).to eq('application/octet-stream') end it 'returns raw data when content type binary' do @@ -1230,7 +1230,7 @@ class DummyFormatClass subject.format :binary subject.get('/binary_file') { File.binread(image_filename) } get '/binary_file' - expect(last_response.headers[Grape::Http::Headers::CONTENT_TYPE]).to eq('application/octet-stream') + expect(last_response.headers[Rack::CONTENT_TYPE]).to eq('application/octet-stream') expect(last_response.body).to eq(file) end @@ -1242,8 +1242,8 @@ class DummyFormatClass subject.get('/file') { file test_file } get '/file' - expect(last_response.headers[Grape::Http::Headers::CONTENT_LENGTH]).to eq('25') - expect(last_response.headers[Grape::Http::Headers::CONTENT_TYPE]).to eq('text/plain') + expect(last_response.headers[Rack::CONTENT_LENGTH]).to eq('25') + expect(last_response.headers[Rack::CONTENT_TYPE]).to eq('text/plain') expect(last_response.body).to eq(file_content) end @@ -1257,9 +1257,9 @@ class DummyFormatClass subject.get('/stream') { stream test_stream } get '/stream', {}, 'HTTP_VERSION' => 'HTTP/1.1', 'SERVER_PROTOCOL' => 'HTTP/1.1' - expect(last_response.headers[Grape::Http::Headers::CONTENT_TYPE]).to eq('text/plain') - expect(last_response.headers[Grape::Http::Headers::CONTENT_LENGTH]).to be_nil - expect(last_response.headers[Grape::Http::Headers::CACHE_CONTROL]).to eq('no-cache') + expect(last_response.headers[Rack::CONTENT_TYPE]).to eq('text/plain') + expect(last_response.headers[Rack::CONTENT_LENGTH]).to be_nil + expect(last_response.headers[Rack::CACHE_CONTROL]).to eq('no-cache') expect(last_response.headers[Grape::Http::Headers::TRANSFER_ENCODING]).to eq('chunked') expect(last_response.body).to eq("c\r\nThis is some\r\nd\r\n file content\r\n0\r\n\r\n") @@ -1268,7 +1268,7 @@ class DummyFormatClass it 'sets content type for error' do subject.get('/error') { error!('error in plain text', 500) } get '/error' - expect(last_response.headers[Grape::Http::Headers::CONTENT_TYPE]).to eql 'text/plain' + expect(last_response.headers[Rack::CONTENT_TYPE]).to eql 'text/plain' end it 'sets content type for json error' do @@ -1276,7 +1276,7 @@ class DummyFormatClass subject.get('/error') { error!('error in json', 500) } get '/error.json' expect(last_response.status).to be 500 - expect(last_response.headers[Grape::Http::Headers::CONTENT_TYPE]).to eql 'application/json' + expect(last_response.headers[Rack::CONTENT_TYPE]).to eql 'application/json' end it 'sets content type for xml error' do @@ -1284,7 +1284,7 @@ class DummyFormatClass subject.get('/error') { error!('error in xml', 500) } get '/error' expect(last_response.status).to be 500 - expect(last_response.headers[Grape::Http::Headers::CONTENT_TYPE]).to eql 'application/xml' + expect(last_response.headers[Rack::CONTENT_TYPE]).to eql 'application/xml' end it 'includes extension in format' do @@ -1314,12 +1314,12 @@ class DummyFormatClass it 'sets content type' do get '/custom.custom' - expect(last_response.headers[Grape::Http::Headers::CONTENT_TYPE]).to eql 'application/custom' + expect(last_response.headers[Rack::CONTENT_TYPE]).to eql 'application/custom' end it 'sets content type for error' do get '/error.custom' - expect(last_response.headers[Grape::Http::Headers::CONTENT_TYPE]).to eql 'application/custom' + expect(last_response.headers[Rack::CONTENT_TYPE]).to eql 'application/custom' end end @@ -1339,7 +1339,7 @@ class DummyFormatClass image_filename = 'grape.png' post url, file: Rack::Test::UploadedFile.new(image_filename, 'image/png', true) expect(last_response.status).to eq(201) - expect(last_response.headers[Grape::Http::Headers::CONTENT_TYPE]).to eq('image/png') + expect(last_response.headers[Rack::CONTENT_TYPE]).to eq('image/png') expect(last_response.headers['Content-Disposition']).to eq("attachment; filename*=UTF-8''grape.png") File.open(image_filename, 'rb') do |io| expect(last_response.body).to eq io.read @@ -1351,7 +1351,7 @@ class DummyFormatClass filename = __FILE__ post '/attachment.rb', file: Rack::Test::UploadedFile.new(filename, 'application/x-ruby', true) expect(last_response.status).to eq(201) - expect(last_response.headers[Grape::Http::Headers::CONTENT_TYPE]).to eq('application/x-ruby') + expect(last_response.headers[Rack::CONTENT_TYPE]).to eq('application/x-ruby') expect(last_response.headers['Content-Disposition']).to eq("attachment; filename*=UTF-8''api_spec.rb") File.open(filename, 'rb') do |io| expect(last_response.body).to eq io.read diff --git a/spec/grape/dsl/inside_route_spec.rb b/spec/grape/dsl/inside_route_spec.rb index 6dff2d15c..931382e48 100644 --- a/spec/grape/dsl/inside_route_spec.rb +++ b/spec/grape/dsl/inside_route_spec.rb @@ -263,8 +263,8 @@ def initialize end before do - subject.header Grape::Http::Headers::CACHE_CONTROL, 'cache' - subject.header Grape::Http::Headers::CONTENT_LENGTH, 123 + subject.header Rack::CACHE_CONTROL, 'cache' + subject.header Rack::CONTENT_LENGTH, 123 subject.header Grape::Http::Headers::TRANSFER_ENCODING, 'base64' end @@ -283,13 +283,13 @@ def initialize it 'does not change the Cache-Control header' do subject.sendfile file_path - expect(subject.header[Grape::Http::Headers::CACHE_CONTROL]).to eq 'cache' + expect(subject.header[Rack::CACHE_CONTROL]).to eq 'cache' end it 'does not change the Content-Length header' do subject.sendfile file_path - expect(subject.header[Grape::Http::Headers::CONTENT_LENGTH]).to eq 123 + expect(subject.header[Rack::CONTENT_LENGTH]).to eq 123 end it 'does not change the Transfer-Encoding header' do @@ -324,8 +324,8 @@ def initialize end before do - subject.header Grape::Http::Headers::CACHE_CONTROL, 'cache' - subject.header Grape::Http::Headers::CONTENT_LENGTH, 123 + subject.header Rack::CACHE_CONTROL, 'cache' + subject.header Rack::CONTENT_LENGTH, 123 subject.header Grape::Http::Headers::TRANSFER_ENCODING, 'base64' end @@ -344,19 +344,19 @@ def initialize it 'sets Cache-Control header to no-cache' do subject.stream file_path - expect(subject.header[Grape::Http::Headers::CACHE_CONTROL]).to eq 'no-cache' + expect(subject.header[Rack::CACHE_CONTROL]).to eq 'no-cache' end it 'does not change Cache-Control header' do subject.stream - expect(subject.header[Grape::Http::Headers::CACHE_CONTROL]).to eq 'cache' + expect(subject.header[Rack::CACHE_CONTROL]).to eq 'cache' end it 'sets Content-Length header to nil' do subject.stream file_path - expect(subject.header[Grape::Http::Headers::CONTENT_LENGTH]).to be_nil + expect(subject.header[Rack::CONTENT_LENGTH]).to be_nil end it 'sets Transfer-Encoding header to nil' do @@ -374,8 +374,8 @@ def initialize end before do - subject.header Grape::Http::Headers::CACHE_CONTROL, 'cache' - subject.header Grape::Http::Headers::CONTENT_LENGTH, 123 + subject.header Rack::CACHE_CONTROL, 'cache' + subject.header Rack::CONTENT_LENGTH, 123 subject.header Grape::Http::Headers::TRANSFER_ENCODING, 'base64' end @@ -394,13 +394,13 @@ def initialize it 'sets Cache-Control header to no-cache' do subject.stream stream_object - expect(subject.header[Grape::Http::Headers::CACHE_CONTROL]).to eq 'no-cache' + expect(subject.header[Rack::CACHE_CONTROL]).to eq 'no-cache' end it 'sets Content-Length header to nil' do subject.stream stream_object - expect(subject.header[Grape::Http::Headers::CONTENT_LENGTH]).to be_nil + expect(subject.header[Rack::CONTENT_LENGTH]).to be_nil end it 'sets Transfer-Encoding header to nil' do @@ -421,7 +421,7 @@ def initialize it 'returns default' do expect(subject.stream).to be_nil - expect(subject.header[Grape::Http::Headers::CACHE_CONTROL]).to be_nil + expect(subject.header[Rack::CACHE_CONTROL]).to be_nil end end diff --git a/spec/grape/endpoint_spec.rb b/spec/grape/endpoint_spec.rb index e81fbc806..400b45c40 100644 --- a/spec/grape/endpoint_spec.rb +++ b/spec/grape/endpoint_spec.rb @@ -146,7 +146,7 @@ def app it 'includes additional request headers' do get '/headers', nil, 'HTTP_X_GRAPE_CLIENT' => '1' - x_grape_client_header = Grape.rack3? ? 'x-grape-client' : 'X-Grape-Client' + x_grape_client_header = Grape.lowercase_headers? ? 'x-grape-client' : 'X-Grape-Client' expect(JSON.parse(last_response.body)[x_grape_client_header]).to eq('1') end @@ -154,7 +154,7 @@ def app env = Rack::MockRequest.env_for('/headers') env[:HTTP_SYMBOL_HEADER] = 'Goliath passes symbols' body = read_chunks(subject.call(env)[2]).join - symbol_header = Grape.rack3? ? 'symbol-header' : 'Symbol-Header' + symbol_header = Grape.lowercase_headers? ? 'symbol-header' : 'Symbol-Header' expect(JSON.parse(body)[symbol_header]).to eq('Goliath passes symbols') end end @@ -499,7 +499,7 @@ def app end it 'responses with given content type in headers' do - expect(last_response.headers[Grape::Http::Headers::CONTENT_TYPE]).to eq 'application/json; charset=utf-8' + expect(last_response.headers[Rack::CONTENT_TYPE]).to eq 'application/json; charset=utf-8' end end diff --git a/spec/grape/request_spec.rb b/spec/grape/request_spec.rb index 3c0c9e2de..b84b6dffd 100644 --- a/spec/grape/request_spec.rb +++ b/spec/grape/request_spec.rb @@ -90,7 +90,7 @@ module Grape } end let(:x_grape_is_cool_header) do - Grape.rack3? ? 'x-grape-is-cool' : 'X-Grape-Is-Cool' + Grape.lowercase_headers? ? 'x-grape-is-cool' : 'X-Grape-Is-Cool' end it 'cuts HTTP_ prefix and capitalizes header name words' do @@ -120,7 +120,7 @@ module Grape default_env.merge(request_headers) end let(:grape_likes_symbolic_header) do - Grape.rack3? ? 'grape-likes-symbolic' : 'Grape-Likes-Symbolic' + Grape.lowercase_headers? ? 'grape-likes-symbolic' : 'Grape-Likes-Symbolic' end it 'converts them to string' do diff --git a/spec/integration/rack/v2/headers_spec.rb b/spec/integration/rack/v2/headers_spec.rb index 63c8e4572..4819f21dc 100644 --- a/spec/integration/rack/v2/headers_spec.rb +++ b/spec/integration/rack/v2/headers_spec.rb @@ -2,9 +2,6 @@ describe Grape::Http::Headers do it { expect(described_class::ALLOW).to eq('Allow') } - it { expect(described_class::CACHE_CONTROL).to eq('Cache-Control') } - it { expect(described_class::CONTENT_LENGTH).to eq('Content-Length') } - it { expect(described_class::CONTENT_TYPE).to eq('Content-Type') } it { expect(described_class::LOCATION).to eq('Location') } it { expect(described_class::TRANSFER_ENCODING).to eq('Transfer-Encoding') } it { expect(described_class::X_CASCADE).to eq('X-Cascade') } diff --git a/spec/integration/rack/v3/headers_spec.rb b/spec/integration/rack/v3/headers_spec.rb index 3f5375e0c..3be2c1e28 100644 --- a/spec/integration/rack/v3/headers_spec.rb +++ b/spec/integration/rack/v3/headers_spec.rb @@ -2,9 +2,6 @@ describe Grape::Http::Headers do it { expect(described_class::ALLOW).to eq('allow') } - it { expect(described_class::CACHE_CONTROL).to eq('cache-control') } - it { expect(described_class::CONTENT_LENGTH).to eq('content-length') } - it { expect(described_class::CONTENT_TYPE).to eq('content-type') } it { expect(described_class::LOCATION).to eq('location') } it { expect(described_class::TRANSFER_ENCODING).to eq('transfer-encoding') } it { expect(described_class::X_CASCADE).to eq('x-cascade') } From 4753f67bd3425bafd54c5790a99d35c810af2c00 Mon Sep 17 00:00:00 2001 From: Manabu Niseki Date: Thu, 26 Oct 2023 03:29:42 +0900 Subject: [PATCH 174/304] Remove Rack::Auth::Digest (#2361) * Remove Rack::Auth::Digest * Update README.md to remove digest auth * Update UPGRADING and CHANGELOG * Fix typo * Bump the version up to 2.0.0 * Quote the class name * Update Stable Release version --- CHANGELOG.md | 3 +- README.md | 22 +---- UPGRADING.md | 8 +- lib/grape.rb | 1 - lib/grape/middleware/auth/strategies.rb | 3 +- lib/grape/version.rb | 2 +- spec/grape/middleware/auth/strategies_spec.rb | 88 ------------------- 7 files changed, 15 insertions(+), 112 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54906d7d1..aabf664f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,11 @@ -### 1.9.0 (Next) +### 2.0.0 (Next) #### Features * [#2353](https://github.com/ruby-grape/grape/pull/2353): Added Rails 7.1 support - [@ericproulx](https://github.com/ericproulx). * [#2355](https://github.com/ruby-grape/grape/pull/2355): Set response headers based on Rack version - [@schinery](https://github.com/schinery). * [#2360](https://github.com/ruby-grape/grape/pull/2360): Reduce gem size by removing specs - [@ericproulx](https://github.com/ericproulx). +* [#2361](https://github.com/ruby-grape/grape/pull/2361): Remove `Rack::Auth::Digest` - [@ninoseki](https://github.com/ninoseki). * Your contribution here. #### Fixes diff --git a/README.md b/README.md index 0e28d24c1..f0d66f809 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ - [Active Model Serializers](#active-model-serializers) - [Sending Raw or No Data](#sending-raw-or-no-data) - [Authentication](#authentication) - - [Basic and Digest Auth](#basic-and-digest-auth) + - [Basic Auth](#basic-auth) - [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) @@ -160,7 +160,7 @@ content negotiation, versioning and much more. ## Stable Release -You're reading the documentation for the next release of Grape, which should be **1.9.0**. +You're reading the documentation for the next release of Grape, which should be **2.0.0**. Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. The current stable release is [1.8.0](https://github.com/ruby-grape/grape/blob/v1.8.0/README.md). @@ -3422,9 +3422,9 @@ end ## Authentication -### Basic and Digest Auth +### Basic Auth -Grape has built-in Basic and Digest authentication (the given `block` +Grape has built-in Basic authentication (the given `block` is executed in the context of the current `Endpoint`). Authentication applies to the current namespace and any children, but not parents. @@ -3435,20 +3435,6 @@ http_basic do |username, password| end ``` -Digest auth supports clear-text passwords and password hashes. - -```ruby -http_digest({ realm: 'Test Api', opaque: 'app secret' }) do |username| - # lookup the user's password here -end -``` - -```ruby -http_digest(realm: { realm: 'Test Api', opaque: 'app secret', passwords_hashed: true }) do |username| - # lookup the user's password hash here -end -``` - ### Register custom middleware for authentication Grape can use custom Middleware for authentication. How to implement these diff --git a/UPGRADING.md b/UPGRADING.md index b799dc4c0..b9877889e 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,7 +1,7 @@ Upgrading Grape =============== -### Upgrading to >= 1.9.0 +### Upgrading to >= 2.0.0 #### Headers @@ -30,6 +30,12 @@ end See [#2355](https://github.com/ruby-grape/grape/pull/2355) for more information. +#### Digest auth deprecation + +Digest auth has been removed along with the deprecation of `Rack::Auth::Digest` in Rack 3. + +See [#2294](https://github.com/ruby-grape/grape/issues/2294) for more information. + ### Upgrading to >= 1.7.0 #### Exceptions renaming diff --git a/lib/grape.rb b/lib/grape.rb index cb36ebd1d..9eeecbde5 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -5,7 +5,6 @@ require 'rack/builder' require 'rack/accept' require 'rack/auth/basic' -require 'rack/auth/digest/md5' require 'set' require 'bigdecimal' require 'date' diff --git a/lib/grape/middleware/auth/strategies.rb b/lib/grape/middleware/auth/strategies.rb index dc36eea48..56855263e 100644 --- a/lib/grape/middleware/auth/strategies.rb +++ b/lib/grape/middleware/auth/strategies.rb @@ -12,8 +12,7 @@ def add(label, strategy, option_fetcher = ->(_) { [] }) def auth_strategies @auth_strategies ||= { - http_basic: StrategyInfo.new(Rack::Auth::Basic, ->(settings) { [settings[:realm]] }), - http_digest: StrategyInfo.new(Rack::Auth::Digest::MD5, ->(settings) { [settings[:realm], settings[:opaque]] }) + http_basic: StrategyInfo.new(Rack::Auth::Basic, ->(settings) { [settings[:realm]] }) } end diff --git a/lib/grape/version.rb b/lib/grape/version.rb index 9b8501a1e..c6660d510 100644 --- a/lib/grape/version.rb +++ b/lib/grape/version.rb @@ -2,5 +2,5 @@ module Grape # The current version of Grape. - VERSION = '1.9.0' + VERSION = '2.0.0' end diff --git a/spec/grape/middleware/auth/strategies_spec.rb b/spec/grape/middleware/auth/strategies_spec.rb index 29749c551..f6996695b 100644 --- a/spec/grape/middleware/auth/strategies_spec.rb +++ b/spec/grape/middleware/auth/strategies_spec.rb @@ -29,92 +29,4 @@ def app expect(last_response.status).to eq(401) end end - - context 'Digest MD5 Auth' do - RSpec::Matchers.define :be_challenge do - match do |actual_response| - actual_response.status == 401 && - actual_response['WWW-Authenticate'].start_with?('Digest ') && - actual_response.body.empty? - end - end - - module StrategiesSpec - class PasswordHashed < Grape::API - http_digest(realm: { realm: 'Test Api', opaque: 'secret', passwords_hashed: true }) do |username| - { 'foo' => Digest::MD5.hexdigest(['foo', 'Test Api', 'bar'].join(':')) }[username] - end - - get '/test' do - [{ hey: 'you' }, { there: 'bar' }, { foo: 'baz' }] - end - end - - class PasswordIsNotHashed < Grape::API - http_digest(realm: 'Test Api', opaque: 'secret') do |username| - { 'foo' => 'bar' }[username] - end - - get '/test' do - [{ hey: 'you' }, { there: 'bar' }, { foo: 'baz' }] - end - end - end - - context 'when password is hashed' do - def app - StrategiesSpec::PasswordHashed - end - - it 'is a digest authentication challenge' do - get '/test' - expect(last_response).to be_challenge - end - - it 'throws a 401 if no auth is given' do - get '/test' - expect(last_response.status).to eq(401) - end - - it 'authenticates if given valid creds' do - digest_authorize 'foo', 'bar' - get '/test' - expect(last_response.status).to eq(200) - end - - it 'throws a 401 if given invalid creds' do - digest_authorize 'bar', 'foo' - get '/test' - expect(last_response.status).to eq(401) - end - end - - context 'when password is not hashed' do - def app - StrategiesSpec::PasswordIsNotHashed - end - - it 'is a digest authentication challenge' do - get '/test' - expect(last_response).to be_challenge - end - - it 'throws a 401 if no auth is given' do - get '/test' - expect(last_response.status).to eq(401) - end - - it 'authenticates if given valid creds' do - digest_authorize 'foo', 'bar' - get '/test' - expect(last_response.status).to eq(200) - end - - it 'throws a 401 if given invalid creds' do - digest_authorize 'bar', 'foo' - get '/test' - expect(last_response.status).to eq(401) - end - end - end end From 8de048ed5681a69730f8b5681fb5dd1fb80ddd5e Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Mon, 30 Oct 2023 19:01:42 +0100 Subject: [PATCH 175/304] Add missing requires (#2365) * Add require 'active_support/core_ext/enumerable' * Add changelog * Replace first, second by [0], [1] require missing core_ext/object/deep_dup && core_ext/string/exclude * grape_entity require false * Update CHANGELOG.md --- CHANGELOG.md | 1 + Gemfile | 2 +- gemfiles/multi_json.gemfile | 2 +- gemfiles/multi_xml.gemfile | 2 +- gemfiles/rack_1_0.gemfile | 2 +- gemfiles/rack_2_0.gemfile | 2 +- gemfiles/rack_3_0.gemfile | 2 +- gemfiles/rack_edge.gemfile | 2 +- gemfiles/rails_5_2.gemfile | 2 +- gemfiles/rails_6_0.gemfile | 2 +- gemfiles/rails_6_1.gemfile | 2 +- gemfiles/rails_7_0.gemfile | 2 +- gemfiles/rails_7_1.gemfile | 2 +- gemfiles/rails_edge.gemfile | 2 +- lib/grape.rb | 3 +++ spec/grape/endpoint_spec.rb | 4 ++-- 16 files changed, 19 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aabf664f2..6618fadeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ #### Fixes +* [#2364](https://github.com/ruby-grape/grape/pull/2364): Add missing requires - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 1.8.0 (2023/08/30) diff --git a/Gemfile b/Gemfile index 7bce1e598..ddd2a49f2 100644 --- a/Gemfile +++ b/Gemfile @@ -26,7 +26,7 @@ end group :test do gem 'cookiejar' - gem 'grape-entity', '~> 0.6' + gem 'grape-entity', '~> 0.6', require: false gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' diff --git a/gemfiles/multi_json.gemfile b/gemfiles/multi_json.gemfile index 84d5e1638..b1c6e91e0 100644 --- a/gemfiles/multi_json.gemfile +++ b/gemfiles/multi_json.gemfile @@ -26,7 +26,7 @@ end group :test do gem 'cookiejar' - gem 'grape-entity', '~> 0.6' + gem 'grape-entity', '~> 0.6', require: false gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' diff --git a/gemfiles/multi_xml.gemfile b/gemfiles/multi_xml.gemfile index eebdc1b08..02fdd91cf 100644 --- a/gemfiles/multi_xml.gemfile +++ b/gemfiles/multi_xml.gemfile @@ -26,7 +26,7 @@ end group :test do gem 'cookiejar' - gem 'grape-entity', '~> 0.6' + gem 'grape-entity', '~> 0.6', require: false gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' diff --git a/gemfiles/rack_1_0.gemfile b/gemfiles/rack_1_0.gemfile index 862236c80..7aa8c6451 100644 --- a/gemfiles/rack_1_0.gemfile +++ b/gemfiles/rack_1_0.gemfile @@ -26,7 +26,7 @@ end group :test do gem 'cookiejar' - gem 'grape-entity', '~> 0.6' + gem 'grape-entity', '~> 0.6', require: false gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' diff --git a/gemfiles/rack_2_0.gemfile b/gemfiles/rack_2_0.gemfile index e75b1699c..69d7ec28f 100644 --- a/gemfiles/rack_2_0.gemfile +++ b/gemfiles/rack_2_0.gemfile @@ -26,7 +26,7 @@ end group :test do gem 'cookiejar' - gem 'grape-entity', '~> 0.6' + gem 'grape-entity', '~> 0.6', require: false gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' diff --git a/gemfiles/rack_3_0.gemfile b/gemfiles/rack_3_0.gemfile index 2da2241a9..24ad9ac31 100644 --- a/gemfiles/rack_3_0.gemfile +++ b/gemfiles/rack_3_0.gemfile @@ -26,7 +26,7 @@ end group :test do gem 'cookiejar' - gem 'grape-entity', '~> 0.6' + gem 'grape-entity', '~> 0.6', require: false gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' diff --git a/gemfiles/rack_edge.gemfile b/gemfiles/rack_edge.gemfile index 5d9c93ae0..0e1133d74 100644 --- a/gemfiles/rack_edge.gemfile +++ b/gemfiles/rack_edge.gemfile @@ -26,7 +26,7 @@ end group :test do gem 'cookiejar' - gem 'grape-entity', '~> 0.6' + gem 'grape-entity', '~> 0.6', require: false gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' diff --git a/gemfiles/rails_5_2.gemfile b/gemfiles/rails_5_2.gemfile index 3ff997974..f5e3f2a55 100644 --- a/gemfiles/rails_5_2.gemfile +++ b/gemfiles/rails_5_2.gemfile @@ -26,7 +26,7 @@ end group :test do gem 'cookiejar' - gem 'grape-entity', '~> 0.6' + gem 'grape-entity', '~> 0.6', require: false gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' diff --git a/gemfiles/rails_6_0.gemfile b/gemfiles/rails_6_0.gemfile index e0f12b62f..996b1210b 100644 --- a/gemfiles/rails_6_0.gemfile +++ b/gemfiles/rails_6_0.gemfile @@ -26,7 +26,7 @@ end group :test do gem 'cookiejar' - gem 'grape-entity', '~> 0.6' + gem 'grape-entity', '~> 0.6', require: false gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' diff --git a/gemfiles/rails_6_1.gemfile b/gemfiles/rails_6_1.gemfile index 10a8c9eef..3b8c16a3e 100644 --- a/gemfiles/rails_6_1.gemfile +++ b/gemfiles/rails_6_1.gemfile @@ -26,7 +26,7 @@ end group :test do gem 'cookiejar' - gem 'grape-entity', '~> 0.6' + gem 'grape-entity', '~> 0.6', require: false gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' diff --git a/gemfiles/rails_7_0.gemfile b/gemfiles/rails_7_0.gemfile index 97b71f0e7..914f94c8f 100644 --- a/gemfiles/rails_7_0.gemfile +++ b/gemfiles/rails_7_0.gemfile @@ -26,7 +26,7 @@ end group :test do gem 'cookiejar' - gem 'grape-entity', '~> 0.6' + gem 'grape-entity', '~> 0.6', require: false gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' diff --git a/gemfiles/rails_7_1.gemfile b/gemfiles/rails_7_1.gemfile index 01fbd7900..4fc39fe8f 100644 --- a/gemfiles/rails_7_1.gemfile +++ b/gemfiles/rails_7_1.gemfile @@ -27,7 +27,7 @@ end group :test do gem 'cookiejar' - gem 'grape-entity', '~> 0.6' + gem 'grape-entity', '~> 0.6', require: false gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' diff --git a/gemfiles/rails_edge.gemfile b/gemfiles/rails_edge.gemfile index cc01d9f97..cb144118e 100644 --- a/gemfiles/rails_edge.gemfile +++ b/gemfiles/rails_edge.gemfile @@ -26,7 +26,7 @@ end group :test do gem 'cookiejar' - gem 'grape-entity', '~> 0.6' + gem 'grape-entity', '~> 0.6', require: false gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' diff --git a/lib/grape.rb b/lib/grape.rb index 9eeecbde5..6d4d90e1c 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -16,6 +16,7 @@ require 'active_support/core_ext/array/conversions' require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/array/wrap' +require 'active_support/core_ext/enumerable' require 'active_support/core_ext/hash/conversions' require 'active_support/core_ext/hash/deep_merge' require 'active_support/core_ext/hash/except' @@ -24,7 +25,9 @@ require 'active_support/core_ext/hash/reverse_merge' require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/object/deep_dup' require 'active_support/core_ext/object/duplicable' +require 'active_support/core_ext/string/exclude' require 'active_support/dependencies/autoload' require 'active_support/deprecation' require 'active_support/inflector' diff --git a/spec/grape/endpoint_spec.rb b/spec/grape/endpoint_spec.rb index 400b45c40..8cdcc1cff 100644 --- a/spec/grape/endpoint_spec.rb +++ b/spec/grape/endpoint_spec.rb @@ -201,8 +201,8 @@ def app get('/username', {}, 'HTTP_COOKIE' => 'username=user; sandbox=false') expect(last_response.body).to eq('user_test') cookies = Array(last_response.headers['Set-Cookie']).flat_map { |h| h.split("\n") } - expect(cookies.first).to match(/username=user_test/) - expect(cookies.second).to match(/sandbox=true/) + expect(cookies[0]).to match(/username=user_test/) + expect(cookies[1]).to match(/sandbox=true/) end it 'deletes cookie' do From 32b8d224ae9144016cb09c4dae4ee0b71d7a3173 Mon Sep 17 00:00:00 2001 From: Hidde Wieringa Date: Tue, 7 Nov 2023 18:34:03 +0100 Subject: [PATCH 176/304] Quality arguments to `Accept` header should default to 1.0 (#2366) * Quality arguments to Accept header should default to 1.0 * space * Fix spec that results in XML * sort only once * Simplify * Ensure empty and invalid quality values also parse correctly, specs * Update CHANGELOG.md * Comments for readme and changelog --- CHANGELOG.md | 3 ++- README.md | 6 ++++- lib/grape/middleware/formatter.rb | 4 ++-- spec/grape/middleware/formatter_spec.rb | 31 +++++++++++++++++++++++++ 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6618fadeb..d17ce560c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,11 @@ * [#2360](https://github.com/ruby-grape/grape/pull/2360): Reduce gem size by removing specs - [@ericproulx](https://github.com/ericproulx). * [#2361](https://github.com/ruby-grape/grape/pull/2361): Remove `Rack::Auth::Digest` - [@ninoseki](https://github.com/ninoseki). * Your contribution here. - + #### Fixes * [#2364](https://github.com/ruby-grape/grape/pull/2364): Add missing requires - [@ericproulx](https://github.com/ericproulx). +* [#2366](https://github.com/ruby-grape/grape/pull/2366): Default quality to 1.0 in the `Accept` header when omitted - [@hiddewie](https://github.com/hiddewie). * Your contribution here. ### 1.8.0 (2023/08/30) diff --git a/README.md b/README.md index f0d66f809..e1d1295af 100644 --- a/README.md +++ b/README.md @@ -596,6 +596,10 @@ When an invalid `Accept` header is supplied, a `406 Not Acceptable` error is ret option is set to `false`. Otherwise a `404 Not Found` error is returned by Rack if no other route matches. +Grape will evaluate the relative quality preference included in Accept headers and default to a quality of 1.0 when omitted. In the following example a Grape API that supports XML and JSON in that order will return JSON: + + curl -H "Accept: text/xml;q=0.8, application/json;q=0.9" localhost:1234/resource + ### Accept-Version Header ```ruby @@ -1600,7 +1604,7 @@ Note endless ranges are also supported with ActiveSupport >= 6.0, but they requi ```ruby params do requires :minimum, type: Integer, values: 10.. - optional :maximum, type: Integer, values: ..10 + optional :maximum, type: Integer, values: ..10 end ``` diff --git a/lib/grape/middleware/formatter.rb b/lib/grape/middleware/formatter.rb index 0f8e7cdb3..0e87373d0 100644 --- a/lib/grape/middleware/formatter.rb +++ b/lib/grape/middleware/formatter.rb @@ -164,14 +164,14 @@ def mime_array \w+/[\w+.-]+) # eg application/vnd.example.myformat+xml (?: (?:;[^,]*?)? # optionally multiple formats in a row - ;\s*q=([\d.]+) # optional "quality" preference (eg q=0.5) + ;\s*q=([\w.]+) # optional "quality" preference (eg q=0.5) )? }x vendor_prefix_pattern = /vnd\.[^+]+\+/ accept.scan(accept_into_mime_and_quality) - .sort_by { |_, quality_preference| -quality_preference.to_f } + .sort_by { |_, quality_preference| -(quality_preference ? quality_preference.to_f : 1.0) } .flat_map { |mime, _| [mime, mime.sub(vendor_prefix_pattern, '')] } end end diff --git a/spec/grape/middleware/formatter_spec.rb b/spec/grape/middleware/formatter_spec.rb index 29f49f88b..59245ad11 100644 --- a/spec/grape/middleware/formatter_spec.rb +++ b/spec/grape/middleware/formatter_spec.rb @@ -125,20 +125,51 @@ def to_xml it 'uses quality rankings to determine formats' do subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json; q=0.3,application/xml; q=1.0') expect(subject.env['api.format']).to eq(:xml) + subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json; q=1.0,application/xml; q=0.3') expect(subject.env['api.format']).to eq(:json) end it 'handles quality rankings mixed with nothing' do subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json,application/xml; q=1.0') + expect(subject.env['api.format']).to eq(:json) + + subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/xml; q=1.0,application/json') expect(subject.env['api.format']).to eq(:xml) end + it 'handles quality rankings that have a default 1.0 value' do + subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json,application/xml;q=0.5') + expect(subject.env['api.format']).to eq(:json) + subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/xml;q=0.5,application/json') + expect(subject.env['api.format']).to eq(:json) + end + it 'parses headers with other attributes' do subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json; abc=2.3; q=1.0,application/xml; q=0.7') expect(subject.env['api.format']).to eq(:json) end + it 'ensures that a quality of 0 is less preferred than any other content type' do + subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json;q=0.0,application/xml') + expect(subject.env['api.format']).to eq(:xml) + subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/xml,application/json;q=0.0') + expect(subject.env['api.format']).to eq(:xml) + end + + it 'ignores invalid quality rankings' do + subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json;q=invalid,application/xml;q=0.5') + expect(subject.env['api.format']).to eq(:xml) + subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/xml;q=0.5,application/json;q=invalid') + expect(subject.env['api.format']).to eq(:xml) + + subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json;q=,application/xml;q=0.5') + expect(subject.env['api.format']).to eq(:json) + + subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json;q=nil,application/xml;q=0.5') + expect(subject.env['api.format']).to eq(:xml) + end + it 'parses headers with vendor and api version' do subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/vnd.test-v1+xml') expect(subject.env['api.format']).to eq(:xml) From 0c03b5d0b6866935c0c20c342952f6ac1ec2d0bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Garc=C3=ADa=20del=20Canto?= Date: Sat, 11 Nov 2023 03:59:24 +0100 Subject: [PATCH 177/304] fix(#2236): Stripping the internals of `Grape::Endpoint` when `NoMethodError` is raised (#2368) * fix(#2236): Stripping the internals of `Grape::Endpoint` when `NoMethodError` is raised * fix(#2236): Follow quote ruby style and including the endpoint in the error message * fix(#2236): Running rubocop and applying corrections --- CHANGELOG.md | 1 + lib/grape/endpoint.rb | 8 ++++++++ spec/grape/endpoint_spec.rb | 24 ++++++++++++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d17ce560c..e19d4928a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ * [#2364](https://github.com/ruby-grape/grape/pull/2364): Add missing requires - [@ericproulx](https://github.com/ericproulx). * [#2366](https://github.com/ruby-grape/grape/pull/2366): Default quality to 1.0 in the `Accept` header when omitted - [@hiddewie](https://github.com/hiddewie). +* [#2368](https://github.com/ruby-grape/grape/pull/2368): Stripping the internals of `Grape::Endpoint` when `NoMethodError` is raised - [@jcagarcia](https://github.com/jcagarcia). * Your contribution here. ### 1.8.0 (2023/08/30) diff --git a/lib/grape/endpoint.rb b/lib/grape/endpoint.rb index 663a3e8f1..f50084270 100644 --- a/lib/grape/endpoint.rb +++ b/lib/grape/endpoint.rb @@ -403,5 +403,13 @@ def options? options[:options_route_enabled] && env[Grape::Http::Headers::REQUEST_METHOD] == Grape::Http::Headers::OPTIONS end + + def method_missing(name, *_args) + raise NoMethodError.new("undefined method `#{name}' for #{self.class} in `#{route.origin}' endpoint") + end + + def respond_to_missing?(method_name, include_private = false) + super + end end end diff --git a/spec/grape/endpoint_spec.rb b/spec/grape/endpoint_spec.rb index 8cdcc1cff..5788bd913 100644 --- a/spec/grape/endpoint_spec.rb +++ b/spec/grape/endpoint_spec.rb @@ -692,6 +692,30 @@ def app end end + describe '#method_missing' do + context 'when referencing an undefined local variable' do + it 'raises NoMethodError but stripping the internals of the Grape::Endpoint class and including the API route' do + subject.get('/hey') do + undefined_helper + end + expect do + get '/hey' + end.to raise_error(NoMethodError, %r{^undefined method `undefined_helper' for # in `/hey' endpoint}) + end + end + + context 'when performing an undefined method of an instance inside the API' do + it 'raises NoMethodError but stripping the internals of the Object class' do + subject.get('/hey') do + Object.new.x + end + expect do + get '/hey' + end.to raise_error(NoMethodError, /^undefined method `x' for #$/) + end + end + end + it 'does not persist params between calls' do subject.post('/new') do params[:text] From 8bd49222026cbac8cec25adb0c232fa801022c97 Mon Sep 17 00:00:00 2001 From: "Daniel (dB.) Doubrovkine" Date: Sat, 11 Nov 2023 09:48:04 -0500 Subject: [PATCH 178/304] Preparing for the next release, 2.0. --- CHANGELOG.md | 4 +--- README.md | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e19d4928a..bd450b97e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -### 2.0.0 (Next) +### 2.0.0 (2023/11/11) #### Features @@ -6,14 +6,12 @@ * [#2355](https://github.com/ruby-grape/grape/pull/2355): Set response headers based on Rack version - [@schinery](https://github.com/schinery). * [#2360](https://github.com/ruby-grape/grape/pull/2360): Reduce gem size by removing specs - [@ericproulx](https://github.com/ericproulx). * [#2361](https://github.com/ruby-grape/grape/pull/2361): Remove `Rack::Auth::Digest` - [@ninoseki](https://github.com/ninoseki). -* Your contribution here. #### Fixes * [#2364](https://github.com/ruby-grape/grape/pull/2364): Add missing requires - [@ericproulx](https://github.com/ericproulx). * [#2366](https://github.com/ruby-grape/grape/pull/2366): Default quality to 1.0 in the `Accept` header when omitted - [@hiddewie](https://github.com/hiddewie). * [#2368](https://github.com/ruby-grape/grape/pull/2368): Stripping the internals of `Grape::Endpoint` when `NoMethodError` is raised - [@jcagarcia](https://github.com/jcagarcia). -* Your contribution here. ### 1.8.0 (2023/08/30) diff --git a/README.md b/README.md index e1d1295af..847883915 100644 --- a/README.md +++ b/README.md @@ -160,9 +160,8 @@ content negotiation, versioning and much more. ## Stable Release -You're reading the documentation for the next release of Grape, which should be **2.0.0**. +You're reading the documentation for the stable release of Grape, **2.0.0**. Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. -The current stable release is [1.8.0](https://github.com/ruby-grape/grape/blob/v1.8.0/README.md). ## Project Resources From b34c722f10cd4e42d683de866e0ea696a9a7fefd Mon Sep 17 00:00:00 2001 From: "Daniel (dB.) Doubrovkine" Date: Sat, 11 Nov 2023 09:49:51 -0500 Subject: [PATCH 179/304] Preparing for next developer iteration, 2.0.1. --- CHANGELOG.md | 10 ++++++++++ README.md | 3 ++- lib/grape/version.rb | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd450b97e..8d85e957e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +### 2.0.1 (Next) + +#### Features + +* Your contribution here. + +#### Fixes + +* Your contribution here. + ### 2.0.0 (2023/11/11) #### Features diff --git a/README.md b/README.md index 847883915..81d6ddc81 100644 --- a/README.md +++ b/README.md @@ -160,8 +160,9 @@ content negotiation, versioning and much more. ## Stable Release -You're reading the documentation for the stable release of Grape, **2.0.0**. +You're reading the documentation for the next release of Grape, which should be **2.0.1**. Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. +The current stable release is [2.0.0](https://github.com/ruby-grape/grape/blob/v2.0.0/README.md). ## Project Resources diff --git a/lib/grape/version.rb b/lib/grape/version.rb index c6660d510..df942655a 100644 --- a/lib/grape/version.rb +++ b/lib/grape/version.rb @@ -2,5 +2,5 @@ module Grape # The current version of Grape. - VERSION = '2.0.0' + VERSION = '2.0.1' end From c480bfd21e41d50d13cd56f15593ca93834f07dd Mon Sep 17 00:00:00 2001 From: Juan Carlos Garcia Date: Fri, 17 Nov 2023 22:23:56 +0100 Subject: [PATCH 180/304] fix(#2195): Fix `declared` method for hash params with overlapping names --- CHANGELOG.md | 1 + lib/grape/dsl/inside_route.rb | 2 +- spec/grape/endpoint/declared_spec.rb | 4 +++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d85e957e..e85f6072d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ #### Fixes +* [#2372](https://github.com/ruby-grape/grape/pull/2372): Fix `declared` method for hash params with overlapping names - [@jcagarcia](https://github.com/jcagarcia). * Your contribution here. ### 2.0.0 (2023/11/11) diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index bcfd3e84c..7ebd6aab8 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -99,7 +99,7 @@ def handle_passed_param(params_nested_path, has_passed_children = false, &_block 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) } + has_children = route_options_params.keys.any? { |k| k != key && k.start_with?("#{key}[") } if type == 'Hash' && !has_children {} diff --git a/spec/grape/endpoint/declared_spec.rb b/spec/grape/endpoint/declared_spec.rb index f4402416d..b73538fdb 100644 --- a/spec/grape/endpoint/declared_spec.rb +++ b/spec/grape/endpoint/declared_spec.rb @@ -39,6 +39,7 @@ def app optional :empty_arr, type: Array optional :empty_typed_arr, type: Array[String] optional :empty_hash, type: Hash + optional :empty_hash_two, type: Hash optional :empty_set, type: Set optional :empty_typed_set, type: Set[String] end @@ -122,7 +123,7 @@ def app end get '/declared?first=present' expect(last_response.status).to eq(200) - expect(JSON.parse(last_response.body).keys.size).to eq(11) + expect(JSON.parse(last_response.body).keys.size).to eq(12) end it 'has a optional param with default value all the time' do @@ -201,6 +202,7 @@ def app body = JSON.parse(last_response.body) expect(body['empty_hash']).to eq({}) + expect(body['empty_hash_two']).to eq({}) expect(body['nested']).to be_a(Hash) expect(body['nested']['empty_hash']).to eq({}) expect(body['nested']['nested_two']).to be_a(Hash) From 34f90f8e8331c970870bd7fa2c9feb27f061e98a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Garc=C3=ADa=20del=20Canto?= Date: Sun, 19 Nov 2023 15:08:53 +0100 Subject: [PATCH 181/304] feature(#2290): Use a param value as the `default` value of other param (#2371) * feature(#2290): Use a param value as the `default` value of other param * feature(#2290): Updating version to 2.1.0 * feature(#2290): Updating stable release section with 2.1.0 * feature(#2290): Updating README for following one-line format --- CHANGELOG.md | 3 ++- README.md | 11 +++++++- .../validators/default_validator.rb | 6 ++++- lib/grape/version.rb | 2 +- .../validations/validators/default_spec.rb | 26 +++++++++++++++++++ 5 files changed, 44 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e85f6072d..55682fecd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ -### 2.0.1 (Next) +### 2.1.0 (Next) #### Features +* [#2371](https://github.com/ruby-grape/grape/pull/2371): Use a param value as the `default` value of other param - [@jcagarcia](https://github.com/jcagarcia). * Your contribution here. #### Fixes diff --git a/README.md b/README.md index 81d6ddc81..58d391f7b 100644 --- a/README.md +++ b/README.md @@ -160,7 +160,7 @@ content negotiation, versioning and much more. ## Stable Release -You're reading the documentation for the next release of Grape, which should be **2.0.1**. +You're reading the documentation for the next release of Grape, which should be **2.1.0**. Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. The current stable release is [2.0.0](https://github.com/ruby-grape/grape/blob/v2.0.0/README.md). @@ -1242,6 +1242,15 @@ params do end ``` +You can use the value of one parameter as the default value of some other parameter. In this case, if the `primary_color` parameter is not provided, it will have the same value as the `color` one. If both of them not provided, both of them will have `blue` value. + +```ruby +params do + optional :color, type: String, default: 'blue' + optional :primary_color, type: String, default: -> (params) { params[:color] } +end +``` + ### Supported Parameter Types The following are all valid types, supported out of the box by Grape: diff --git a/lib/grape/validations/validators/default_validator.rb b/lib/grape/validations/validators/default_validator.rb index 4058d7b1d..9a59e5da1 100644 --- a/lib/grape/validations/validators/default_validator.rb +++ b/lib/grape/validations/validators/default_validator.rb @@ -11,7 +11,11 @@ def initialize(attrs, options, required, scope, **opts) def validate_param!(attr_name, params) params[attr_name] = if @default.is_a? Proc - @default.call + if @default.parameters.empty? + @default.call + else + @default.call(params) + end elsif @default.frozen? || !@default.duplicable? @default else diff --git a/lib/grape/version.rb b/lib/grape/version.rb index df942655a..1013166e1 100644 --- a/lib/grape/version.rb +++ b/lib/grape/version.rb @@ -2,5 +2,5 @@ module Grape # The current version of Grape. - VERSION = '2.0.1' + VERSION = '2.1.0' end diff --git a/spec/grape/validations/validators/default_spec.rb b/spec/grape/validations/validators/default_spec.rb index 980fa2304..2bb59792d 100644 --- a/spec/grape/validations/validators/default_spec.rb +++ b/spec/grape/validations/validators/default_spec.rb @@ -89,6 +89,19 @@ get '/another_nested_optional_array' do { root: params[:root] } end + + params do + requires :foo + optional :bar, default: ->(params) { params[:foo] } + optional :qux, default: ->(params) { params[:bar] } + end + get '/default_values_from_other_params' do + { + foo: params[:foo], + bar: params[:bar], + qux: params[:qux] + } + end end end @@ -460,4 +473,17 @@ def app expect(JSON.parse(last_response.body)).to eq(expected) end end + + it 'sets default value for optional params using other params values' do + expected_foo_value = 'foo-value' + + get("/default_values_from_other_params?foo=#{expected_foo_value}") + + expect(last_response.status).to eq(200) + expect(last_response.body).to eq({ + foo: expected_foo_value, + bar: expected_foo_value, + qux: expected_foo_value + }.to_json) + end end From cd293963188387e6bd73eb9605c7168575b8741b Mon Sep 17 00:00:00 2001 From: Juan Carlos Garcia Date: Sun, 19 Nov 2023 22:36:51 +0100 Subject: [PATCH 182/304] fix: Updating markdown files for following one-line format --- CHANGELOG.md | 1 + README.md | 329 ++++++++++++++------------------------------------- UPGRADING.md | 9 +- 3 files changed, 92 insertions(+), 247 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55682fecd..b4f099314 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ #### Fixes * [#2372](https://github.com/ruby-grape/grape/pull/2372): Fix `declared` method for hash params with overlapping names - [@jcagarcia](https://github.com/jcagarcia). +* [#2373](https://github.com/ruby-grape/grape/pull/2373): Fix markdown files for following 1-line format - [@jcagarcia](https://github.com/jcagarcia). * Your contribution here. ### 2.0.0 (2023/11/11) diff --git a/README.md b/README.md index 58d391f7b..0e38b5663 100644 --- a/README.md +++ b/README.md @@ -152,11 +152,7 @@ ## What is Grape? -Grape is a REST-like API framework for Ruby. It's designed to run on Rack -or complement existing web application frameworks such as Rails and Sinatra by -providing a simple DSL to easily develop RESTful APIs. It has built-in support -for common conventions, including multiple formats, subdomain/prefix restriction, -content negotiation, versioning and much more. +Grape is a REST-like API framework for Ruby. It's designed to run on Rack or complement existing web application frameworks such as Rails and Sinatra by providing a simple DSL to easily develop RESTful APIs. It has built-in support for common conventions, including multiple formats, subdomain/prefix restriction, content negotiation, versioning and much more. ## Stable Release @@ -189,8 +185,7 @@ Grape is available as a gem, to install it run: ## Basic Usage Grape APIs are Rack applications that are created by subclassing `Grape::API`. -Below is a simple example showing some of the more common features of Grape in -the context of recreating parts of the Twitter API. +Below is a simple example showing some of the more common features of Grape in the context of recreating parts of the Twitter API. ```ruby module Twitter @@ -288,8 +283,7 @@ This can be added to your `config.ru` (if using rackup), `application.rb` (if us ### Rack -The above sample creates a Rack application that can be run from a rackup `config.ru` file -with `rackup`: +The above sample creates a Rack application that can be run from a rackup `config.ru` file with `rackup`: ```ruby run Twitter::API @@ -315,13 +309,11 @@ Grape will also automatically respond to HEAD and OPTIONS for all GET, and just ### ActiveRecord without Rails -If you want to use ActiveRecord within Grape, you will need to make sure that ActiveRecord's connection pool -is handled correctly. +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.: +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 @@ -337,8 +329,7 @@ use OTR::ActiveRecord::ConnectionManagement ### Alongside Sinatra (or other frameworks) -If you wish to mount Grape alongside another Rack framework such as Sinatra, you can do so easily using -`Rack::Cascade`: +If you wish to mount Grape alongside another Rack framework such as Sinatra, you can do so easily using `Rack::Cascade`: ```ruby # Example config.ru @@ -398,8 +389,7 @@ end ### Modules -You can mount multiple API implementations inside another one. These don't have to be -different versions, but may be components of the same API. +You can mount multiple API implementations inside another one. These don't have to be different versions, but may be components of the same API. ```ruby class Twitter::API < Grape::API @@ -587,14 +577,9 @@ Using this versioning strategy, clients should pass the desired version in the H curl -H Accept:application/vnd.twitter-v1+json http://localhost:9292/statuses/public_timeline -By default, the first matching version is used when no `Accept` header is -supplied. This behavior is similar to routing in Rails. To circumvent this default behavior, -one could use the `:strict` option. When this option is set to `true`, a `406 Not Acceptable` error -is returned when no correct `Accept` header is supplied. +By default, the first matching version is used when no `Accept` header is supplied. This behavior is similar to routing in Rails. To circumvent this default behavior, one could use the `:strict` option. When this option is set to `true`, a `406 Not Acceptable` error is returned when no correct `Accept` header is supplied. -When an invalid `Accept` header is supplied, a `406 Not Acceptable` error is returned if the `:cascade` -option is set to `false`. Otherwise a `404 Not Found` error is returned by Rack if no other route -matches. +When an invalid `Accept` header is supplied, a `406 Not Acceptable` error is returned if the `:cascade` option is set to `false`. Otherwise a `404 Not Found` error is returned by Rack if no other route matches. Grape will evaluate the relative quality preference included in Accept headers and default to a quality of 1.0 when omitted. In the following example a Grape API that supports XML and JSON in that order will return JSON: @@ -610,11 +595,7 @@ Using this versioning strategy, clients should pass the desired version in the H curl -H "Accept-Version:v1" http://localhost:9292/statuses/public_timeline -By default, the first matching version is used when no `Accept-Version` header is -supplied. This behavior is similar to routing in Rails. To circumvent this default behavior, -one could use the `:strict` option. When this option is set to `true`, a `406 Not Acceptable` error -is returned when no correct `Accept` header is supplied and the `:cascade` option is set to `false`. -Otherwise a `404 Not Found` error is returned by Rack if no other route matches. +By default, the first matching version is used when no `Accept-Version` header is supplied. This behavior is similar to routing in Rails. To circumvent this default behavior, one could use the `:strict` option. When this option is set to `true`, a `406 Not Acceptable` error is returned when no correct `Accept` header is supplied and the `:cascade` option is set to `false`. Otherwise a `404 Not Found` error is returned by Rack if no other route matches. ### Param @@ -622,8 +603,7 @@ Otherwise a `404 Not Found` error is returned by Rack if no other route matches. version 'v1', using: :param ``` -Using this versioning strategy, clients should pass the desired version as a request parameter, -either in the URL query string or in the request body. +Using this versioning strategy, clients should pass the desired version as a request parameter, either in the URL query string or in the request body. curl http://localhost:9292/statuses/public_timeline?apiver=v1 @@ -714,13 +694,11 @@ API.configure do |config| end ``` -This will be available inside the API with `configuration`, as if it were -[mount configuration](#mount-configuration). +This will be available inside the API with `configuration`, as if it were [mount configuration](#mount-configuration). ## Parameters -Request parameters are available through the `params` hash object. This includes `GET`, `POST` -and `PUT` parameters, along with any named parameters you specify in your route strings. +Request parameters are available through the `params` hash object. This includes `GET`, `POST` and `PUT` parameters, along with any named parameters you specify in your route strings. ```ruby get :public_timeline do @@ -728,8 +706,7 @@ get :public_timeline do end ``` -Parameters are automatically populated from the request body on `POST` and `PUT` for form input, JSON and -XML content-types. +Parameters are automatically populated from the request body on `POST` and `PUT` for form input, JSON and XML content-types. The request: @@ -1068,8 +1045,7 @@ curl -X POST -H "Content-Type: application/json" localhost:9292/users/signup -d } ```` -Note that an attribute with a `nil` value is not considered *missing* and will also be returned -when `include_missing` is set to `false`: +Note that an attribute with a `nil` value is not considered *missing* and will also be returned when `include_missing` is set to `false`: **Request** @@ -1208,8 +1184,7 @@ put ':id' do end ``` -When a type is specified an implicit validation is done after the coercion to ensure -the output type is the one declared. +When a type is specified an implicit validation is done after the coercion to ensure the output type is the one declared. Optional parameters can have a default value. @@ -1221,9 +1196,7 @@ params do end ``` -Default values are eagerly evaluated. Above `:non_random_number` will evaluate to the same -number for each call to the endpoint of this `params` block. To have the default evaluate -lazily with each request use a lambda, like `:random_number` above. +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. @@ -1292,12 +1265,7 @@ get '/int' integers: { int: '45' } ### Custom Types and Coercions -Aside from the default set of supported types listed above, any class can be -used as a type as long as an explicit coercion method is supplied. If the type -implements a class-level `parse` method, Grape will use it automatically. -This method must take one string argument and return an instance of the correct -type, or return an instance of `Grape::Types::InvalidValue` which optionally -accepts a message to be returned in the response. +Aside from the default set of supported types listed above, any class can be used as a type as long as an explicit coercion method is supplied. If the type implements a class-level `parse` method, Grape will use it automatically. This method must take one string argument and return an instance of the correct type, or return an instance of `Grape::Types::InvalidValue` which optionally accepts a message to be returned in the response. ```ruby class Color @@ -1325,10 +1293,7 @@ get '/stuff' do end ``` -Alternatively, a custom coercion method may be supplied for any type of parameter -using `coerce_with`. Any class or object may be given that implements a `parse` or -`call` method, in that order of precedence. The method must accept a single string -parameter, and the return value must match the given `type`. +Alternatively, a custom coercion method may be supplied for any type of parameter using `coerce_with`. Any class or object may be given that implements a `parse` or `call` method, in that order of precedence. The method must accept a single string parameter, and the return value must match the given `type`. ```ruby params do @@ -1352,9 +1317,7 @@ params do end ``` -Grape will assert that coerced values match the given `type`, and will reject the request -if they do not. To override this behaviour, custom types may implement a `parsed?` method -that should accept a single argument and return `true` if the value passes type validation. +Grape will assert that coerced values match the given `type`, and will reject the request if they do not. To override this behaviour, custom types may implement a `parsed?` method that should accept a single argument and return `true` if the value passes type validation. ```ruby class SecureUri @@ -1389,9 +1352,7 @@ end ### First-Class `JSON` Types -Grape supports complex parameters given as JSON-formatted strings using the special `type: JSON` -declaration. JSON objects and arrays of objects are accepted equally, with nested validation -rules applied to all objects in either case: +Grape supports complex parameters given as JSON-formatted strings using the special `type: JSON` declaration. JSON objects and arrays of objects are accepted equally, with nested validation rules applied to all objects in either case: ```ruby params do @@ -1410,8 +1371,7 @@ client.get('/', json: '{"int":4}') # => HTTP 400 client.get('/', json: '[{"int":4}]') # => HTTP 400 ``` -Additionally `type: Array[JSON]` may be used, which explicitly marks the parameter as an array -of objects. If a single object is supplied it will be wrapped. +Additionally `type: Array[JSON]` may be used, which explicitly marks the parameter as an array of objects. If a single object is supplied it will be wrapped. ```ruby params do @@ -1423,8 +1383,7 @@ get '/' do params[:json].each { |obj| ... } # always works end ``` -For stricter control over the type of JSON structure which may be supplied, -use `type: Array, coerce_with: JSON` or `type: Hash, coerce_with: JSON`. +For stricter control over the type of JSON structure which may be supplied, use `type: Array, coerce_with: JSON` or `type: Hash, coerce_with: JSON`. ### Multiple Allowed Types @@ -1443,8 +1402,7 @@ client.get('/', status_code: 300) # => 300 client.get('/', status_code: %w(404 NOT FOUND)) # => [404, "NOT", "FOUND"] ``` -As a special case, variant-member-type collections may also be declared, by -passing a `Set` or `Array` with more than one member to `type`: +As a special case, variant-member-type collections may also be declared, by passing a `Set` or `Array` with more than one member to `type`: ```ruby params do @@ -1460,11 +1418,8 @@ client.get('/', status_codes: %w(1 two)) # => [1, "two"] ### Validation of Nested Parameters Parameters can be nested using `group` or by calling `requires` or `optional` with a block. -In the [above example](#parameter-validation-and-coercion), this means `params[:media][:url]` is required along with `params[:id]`, -and `params[:audio][:format]` is required only if `params[:audio]` is present. -With a block, `group`, `requires` and `optional` accept an additional option `type` which can -be either `Array` or `Hash`, and defaults to `Array`. Depending on the value, the nested -parameters will be treated either as values of a hash or as values of hashes in an array. +In the [above example](#parameter-validation-and-coercion), this means `params[:media][:url]` is required along with `params[:id]`, and `params[:audio][:format]` is required only if `params[:audio]` is present. +With a block, `group`, `requires` and `optional` accept an additional option `type` which can be either `Array` or `Hash`, and defaults to `Array`. Depending on the value, the nested parameters will be treated either as values of a hash or as values of hashes in an array. ```ruby params do @@ -1482,9 +1437,7 @@ end ### Dependent Parameters -Suppose some of your parameters are only relevant if another parameter is given; -Grape allows you to express this relationship through the `given` method in your -parameters block, like so: +Suppose some of your parameters are only relevant if another parameter is given; Grape allows you to express this relationship through the `given` method in your parameters block, like so: ```ruby params do @@ -1523,9 +1476,7 @@ Note: param in `given` should be the renamed one. In the example, it should be ` ### Group Options -Parameters options can be grouped. It can be useful if you want to extract -common validation or types for several parameters. The example below presents a -typical case when parameters share common options. +Parameters options can be grouped. It can be useful if you want to extract common validation or types for several parameters. The example below presents a typical case when parameters share common options. ```ruby params do @@ -1535,8 +1486,7 @@ params do end ``` -Grape allows you to present the same logic through the `with` method in your -parameters block, like so: +Grape allows you to present the same logic through the `with` method in your parameters block, like so: ```ruby params do @@ -1570,13 +1520,9 @@ The value passed to `as` will be the key when calling `declared(params)`. #### `allow_blank` -Parameters can be defined as `allow_blank`, ensuring that they contain a value. By default, `requires` -only validates that a parameter was sent in the request, regardless its value. With `allow_blank: false`, -empty values or whitespace only values are invalid. +Parameters can be defined as `allow_blank`, ensuring that they contain a value. By default, `requires` only validates that a parameter was sent in the request, regardless its value. With `allow_blank: false`, empty values or whitespace only values are invalid. -`allow_blank` can be combined with both `requires` and `optional`. If the parameter is required, it has to contain -a value. If it's optional, it's possible to not send it in the request, but if it's being sent, it has to have -some value, and not an empty string/only whitespaces. +`allow_blank` can be combined with both `requires` and `optional`. If the parameter is required, it has to contain a value. If it's optional, it's possible to not send it in the request, but if it's being sent, it has to have some value, and not an empty string/only whitespaces. ```ruby @@ -1627,11 +1573,9 @@ end ``` The `:values` option can also be supplied with a `Proc`, evaluated lazily with each request. -If the Proc has arity zero (i.e. it takes no arguments) it is expected to return either a list -or a range which will then be used to validate the parameter. +If the Proc has arity zero (i.e. it takes no arguments) it is expected to return either a list or a range which will then be used to validate the parameter. -For example, given a status model you may want to restrict by hashtags that you have -previously defined in the `HashTag` model. +For example, given a status model you may want to restrict by hashtags that you have previously defined in the `HashTag` model. ```ruby params do @@ -1639,10 +1583,7 @@ params do end ``` -Alternatively, a Proc with arity one (i.e. taking one argument) can be used to explicitly validate -each parameter value. In that case, the Proc is expected to return a truthy value if the parameter -value is valid. The parameter will be considered invalid if the Proc returns a falsy value or if it -raises a StandardError. +Alternatively, a Proc with arity one (i.e. taking one argument) can be used to explicitly validate each parameter value. In that case, the Proc is expected to return a truthy value if the parameter value is valid. The parameter will be considered invalid if the Proc returns a falsy value or if it raises a StandardError. ```ruby params do @@ -1664,9 +1605,7 @@ end Parameters can be restricted from having a specific set of values with the `:except_values` option. -The `except_values` validator behaves similarly to the `values` validator in that it accepts either -an Array, a Range, or a Proc. Unlike the `values` validator, however, `except_values` only accepts -Procs with arity zero. +The `except_values` validator behaves similarly to the `values` validator in that it accepts either an Array, a Range, or a Proc. Unlike the `values` validator, however, `except_values` only accepts Procs with arity zero. ```ruby params do @@ -1689,9 +1628,7 @@ end #### `regexp` -Parameters can be restricted to match a specific regular expression with the `:regexp` option. If the value -does not match the regular expression an error will be returned. Note that this is true for both `requires` -and `optional` parameters. +Parameters can be restricted to match a specific regular expression with the `:regexp` option. If the value does not match the regular expression an error will be returned. Note that this is true for both `requires` and `optional` parameters. ```ruby params do @@ -1826,8 +1763,7 @@ namespace :statuses do end ``` -The `namespace` method has a number of aliases, including: `group`, `resource`, -`resources`, and `segment`. Use whichever reads the best for your API. +The `namespace` method has a number of aliases, including: `group`, `resource`, `resources`, and `segment`. Use whichever reads the best for your API. You can conveniently define a route parameter as a namespace using `route_param`. @@ -1982,8 +1918,7 @@ end ### I18n -Grape supports I18n for parameter-related error messages, but will fallback to English if -translations for the default locale have not been provided. See [en.yml](lib/grape/locale/en.yml) for message keys. +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. @@ -2215,8 +2150,7 @@ namespace ':id' do 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. +Optionally, you can define requirements for your named route parameters using regular expressions on namespace or endpoint. The route will match only if all requirements are met. ```ruby get ':id', requirements: { id: /[0-9]*/ } do @@ -2234,8 +2168,7 @@ end ## Helpers -You can define helper methods that your endpoints can use with the `helpers` -macro by either giving a block or an array of modules. +You can define helper methods that your endpoints can use with the `helpers` macro by either giving a block or an array of modules. ```ruby module StatusHelpers @@ -2476,9 +2409,7 @@ API.recognize_path '/statuses' ## Allowed Methods -When you add a `GET` route for a resource, a route for the `HEAD` -method will also be added automatically. You can disable this -behavior with `do_not_route_head!`. +When you add a `GET` route for a resource, a route for the `HEAD` method will also be added automatically. You can disable this behavior with `do_not_route_head!`. ``` ruby class API < Grape::API @@ -2490,11 +2421,7 @@ class API < Grape::API end ``` -When you add a route for a resource, a route for the `OPTIONS` -method will also be added. The response to an OPTIONS request will -include an "Allow" header listing the supported methods. If the resource -has `before` and `after` callbacks they will be executed, but no other callbacks will -run. +When you add a route for a resource, a route for the `OPTIONS` method will also be added. The response to an OPTIONS request will include an "Allow" header listing the supported methods. If the resource has `before` and `after` callbacks they will be executed, but no other callbacks will run. ```ruby class API < Grape::API @@ -2523,10 +2450,7 @@ curl -v -X OPTIONS http://localhost:3000/rt_count You can disable this behavior with `do_not_route_options!`. -If a request for a resource is made with an unsupported HTTP method, an -HTTP 405 (Method Not Allowed) response will be returned. If the resource -has `before` callbacks they will be executed, but no other callbacks will -run. +If a request for a resource is made with an unsupported HTTP method, an HTTP 405 (Method Not Allowed) response will be returned. If the resource has `before` callbacks they will be executed, but no other callbacks will run. ``` shell curl -X DELETE -v http://localhost:3000/rt_count/ @@ -2552,8 +2476,7 @@ Anything that responds to `#to_s` can be given as a first argument to `error!`. error! :not_found, 404 ``` -You can also return JSON formatted objects by raising error! and passing a hash -instead of a message. +You can also return JSON formatted objects by raising error! and passing a hash instead of a message. ```ruby error!({ error: 'unexpected error', detail: 'missing widget' }, 500) @@ -2618,8 +2541,7 @@ route :any, '*path' do end ``` -It is very crucial to __define this endpoint at the very end of your API__, as it -literally accepts every request. +It is very crucial to __define this endpoint at the very end of your API__, as it literally accepts every request. ## Exception Handling @@ -2863,15 +2785,9 @@ This is following [standard recommendations for exceptions handling](https://rub ### Rails 3.x -When mounted inside containers, such as Rails 3.x, errors such as "404 Not Found" or -"406 Not Acceptable" will likely be handled and rendered by Rails handlers. For instance, -accessing a nonexistent route "/api/foo" raises a 404, which inside rails will ultimately -be translated to an `ActionController::RoutingError`, which most likely will get rendered -to a HTML error page. +When mounted inside containers, such as Rails 3.x, errors such as "404 Not Found" or "406 Not Acceptable" will likely be handled and rendered by Rails handlers. For instance, accessing a nonexistent route "/api/foo" raises a 404, which inside rails will ultimately be translated to an `ActionController::RoutingError`, which most likely will get rendered to a HTML error page. -Most APIs will enjoy preventing downstream handlers from handling errors. You may set the -`:cascade` option to `false` for the entire API or separately on specific `version` definitions, -which will remove the `X-Cascade: true` header from API responses. +Most APIs will enjoy preventing downstream handlers from handling errors. You may set the `:cascade` option to `false` for the entire API or separately on specific `version` definitions, which will remove the `X-Cascade: true` header from API responses. ```ruby cascade false @@ -2883,11 +2799,9 @@ version 'v1', using: :header, vendor: 'twitter', cascade: false ## Logging -`Grape::API` provides a `logger` method which by default will return an instance of the `Logger` -class from Ruby's standard library. +`Grape::API` provides a `logger` method which by default will return an instance of the `Logger` class from Ruby's standard library. -To log messages from within an endpoint, you need to define a helper to make the logger -available in the endpoint context. +To log messages from within an endpoint, you need to define a helper to make the logger available in the endpoint context. ```ruby class API < Grape::API @@ -2936,9 +2850,7 @@ For similar to Rails request logging try the [grape_logging](https://github.com/ ## API Formats -Your API can declare which content-types to support by using `content_type`. If you do not specify any, Grape will support -_XML_, _JSON_, _BINARY_, and _TXT_ content-types. The default format is `:txt`; you can change this with `default_format`. -Essentially, the two APIs below are equivalent. +Your API can declare which content-types to support by using `content_type`. If you do not specify any, Grape will support _XML_, _JSON_, _BINARY_, and _TXT_ content-types. The default format is `:txt`; you can change this with `default_format`. Essentially, the two APIs below are equivalent. ```ruby class Twitter::API < Grape::API @@ -2957,9 +2869,7 @@ class Twitter::API < Grape::API end ``` -If you declare any `content_type` whatsoever, the Grape defaults will be overridden. For example, the following API will only -support the `:xml` and `:rss` content-types, but not `:txt`, `:json`, or `:binary`. Importantly, this means the `:txt` -default format is not supported! So, make sure to set a new `default_format`. +If you declare any `content_type` whatsoever, the Grape defaults will be overridden. For example, the following API will only support the `:xml` and `:rss` content-types, but not `:txt`, `:json`, or `:binary`. Importantly, this means the `:txt` default format is not supported! So, make sure to set a new `default_format`. ```ruby class Twitter::API < Grape::API @@ -2970,8 +2880,7 @@ class Twitter::API < Grape::API end ``` -Serialization takes place automatically. For example, you do not have to call `to_json` in each JSON API endpoint -implementation. The response format (and thus the automatic serialization) is determined in the following order: +Serialization takes place automatically. For example, you do not have to call `to_json` in each JSON API endpoint implementation. The response format (and thus the automatic serialization) is determined in the following order: * Use the file extension, if specified. If the file is .json, choose the JSON format. * Use the value of the `format` parameter in the query string, if specified. * Use the format set by the `format` option, if specified. @@ -2994,18 +2903,13 @@ class MultipleFormatAPI < Grape::API end ``` -* `GET /hello` (with an `Accept: */*` header) does not have an extension or a `format` parameter, so it will respond with - JSON (the default format). +* `GET /hello` (with an `Accept: */*` header) does not have an extension or a `format` parameter, so it will respond with JSON (the default format). * `GET /hello.xml` has a recognized extension, so it will respond with XML. * `GET /hello?format=xml` has a recognized `format` parameter, so it will respond with XML. -* `GET /hello.xml?format=json` has a recognized extension (which takes precedence over the `format` parameter), so it will - respond with XML. -* `GET /hello.xls` (with an `Accept: */*` header) has an extension, but that extension is not recognized, so it will respond - with JSON (the default format). -* `GET /hello.xls` with an `Accept: application/xml` header has an unrecognized extension, but the `Accept` header - corresponds to a recognized format, so it will respond with XML. -* `GET /hello.xls` with an `Accept: text/plain` header has an unrecognized extension *and* an unrecognized `Accept` header, - so it will respond with JSON (the default format). +* `GET /hello.xml?format=json` has a recognized extension (which takes precedence over the `format` parameter), so it will respond with XML. +* `GET /hello.xls` (with an `Accept: */*` header) has an extension, but that extension is not recognized, so it will respond with JSON (the default format). +* `GET /hello.xls` with an `Accept: application/xml` header has an unrecognized extension, but the `Accept` header corresponds to a recognized format, so it will respond with XML. +* `GET /hello.xls` with an `Accept: text/plain` header has an unrecognized extension *and* an unrecognized `Accept` header, so it will respond with JSON (the default format). You can override this process explicitly by specifying `env['api.format']` in the API itself. For example, the following API will let you upload arbitrary files and return their contents as an attachment with the correct MIME type. @@ -3022,8 +2926,7 @@ class Twitter::API < Grape::API end ``` -You can have your API only respond to a single format with `format`. If you use this, the API will **not** respond to file -extensions other than specified in `format`. For example, consider the following API. +You can have your API only respond to a single format with `format`. If you use this, the API will **not** respond to file extensions other than specified in `format`. For example, consider the following API. ```ruby class SingleFormatAPI < Grape::API @@ -3038,14 +2941,10 @@ end * `GET /hello` will respond with JSON. * `GET /hello.json` will respond with JSON. * `GET /hello.xml`, `GET /hello.foobar`, or *any* other extension will respond with an HTTP 404 error code. -* `GET /hello?format=xml` will respond with an HTTP 406 error code, because the XML format specified by the request parameter - is not supported. -* `GET /hello` with an `Accept: application/xml` header will still respond with JSON, since it could not negotiate a - recognized content-type from the headers and JSON is the effective default. +* `GET /hello?format=xml` will respond with an HTTP 406 error code, because the XML format specified by the request parameter is not supported. +* `GET /hello` with an `Accept: application/xml` header will still respond with JSON, since it could not negotiate a recognized content-type from the headers and JSON is the effective default. -The formats apply to parsing, too. The following API will only respond to the JSON content-type and will not parse any other -input than `application/json`, `application/x-www-form-urlencoded`, `multipart/form-data`, `multipart/related` and -`multipart/mixed`. All other requests will fail with an HTTP 406 error code. +The formats apply to parsing, too. The following API will only respond to the JSON content-type and will not parse any other input than `application/json`, `application/x-www-form-urlencoded`, `multipart/form-data`, `multipart/related` and `multipart/mixed`. All other requests will fail with an HTTP 406 error code. ```ruby class Twitter::API < Grape::API @@ -3106,18 +3005,13 @@ Built-in formatters are the following. * `:serializable_hash`: use object's `serializable_hash` when available, otherwise fallback to `:json` * `:binary`: data will be returned "as is" -If a body is present in a request to an API, with a Content-Type header value that is of an unsupported type a -"415 Unsupported Media Type" error code will be returned by Grape. +If a body is present in a request to an API, with a Content-Type header value that is of an unsupported type a "415 Unsupported Media Type" error code will be returned by Grape. -Response statuses that indicate no content as defined by [Rack](https://github.com/rack) -[here](https://github.com/rack/rack/blob/master/lib/rack/utils.rb#L567) -will bypass serialization and the body entity - though there should be none - -will not be modified. +Response statuses that indicate no content as defined by [Rack](https://github.com/rack) [here](https://github.com/rack/rack/blob/master/lib/rack/utils.rb#L567) will bypass serialization and the body entity - though there should be none - will not be modified. ### JSONP -Grape supports JSONP via [Rack::JSONP](https://github.com/rack/rack-contrib), part of the -[rack-contrib](https://github.com/rack/rack-contrib) gem. Add `rack-contrib` to your `Gemfile`. +Grape supports JSONP via [Rack::JSONP](https://github.com/rack/rack-contrib), part of the [rack-contrib](https://github.com/rack/rack-contrib) gem. Add `rack-contrib` to your `Gemfile`. ```ruby require 'rack/contrib' @@ -3133,9 +3027,7 @@ end ### CORS -Grape supports CORS via [Rack::CORS](https://github.com/cyu/rack-cors), part of the -[rack-cors](https://github.com/cyu/rack-cors) gem. Add `rack-cors` to your `Gemfile`, -then use the middleware in your config.ru file. +Grape supports CORS via [Rack::CORS](https://github.com/cyu/rack-cors), part of the [rack-cors](https://github.com/cyu/rack-cors) gem. Add `rack-cors` to your `Gemfile`, then use the middleware in your config.ru file. ```ruby require 'rack/cors' @@ -3153,8 +3045,7 @@ run Twitter::API ## Content-type -Content-type is set by the formatter. You can override the content-type of the response at runtime -by setting the `Content-Type` header. +Content-type is set by the formatter. You can override the content-type of the response at runtime by setting the `Content-Type` header. ```ruby class API < Grape::API @@ -3167,16 +3058,12 @@ end ## API Data Formats -Grape accepts and parses input data sent with the POST and PUT methods as described in the Parameters -section above. It also supports custom data formats. You must declare additional content-types via -`content_type` and optionally supply a parser via `parser` unless a parser is already available within -Grape to enable a custom format. Such a parser can be a function or a class. +Grape accepts and parses input data sent with the POST and PUT methods as described in the Parameters section above. It also supports custom data formats. You must declare additional content-types via `content_type` and optionally supply a parser via `parser` unless a parser is already available within Grape to enable a custom format. Such a parser can be a function or a class. With a parser, parsed data is available "as-is" in `env['api.request.body']`. Without a parser, data is available "as-is" and in `env['api.request.input']`. -The following example is a trivial parser that will assign any input with the "text/custom" content-type -to `:value`. The parameter will be available via `params[:value]` inside the API call. +The following example is a trivial parser that will assign any input with the "text/custom" content-type to `:value`. The parameter will be available via `params[:value]` inside the API call. ```ruby module CustomParser @@ -3210,9 +3097,7 @@ Grape uses `JSON` and `ActiveSupport::XmlMini` for JSON and XML parsing by defau ## RESTful Model Representations -Grape supports a range of ways to present your data with some help from a generic `present` method, -which accepts two arguments: the object to be presented and the options associated with it. The options -hash may include `:with`, which defines the entity to expose. +Grape supports a range of ways to present your data with some help from a generic `present` method, which accepts two arguments: the object to be presented and the options associated with it. The options hash may include `:with`, which defines the entity to expose. ### Grape Entities @@ -3291,8 +3176,7 @@ The response will be } ``` -In addition to separately organizing entities, it may be useful to put them as namespaced -classes underneath the model they represent. +In addition to separately organizing entities, it may be useful to put them as namespaced classes underneath the model they represent. ```ruby class Status @@ -3306,11 +3190,7 @@ class Status end ``` -If you organize your entities this way, Grape will automatically detect the `Entity` class and -use it to present your models. In this example, if you added `present Status.new` to your endpoint, -Grape will automatically detect that there is a `Status::Entity` class and use that as the -representative entity. This can still be overridden by using the `:with` option or an explicit -`represents` call. +If you organize your entities this way, Grape will automatically detect the `Entity` class and use it to present your models. In this example, if you added `present Status.new` to your endpoint, Grape will automatically detect that there is a `Status::Entity` class and use that as the representative entity. This can still be overridden by using the `:with` option or an explicit `represents` call. You can present `hash` with `Grape::Presenters::Presenter` to keep things consistent. @@ -3343,15 +3223,11 @@ You can use [Roar](https://github.com/apotonick/roar) to render HAL or Collectio ### Rabl -You can use [Rabl](https://github.com/nesquena/rabl) templates with the help of the -[grape-rabl](https://github.com/ruby-grape/grape-rabl) gem, which defines a custom Grape Rabl -formatter. +You can use [Rabl](https://github.com/nesquena/rabl) templates with the help of the [grape-rabl](https://github.com/ruby-grape/grape-rabl) gem, which defines a custom Grape Rabl formatter. ### Active Model Serializers -You can use [Active Model Serializers](https://github.com/rails-api/active_model_serializers) serializers with the help of the -[grape-active_model_serializers](https://github.com/jrhe/grape-active_model_serializers) gem, which defines a custom Grape AMS -formatter. +You can use [Active Model Serializers](https://github.com/rails-api/active_model_serializers) serializers with the help of the [grape-active_model_serializers](https://github.com/jrhe/grape-active_model_serializers) gem, which defines a custom Grape AMS formatter. ## Sending Raw or No Data @@ -3391,9 +3267,7 @@ class API < Grape::API end ``` -You can also set the response to a file with `sendfile`. This works with the -[Rack::Sendfile](https://www.rubydoc.info/gems/rack/Rack/Sendfile) middleware to optimally send -the file through your web server software. +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 @@ -3437,9 +3311,7 @@ end ### Basic Auth -Grape has built-in Basic authentication (the given `block` -is executed in the context of the current `Endpoint`). Authentication -applies to the current namespace and any children, but not parents. +Grape has built-in Basic authentication (the given `block` is executed in the context of the current `Endpoint`). Authentication applies to the current namespace and any children, but not parents. ```ruby http_basic do |username, password| @@ -3450,16 +3322,13 @@ end ### Register custom middleware for authentication -Grape can use custom Middleware for authentication. How to implement these -Middleware have a look at `Rack::Auth::Basic` or similar implementations. - +Grape can use custom Middleware for authentication. How to implement these Middleware have a look at `Rack::Auth::Basic` or similar implementations. For registering a Middleware you need the following options: * `label` - the name for your authenticator to use it later * `MiddlewareClass` - the MiddlewareClass to use for authentication -* `option_lookup_proc` - A Proc with one Argument to lookup the options at -runtime (return value is an `Array` as Parameter for the Middleware). +* `option_lookup_proc` - A Proc with one Argument to lookup the options at runtime (return value is an `Array` as Parameter for the Middleware). Example: @@ -3531,10 +3400,7 @@ class MyAPI < Grape::API end ``` -The current endpoint responding to the request is `self` within the API block -or `env['api.endpoint']` elsewhere. The endpoint has some interesting properties, -such as `source` which gives you access to the original code block of the API -implementation. This can be particularly useful for building a logger middleware. +The current endpoint responding to the request is `self` within the API block or `env['api.endpoint']` elsewhere. The endpoint has some interesting properties, such as `source` which gives you access to the original code block of the API implementation. This can be particularly useful for building a logger middleware. ```ruby class ApiLogger < Grape::Middleware::Base @@ -3548,10 +3414,8 @@ end ## Before, After and Finally -Blocks can be executed before or after every API call, using `before`, `after`, -`before_validation` and `after_validation`. -If the API fails the `after` call will not be triggered, if you need code to execute for sure -use the `finally`. +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: @@ -3565,13 +3429,9 @@ Before and after callbacks execute in the following order: Steps 4, 5 and 6 only happen if validation succeeds. -If a request for a resource is made with an unsupported HTTP method (returning -HTTP 405) only `before` callbacks will be executed. The remaining callbacks will -be bypassed. +If a request for a resource is made with an unsupported HTTP method (returning HTTP 405) only `before` callbacks will be executed. The remaining callbacks will be bypassed. -If a request for a resource is made that triggers the built-in `OPTIONS` handler, -only `before` and `after` callbacks will be executed. The remaining callbacks will -be bypassed. +If a request for a resource is made that triggers the built-in `OPTIONS` handler, only `before` and `after` callbacks will be executed. The remaining callbacks will be bypassed. For example, using a simple `before` block to set a header. @@ -3716,11 +3576,7 @@ Instead of altering a response, you can also terminate and rewrite it from any c ## Anchoring -Grape by default anchors all request paths, which means that the request URL -should match from start to end to match, otherwise a `404 Not Found` is -returned. However, this is sometimes not what you want, because it is not always -known upfront what can be expected from the call. This is because Rack-mount by -default anchors requests to match from the start to the end, or not at all. +Grape by default anchors all request paths, which means that the request URL should match from start to end to match, otherwise a `404 Not Found` is returned. However, this is sometimes not what you want, because it is not always known upfront what can be expected from the call. This is because Rack-mount by default anchors requests to match from the start to the end, or not at all. Rails solves this problem by using a `anchor: false` option in your routes. In Grape this option can be used as well when a method is defined. @@ -3736,12 +3592,8 @@ class TwitterAPI < Grape::API end ``` -This will match all paths starting with '/statuses/'. There is one caveat though: -the `params[:status]` parameter only holds the first part of the request url. -Luckily this can be circumvented by using the described above syntax for path -specification and using the `PATH_INFO` Rack environment variable, using -`env['PATH_INFO']`. This will hold everything that comes after the '/statuses/' -part. +This will match all paths starting with '/statuses/'. There is one caveat though: the `params[:status]` parameter only holds the first part of the request url. +Luckily this can be circumvented by using the described above syntax for path specification and using the `PATH_INFO` Rack environment variable, using `env['PATH_INFO']`. This will hold everything that comes after the '/statuses/' part. ## Using Custom Middleware @@ -3950,8 +3802,7 @@ describe Twitter::API do end ``` -In Rails, HTTP request tests would go into the `spec/requests` group. You may want your API code to go into -`app/api` - you can match that layout under `spec` by adding the following in `spec/rails_helper.rb`. +In Rails, HTTP request tests would go into the `spec/requests` group. You may want your API code to go into `app/api` - you can match that layout under `spec` by adding the following in `spec/rails_helper.rb`. ```ruby RSpec.configure do |config| @@ -3985,10 +3836,7 @@ end ### Stubbing Helpers -Because helpers are mixed in based on the context when an endpoint is defined, it can -be difficult to stub or mock them for testing. The `Grape::Endpoint.before_each` method -can help by allowing you to define behavior on the endpoint that will run before every -request. +Because helpers are mixed in based on the context when an endpoint is defined, it can be difficult to stub or mock them for testing. The `Grape::Endpoint.before_each` method can help by allowing you to define behavior on the endpoint that will run before every request. ```ruby describe 'an endpoint that needs helpers stubbed' do @@ -4114,8 +3962,7 @@ Grape integrates with following third-party tools: ## Contributing to Grape -Grape is work of hundreds of contributors. You're encouraged to submit pull requests, propose -features and discuss issues. +Grape is work of hundreds of contributors. You're encouraged to submit pull requests, propose features and discuss issues. See [CONTRIBUTING](CONTRIBUTING.md). diff --git a/UPGRADING.md b/UPGRADING.md index b9877889e..912bbb41b 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -474,8 +474,7 @@ 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`. +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: @@ -855,8 +854,7 @@ See [#1114](https://github.com/ruby-grape/grape/pull/1114) for more information. #### Bypasses formatters when status code indicates no content -To be consistent with rack and it's handling of standard responses associated with no content, both default and custom formatters will now -be bypassed when processing responses for status codes defined [by rack](https://github.com/rack/rack/blob/master/lib/rack/utils.rb#L567) +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. @@ -1297,8 +1295,7 @@ As replacement can be used * `Grape::Middleware::Auth::Digest` => [`Rack::Auth::Digest::MD5`](https://github.com/rack/rack/blob/master/lib/rack/auth/digest/md5.rb) * `Grape::Middleware::Auth::OAuth2` => [warden-oauth2](https://github.com/opperator/warden-oauth2) or [rack-oauth2](https://github.com/nov/rack-oauth2) -If this is not possible you can extract the middleware files from [grape v0.7.0](https://github.com/ruby-grape/grape/tree/v0.7.0/lib/grape/middleware/auth) -and host these files within your application +If this is not possible you can extract the middleware files from [grape v0.7.0](https://github.com/ruby-grape/grape/tree/v0.7.0/lib/grape/middleware/auth) and host these files within your application See [#703](https://github.com/ruby-grape/Grape/pull/703) for more information. From 3f01d03b7a28b088a52313d8d264f29aeacd41fa Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Mon, 20 Nov 2023 18:03:22 -0500 Subject: [PATCH 183/304] method missing to_ary (#2370) * Add to_ary nil * Add changelog * Remove method_missing and warning * Update CHANGELOG.md * Add upgrading notes Replace route_params by params * Update README.md --- CHANGELOG.md | 1 + README.md | 6 +++--- UPGRADING.md | 8 ++++++++ lib/grape/router/route.rb | 37 ------------------------------------- spec/grape/api_spec.rb | 3 +-- 5 files changed, 13 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4f099314..40acedb2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ #### Fixes +* [#2370](https://github.com/ruby-grape/grape/pull/2370): Remove route_xyz method_missing deprecation - [@ericproulx](https://github.com/ericproulx). * [#2372](https://github.com/ruby-grape/grape/pull/2372): Fix `declared` method for hash params with overlapping names - [@jcagarcia](https://github.com/jcagarcia). * [#2373](https://github.com/ruby-grape/grape/pull/2373): Fix markdown files for following 1-line format - [@jcagarcia](https://github.com/jcagarcia). * Your contribution here. diff --git a/README.md b/README.md index 0e38b5663..e6b68bc9d 100644 --- a/README.md +++ b/README.md @@ -3352,7 +3352,7 @@ You can access the controller params, headers, and helpers through the context w Grape routes can be reflected at runtime. This can notably be useful for generating documentation. -Grape exposes arrays of API versions and compiled routes. Each route contains a `route_prefix`, `route_version`, `route_namespace`, `route_method`, `route_path` and `route_params`. You can add custom route settings to the route metadata with `route_setting`. +Grape exposes arrays of API versions and compiled routes. Each route contains a `prefix`, `version`, `namespace`, `method` and `params`. You can add custom route settings to the route metadata with `route_setting`. ```ruby class TwitterAPI < Grape::API @@ -3375,7 +3375,7 @@ TwitterAPI::routes[0].description # => 'Includes custom settings.' TwitterAPI::routes[0].settings[:custom] # => { key: 'value' } ``` -Note that `Route#route_xyz` methods have been deprecated since 0.15.0. +Note that `Route#route_xyz` methods have been deprecated since 0.15.0 and removed since 2.0.1. Please use `Route#xyz` instead. @@ -3395,7 +3395,7 @@ class MyAPI < Grape::API requires :id, type: Integer, desc: 'Identity.' end get 'params/:id' do - route.route_params[params[:id]] # yields the parameter description + route.params[params[:id]] # yields the parameter description end end ``` diff --git a/UPGRADING.md b/UPGRADING.md index 912bbb41b..4cb1d4c62 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,6 +1,14 @@ Upgrading Grape =============== +### Upgrading to >= 2.0.1 + +#### Grape::Router::Route.route_xxx methods have been removed + +- `route_method` is accessible through `request_method` +- `route_path` is accessible through `path` +- Any other `route_xyz` are accessible through `options[xyz]` + ### Upgrading to >= 2.0.0 #### Headers diff --git a/lib/grape/router/route.rb b/lib/grape/router/route.rb index d5600df1c..ea49bac78 100644 --- a/lib/grape/router/route.rb +++ b/lib/grape/router/route.rb @@ -3,13 +3,10 @@ require 'grape/router/pattern' require 'grape/router/attribute_translator' require 'forwardable' -require 'pathname' module Grape class Router class Route - 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, :options @@ -20,31 +17,6 @@ class Route 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) - if match - method_name = match.captures.last.to_sym - warn_route_methods(method_name, caller(1).shift) - @options[method_name] - else - super - end - end - - def respond_to_missing?(method_id, _) - ROUTE_ATTRIBUTE_REGEXP.match?(method_id.to_s) - end - - def route_method - warn_route_methods(:method, caller(1).shift, :request_method) - request_method - end - - def route_path - warn_route_methods(:path, caller(1).shift) - pattern.path - end - def initialize(method, pattern, **options) method_s = method.to_s method_upcase = Grape::Http::Headers.find_supported_method(method_s) || method_s.upcase @@ -77,15 +49,6 @@ def params(input = nil) parsed ? parsed.delete_if { |_, value| value.nil? }.symbolize_keys : {} end end - - private - - 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 - Grape.deprecator.warn("#{path}:#{line}: The route_xxx methods such as route_#{name} have been deprecated, please use #{expected}.") - end end end end diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index 161b561cb..b67dcbe24 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -3050,7 +3050,6 @@ def static expect(subject.routes.length).to eq(1) route = subject.routes.first expect(route.description).to eq('first method') - expect(route.route_foo).to be_nil expect(route.params).to eq({}) expect(route.options).to be_a(Hash) end @@ -3095,7 +3094,7 @@ def static get 'second' end expect(subject.routes.map do |route| - { description: route.description, foo: route.route_foo, params: route.params } + { description: route.description, foo: route.options[:foo], params: route.params } end).to eq [ { description: 'ns second', foo: 'bar', params: {} } ] From 103928adc7fa09846011a9e00b4e716f7dea0465 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Garc=C3=ADa=20del=20Canto?= Date: Fri, 24 Nov 2023 15:50:26 +0100 Subject: [PATCH 184/304] fix(#1922): Allow to use instance variables defined in the endpoints inside rescue_from (#2377) * fix(#1922): Allow to use instance variables defined in the endpoints when rescue_from * fix(#1922): Update CHANGELOG and running rubocop * fix(#1922): Updating UPGRADING and README files explaining the instance variables behavior. Extra tests added * fix(#1922): Send endpoint parameter as the last param of the run_rescue_handler method * fix(#1922): Adding short before/after example to UPGRADING * fix(#1922): Fixing CHANGELOG format style --- CHANGELOG.md | 1 + README.md | 37 +++++++++++++++++++++++ UPGRADING.md | 44 ++++++++++++++++++++++++++- lib/grape/dsl/inside_route.rb | 17 +++++++++++ lib/grape/middleware/error.rb | 16 +++++++--- spec/grape/api_spec.rb | 57 +++++++++++++++++++++++++++++++++++ 6 files changed, 167 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40acedb2a..c1fa986c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ #### Features * [#2371](https://github.com/ruby-grape/grape/pull/2371): Use a param value as the `default` value of other param - [@jcagarcia](https://github.com/jcagarcia). +* [#2377](https://github.com/ruby-grape/grape/pull/2377): Allow to use instance variables values inside `rescue_from` - [@jcagarcia](https://github.com/jcagarcia). * Your contribution here. #### Fixes diff --git a/README.md b/README.md index e6b68bc9d..f599d5bc4 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,7 @@ - [Current Route and Endpoint](#current-route-and-endpoint) - [Before, After and Finally](#before-after-and-finally) - [Anchoring](#anchoring) +- [Instance Variables](#instance-variables) - [Using Custom Middleware](#using-custom-middleware) - [Grape Middleware](#grape-middleware) - [Rails Middleware](#rails-middleware) @@ -3595,6 +3596,42 @@ end This will match all paths starting with '/statuses/'. There is one caveat though: the `params[:status]` parameter only holds the first part of the request url. Luckily this can be circumvented by using the described above syntax for path specification and using the `PATH_INFO` Rack environment variable, using `env['PATH_INFO']`. This will hold everything that comes after the '/statuses/' part. +## Instance Variables + +You can use instance variables to pass information across the various stages of a request. An instance variable set within a `before` validator is accessible within the endpoint's code and can also be utilized within the `rescue_from` handler. + +```ruby +class TwitterAPI < Grape::API + before do + @var = 1 + end + + get '/' do + puts @var # => 1 + raise + end + + rescue_from :all do + puts @var # => 1 + end +end +``` + +The values of instance variables cannot be shared among various endpoints within the same API. This limitation arises due to Grape generating a new instance for each request made. Consequently, instance variables set within an endpoint during one request differ from those set during a subsequent request, as they exist within separate instances. + +```ruby +class TwitterAPI < Grape::API + get '/first' do + @var = 1 + puts @var # => 1 + end + + get '/second' do + puts @var # => nil + end +end +``` + ## Using Custom Middleware ### Grape Middleware diff --git a/UPGRADING.md b/UPGRADING.md index 4cb1d4c62..fb53565d0 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,7 +1,7 @@ Upgrading Grape =============== -### Upgrading to >= 2.0.1 +### Upgrading to >= 2.1.0 #### Grape::Router::Route.route_xxx methods have been removed @@ -9,6 +9,48 @@ Upgrading Grape - `route_path` is accessible through `path` - Any other `route_xyz` are accessible through `options[xyz]` +#### Instance variables scope + +Due to the changes done in [#2377](https://github.com/ruby-grape/grape/pull/2377), the instance variables defined inside each of the endpoints (or inside a `before` validator) are now accessible inside the `rescue_from`. The behavior of the instance variables was undefined until `2.1.0`. + +If you were using the same variable name defined inside an endpoint or `before` validator inside a `rescue_from` handler, you need to take in mind that you can start getting different values or you can be overriding values. + +Before: +```ruby +class TwitterAPI < Grape::API + before do + @var = 1 + end + + get '/' do + puts @var # => 1 + raise + end + + rescue_from :all do + puts @var # => nil + end +end +``` + +After: +```ruby +class TwitterAPI < Grape::API + before do + @var = 1 + end + + get '/' do + puts @var # => 1 + raise + end + + rescue_from :all do + puts @var # => 1 + end +end +``` + ### Upgrading to >= 2.0.0 #### Headers diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index 7ebd6aab8..0286595b8 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -167,6 +167,23 @@ def error!(message, status = nil, additional_headers = nil) throw :error, message: message, status: self.status, headers: headers end + # Creates a Rack response based on the provided message, status, and headers. + # The content type in the headers is set to the default content type unless provided. + # The message is HTML-escaped if the content type is 'text/html'. + # + # @param message [String] The content of the response. + # @param status [Integer] The HTTP status code. + # @params headers [Hash] (optional) Headers for the response + # (default: {Rack::CONTENT_TYPE => content_type}). + # + # Returns: + # A Rack::Response object containing the specified message, status, and headers. + # + def rack_response(message, status = 200, headers = { Rack::CONTENT_TYPE => content_type }) + message = ERB::Util.html_escape(message) if headers[Rack::CONTENT_TYPE] == 'text/html' + Rack::Response.new([message], Rack::Utils.status_code(status), headers) + end + # Redirect to a new url. # # @param url [String] The url to be redirect. diff --git a/lib/grape/middleware/error.rb b/lib/grape/middleware/error.rb index 05fc0312f..02db9aae0 100644 --- a/lib/grape/middleware/error.rb +++ b/lib/grape/middleware/error.rb @@ -46,7 +46,7 @@ def call!(env) rescue_handler_for_any_class(e.class) || raise - run_rescue_handler(handler, e) + run_rescue_handler(handler, e, @env[Grape::Env::API_ENDPOINT]) end end @@ -119,21 +119,29 @@ def rescue_handler_for_any_class(klass) options[:all_rescue_handler] || :default_rescue_handler end - def run_rescue_handler(handler, error) + def run_rescue_handler(handler, error, endpoint) if handler.instance_of?(Symbol) raise NoMethodError, "undefined method '#{handler}'" unless respond_to?(handler) handler = public_method(handler) end - response = handler.arity.zero? ? instance_exec(&handler) : instance_exec(error, &handler) + response = (catch(:error) do + handler.arity.zero? ? endpoint.instance_exec(&handler) : endpoint.instance_exec(error, &handler) + end) + + response = error!(response[:message], response[:status], response[:headers]) if error?(response) if response.is_a?(Rack::Response) response else - run_rescue_handler(:default_rescue_handler, Grape::Exceptions::InvalidResponse.new) + run_rescue_handler(:default_rescue_handler, Grape::Exceptions::InvalidResponse.new, endpoint) end end + + def error?(response) + response.is_a?(Hash) && response[:message] && response[:status] && response[:headers] + end end end end diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index b67dcbe24..f2ca6d55d 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -4352,4 +4352,61 @@ def uniqe_id_route expect(last_response.body).to be_eql('1-2') end end + + context 'instance variables' do + context 'when setting instance variables in a before validation' do + it 'is accessible inside the endpoint' do + expected_instance_variable_value = 'wadus' + + subject.before do + @my_var = expected_instance_variable_value + end + + subject.get('/') do + { my_var: @my_var }.to_json + end + + get '/' + expect(last_response.body).to eq({ my_var: expected_instance_variable_value }.to_json) + end + end + + context 'when setting instance variables inside the endpoint code' do + it 'is accessible inside the rescue_from handler' do + expected_instance_variable_value = 'wadus' + + subject.rescue_from(:all) do + body = { my_var: @my_var } + error!(body, 400) + end + + subject.get('/') do + @my_var = expected_instance_variable_value + raise + end + + get '/' + expect(last_response.status).to be 400 + expect(last_response.body).to eq({ my_var: expected_instance_variable_value }.to_json) + end + + it 'is NOT available in other endpoints of the same api' do + expected_instance_variable_value = 'wadus' + + subject.get('/first') do + @my_var = expected_instance_variable_value + { my_var: @my_var }.to_json + end + + subject.get('/second') do + { my_var: @my_var }.to_json + end + + get '/first' + expect(last_response.body).to eq({ my_var: expected_instance_variable_value }.to_json) + get '/second' + expect(last_response.body).to eq({ my_var: nil }.to_json) + end + end + end end From e922b1b1b10fd2cc7e74a864a77f309e096304f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Garc=C3=ADa=20del=20Canto?= Date: Sun, 26 Nov 2023 16:34:18 +0100 Subject: [PATCH 185/304] fix(#2350): Update `recognize_paths` taking into account the `route_param` type (#2379) * fix(#2350): Use musterman-grape 1.1.0 for recognize_paths taking into account the route_param type * fix(#2350): Updating wording and passing rubocop --- CHANGELOG.md | 1 + README.md | 27 +++++++++++++ UPGRADING.md | 44 +++++++++++++++++++++ grape.gemspec | 2 +- lib/grape/router/pattern.rb | 2 + spec/grape/api/recognize_path_spec.rb | 55 +++++++++++++++++++++++++++ spec/grape/api_spec.rb | 23 +++++++++-- 7 files changed, 150 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1fa986c2..bdda7ab3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * [#2371](https://github.com/ruby-grape/grape/pull/2371): Use a param value as the `default` value of other param - [@jcagarcia](https://github.com/jcagarcia). * [#2377](https://github.com/ruby-grape/grape/pull/2377): Allow to use instance variables values inside `rescue_from` - [@jcagarcia](https://github.com/jcagarcia). +* [#2379](https://github.com/ruby-grape/grape/pull/2379): `recognize_path` now takes into account the `route_param` type - [@jcagarcia](https://github.com/jcagarcia). * Your contribution here. #### Fixes diff --git a/README.md b/README.md index f599d5bc4..4116daf7d 100644 --- a/README.md +++ b/README.md @@ -2408,6 +2408,33 @@ end API.recognize_path '/statuses' ``` +Since version `2.1.0`, the `recognize_path` method takes into account the parameters type to determine which endpoint should match with given path. + +```ruby +class Books < Grape::API + resource :books do + route_param :id, type: Integer do + # GET /books/:id + get do + #... + end + end + + resource :share do + # POST /books/share + post do + # .... + end + end + end +end + +API.recognize_path '/books/1' # => /books/:id +API.recognize_path '/books/share' # => /books/share +API.recognize_path '/books/other' # => nil +``` + + ## Allowed Methods When you add a `GET` route for a resource, a route for the `HEAD` method will also be added automatically. You can disable this behavior with `do_not_route_head!`. diff --git a/UPGRADING.md b/UPGRADING.md index fb53565d0..f8cdb93b3 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -51,6 +51,50 @@ class TwitterAPI < Grape::API end ``` +#### Recognizing Path + +Grape now considers the types of the configured `route_params` in order to determine the endpoint that matches with the performed request. + +So taking into account this `Grape::API` class + +```ruby +class Books < Grape::API + resource :books do + route_param :id, type: Integer do + # GET /books/:id + get do + #... + end + end + + resource :share do + # POST /books/share + post do + # .... + end + end + end +end +``` + +Before: +```ruby +API.recognize_path '/books/1' # => /books/:id +API.recognize_path '/books/share' # => /books/:id +API.recognize_path '/books/other' # => /books/:id +``` + +After: +```ruby +API.recognize_path '/books/1' # => /books/:id +API.recognize_path '/books/share' # => /books/share +API.recognize_path '/books/other' # => nil +``` + +This implies that before this changes, when you performed `/books/other` and it matched with the `/books/:id` endpoint, you get a `400 Bad Request` response because the type of the provided `:id` param was not an `Integer`. However, after upgrading to version `2.1.0` you will get a `404 Not Found` response, because there is not a defined endpoint that matches with `/books/other`. + +See [#2379](https://github.com/ruby-grape/grape/pull/2379) for more information. + ### Upgrading to >= 2.0.0 #### Headers diff --git a/grape.gemspec b/grape.gemspec index 9e53ddea7..7dbc25a0e 100644 --- a/grape.gemspec +++ b/grape.gemspec @@ -23,7 +23,7 @@ Gem::Specification.new do |s| s.add_runtime_dependency 'activesupport', '>= 5' 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 'mustermann-grape', '~> 1.1.0' s.add_runtime_dependency 'rack', '>= 1.3.0' s.add_runtime_dependency 'rack-accept' diff --git a/lib/grape/router/pattern.rb b/lib/grape/router/pattern.rb index a23998048..6d7047773 100644 --- a/lib/grape/router/pattern.rb +++ b/lib/grape/router/pattern.rb @@ -28,8 +28,10 @@ def initialize(pattern, **options) def pattern_options(options) capture = extract_capture(**options) + params = options[:params] options = DEFAULT_PATTERN_OPTIONS.dup options[:capture] = capture if capture.present? + options[:params] = params if params.present? options end diff --git a/spec/grape/api/recognize_path_spec.rb b/spec/grape/api/recognize_path_spec.rb index b3e9afa96..04b0b48b8 100644 --- a/spec/grape/api/recognize_path_spec.rb +++ b/spec/grape/api/recognize_path_spec.rb @@ -17,5 +17,60 @@ subject.get {} expect(subject.recognize_path('/bar/1234')).to be_nil end + + context 'when parametrized route with type specified together with a static route' do + subject do + Class.new(described_class) do + resource :books do + route_param :id, type: Integer do + get do + end + + resource :loans do + route_param :loan_id, type: Integer do + get do + end + end + + resource :print do + post do + end + end + end + end + + resource :share do + post do + end + end + end + end + end + + it 'recognizes the static route when the parameter does not match with the specified type' do + actual = subject.recognize_path('/books/share').routes[0].origin + expect(actual).to eq('/books/share') + end + + it 'does not recognize any endpoint when there is not other endpoint that matches with the requested path' do + actual = subject.recognize_path('/books/other') + expect(actual).to be_nil + end + + it 'recognizes the parametrized route when the parameter matches with the specified type' do + actual = subject.recognize_path('/books/1').routes[0].origin + expect(actual).to eq('/books/:id') + end + + it 'recognizes the static nested route when the parameter does not match with the specified type' do + actual = subject.recognize_path('/books/1/loans/print').routes[0].origin + expect(actual).to eq('/books/:id/loans/print') + end + + it 'recognizes the nested parametrized route when the parameter matches with the specified type' do + actual = subject.recognize_path('/books/1/loans/33').routes[0].origin + expect(actual).to eq('/books/:id/loans/:loan_id') + end + end end end diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index f2ca6d55d..9826dd867 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -1132,7 +1132,7 @@ class DummyFormatClass d = double('after mock') subject.params do - requires :id, type: Integer + requires :id, type: Integer, values: [1, 2, 3] end subject.resource ':id' do before { a.do_something! } @@ -1151,9 +1151,9 @@ class DummyFormatClass expect(c).to receive(:do_something!).exactly(0).times expect(d).to receive(:do_something!).exactly(0).times - get '/abc' + get '/4' expect(last_response.status).to be 400 - expect(last_response.body).to eql 'id is invalid' + expect(last_response.body).to eql 'id does not have a valid value' end it 'calls filters in the correct order' do @@ -4408,5 +4408,22 @@ def uniqe_id_route expect(last_response.body).to eq({ my_var: nil }.to_json) end end + + context 'when set type to a route_param' do + context 'and the param does not match' do + it 'returns a 404 response' do + subject.namespace :books do + route_param :id, type: Integer do + get do + params[:id] + end + end + end + + get '/books/other' + expect(last_response.status).to be 404 + end + end + end end end From afdeb6681ac718591ef93ea6735fce021ef22471 Mon Sep 17 00:00:00 2001 From: Juan Carlos Garcia Date: Tue, 28 Nov 2023 22:51:49 +0100 Subject: [PATCH 186/304] doc(#2380): Updating versioning section inside the README --- README.md | 80 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 70 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 4116daf7d..3c3105c8a 100644 --- a/README.md +++ b/README.md @@ -30,10 +30,11 @@ - [Remounting](#remounting) - [Mount Configuration](#mount-configuration) - [Versioning](#versioning) - - [Path](#path) - - [Header](#header) - - [Accept-Version Header](#accept-version-header) - - [Param](#param) + - [Strategies](#strategies) + - [Path](#path) + - [Header](#header) + - [Accept-Version Header](#accept-version-header) + - [Param](#param) - [Describing Methods](#describing-methods) - [Configuration](#configuration) - [Parameters](#parameters) @@ -547,10 +548,69 @@ end ## Versioning -There are four strategies in which clients can reach your API's endpoints: `:path`, -`:header`, `:accept_version_header` and `:param`. The default strategy is `:path`. +You have the option to provide various versions of your API by establishing a separate `Grape::API` class for each offered version and then integrating them into a primary `Grape::API` class. Ensure that newer versions are mounted before older ones. The default approach to versioning directs the request to the subsequent Rack middleware if a specific version is not found. -### Path +```ruby +require 'v1' +require 'v2' +require 'v3' +class App < Grape::API + mount V3 + mount V2 + mount V1 +end +``` + +To maintain the same endpoints from earlier API versions without rewriting them, you can indicate multiple versions within the previous API versions. + +```ruby +class V1 < Grape::API + version 'v1', 'v2', 'v3' + + get '/foo' do + # your code for GET /foo + end + + get '/other' do + # your code for GET /other + end +end + +class V2 < Grape::API + version 'v2', 'v3' + + get '/var' do + # your code for GET /var + end +end + +class V3 < Grape::API + version 'v3' + + get '/foo' do + # your new code for GET /foo + end +end +``` + +Using the example provided, the subsequent endpoints will be accessible across various versions: + +```shell +GET /v1/foo +GET /v1/other +GET /v2/foo # => Same behavior as v1 +GET /v2/other # => Same behavior as v1 +GET /v2/var # => New endpoint not available in v1 +GET /v3/foo # => Different behavior to v1 and v2 +GET /v3/other # => Same behavior as v1 and v2 +GET /v3/var # => Same behavior as v2 +``` + +There are four strategies in which clients can reach your API's endpoints: `:path`, `:header`, `:accept_version_header` and `:param`. The default strategy is `:path`. + +### Strategies + +#### Path ```ruby version 'v1', using: :path @@ -560,7 +620,7 @@ Using this versioning strategy, clients should pass the desired version in the U curl http://localhost:9292/v1/statuses/public_timeline -### Header +#### Header ```ruby version 'v1', using: :header, vendor: 'twitter' @@ -586,7 +646,7 @@ Grape will evaluate the relative quality preference included in Accept headers a curl -H "Accept: text/xml;q=0.8, application/json;q=0.9" localhost:1234/resource -### Accept-Version Header +#### Accept-Version Header ```ruby version 'v1', using: :accept_version_header @@ -598,7 +658,7 @@ Using this versioning strategy, clients should pass the desired version in the H By default, the first matching version is used when no `Accept-Version` header is supplied. This behavior is similar to routing in Rails. To circumvent this default behavior, one could use the `:strict` option. When this option is set to `true`, a `406 Not Acceptable` error is returned when no correct `Accept` header is supplied and the `:cascade` option is set to `false`. Otherwise a `404 Not Found` error is returned by Rack if no other route matches. -### Param +#### Param ```ruby version 'v1', using: :param From 4e3b5fd544dcdeb30b11fa2d06a7b8d101748e80 Mon Sep 17 00:00:00 2001 From: Andrei Subbota Date: Wed, 6 Dec 2023 16:18:20 +0100 Subject: [PATCH 187/304] Fix ValuesValidator for params wrapped in `with` block (#2382) * Fix values validator when params wrapped by with block * Update CHANGELOG --- CHANGELOG.md | 1 + .../validations/validators/values_validator.rb | 7 ++++++- spec/grape/validations/validators/values_spec.rb | 16 ++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bdda7ab3b..6ab7a42b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ * [#2370](https://github.com/ruby-grape/grape/pull/2370): Remove route_xyz method_missing deprecation - [@ericproulx](https://github.com/ericproulx). * [#2372](https://github.com/ruby-grape/grape/pull/2372): Fix `declared` method for hash params with overlapping names - [@jcagarcia](https://github.com/jcagarcia). * [#2373](https://github.com/ruby-grape/grape/pull/2373): Fix markdown files for following 1-line format - [@jcagarcia](https://github.com/jcagarcia). +* [#2382](https://github.com/ruby-grape/grape/pull/2382): Fix values validator for params wrapped in `with` block - [@numbata](https://github.com/numbata). * Your contribution here. ### 2.0.0 (2023/11/11) diff --git a/lib/grape/validations/validators/values_validator.rb b/lib/grape/validations/validators/values_validator.rb index 3be7d609b..8475ffb82 100644 --- a/lib/grape/validations/validators/values_validator.rb +++ b/lib/grape/validations/validators/values_validator.rb @@ -85,7 +85,12 @@ def except_message end def required_for_root_scope? - @required && @scope.root? + return false unless @required + + scope = @scope + scope = scope.parent while scope.lateral? + + scope.root? end def validation_exception(attr_name, message) diff --git a/spec/grape/validations/validators/values_spec.rb b/spec/grape/validations/validators/values_spec.rb index d8ef17c95..5d8daab91 100644 --- a/spec/grape/validations/validators/values_spec.rb +++ b/spec/grape/validations/validators/values_spec.rb @@ -261,6 +261,13 @@ def even?(value) optional :name, type: String, values: %w[a b], allow_blank: true end get '/allow_blank' + + params do + with(type: String) do + requires :type, values: ValuesModel.values + end + end + get 'values_wrapped_by_with_block' end end @@ -730,4 +737,13 @@ def app end end end + + context 'when wrapped by with block' do + it 'rejects an invalid value' do + get 'values_wrapped_by_with_block' + + expect(last_response.status).to eq 400 + expect(last_response.body).to eq({ error: 'type is missing, type does not have a valid value' }.to_json) + end + end end From 045679712a5f7b323882f883400fc033d37d1879 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Tue, 12 Dec 2023 02:49:02 +0100 Subject: [PATCH 188/304] Use regex block instead of if (#2383) * Use regex block instead of if * fix rubocop space + CHANGELOG.md --------- Co-authored-by: Daniel (dB.) Doubrovkine --- CHANGELOG.md | 3 ++- lib/grape/router.rb | 11 ++--------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ab7a42b3..c91454a95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ * [#2371](https://github.com/ruby-grape/grape/pull/2371): Use a param value as the `default` value of other param - [@jcagarcia](https://github.com/jcagarcia). * [#2377](https://github.com/ruby-grape/grape/pull/2377): Allow to use instance variables values inside `rescue_from` - [@jcagarcia](https://github.com/jcagarcia). -* [#2379](https://github.com/ruby-grape/grape/pull/2379): `recognize_path` now takes into account the `route_param` type - [@jcagarcia](https://github.com/jcagarcia). +* [#2379](https://github.com/ruby-grape/grape/pull/2379): Take into account the `route_param` type in `recognize_path` - [@jcagarcia](https://github.com/jcagarcia). +* [#2383](https://github.com/ruby-grape/grape/pull/2383): Use regex block instead of if - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/lib/grape/router.rb b/lib/grape/router.rb index 691889cd6..3c4142b38 100644 --- a/lib/grape/router.rb +++ b/lib/grape/router.rb @@ -141,18 +141,11 @@ def default_response end def match?(input, method) - current_regexp = @optimized_map[method] - return unless current_regexp.match(input) - - last_match = Regexp.last_match - @map[method].detect { |route| last_match["_#{route.index}"] } + @optimized_map[method].match(input) { |m| @map[method].detect { |route| m["_#{route.index}"] } } end def greedy_match?(input) - return unless @union.match(input) - - last_match = Regexp.last_match - @neutral_map.detect { |route| last_match["_#{route.index}"] } + @union.match(input) { |m| @neutral_map.detect { |route| m["_#{route.index}"] } } end def call_with_allow_headers(env, route) From e37831c3d2033395579470a07895fc503dfcaabe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Garc=C3=ADa=20del=20Canto?= Date: Tue, 12 Dec 2023 15:06:29 +0100 Subject: [PATCH 189/304] fix(#1975): Allow to use `before/after/rescue_from` methods in any order when using `mount` (#2384) * fix(#1975): Allow to use `before/after/rescue_from` methods in any order when using `mount` * fix(#1975): Apply suggestions --- .rubocop_todo.yml | 9 + CHANGELOG.md | 1 + README.md | 14 +- lib/grape/api.rb | 13 ++ lib/grape/dsl/routing.rb | 20 +- .../grape/api/mount_and_helpers_order_spec.rb | 171 ++++++++++++++++++ spec/grape/api/mount_and_rescue_from_spec.rb | 80 ++++++++ 7 files changed, 305 insertions(+), 3 deletions(-) create mode 100644 spec/grape/api/mount_and_helpers_order_spec.rb create mode 100644 spec/grape/api/mount_and_rescue_from_spec.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 57e7a83bd..cffa61681 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -27,6 +27,8 @@ Lint/ConstantDefinitionInBlock: Exclude: - 'spec/grape/api/defines_boolean_in_params_spec.rb' - 'spec/grape/api/inherited_helpers_spec.rb' + - 'spec/grape/api/mount_and_helpers_order_spec.rb' + - 'spec/grape/api/mount_and_rescue_from_spec.rb' - 'spec/grape/api/nested_helpers_spec.rb' - 'spec/grape/api/patch_method_helpers_spec.rb' - 'spec/grape/api_spec.rb' @@ -235,6 +237,8 @@ RSpec/FilePath: - 'spec/grape/api/documentation_spec.rb' - 'spec/grape/api/inherited_helpers_spec.rb' - 'spec/grape/api/invalid_format_spec.rb' + - 'spec/grape/api/mount_and_helpers_order_spec.rb' + - 'spec/grape/api/mount_and_rescue_from_spec.rb' - 'spec/grape/api/namespace_parameters_in_route_spec.rb' - 'spec/grape/api/nested_helpers_spec.rb' - 'spec/grape/api/optional_parameters_in_route_spec.rb' @@ -289,6 +293,7 @@ RSpec/IndexedLet: # Configuration parameters: AssignmentOnly. RSpec/InstanceVariable: Exclude: + - 'spec/grape/api/mount_and_helpers_order_spec.rb' - 'spec/grape/api_spec.rb' - 'spec/grape/endpoint_spec.rb' - 'spec/grape/middleware/base_spec.rb' @@ -301,6 +306,8 @@ RSpec/LeakyConstantDeclaration: Exclude: - 'spec/grape/api/defines_boolean_in_params_spec.rb' - 'spec/grape/api/inherited_helpers_spec.rb' + - 'spec/grape/api/mount_and_helpers_order_spec.rb' + - 'spec/grape/api/mount_and_rescue_from_spec.rb' - 'spec/grape/api/nested_helpers_spec.rb' - 'spec/grape/api/patch_method_helpers_spec.rb' - 'spec/grape/api_spec.rb' @@ -343,6 +350,8 @@ RSpec/MultipleExpectations: - 'spec/grape/api/deeply_included_options_spec.rb' - 'spec/grape/api/defines_boolean_in_params_spec.rb' - 'spec/grape/api/invalid_format_spec.rb' + - 'spec/grape/api/mount_and_helpers_order_spec.rb' + - 'spec/grape/api/mount_and_rescue_from_spec.rb' - 'spec/grape/api/namespace_parameters_in_route_spec.rb' - 'spec/grape/api/optional_parameters_in_route_spec.rb' - 'spec/grape/api/parameters_modification_spec.rb' diff --git a/CHANGELOG.md b/CHANGELOG.md index c91454a95..c715cba46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * [#2377](https://github.com/ruby-grape/grape/pull/2377): Allow to use instance variables values inside `rescue_from` - [@jcagarcia](https://github.com/jcagarcia). * [#2379](https://github.com/ruby-grape/grape/pull/2379): Take into account the `route_param` type in `recognize_path` - [@jcagarcia](https://github.com/jcagarcia). * [#2383](https://github.com/ruby-grape/grape/pull/2383): Use regex block instead of if - [@ericproulx](https://github.com/ericproulx). +* [#2384](https://github.com/ruby-grape/grape/pull/2384): Allow to use `before/after/rescue_from` methods in any order when using `mount` - [@jcagarcia](https://github.com/jcagarcia). * Your contribution here. #### Fixes diff --git a/README.md b/README.md index 3c3105c8a..bc2509cf5 100644 --- a/README.md +++ b/README.md @@ -408,7 +408,7 @@ class Twitter::API < Grape::API end ``` -Keep in mind such declarations as `before/after/rescue_from` must be placed before `mount` in a case where they should be inherited. +Declarations as `before/after/rescue_from` can be placed before or after `mount`. In any case they will be inherited. ```ruby class Twitter::API < Grape::API @@ -416,8 +416,20 @@ class Twitter::API < Grape::API header 'X-Base-Header', 'will be defined for all APIs that are mounted below' end + rescue_from :all do + error!({ "error" => "Internal Server Error" }, 500) + end + mount Twitter::Users mount Twitter::Search + + after do + clean_cache! + end + + rescue_from ZeroDivisionError do + error!({ "error" => "Not found" }, 404) + end end ``` diff --git a/lib/grape/api.rb b/lib/grape/api.rb index 1fc057bfb..ef6cb5cf3 100644 --- a/lib/grape/api.rb +++ b/lib/grape/api.rb @@ -150,6 +150,19 @@ def add_setup(method, *args, &block) @instances.each do |instance| last_response = replay_step_on(instance, setup_step) end + + # Updating all previously mounted classes in the case that new methods have been executed. + if method != :mount && @setup.any? + previous_mount_steps = @setup.select { |step| step[:method] == :mount } + previous_mount_steps.each do |mount_step| + refresh_mount_step = mount_step.merge(method: :refresh_mounted_api) + @setup += [refresh_mount_step] + @instances.each do |instance| + replay_step_on(instance, refresh_mount_step) + end + end + end + last_response end diff --git a/lib/grape/dsl/routing.rb b/lib/grape/dsl/routing.rb index 158db99f5..a422c34d0 100644 --- a/lib/grape/dsl/routing.rb +++ b/lib/grape/dsl/routing.rb @@ -85,8 +85,8 @@ 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 }) + opts_with = opts.any? ? opts.first[:with] : {} + mount({ app.mount_instance(configuration: opts_with) => path }, *opts) next end in_setting = inheritable_setting @@ -103,6 +103,15 @@ def mount(mounts, *opts) change! end + # When trying to mount multiple times the same endpoint, remove the previous ones + # from the list of endpoints if refresh_already_mounted parameter is true + refresh_already_mounted = opts.any? ? opts.first[:refresh_already_mounted] : false + if refresh_already_mounted && !endpoints.empty? + endpoints.delete_if do |endpoint| + endpoint.options[:app].to_s == app.to_s + end + end + endpoints << Grape::Endpoint.new( in_setting, method: :any, @@ -225,6 +234,13 @@ def route_param(param, options = {}, &block) def versions @versions ||= [] end + + private + + def refresh_mounted_api(mounts, *opts) + opts << { refresh_already_mounted: true } + mount(mounts, *opts) + end end end end diff --git a/spec/grape/api/mount_and_helpers_order_spec.rb b/spec/grape/api/mount_and_helpers_order_spec.rb new file mode 100644 index 000000000..a00423a14 --- /dev/null +++ b/spec/grape/api/mount_and_helpers_order_spec.rb @@ -0,0 +1,171 @@ +# frozen_string_literal: true + +describe Grape::API do + def app + subject + end + + describe 'rescue_from' do + context 'when the API is mounted AFTER defining the class rescue_from handler' do + class APIRescueFrom < Grape::API + rescue_from :all do + error!({ type: 'all' }, 404) + end + + get do + { count: 1 / 0 } + end + end + + class MainRescueFromAfter < Grape::API + rescue_from ZeroDivisionError do + error!({ type: 'zero' }, 500) + end + + mount APIRescueFrom + end + + subject { MainRescueFromAfter } + + it 'is rescued by the rescue_from ZeroDivisionError handler from Main class' do + get '/' + + expect(last_response.status).to eq(500) + expect(last_response.body).to eq({ type: 'zero' }.to_json) + end + end + + context 'when the API is mounted BEFORE defining the class rescue_from handler' do + class APIRescueFrom < Grape::API + rescue_from :all do + error!({ type: 'all' }, 404) + end + + get do + { count: 1 / 0 } + end + end + + class MainRescueFromBefore < Grape::API + mount APIRescueFrom + + rescue_from ZeroDivisionError do + error!({ type: 'zero' }, 500) + end + end + + subject { MainRescueFromBefore } + + it 'is rescued by the rescue_from ZeroDivisionError handler from Main class' do + get '/' + + expect(last_response.status).to eq(500) + expect(last_response.body).to eq({ type: 'zero' }.to_json) + end + end + end + + describe 'before' do + context 'when the API is mounted AFTER defining the before helper' do + class APIBeforeHandler < Grape::API + get do + { count: @count }.to_json + end + end + + class MainBeforeHandlerAfter < Grape::API + before do + @count = 1 + end + + mount APIBeforeHandler + end + + subject { MainBeforeHandlerAfter } + + it 'is able to access the variables defined in the before helper' do + get '/' + + expect(last_response.status).to eq(200) + expect(last_response.body).to eq({ count: 1 }.to_json) + end + end + + context 'when the API is mounted BEFORE defining the before helper' do + class APIBeforeHandler < Grape::API + get do + { count: @count }.to_json + end + end + + class MainBeforeHandlerBefore < Grape::API + mount APIBeforeHandler + + before do + @count = 1 + end + end + + subject { MainBeforeHandlerBefore } + + it 'is able to access the variables defined in the before helper' do + get '/' + + expect(last_response.status).to eq(200) + expect(last_response.body).to eq({ count: 1 }.to_json) + end + end + end + + describe 'after' do + context 'when the API is mounted AFTER defining the after handler' do + class APIAfterHandler < Grape::API + get do + { count: 1 }.to_json + end + end + + class MainAfterHandlerAfter < Grape::API + after do + error!({ type: 'after' }, 500) + end + + mount APIAfterHandler + end + + subject { MainAfterHandlerAfter } + + it 'is able to access the variables defined in the after helper' do + get '/' + + expect(last_response.status).to eq(500) + expect(last_response.body).to eq({ type: 'after' }.to_json) + end + end + + context 'when the API is mounted BEFORE defining the after helper' do + class APIAfterHandler < Grape::API + get do + { count: 1 }.to_json + end + end + + class MainAfterHandlerBefore < Grape::API + mount APIAfterHandler + + after do + error!({ type: 'after' }, 500) + end + end + + subject { MainAfterHandlerBefore } + + it 'is able to access the variables defined in the after helper' do + get '/' + + expect(last_response.status).to eq(500) + expect(last_response.body).to eq({ type: 'after' }.to_json) + end + end + end +end diff --git a/spec/grape/api/mount_and_rescue_from_spec.rb b/spec/grape/api/mount_and_rescue_from_spec.rb new file mode 100644 index 000000000..0dfc4a35d --- /dev/null +++ b/spec/grape/api/mount_and_rescue_from_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +describe Grape::API do + def app + subject + end + + context 'when multiple classes defines the same rescue_from' do + class AnAPI < Grape::API + rescue_from ZeroDivisionError do + error!({ type: 'an-api-zero' }, 404) + end + + get '/an-api' do + { count: 1 / 0 } + end + end + + class AnotherAPI < Grape::API + rescue_from ZeroDivisionError do + error!({ type: 'another-api-zero' }, 322) + end + + get '/another-api' do + { count: 1 / 0 } + end + end + + class OtherMain < Grape::API + mount AnAPI + mount AnotherAPI + end + + subject { OtherMain } + + it 'is rescued by the rescue_from ZeroDivisionError handler defined inside each of the classes' do + get '/an-api' + + expect(last_response.status).to eq(404) + expect(last_response.body).to eq({ type: 'an-api-zero' }.to_json) + + get '/another-api' + + expect(last_response.status).to eq(322) + expect(last_response.body).to eq({ type: 'another-api-zero' }.to_json) + end + + context 'when some class does not define a rescue_from but it was defined in a previous mounted endpoint' do + class AnAPIWithoutDefinedRescueFrom < Grape::API + get '/another-api-without-defined-rescue-from' do + { count: 1 / 0 } + end + end + + class OtherMainWithNotDefinedRescueFrom < Grape::API + mount AnAPI + mount AnotherAPI + mount AnAPIWithoutDefinedRescueFrom + end + + subject { OtherMainWithNotDefinedRescueFrom } + + it 'is not rescued by any of the previous defined rescue_from ZeroDivisionError handlers' do + get '/an-api' + + expect(last_response.status).to eq(404) + expect(last_response.body).to eq({ type: 'an-api-zero' }.to_json) + + get '/another-api' + + expect(last_response.status).to eq(322) + expect(last_response.body).to eq({ type: 'another-api-zero' }.to_json) + + expect do + get '/another-api-without-defined-rescue-from' + end.to raise_error(ZeroDivisionError) + end + end + end +end From af36b1dccd33cd416e9706cc4f317d47332365a1 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sun, 17 Dec 2023 20:49:54 +0100 Subject: [PATCH 190/304] Use rubygems default instead of latest (#2387) * Use ruby-version latest whenever possible Use rubygems default while testing * Set ruby-version to 3.2 since latest doesn't exist * Set ruby-version to 2.7 for danger and keeping default rubygems-update * Add CHANGELOG.md --- .github/workflows/danger.yml | 1 - .github/workflows/edge.yml | 1 - .github/workflows/test.yml | 3 +-- CHANGELOG.md | 1 + 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml index a8d1ba96d..dffe84d15 100644 --- a/.github/workflows/danger.yml +++ b/.github/workflows/danger.yml @@ -14,7 +14,6 @@ jobs: with: ruby-version: 2.7 bundler-cache: true - rubygems: latest - name: Run Danger run: | # the token is public, has public_repo scope and belongs to the grape-bot user owned by @dblock, this is ok diff --git a/.github/workflows/edge.yml b/.github/workflows/edge.yml index fe962dbff..bea28ef24 100644 --- a/.github/workflows/edge.yml +++ b/.github/workflows/edge.yml @@ -20,7 +20,6 @@ jobs: with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - rubygems: latest - name: Run tests run: bundle exec rake spec diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 150f58840..d5db9ab2c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: 2.7 + ruby-version: 3.2 bundler-cache: true rubygems: latest @@ -45,7 +45,6 @@ jobs: with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - rubygems: latest - name: Run tests run: bundle exec rake spec diff --git a/CHANGELOG.md b/CHANGELOG.md index c715cba46..76f3ce19a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ * [#2372](https://github.com/ruby-grape/grape/pull/2372): Fix `declared` method for hash params with overlapping names - [@jcagarcia](https://github.com/jcagarcia). * [#2373](https://github.com/ruby-grape/grape/pull/2373): Fix markdown files for following 1-line format - [@jcagarcia](https://github.com/jcagarcia). * [#2382](https://github.com/ruby-grape/grape/pull/2382): Fix values validator for params wrapped in `with` block - [@numbata](https://github.com/numbata). +* [#2387](https://github.com/ruby-grape/grape/pull/2387): Fix rubygems version within workflows - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 2.0.0 (2023/11/11) From bc1d7902eed8aa095a0b95ad33854bf803102764 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Tue, 19 Dec 2023 15:14:06 +0100 Subject: [PATCH 191/304] Drop support ruby 2.6 and Rails 5 (#2390) * Remove ruby 2.6 from CI workflows Run rubocop on ruby 2.7 Update README * Autocorrect rubocop since running on ruby 2.7 * Add changelog --- .github/workflows/test.yml | 2 -- .rubocop.yml | 2 +- .rubocop_todo.yml | 22 +++++++++---------- CHANGELOG.md | 1 + README.md | 2 +- gemfiles/rails_5_2.gemfile | 44 -------------------------------------- grape.gemspec | 4 ++-- lib/grape/api.rb | 8 +++---- lib/grape/endpoint.rb | 6 +++--- 9 files changed, 22 insertions(+), 69 deletions(-) delete mode 100644 gemfiles/rails_5_2.gemfile diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d5db9ab2c..daf28bdd6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,8 +26,6 @@ jobs: ruby: ['2.7', '3.0', '3.1', '3.2'] gemfile: [rack_2_0, rack_3_0, rails_6_0, rails_6_1, rails_7_0, rails_7_1] include: - - ruby: '2.6' - gemfile: rails_5_2 - ruby: '2.7' gemfile: rack_1_0 - ruby: '2.7' diff --git a/.rubocop.yml b/.rubocop.yml index 089a33cf6..1776de3c7 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,6 +1,6 @@ AllCops: NewCops: enable - TargetRubyVersion: 2.6 + TargetRubyVersion: 2.7 SuggestExtensions: false Exclude: - vendor/**/* diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index cffa61681..b065e043f 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 5000` -# on 2023-07-04 00:22:04 UTC using RuboCop version 1.50.2. +# on 2023-12-19 10:12:38 UTC using RuboCop version 1.50.2. # 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 @@ -20,7 +20,7 @@ Lint/AmbiguousBlockAssociation: Exclude: - 'spec/grape/dsl/routing_spec.rb' -# Offense count: 40 +# Offense count: 56 # Configuration parameters: AllowedMethods. # AllowedMethods: enums Lint/ConstantDefinitionInBlock: @@ -34,7 +34,6 @@ Lint/ConstantDefinitionInBlock: - 'spec/grape/api_spec.rb' - 'spec/grape/entity_spec.rb' - 'spec/grape/loading_spec.rb' - - 'spec/grape/middleware/auth/strategies_spec.rb' - 'spec/grape/middleware/base_spec.rb' - 'spec/grape/middleware/error_spec.rb' - 'spec/grape/middleware/formatter_spec.rb' @@ -52,7 +51,7 @@ Lint/DuplicateBranch: Exclude: - 'spec/support/versioned_helpers.rb' -# Offense count: 71 +# Offense count: 75 # Configuration parameters: AllowComments, AllowEmptyLambdas. Lint/EmptyBlock: Exclude: @@ -134,7 +133,7 @@ RSpec/AnyInstance: - 'spec/grape/api_spec.rb' - 'spec/grape/middleware/base_spec.rb' -# Offense count: 343 +# Offense count: 344 # Configuration parameters: Prefixes, AllowedPatterns. # Prefixes: when, with, without RSpec/ContextWording: @@ -226,7 +225,7 @@ RSpec/ExpectInHook: - 'spec/grape/api_spec.rb' - 'spec/grape/validations/validators/values_spec.rb' -# Offense count: 43 +# Offense count: 47 # Configuration parameters: Include, CustomTransform, IgnoreMethods, SpecSuffixOnly. # Include: **/*_spec*rb*, **/spec/**/* RSpec/FilePath: @@ -289,7 +288,7 @@ RSpec/IndexedLet: - 'spec/grape/presenters/presenter_spec.rb' - 'spec/shared/versioning_examples.rb' -# Offense count: 38 +# Offense count: 44 # Configuration parameters: AssignmentOnly. RSpec/InstanceVariable: Exclude: @@ -301,7 +300,7 @@ RSpec/InstanceVariable: - 'spec/grape/middleware/versioner/header_spec.rb' - 'spec/grape/validations/validators/except_values_spec.rb' -# Offense count: 84 +# Offense count: 98 RSpec/LeakyConstantDeclaration: Exclude: - 'spec/grape/api/defines_boolean_in_params_spec.rb' @@ -313,7 +312,6 @@ RSpec/LeakyConstantDeclaration: - 'spec/grape/api_spec.rb' - 'spec/grape/entity_spec.rb' - 'spec/grape/loading_spec.rb' - - 'spec/grape/middleware/auth/strategies_spec.rb' - 'spec/grape/middleware/base_spec.rb' - 'spec/grape/middleware/error_spec.rb' - 'spec/grape/middleware/exception_spec.rb' @@ -342,7 +340,7 @@ RSpec/MissingExampleGroupArgument: Exclude: - 'spec/grape/middleware/exception_spec.rb' -# Offense count: 772 +# Offense count: 788 # Configuration parameters: Max. RSpec/MultipleExpectations: Exclude: @@ -422,7 +420,7 @@ RSpec/MultipleMemoizedHelpers: - 'spec/grape/request_spec.rb' - 'spec/grape/validations/attributes_doc_spec.rb' -# Offense count: 2150 +# Offense count: 2182 # Configuration parameters: EnforcedStyle, IgnoreSharedExamples. # SupportedStyles: always, named_only RSpec/NamedSubject: @@ -487,7 +485,7 @@ RSpec/NamedSubject: - 'spec/grape/validations/validators/presence_spec.rb' - 'spec/grape/validations_spec.rb' -# Offense count: 173 +# Offense count: 174 # Configuration parameters: Max, AllowedGroups. RSpec/NestedGroups: Exclude: diff --git a/CHANGELOG.md b/CHANGELOG.md index 76f3ce19a..821d3fd3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * [#2379](https://github.com/ruby-grape/grape/pull/2379): Take into account the `route_param` type in `recognize_path` - [@jcagarcia](https://github.com/jcagarcia). * [#2383](https://github.com/ruby-grape/grape/pull/2383): Use regex block instead of if - [@ericproulx](https://github.com/ericproulx). * [#2384](https://github.com/ruby-grape/grape/pull/2384): Allow to use `before/after/rescue_from` methods in any order when using `mount` - [@jcagarcia](https://github.com/jcagarcia). +* [#2390](https://github.com/ruby-grape/grape/pull/2390): Drop support for Ruby 2.6 and Rails 5 - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/README.md b/README.md index bc2509cf5..3d4f586f8 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,7 @@ The maintainers of Grape are working with Tidelift to deliver commercial support ## Installation -Ruby 2.6 or newer is required. +Ruby 2.7 or newer is required. Grape is available as a gem, to install it run: diff --git a/gemfiles/rails_5_2.gemfile b/gemfiles/rails_5_2.gemfile deleted file mode 100644 index f5e3f2a55..000000000 --- a/gemfiles/rails_5_2.gemfile +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -# This file was generated by Appraisal - -source 'https://rubygems.org' - -gem 'rails', '~> 5.2' - -group :development, :test do - gem 'bundler' - gem 'hashie' - gem 'rake' - gem 'rubocop', '1.50.2', require: false - gem 'rubocop-performance', '1.17.1', require: false - gem 'rubocop-rspec', '2.20.0', 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 'grape-entity', '~> 0.6', require: false - gem 'mime-types' - gem 'rack-jsonp', require: 'rack/jsonp' - gem 'rack-test', '< 2.1' - gem 'rspec', '< 4' - gem 'ruby-grape-danger', '~> 0.2.0', require: false - gem 'simplecov', '~> 0.21.2' - gem 'simplecov-lcov', '~> 0.8.0' - gem 'test-prof', require: false -end - -platforms :jruby do - gem 'racc' -end - -gemspec path: '../' diff --git a/grape.gemspec b/grape.gemspec index 7dbc25a0e..3ea7193b7 100644 --- a/grape.gemspec +++ b/grape.gemspec @@ -20,7 +20,7 @@ Gem::Specification.new do |s| 'source_code_uri' => "https://github.com/ruby-grape/grape/tree/v#{s.version}" } - s.add_runtime_dependency 'activesupport', '>= 5' + s.add_runtime_dependency 'activesupport', '>= 6' s.add_runtime_dependency 'builder' s.add_runtime_dependency 'dry-types', '>= 1.1' s.add_runtime_dependency 'mustermann-grape', '~> 1.1.0' @@ -29,5 +29,5 @@ Gem::Specification.new do |s| s.files = Dir['lib/**/*', 'CHANGELOG.md', 'CONTRIBUTING.md', 'README.md', 'grape.png', 'UPGRADING.md', 'LICENSE', 'grape.gemspec'] s.require_paths = ['lib'] - s.required_ruby_version = '>= 2.6.0' + s.required_ruby_version = '>= 2.7.0' end diff --git a/lib/grape/api.rb b/lib/grape/api.rb index ef6cb5cf3..536d997e9 100644 --- a/lib/grape/api.rb +++ b/lib/grape/api.rb @@ -26,8 +26,8 @@ class << self attr_accessor :base_instance, :instances # Rather than initializing an object of type Grape::API, create an object of type Instance - def new(*args, &block) - base_instance.new(*args, &block) + def new(...) + base_instance.new(...) end # When inherited, will create a list of all instances (times the API was mounted) @@ -77,8 +77,8 @@ def configure # the headers, and the body. See [the rack specification] # (http://www.rubydoc.info/github/rack/rack/master/file/SPEC) for more. # NOTE: This will only be called on an API directly mounted on RACK - def call(*args, &block) - instance_for_rack.call(*args, &block) + def call(...) + instance_for_rack.call(...) end # Alleviates problems with autoloading by tring to search for the constant diff --git a/lib/grape/endpoint.rb b/lib/grape/endpoint.rb index f50084270..121b1c4d6 100644 --- a/lib/grape/endpoint.rb +++ b/lib/grape/endpoint.rb @@ -13,8 +13,8 @@ class Endpoint attr_reader :env, :request, :headers, :params class << self - def new(*args, &block) - self == Endpoint ? Class.new(Endpoint).new(*args, &block) : super + def new(...) + self == Endpoint ? Class.new(Endpoint).new(...) : super end def before_each(new_setup = false, &block) @@ -55,7 +55,7 @@ def generate_api_method(method_name, &block) proc do |endpoint_instance| ActiveSupport::Notifications.instrument('endpoint_render.grape', endpoint: endpoint_instance) do - method.bind(endpoint_instance).call + method.bind_call(endpoint_instance) end end end From da31f80def3044137d020b3b3d9c7fb89c6785b5 Mon Sep 17 00:00:00 2001 From: Jell Date: Mon, 20 Nov 2023 13:35:59 +0100 Subject: [PATCH 192/304] Fix attribute translator setter This was simply not working before. --- CHANGELOG.md | 1 + lib/grape/router/attribute_translator.rb | 8 +++--- .../grape/router/attribute_translator_spec.rb | 26 +++++++++++++++++++ 3 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 spec/grape/router/attribute_translator_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 821d3fd3b..1afadbf21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ #### Fixes +* [#2375](https://github.com/ruby-grape/grape/pull/2375): Fix setter methods for `Grape::Router::AttributeTranslator` - [@Jell](https://github.com/Jell). * [#2370](https://github.com/ruby-grape/grape/pull/2370): Remove route_xyz method_missing deprecation - [@ericproulx](https://github.com/ericproulx). * [#2372](https://github.com/ruby-grape/grape/pull/2372): Fix `declared` method for hash params with overlapping names - [@jcagarcia](https://github.com/jcagarcia). * [#2373](https://github.com/ruby-grape/grape/pull/2373): Fix markdown files for following 1-line format - [@jcagarcia](https://github.com/jcagarcia). diff --git a/lib/grape/router/attribute_translator.rb b/lib/grape/router/attribute_translator.rb index 8264e2196..e45efc53b 100644 --- a/lib/grape/router/attribute_translator.rb +++ b/lib/grape/router/attribute_translator.rb @@ -38,15 +38,15 @@ def to_h end def method_missing(method_name, *args) - if setter?(method_name[-1]) - attributes[method_name[0..]] = *args + if setter?(method_name) + attributes[method_name.to_s.chomp('=').to_sym] = args.first else attributes[method_name] end end def respond_to_missing?(method_name, _include_private = false) - if setter?(method_name[-1]) + if setter?(method_name) true else @attributes.key?(method_name) @@ -56,7 +56,7 @@ def respond_to_missing?(method_name, _include_private = false) private def setter?(method_name) - method_name[-1] == '=' + method_name.end_with?('=') end end end diff --git a/spec/grape/router/attribute_translator_spec.rb b/spec/grape/router/attribute_translator_spec.rb new file mode 100644 index 000000000..0254a279e --- /dev/null +++ b/spec/grape/router/attribute_translator_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +describe Grape::Router::AttributeTranslator do + (Grape::Router::AttributeTranslator::ROUTE_ATTRIBUTES + Grape::Router::AttributeTranslator::ROUTE_ATTRIBUTES).each do |attribute| + describe "##{attribute}" do + it "returns value from #{attribute} key if present" do + translator = described_class.new(attribute => 'value') + expect(translator.public_send(attribute)).to eq('value') + end + + it "returns nil from #{attribute} key if missing" do + translator = described_class.new + expect(translator.public_send(attribute)).to be_nil + end + end + + describe "##{attribute}=" do + it "sets value for #{attribute}", :aggregate_failures do + translator = described_class.new(attribute => 'value') + expect do + translator.public_send("#{attribute}=", 'new_value') + end.to change(translator, attribute).from('value').to('new_value') + end + end + end +end From 63a0416f5d0b7b4792b8ec3f20ccf7537019a5bb Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Thu, 28 Dec 2023 14:34:38 +0100 Subject: [PATCH 193/304] Optimize AttributeTranslator (#2393) * Add GreedyRoute Define setter methods in AttributeTranslator Combine default route attributes + desc attributes Refactor Pattern Remove description in settings * Add greedy_route_spec.rb Remove delete options * Revert settings description * Rubocop * Remove details and replace spec `details` with detail * Add CHANGELOG.md entry --- CHANGELOG.md | 1 + lib/grape/dsl/desc.rb | 42 ++++++------ lib/grape/router.rb | 5 +- lib/grape/router/attribute_translator.rb | 52 +++++++-------- lib/grape/router/greedy_route.rb | 31 +++++++++ lib/grape/router/pattern.rb | 65 ++++++++++--------- lib/grape/router/route.rb | 46 +++++++------ spec/grape/api_spec.rb | 8 +-- .../grape/router/attribute_translator_spec.rb | 2 +- spec/grape/router/greedy_route_spec.rb | 43 ++++++++++++ spec/integration/rack/v2/headers_spec.rb | 2 +- 11 files changed, 194 insertions(+), 103 deletions(-) create mode 100644 lib/grape/router/greedy_route.rb create mode 100644 spec/grape/router/greedy_route_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 1afadbf21..82aa7ffc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * [#2383](https://github.com/ruby-grape/grape/pull/2383): Use regex block instead of if - [@ericproulx](https://github.com/ericproulx). * [#2384](https://github.com/ruby-grape/grape/pull/2384): Allow to use `before/after/rescue_from` methods in any order when using `mount` - [@jcagarcia](https://github.com/jcagarcia). * [#2390](https://github.com/ruby-grape/grape/pull/2390): Drop support for Ruby 2.6 and Rails 5 - [@ericproulx](https://github.com/ericproulx). +* [#2393](https://github.com/ruby-grape/grape/pull/2393): Optimize AttributeTranslator - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/lib/grape/dsl/desc.rb b/lib/grape/dsl/desc.rb index e1611bf7f..f83eb8b00 100644 --- a/lib/grape/dsl/desc.rb +++ b/lib/grape/dsl/desc.rb @@ -5,6 +5,27 @@ module DSL module Desc include Grape::DSL::Settings + ROUTE_ATTRIBUTES = %i[ + body_name + consumes + default + deprecated + description + detail + entity + headers + hidden + http_codes + is_array + named + nickname + params + produces + security + summary + tags + ].freeze + # Add a description to the next namespace or function. # @param description [String] descriptive string for this endpoint # or namespace @@ -81,26 +102,7 @@ def desc(description, options = {}, &config_block) # Returns an object which configures itself via an instance-context DSL. def desc_container(endpoint_configuration) Module.new do - include Grape::Util::StrictHashConfiguration.module( - :summary, - :description, - :detail, - :params, - :entity, - :http_codes, - :named, - :body_name, - :headers, - :hidden, - :deprecated, - :is_array, - :nickname, - :produces, - :consumes, - :security, - :tags, - :default - ) + include Grape::Util::StrictHashConfiguration.module(*ROUTE_ATTRIBUTES) config_context.define_singleton_method(:configuration) do endpoint_configuration end diff --git a/lib/grape/router.rb b/lib/grape/router.rb index 3c4142b38..51107efba 100644 --- a/lib/grape/router.rb +++ b/lib/grape/router.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'grape/router/route' +require 'grape/router/greedy_route' require 'grape/util/cache' module Grape @@ -48,7 +49,7 @@ def append(route) def associate_routes(pattern, **options) @neutral_regexes << Regexp.new("(?<_#{@neutral_map.length}>)#{pattern.to_regexp}") - @neutral_map << Grape::Router::AttributeTranslator.new(**options, pattern: pattern, index: @neutral_map.length) + @neutral_map << Grape::Router::GreedyRoute.new(pattern: pattern, index: @neutral_map.length, **options) end def call(env) @@ -122,7 +123,7 @@ def process_route(route, env) 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) diff --git a/lib/grape/router/attribute_translator.rb b/lib/grape/router/attribute_translator.rb index e45efc53b..ffe72711c 100644 --- a/lib/grape/router/attribute_translator.rb +++ b/lib/grape/router/attribute_translator.rb @@ -3,54 +3,54 @@ 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 + # fixed >= 3.0 class AttributeTranslator - attr_reader :attributes - - ROUTE_ATTRIBUTES = %i[ - prefix - version - settings + ROUTE_ATTRIBUTES = (%i[ + allow_header + anchor + endpoint format - description - http_codes - headers - entity - details - requirements - request_method + forward_match namespace - ].freeze - - ROUTER_ATTRIBUTES = %i[pattern index].freeze + not_allowed_method + prefix + request_method + requirements + settings + suffix + version + ] | Grape::DSL::Desc::ROUTE_ATTRIBUTES).freeze def initialize(**attributes) @attributes = attributes end - (ROUTER_ATTRIBUTES + ROUTE_ATTRIBUTES).each do |attr| + ROUTE_ATTRIBUTES.each do |attr| define_method attr do - attributes[attr] + @attributes[attr] + end + + define_method("#{attr}=") do |val| + @attributes[attr] = val end end def to_h - attributes + @attributes end def method_missing(method_name, *args) if setter?(method_name) - attributes[method_name.to_s.chomp('=').to_sym] = args.first + @attributes[method_name.to_s.chomp('=').to_sym] = args.first else - attributes[method_name] + @attributes[method_name] end end def respond_to_missing?(method_name, _include_private = false) - if setter?(method_name) - true - else - @attributes.key?(method_name) - end + return true if setter?(method_name) + + @attributes.key?(method_name) end private diff --git a/lib/grape/router/greedy_route.rb b/lib/grape/router/greedy_route.rb new file mode 100644 index 000000000..ac577eb56 --- /dev/null +++ b/lib/grape/router/greedy_route.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'grape/router/attribute_translator' +require 'forwardable' + +# Act like a Grape::Router::Route but for greedy_match +# see @neutral_map + +module Grape + class Router + class GreedyRoute + extend Forwardable + + attr_reader :index, :pattern, :options, :attributes + + delegate Grape::Router::AttributeTranslator::ROUTE_ATTRIBUTES => :@attributes + + def initialize(index:, pattern:, **options) + @index = index + @pattern = pattern + @options = options + @attributes = Grape::Router::AttributeTranslator.new(**options) + end + + # Grape::Router:Route defines params as a function + def params(_input = nil) + @attributes.params || {} + end + end + end +end diff --git a/lib/grape/router/pattern.rb b/lib/grape/router/pattern.rb index 6d7047773..a1fce07a0 100644 --- a/lib/grape/router/pattern.rb +++ b/lib/grape/router/pattern.rb @@ -7,10 +7,7 @@ module Grape class Router class Pattern - DEFAULT_PATTERN_OPTIONS = { uri_decode: true }.freeze - DEFAULT_SUPPORTED_CAPTURE = %i[format version].freeze - - attr_reader :origin, :path, :pattern, :to_regexp + attr_reader :origin, :path, :pattern, :to_regexp, :captures_default extend Forwardable def_delegators :pattern, :named_captures, :params @@ -18,42 +15,52 @@ class Pattern alias match? === def initialize(pattern, **options) - @origin = pattern - @path = build_path(pattern, **options) - @pattern = Mustermann::Grape.new(@path, **pattern_options(options)) + @origin = pattern + @path = build_path(pattern, anchor: options[:anchor], suffix: options[:suffix]) + @pattern = build_pattern(@path, options) @to_regexp = @pattern.to_regexp + @captures_default = regex_captures_default(@to_regexp) end private - def pattern_options(options) - capture = extract_capture(**options) - params = options[:params] - options = DEFAULT_PATTERN_OPTIONS.dup - options[:capture] = capture if capture.present? - options[:params] = params if params.present? - options + def build_pattern(path, options) + Mustermann::Grape.new( + path, + uri_decode: true, + params: options[:params], + capture: extract_capture(**options) + ) end - 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 + def build_path(pattern, anchor: false, suffix: nil) + PatternCache[[build_path_from_pattern(pattern, anchor: anchor), suffix]] + end - pattern = -pattern.split('/').tap do |parts| - parts[parts.length - 1] = "?#{parts.last}" - end.join('/') if pattern.end_with?('*path') + def extract_capture(**options) + sliced_options = options + .slice(:format, :version) + .delete_if { |_k, v| v.blank? } + .transform_values { |v| Array.wrap(v).map(&:to_s) } + return sliced_options if options[:requirements].blank? + + options[:requirements].merge(sliced_options) + end - PatternCache[[pattern, suffix]] + def regex_captures_default(regex) + names = regex.names - %w[format version] # remove default format and version + names.to_h { |k| [k, ''] } end - def extract_capture(requirements: {}, **options) - requirements = {}.merge(requirements) - DEFAULT_SUPPORTED_CAPTURE.each_with_object(requirements) do |field, capture| - option = Array(options[field]) - capture[field] = option.map(&:to_s) if option.present? + def build_path_from_pattern(pattern, anchor: false) + if pattern.end_with?('*path') + pattern.dup.insert(pattern.rindex('/') + 1, '?') + elsif anchor + pattern + elsif pattern.end_with?('/') + "#{pattern}?*path" + else + "#{pattern}/?*path" end end diff --git a/lib/grape/router/route.rb b/lib/grape/router/route.rb index ea49bac78..616bc2116 100644 --- a/lib/grape/router/route.rb +++ b/lib/grape/router/route.rb @@ -7,23 +7,18 @@ module Grape class Router class Route - FIXED_NAMED_CAPTURES = %w[format version].freeze - - attr_accessor :pattern, :translator, :app, :index, :options + extend Forwardable - alias attributes translator + attr_reader :app, :pattern, :options, :attributes + attr_accessor :index - extend Forwardable def_delegators :pattern, :path, :origin delegate Grape::Router::AttributeTranslator::ROUTE_ATTRIBUTES => :attributes def initialize(method, pattern, **options) - 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_upcase) + @options = options + @pattern = Grape::Router::Pattern.new(pattern, **options) + @attributes = Grape::Router::AttributeTranslator.new(**options, request_method: upcase_method(method)) end def exec(env) @@ -36,18 +31,29 @@ def apply(app) end def match?(input) - translator.respond_to?(:forward_match) && translator.forward_match ? input.start_with?(pattern.origin) : pattern.match?(input) + return if input.blank? + + attributes.forward_match ? input.start_with?(pattern.origin) : pattern.match?(input) end def params(input = nil) - if input.nil? - pattern.named_captures.keys.each_with_object(translator.params) do |(key), defaults| - defaults[key] ||= '' unless FIXED_NAMED_CAPTURES.include?(key) || defaults.key?(key) - end - else - parsed = pattern.params(input) - parsed ? parsed.delete_if { |_, value| value.nil? }.symbolize_keys : {} - end + return params_without_input if input.blank? + + parsed = pattern.params(input) + return {} unless parsed + + parsed.delete_if { |_, value| value.nil? }.symbolize_keys + end + + private + + def params_without_input + pattern.captures_default.merge(attributes.params) + end + + def upcase_method(method) + method_s = method.to_s + Grape::Http::Headers.find_supported_method(method_s) || method_s.upcase end end end diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index 9826dd867..9ed925358 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -3100,13 +3100,13 @@ def static ] end - it 'includes details' do - subject.desc 'method', details: 'method details' + it 'includes detail' do + subject.desc 'method', detail: 'method details' subject.get 'method' expect(subject.routes.map do |route| - { description: route.description, details: route.details, params: route.params } + { description: route.description, detail: route.detail, params: route.params } end).to eq [ - { description: 'method', details: 'method details', params: {} } + { description: 'method', detail: 'method details', params: {} } ] end diff --git a/spec/grape/router/attribute_translator_spec.rb b/spec/grape/router/attribute_translator_spec.rb index 0254a279e..4ba5ffbce 100644 --- a/spec/grape/router/attribute_translator_spec.rb +++ b/spec/grape/router/attribute_translator_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true describe Grape::Router::AttributeTranslator do - (Grape::Router::AttributeTranslator::ROUTE_ATTRIBUTES + Grape::Router::AttributeTranslator::ROUTE_ATTRIBUTES).each do |attribute| + described_class::ROUTE_ATTRIBUTES.each do |attribute| describe "##{attribute}" do it "returns value from #{attribute} key if present" do translator = described_class.new(attribute => 'value') diff --git a/spec/grape/router/greedy_route_spec.rb b/spec/grape/router/greedy_route_spec.rb new file mode 100644 index 000000000..add108ac5 --- /dev/null +++ b/spec/grape/router/greedy_route_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +RSpec.describe Grape::Router::GreedyRoute do + let(:instance) { described_class.new(index: index, pattern: pattern, **options) } + let(:index) { 0 } + let(:pattern) { :pattern } + let(:params) do + { a_param: 1 }.freeze + end + let(:options) do + { params: params }.freeze + end + + describe '#index' do + subject { instance.index } + + it { is_expected.to eq(index) } + end + + describe '#pattern' do + subject { instance.pattern } + + it { is_expected.to eq(pattern) } + end + + describe '#options' do + subject { instance.options } + + it { is_expected.to eq(options) } + end + + describe '#params' do + subject { instance.params } + + it { is_expected.to eq(params) } + end + + describe '#attributes' do + subject { instance.attributes } + + it { is_expected.to be_a(Grape::Router::AttributeTranslator) } + end +end diff --git a/spec/integration/rack/v2/headers_spec.rb b/spec/integration/rack/v2/headers_spec.rb index 4819f21dc..ad3408019 100644 --- a/spec/integration/rack/v2/headers_spec.rb +++ b/spec/integration/rack/v2/headers_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe Grape::Http::Headers do +describe Grape::Http::Headers, if: Gem::Version.new(Rack.release) < Gem::Version.new('3.0.0') do it { expect(described_class::ALLOW).to eq('Allow') } it { expect(described_class::LOCATION).to eq('Location') } it { expect(described_class::TRANSFER_ENCODING).to eq('Transfer-Encoding') } From 15c891c6ec7e3f974a59613c870b4508547f6cc1 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sun, 31 Dec 2023 16:33:10 +0100 Subject: [PATCH 194/304] Remove cookie-jar dependency for ruby 3.3 (#2395) * Remove cookie-jar dependency Remove deleted and set max-age 0 for current cookies Replace HTTP_COOKIE by set-cookie function Replace Set-Cookie by Rack::SET_COOKIE Update specs * Remove delete_set_cookie_header and set_cookie_header since its in rack >= 3.0 * Add cookie_helper to manage different expires value. * Fix rubocop * Add cookiejar to Rack::MockResponse * Add CHANGELOG.md entry * Update CHANGELOG.md * Fix CHANGELOG.md --- CHANGELOG.md | 1 + Gemfile | 1 - gemfiles/multi_json.gemfile | 1 - gemfiles/multi_xml.gemfile | 1 - gemfiles/rack_1_0.gemfile | 1 - gemfiles/rack_2_0.gemfile | 1 - gemfiles/rack_3_0.gemfile | 1 - gemfiles/rack_edge.gemfile | 1 - gemfiles/rails_6_0.gemfile | 1 - gemfiles/rails_6_1.gemfile | 1 - gemfiles/rails_7_0.gemfile | 1 - gemfiles/rails_7_1.gemfile | 1 - gemfiles/rails_edge.gemfile | 1 - lib/grape/cookies.rb | 3 +- spec/grape/api_spec.rb | 1 - spec/grape/endpoint_spec.rb | 65 ++++++++++-------------- spec/integration/rack/v3/headers_spec.rb | 2 +- spec/support/cookie_jar.rb | 54 ++++++++++++++++++++ 18 files changed, 86 insertions(+), 52 deletions(-) create mode 100644 spec/support/cookie_jar.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 82aa7ffc2..fcae35383 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * [#2384](https://github.com/ruby-grape/grape/pull/2384): Allow to use `before/after/rescue_from` methods in any order when using `mount` - [@jcagarcia](https://github.com/jcagarcia). * [#2390](https://github.com/ruby-grape/grape/pull/2390): Drop support for Ruby 2.6 and Rails 5 - [@ericproulx](https://github.com/ericproulx). * [#2393](https://github.com/ruby-grape/grape/pull/2393): Optimize AttributeTranslator - [@ericproulx](https://github.com/ericproulx). +* [#2395](https://github.com/ruby-grape/grape/pull/2395): Set `max-age` to 0 when `cookies.delete` - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/Gemfile b/Gemfile index ddd2a49f2..c2c7a2075 100644 --- a/Gemfile +++ b/Gemfile @@ -25,7 +25,6 @@ group :development do end group :test do - gem 'cookiejar' gem 'grape-entity', '~> 0.6', require: false gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' diff --git a/gemfiles/multi_json.gemfile b/gemfiles/multi_json.gemfile index b1c6e91e0..286f89b12 100644 --- a/gemfiles/multi_json.gemfile +++ b/gemfiles/multi_json.gemfile @@ -25,7 +25,6 @@ group :development do end group :test do - gem 'cookiejar' gem 'grape-entity', '~> 0.6', require: false gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' diff --git a/gemfiles/multi_xml.gemfile b/gemfiles/multi_xml.gemfile index 02fdd91cf..6e2a29733 100644 --- a/gemfiles/multi_xml.gemfile +++ b/gemfiles/multi_xml.gemfile @@ -25,7 +25,6 @@ group :development do end group :test do - gem 'cookiejar' gem 'grape-entity', '~> 0.6', require: false gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' diff --git a/gemfiles/rack_1_0.gemfile b/gemfiles/rack_1_0.gemfile index 7aa8c6451..b02c560d9 100644 --- a/gemfiles/rack_1_0.gemfile +++ b/gemfiles/rack_1_0.gemfile @@ -25,7 +25,6 @@ group :development do end group :test do - gem 'cookiejar' gem 'grape-entity', '~> 0.6', require: false gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' diff --git a/gemfiles/rack_2_0.gemfile b/gemfiles/rack_2_0.gemfile index 69d7ec28f..9c61fba04 100644 --- a/gemfiles/rack_2_0.gemfile +++ b/gemfiles/rack_2_0.gemfile @@ -25,7 +25,6 @@ group :development do end group :test do - gem 'cookiejar' gem 'grape-entity', '~> 0.6', require: false gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' diff --git a/gemfiles/rack_3_0.gemfile b/gemfiles/rack_3_0.gemfile index 24ad9ac31..b11d7a644 100644 --- a/gemfiles/rack_3_0.gemfile +++ b/gemfiles/rack_3_0.gemfile @@ -25,7 +25,6 @@ group :development do end group :test do - gem 'cookiejar' gem 'grape-entity', '~> 0.6', require: false gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' diff --git a/gemfiles/rack_edge.gemfile b/gemfiles/rack_edge.gemfile index 0e1133d74..37e9a1d4a 100644 --- a/gemfiles/rack_edge.gemfile +++ b/gemfiles/rack_edge.gemfile @@ -25,7 +25,6 @@ group :development do end group :test do - gem 'cookiejar' gem 'grape-entity', '~> 0.6', require: false gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' diff --git a/gemfiles/rails_6_0.gemfile b/gemfiles/rails_6_0.gemfile index 996b1210b..af2f0a1a3 100644 --- a/gemfiles/rails_6_0.gemfile +++ b/gemfiles/rails_6_0.gemfile @@ -25,7 +25,6 @@ group :development do end group :test do - gem 'cookiejar' gem 'grape-entity', '~> 0.6', require: false gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' diff --git a/gemfiles/rails_6_1.gemfile b/gemfiles/rails_6_1.gemfile index 3b8c16a3e..d0a55806d 100644 --- a/gemfiles/rails_6_1.gemfile +++ b/gemfiles/rails_6_1.gemfile @@ -25,7 +25,6 @@ group :development do end group :test do - gem 'cookiejar' gem 'grape-entity', '~> 0.6', require: false gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' diff --git a/gemfiles/rails_7_0.gemfile b/gemfiles/rails_7_0.gemfile index 914f94c8f..8bca742d1 100644 --- a/gemfiles/rails_7_0.gemfile +++ b/gemfiles/rails_7_0.gemfile @@ -25,7 +25,6 @@ group :development do end group :test do - gem 'cookiejar' gem 'grape-entity', '~> 0.6', require: false gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' diff --git a/gemfiles/rails_7_1.gemfile b/gemfiles/rails_7_1.gemfile index 4fc39fe8f..fdea5d5e0 100644 --- a/gemfiles/rails_7_1.gemfile +++ b/gemfiles/rails_7_1.gemfile @@ -26,7 +26,6 @@ group :development do end group :test do - gem 'cookiejar' gem 'grape-entity', '~> 0.6', require: false gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' diff --git a/gemfiles/rails_edge.gemfile b/gemfiles/rails_edge.gemfile index cb144118e..b38d71422 100644 --- a/gemfiles/rails_edge.gemfile +++ b/gemfiles/rails_edge.gemfile @@ -25,7 +25,6 @@ group :development do end group :test do - gem 'cookiejar' gem 'grape-entity', '~> 0.6', require: false gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' diff --git a/lib/grape/cookies.rb b/lib/grape/cookies.rb index 51d02112e..7afdb67c2 100644 --- a/lib/grape/cookies.rb +++ b/lib/grape/cookies.rb @@ -33,9 +33,10 @@ def each(&block) @cookies.each(&block) end + # see https://github.com/rack/rack/blob/main/lib/rack/utils.rb#L338-L340 # rubocop:disable Layout/SpaceBeforeBrackets def delete(name, **opts) - options = opts.merge(value: 'deleted', expires: Time.at(0)) + options = opts.merge(max_age: '0', value: '', expires: Time.at(0)) self.[]=(name, options) end # rubocop:enable Layout/SpaceBeforeBrackets diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index 9ed925358..8c9d87603 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -4,7 +4,6 @@ describe Grape::API do subject do - puts described_class Class.new(described_class) end diff --git a/spec/grape/endpoint_spec.rb b/spec/grape/endpoint_spec.rb index 5788bd913..14d99c627 100644 --- a/spec/grape/endpoint_spec.rb +++ b/spec/grape/endpoint_spec.rb @@ -175,37 +175,42 @@ def app get('/get/cookies') - expect(Array(last_response.headers['Set-Cookie']).flat_map { |h| h.split("\n") }.sort).to eql [ - 'cookie3=symbol', - 'cookie4=secret+code+here', - 'my-awesome-cookie1=is+cool', - 'my-awesome-cookie2=is+cool+too; domain=my.example.com; path=/; secure' - ] + expect(last_response.cookie_jar).to contain_exactly( + { 'name' => 'cookie3', 'value' => 'symbol' }, + { 'name' => 'cookie4', 'value' => 'secret code here' }, + { 'name' => 'my-awesome-cookie1', 'value' => 'is cool' }, + { 'name' => 'my-awesome-cookie2', 'value' => 'is cool too', 'domain' => 'my.example.com', 'path' => '/', 'secure' => true } + ) end it 'sets browser cookies and does not set response cookies' do + set_cookie %w[username=mrplum sandbox=true] subject.get('/username') do cookies[:username] end - get('/username', {}, 'HTTP_COOKIE' => 'username=mrplum; sandbox=true') + get '/username' expect(last_response.body).to eq('mrplum') - expect(last_response.headers['Set-Cookie']).to be_nil + expect(last_response.cookie_jar).to be_empty end it 'sets and update browser cookies' do + set_cookie %w[username=user sandbox=false] subject.get('/username') do cookies[:sandbox] = true if cookies[:sandbox] == 'false' cookies[:username] += '_test' end - get('/username', {}, 'HTTP_COOKIE' => 'username=user; sandbox=false') + + get '/username' expect(last_response.body).to eq('user_test') - cookies = Array(last_response.headers['Set-Cookie']).flat_map { |h| h.split("\n") } - expect(cookies[0]).to match(/username=user_test/) - expect(cookies[1]).to match(/sandbox=true/) + expect(last_response.cookie_jar).to contain_exactly( + { 'name' => 'sandbox', 'value' => 'true' }, + { 'name' => 'username', 'value' => 'user_test' } + ) end it 'deletes cookie' do + set_cookie %w[delete_this_cookie=1 and_this=2] subject.get('/test') do sum = 0 cookies.each do |name, val| @@ -214,22 +219,16 @@ def app end sum end - get '/test', {}, 'HTTP_COOKIE' => 'delete_this_cookie=1; and_this=2' + get '/test' expect(last_response.body).to eq('3') - cookies = Array(last_response.headers['Set-Cookie']).flat_map { |h| h.split("\n") }.to_h do |set_cookie| - cookie = CookieJar::Cookie.from_set_cookie 'http://localhost/test', set_cookie - [cookie.name, cookie] - end - expect(cookies.size).to eq(2) - %w[and_this delete_this_cookie].each do |cookie_name| - cookie = cookies[cookie_name] - expect(cookie).not_to be_nil - expect(cookie.value).to eq('deleted') - expect(cookie.expired?).to be true - end + expect(last_response.cookie_jar).to contain_exactly( + { 'name' => 'and_this', 'value' => '', 'max-age' => 0, 'expires' => Time.at(0) }, + { 'name' => 'delete_this_cookie', 'value' => '', 'max-age' => 0, 'expires' => Time.at(0) } + ) end it 'deletes cookies with path' do + set_cookie %w[delete_this_cookie=1 and_this=2] subject.get('/test') do sum = 0 cookies.each do |name, val| @@ -238,20 +237,12 @@ def app end sum end - get('/test', {}, 'HTTP_COOKIE' => 'delete_this_cookie=1; and_this=2') + get '/test' expect(last_response.body).to eq('3') - cookies = Array(last_response.headers['Set-Cookie']).flat_map { |h| h.split("\n") }.to_h do |set_cookie| - cookie = CookieJar::Cookie.from_set_cookie 'http://localhost/test', set_cookie - [cookie.name, cookie] - end - expect(cookies.size).to eq(2) - %w[and_this delete_this_cookie].each do |cookie_name| - cookie = cookies[cookie_name] - expect(cookie).not_to be_nil - expect(cookie.value).to eq('deleted') - expect(cookie.path).to eq('/test') - expect(cookie.expired?).to be true - end + expect(last_response.cookie_jar).to contain_exactly( + { 'name' => 'and_this', 'path' => '/test', 'value' => '', 'max-age' => 0, 'expires' => Time.at(0) }, + { 'name' => 'delete_this_cookie', 'path' => '/test', 'value' => '', 'max-age' => 0, 'expires' => Time.at(0) } + ) end end diff --git a/spec/integration/rack/v3/headers_spec.rb b/spec/integration/rack/v3/headers_spec.rb index 3be2c1e28..1007cd438 100644 --- a/spec/integration/rack/v3/headers_spec.rb +++ b/spec/integration/rack/v3/headers_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe Grape::Http::Headers do +describe Grape::Http::Headers, if: Gem::Version.new(Rack.release) >= Gem::Version.new('3') do it { expect(described_class::ALLOW).to eq('allow') } it { expect(described_class::LOCATION).to eq('location') } it { expect(described_class::TRANSFER_ENCODING).to eq('transfer-encoding') } diff --git a/spec/support/cookie_jar.rb b/spec/support/cookie_jar.rb new file mode 100644 index 000000000..1e67a95a6 --- /dev/null +++ b/spec/support/cookie_jar.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'uri' + +module Rack + class MockResponse + def cookie_jar + @cookie_jar ||= Array(headers['Set-Cookie']).flat_map { |h| h.split("\n") }.map { |c| Cookie.new(c).to_h } + end + + # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie + class Cookie + attr_reader :attributes + + def initialize(raw) + @attributes = raw.split(/;\s*/).flat_map.with_index do |attribute, i| + attribute, value = attribute.split('=', 2) + if i.zero? + [['name', attribute], ['value', unescape(value)]] + else + [[attribute.downcase, parse_value(attribute, value)]] + end + end.to_h.freeze + end + + def to_h + @attributes.dup + end + + def to_s + @attributes.to_s + end + + private + + def unescape(value) + URI.decode_www_form_component(value, Encoding::UTF_8) + end + + def parse_value(attribute, value) + case attribute + when 'expires' + Time.parse(value) + when 'max-age' + value.to_i + when 'secure', 'httponly', 'partitioned' + true + else + unescape(value) + end + end + end + end +end From aec696553dc02537812881ee257076e61c99de1c Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Wed, 3 Jan 2024 04:38:54 +0100 Subject: [PATCH 195/304] Update rubocop* gems (#2399) * Update rubocop* gems Autocorrect new offenses * Add CHANGELOG.md entry * Update CHANGELOG.md Add versions --- .rubocop.yml | 3 + .rubocop_todo.yml | 79 +++++++++++++++++-- CHANGELOG.md | 1 + Gemfile | 6 +- gemfiles/multi_json.gemfile | 6 +- gemfiles/multi_xml.gemfile | 6 +- gemfiles/rack_1_0.gemfile | 6 +- gemfiles/rack_2_0.gemfile | 6 +- gemfiles/rack_3_0.gemfile | 6 +- gemfiles/rack_edge.gemfile | 6 +- gemfiles/rails_6_0.gemfile | 6 +- gemfiles/rails_6_1.gemfile | 6 +- gemfiles/rails_7_0.gemfile | 6 +- gemfiles/rails_7_1.gemfile | 6 +- gemfiles/rails_edge.gemfile | 6 +- lib/grape/exceptions/base.rb | 6 +- lib/grape/exceptions/validation_errors.rb | 2 +- lib/grape/middleware/stack.rb | 5 +- lib/grape/path.rb | 4 +- lib/grape/router/attribute_translator.rb | 2 +- lib/grape/router/route.rb | 4 +- lib/grape/util/strict_hash_configuration.rb | 6 +- lib/grape/validations.rb | 2 +- lib/grape/validations/params_scope.rb | 3 +- .../validations/types/dry_type_coercer.rb | 2 +- spec/grape/middleware/formatter_spec.rb | 17 ++-- spec/grape/path_spec.rb | 22 ++---- .../grape/router/attribute_translator_spec.rb | 2 +- 28 files changed, 146 insertions(+), 86 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 1776de3c7..459c29c9b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -24,6 +24,9 @@ Style/MultilineIfModifier: Style/RaiseArgs: Enabled: false +Style/RedundantArrayConstructor: + Enabled: false # doesn't work well with params definition + Metrics/AbcSize: Max: 45 diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index b065e043f..370f887e4 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 5000` -# on 2023-12-19 10:12:38 UTC using RuboCop version 1.50.2. +# on 2024-01-01 22:17:14 UTC using RuboCop version 1.59.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 @@ -15,12 +15,13 @@ Gemspec/RequireMFA: - 'grape.gemspec' # Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowedMethods, AllowedPatterns. Lint/AmbiguousBlockAssociation: Exclude: - 'spec/grape/dsl/routing_spec.rb' -# Offense count: 56 +# Offense count: 55 # Configuration parameters: AllowedMethods. # AllowedMethods: enums Lint/ConstantDefinitionInBlock: @@ -84,6 +85,7 @@ Lint/EmptyClass: - 'spec/grape/middleware/stack_spec.rb' # Offense count: 6 +# Configuration parameters: AllowedParentClasses. Lint/MissingSuper: Exclude: - 'lib/grape/api/instance.rb' @@ -100,6 +102,7 @@ Metrics/MethodLength: - 'lib/grape/endpoint.rb' # Offense count: 2 +# This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyleForLeadingUnderscores. # SupportedStylesForLeadingUnderscores: disallowed, required, optional Naming/MemoizedInstanceVariableName: @@ -215,7 +218,7 @@ RSpec/ExampleWording: # This cop supports safe autocorrection (--autocorrect). RSpec/ExpectActual: Exclude: - - 'spec/routing/**/*' + - '**/spec/routing/**/*' - 'spec/grape/endpoint/declared_spec.rb' - 'spec/grape/middleware/exception_spec.rb' @@ -278,13 +281,11 @@ RSpec/FilePath: - 'spec/integration/rack/v2/headers_spec.rb' - 'spec/integration/rack/v3/headers_spec.rb' -# Offense count: 12 -# Configuration parameters: Max. +# Offense count: 6 +# Configuration parameters: Max, AllowedIdentifiers, AllowedPatterns. RSpec/IndexedLet: Exclude: - 'spec/grape/exceptions/validation_errors_spec.rb' - - 'spec/grape/middleware/versioner/header_spec.rb' - - 'spec/grape/middleware/versioner/param_spec.rb' - 'spec/grape/presenters/presenter_spec.rb' - 'spec/shared/versioning_examples.rb' @@ -300,7 +301,7 @@ RSpec/InstanceVariable: - 'spec/grape/middleware/versioner/header_spec.rb' - 'spec/grape/validations/validators/except_values_spec.rb' -# Offense count: 98 +# Offense count: 97 RSpec/LeakyConstantDeclaration: Exclude: - 'spec/grape/api/defines_boolean_in_params_spec.rb' @@ -556,6 +557,60 @@ RSpec/ScatteredSetup: - 'spec/grape/util/inheritable_setting_spec.rb' - 'spec/grape/validations_spec.rb' +# Offense count: 47 +# Configuration parameters: Include, CustomTransform, IgnoreMethods, IgnoreMetadata. +# Include: **/*_spec.rb +RSpec/SpecFilePathFormat: + Exclude: + - '**/spec/routing/**/*' + - 'spec/grape/api/custom_validations_spec.rb' + - 'spec/grape/api/deeply_included_options_spec.rb' + - 'spec/grape/api/defines_boolean_in_params_spec.rb' + - 'spec/grape/api/documentation_spec.rb' + - 'spec/grape/api/inherited_helpers_spec.rb' + - 'spec/grape/api/invalid_format_spec.rb' + - 'spec/grape/api/mount_and_helpers_order_spec.rb' + - 'spec/grape/api/mount_and_rescue_from_spec.rb' + - 'spec/grape/api/namespace_parameters_in_route_spec.rb' + - 'spec/grape/api/nested_helpers_spec.rb' + - 'spec/grape/api/optional_parameters_in_route_spec.rb' + - 'spec/grape/api/parameters_modification_spec.rb' + - 'spec/grape/api/patch_method_helpers_spec.rb' + - 'spec/grape/api/recognize_path_spec.rb' + - 'spec/grape/api/required_parameters_in_route_spec.rb' + - 'spec/grape/api/required_parameters_with_invalid_method_spec.rb' + - 'spec/grape/api/routes_with_requirements_spec.rb' + - 'spec/grape/api/shared_helpers_exactly_one_of_spec.rb' + - 'spec/grape/api/shared_helpers_spec.rb' + - 'spec/grape/dsl/inside_route_spec.rb' + - 'spec/grape/endpoint/declared_spec.rb' + - 'spec/grape/exceptions/body_parse_errors_spec.rb' + - 'spec/grape/extensions/param_builders/hash_spec.rb' + - 'spec/grape/extensions/param_builders/hash_with_indifferent_access_spec.rb' + - 'spec/grape/extensions/param_builders/hashie/mash_spec.rb' + - 'spec/grape/integration/global_namespace_function_spec.rb' + - 'spec/grape/integration/rack_sendfile_spec.rb' + - 'spec/grape/loading_spec.rb' + - 'spec/grape/middleware/exception_spec.rb' + - 'spec/grape/validations/attributes_doc_spec.rb' + - 'spec/grape/validations/validators/all_or_none_spec.rb' + - 'spec/grape/validations/validators/allow_blank_spec.rb' + - 'spec/grape/validations/validators/at_least_one_of_spec.rb' + - 'spec/grape/validations/validators/coerce_spec.rb' + - 'spec/grape/validations/validators/default_spec.rb' + - 'spec/grape/validations/validators/exactly_one_of_spec.rb' + - 'spec/grape/validations/validators/except_values_spec.rb' + - 'spec/grape/validations/validators/mutual_exclusion_spec.rb' + - 'spec/grape/validations/validators/presence_spec.rb' + - 'spec/grape/validations/validators/regexp_spec.rb' + - 'spec/grape/validations/validators/same_as_spec.rb' + - 'spec/grape/validations/validators/values_spec.rb' + - 'spec/integration/eager_load/eager_load_spec.rb' + - 'spec/integration/multi_json/json_spec.rb' + - 'spec/integration/multi_xml/xml_spec.rb' + - 'spec/integration/rack/v2/headers_spec.rb' + - 'spec/integration/rack/v3/headers_spec.rb' + # Offense count: 9 RSpec/StubbedMock: Exclude: @@ -612,6 +667,14 @@ Style/CombinableLoops: Style/FormatStringToken: EnforcedStyle: template +# Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: AllowedReceivers. +# AllowedReceivers: Thread.current +Style/HashEachMethods: + Exclude: + - 'lib/grape/middleware/stack.rb' + # Offense count: 12 # Configuration parameters: AllowedMethods. # AllowedMethods: respond_to_missing? diff --git a/CHANGELOG.md b/CHANGELOG.md index fcae35383..fe97db2ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ * [#2390](https://github.com/ruby-grape/grape/pull/2390): Drop support for Ruby 2.6 and Rails 5 - [@ericproulx](https://github.com/ericproulx). * [#2393](https://github.com/ruby-grape/grape/pull/2393): Optimize AttributeTranslator - [@ericproulx](https://github.com/ericproulx). * [#2395](https://github.com/ruby-grape/grape/pull/2395): Set `max-age` to 0 when `cookies.delete` - [@ericproulx](https://github.com/ericproulx). +* [#2399](https://github.com/ruby-grape/grape/pull/2399): Update `rubocop` to 1.59.0, `rubocop-performance` to 1.20.1 and `rubocop-rspec` to 2.25.0 - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/Gemfile b/Gemfile index c2c7a2075..f845da35c 100644 --- a/Gemfile +++ b/Gemfile @@ -10,9 +10,9 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '1.50.2', require: false - gem 'rubocop-performance', '1.17.1', require: false - gem 'rubocop-rspec', '2.20.0', require: false + gem 'rubocop', '1.59.0', require: false + gem 'rubocop-performance', '1.20.1', require: false + gem 'rubocop-rspec', '2.25.0', require: false end group :development do diff --git a/gemfiles/multi_json.gemfile b/gemfiles/multi_json.gemfile index 286f89b12..b7030feaf 100644 --- a/gemfiles/multi_json.gemfile +++ b/gemfiles/multi_json.gemfile @@ -10,9 +10,9 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '1.50.2', require: false - gem 'rubocop-performance', '1.17.1', require: false - gem 'rubocop-rspec', '2.20.0', require: false + gem 'rubocop', '1.59.0', require: false + gem 'rubocop-performance', '1.20.1', require: false + gem 'rubocop-rspec', '2.25.0', require: false end group :development do diff --git a/gemfiles/multi_xml.gemfile b/gemfiles/multi_xml.gemfile index 6e2a29733..b4690fe96 100644 --- a/gemfiles/multi_xml.gemfile +++ b/gemfiles/multi_xml.gemfile @@ -10,9 +10,9 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '1.50.2', require: false - gem 'rubocop-performance', '1.17.1', require: false - gem 'rubocop-rspec', '2.20.0', require: false + gem 'rubocop', '1.59.0', require: false + gem 'rubocop-performance', '1.20.1', require: false + gem 'rubocop-rspec', '2.25.0', require: false end group :development do diff --git a/gemfiles/rack_1_0.gemfile b/gemfiles/rack_1_0.gemfile index b02c560d9..0461250b4 100644 --- a/gemfiles/rack_1_0.gemfile +++ b/gemfiles/rack_1_0.gemfile @@ -10,9 +10,9 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '1.50.2', require: false - gem 'rubocop-performance', '1.17.1', require: false - gem 'rubocop-rspec', '2.20.0', require: false + gem 'rubocop', '1.59.0', require: false + gem 'rubocop-performance', '1.20.1', require: false + gem 'rubocop-rspec', '2.25.0', require: false end group :development do diff --git a/gemfiles/rack_2_0.gemfile b/gemfiles/rack_2_0.gemfile index 9c61fba04..abda41200 100644 --- a/gemfiles/rack_2_0.gemfile +++ b/gemfiles/rack_2_0.gemfile @@ -10,9 +10,9 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '1.50.2', require: false - gem 'rubocop-performance', '1.17.1', require: false - gem 'rubocop-rspec', '2.20.0', require: false + gem 'rubocop', '1.59.0', require: false + gem 'rubocop-performance', '1.20.1', require: false + gem 'rubocop-rspec', '2.25.0', require: false end group :development do diff --git a/gemfiles/rack_3_0.gemfile b/gemfiles/rack_3_0.gemfile index b11d7a644..958b2dd9d 100644 --- a/gemfiles/rack_3_0.gemfile +++ b/gemfiles/rack_3_0.gemfile @@ -10,9 +10,9 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '1.50.2', require: false - gem 'rubocop-performance', '1.17.1', require: false - gem 'rubocop-rspec', '2.20.0', require: false + gem 'rubocop', '1.59.0', require: false + gem 'rubocop-performance', '1.20.1', require: false + gem 'rubocop-rspec', '2.25.0', require: false end group :development do diff --git a/gemfiles/rack_edge.gemfile b/gemfiles/rack_edge.gemfile index 37e9a1d4a..f9bd6c4a8 100644 --- a/gemfiles/rack_edge.gemfile +++ b/gemfiles/rack_edge.gemfile @@ -10,9 +10,9 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '1.50.2', require: false - gem 'rubocop-performance', '1.17.1', require: false - gem 'rubocop-rspec', '2.20.0', require: false + gem 'rubocop', '1.59.0', require: false + gem 'rubocop-performance', '1.20.1', require: false + gem 'rubocop-rspec', '2.25.0', require: false end group :development do diff --git a/gemfiles/rails_6_0.gemfile b/gemfiles/rails_6_0.gemfile index af2f0a1a3..357aa8a34 100644 --- a/gemfiles/rails_6_0.gemfile +++ b/gemfiles/rails_6_0.gemfile @@ -10,9 +10,9 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '1.50.2', require: false - gem 'rubocop-performance', '1.17.1', require: false - gem 'rubocop-rspec', '2.20.0', require: false + gem 'rubocop', '1.59.0', require: false + gem 'rubocop-performance', '1.20.1', require: false + gem 'rubocop-rspec', '2.25.0', require: false end group :development do diff --git a/gemfiles/rails_6_1.gemfile b/gemfiles/rails_6_1.gemfile index d0a55806d..6cbd5d6fc 100644 --- a/gemfiles/rails_6_1.gemfile +++ b/gemfiles/rails_6_1.gemfile @@ -10,9 +10,9 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '1.50.2', require: false - gem 'rubocop-performance', '1.17.1', require: false - gem 'rubocop-rspec', '2.20.0', require: false + gem 'rubocop', '1.59.0', require: false + gem 'rubocop-performance', '1.20.1', require: false + gem 'rubocop-rspec', '2.25.0', require: false end group :development do diff --git a/gemfiles/rails_7_0.gemfile b/gemfiles/rails_7_0.gemfile index 8bca742d1..5d2beedab 100644 --- a/gemfiles/rails_7_0.gemfile +++ b/gemfiles/rails_7_0.gemfile @@ -10,9 +10,9 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '1.50.2', require: false - gem 'rubocop-performance', '1.17.1', require: false - gem 'rubocop-rspec', '2.20.0', require: false + gem 'rubocop', '1.59.0', require: false + gem 'rubocop-performance', '1.20.1', require: false + gem 'rubocop-rspec', '2.25.0', require: false end group :development do diff --git a/gemfiles/rails_7_1.gemfile b/gemfiles/rails_7_1.gemfile index fdea5d5e0..79131db63 100644 --- a/gemfiles/rails_7_1.gemfile +++ b/gemfiles/rails_7_1.gemfile @@ -11,9 +11,9 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '1.50.2', require: false - gem 'rubocop-performance', '1.17.1', require: false - gem 'rubocop-rspec', '2.20.0', require: false + gem 'rubocop', '1.59.0', require: false + gem 'rubocop-performance', '1.20.1', require: false + gem 'rubocop-rspec', '2.25.0', require: false end group :development do diff --git a/gemfiles/rails_edge.gemfile b/gemfiles/rails_edge.gemfile index b38d71422..cf0906d20 100644 --- a/gemfiles/rails_edge.gemfile +++ b/gemfiles/rails_edge.gemfile @@ -10,9 +10,9 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '1.50.2', require: false - gem 'rubocop-performance', '1.17.1', require: false - gem 'rubocop-rspec', '2.20.0', require: false + gem 'rubocop', '1.59.0', require: false + gem 'rubocop-performance', '1.20.1', require: false + gem 'rubocop-rspec', '2.25.0', require: false end group :development do diff --git a/lib/grape/exceptions/base.rb b/lib/grape/exceptions/base.rb index f0f7b20e0..e262646c9 100644 --- a/lib/grape/exceptions/base.rb +++ b/lib/grape/exceptions/base.rb @@ -41,15 +41,15 @@ def compose_message(key, **attributes) end def problem(key, **attributes) - translate_message("#{key}.problem".to_sym, **attributes) + translate_message(:"#{key}.problem", **attributes) end def summary(key, **attributes) - translate_message("#{key}.summary".to_sym, **attributes) + translate_message(:"#{key}.summary", **attributes) end def resolution(key, **attributes) - translate_message("#{key}.resolution".to_sym, **attributes) + translate_message(:"#{key}.resolution", **attributes) end def translate_attributes(keys, **options) diff --git a/lib/grape/exceptions/validation_errors.rb b/lib/grape/exceptions/validation_errors.rb index 23ed6028a..4b3d5b9e0 100644 --- a/lib/grape/exceptions/validation_errors.rb +++ b/lib/grape/exceptions/validation_errors.rb @@ -14,7 +14,7 @@ class ValidationErrors < Grape::Exceptions::Base def initialize(errors: [], headers: {}, **_options) @errors = errors.group_by(&:params) - super message: full_messages.join(', '), status: 400, headers: headers + super(message: full_messages.join(', '), status: 400, headers: headers) end def each diff --git a/lib/grape/middleware/stack.rb b/lib/grape/middleware/stack.rb index 2e143ac4f..9ba339a78 100644 --- a/lib/grape/middleware/stack.rb +++ b/lib/grape/middleware/stack.rb @@ -76,11 +76,10 @@ def insert_after(index, *args, &block) end ruby2_keywords :insert_after if respond_to?(:ruby2_keywords, true) - def use(*args, &block) - middleware = self.class::Middleware.new(*args, &block) + def use(...) + middleware = self.class::Middleware.new(...) middlewares.push(middleware) end - ruby2_keywords :use if respond_to?(:ruby2_keywords, true) def merge_with(middleware_specs) middleware_specs.each do |operation, *args| diff --git a/lib/grape/path.rb b/lib/grape/path.rb index 574855f3c..e08725e52 100644 --- a/lib/grape/path.rb +++ b/lib/grape/path.rb @@ -27,7 +27,7 @@ def root_prefix def uses_specific_format? if settings.key?(:format) && settings.key?(:content_types) - (settings[:format] && Array(settings[:content_types]).size == 1) + settings[:format] && Array(settings[:content_types]).size == 1 else false end @@ -35,7 +35,7 @@ def uses_specific_format? def uses_path_versioning? if settings.key?(:version) && settings[:version_options] && settings[:version_options].key?(:using) - (settings[:version] && settings[:version_options][:using] == :path) + settings[:version] && settings[:version_options][:using] == :path else false end diff --git a/lib/grape/router/attribute_translator.rb b/lib/grape/router/attribute_translator.rb index ffe72711c..2197e0efe 100644 --- a/lib/grape/router/attribute_translator.rb +++ b/lib/grape/router/attribute_translator.rb @@ -30,7 +30,7 @@ def initialize(**attributes) @attributes[attr] end - define_method("#{attr}=") do |val| + define_method(:"#{attr}=") do |val| @attributes[attr] = val end end diff --git a/lib/grape/router/route.rb b/lib/grape/router/route.rb index 616bc2116..5a68af2a9 100644 --- a/lib/grape/router/route.rb +++ b/lib/grape/router/route.rb @@ -31,7 +31,7 @@ def apply(app) end def match?(input) - return if input.blank? + return false if input.blank? attributes.forward_match ? input.start_with?(pattern.origin) : pattern.match?(input) end @@ -42,7 +42,7 @@ def params(input = nil) parsed = pattern.params(input) return {} unless parsed - parsed.delete_if { |_, value| value.nil? }.symbolize_keys + parsed.compact.symbolize_keys end private diff --git a/lib/grape/util/strict_hash_configuration.rb b/lib/grape/util/strict_hash_configuration.rb index 26a866e8a..4ac105856 100644 --- a/lib/grape/util/strict_hash_configuration.rb +++ b/lib/grape/util/strict_hash_configuration.rb @@ -56,19 +56,19 @@ def self.simple_settings_methods(setting_name, new_config_class) def self.nested_settings_methods(setting_name, new_config_class) new_config_class.class_eval do setting_name.each_pair do |key, value| - define_method "#{key}_context" do + define_method :"#{key}_context" do @contexts[key] ||= Grape::Util::StrictHashConfiguration.config_class(*value).new end define_method key do |&block| - send("#{key}_context").instance_exec(&block) + send(:"#{key}_context").instance_exec(&block) end end define_method :to_hash do @settings.to_hash.merge( setting_name.each_key.with_object({}) do |k, merge_hash| - merge_hash[k] = send("#{k}_context").to_hash + merge_hash[k] = send(:"#{k}_context").to_hash end ) end diff --git a/lib/grape/validations.rb b/lib/grape/validations.rb index 4b2e97baf..74d534f37 100644 --- a/lib/grape/validations.rb +++ b/lib/grape/validations.rb @@ -25,7 +25,7 @@ def deregister_validator(short_name) def require_validator(short_name) str_name = short_name.to_s validators.fetch(str_name) do - Grape::Validations::Validators.const_get("#{str_name.camelize}Validator") + Grape::Validations::Validators.const_get(:"#{str_name.camelize}Validator") end rescue NameError raise Grape::Exceptions::UnknownValidator.new(short_name) diff --git a/lib/grape/validations/params_scope.rb b/lib/grape/validations/params_scope.rb index b419d6514..6eaf7abaa 100644 --- a/lib/grape/validations/params_scope.rb +++ b/lib/grape/validations/params_scope.rb @@ -463,8 +463,7 @@ def check_incompatible_option_values(default, values, except_values, excepts) raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :values, values) if values && !values.is_a?(Proc) && !Array(default).all? { |def_val| values.include?(def_val) } if except_values && !except_values.is_a?(Proc) && Array(default).any? { |def_val| except_values.include?(def_val) } - raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :except, except_values) \ - + raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :except, except_values) end return unless excepts && !excepts.is_a?(Proc) diff --git a/lib/grape/validations/types/dry_type_coercer.rb b/lib/grape/validations/types/dry_type_coercer.rb index 97a1f8287..cc529c07c 100644 --- a/lib/grape/validations/types/dry_type_coercer.rb +++ b/lib/grape/validations/types/dry_type_coercer.rb @@ -25,7 +25,7 @@ class << self # #=> Grape::Validations::Types::ArrayCoercer def collection_coercer_for(type) collection_coercers.fetch(type) do - DryTypeCoercer.collection_coercers[type] = Grape::Validations::Types.const_get("#{type.name.camelize}Coercer") + DryTypeCoercer.collection_coercers[type] = Grape::Validations::Types.const_get(:"#{type.name.camelize}Coercer") end end diff --git a/spec/grape/middleware/formatter_spec.rb b/spec/grape/middleware/formatter_spec.rb index 59245ad11..025ab42a5 100644 --- a/spec/grape/middleware/formatter_spec.rb +++ b/spec/grape/middleware/formatter_spec.rb @@ -435,22 +435,25 @@ def to_xml expect(file).to receive(:each).and_yield('data') env = { 'PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/json' } status, headers, body = subject.call(env) - expect(status).to be == 200 - expect(headers.transform_keys(&:downcase)).to be == { 'content-type' => 'application/json' } - expect(read_chunks(body)).to be == ['data'] + expect(status).to eq 200 + expect(headers.transform_keys(&:downcase)).to eq({ 'content-type' => 'application/json' }) + expect(read_chunks(body)).to eq ['data'] end end context 'inheritable formatters' do - class InvalidFormatter - def self.call(_, _) - { message: 'invalid' }.to_json + let(:invalid_formatter) do + Class.new do + def self.call(_, _) + { message: 'invalid' }.to_json + end end end + let(:app) { ->(_env) { [200, {}, ['']] } } before do - Grape::Formatter.register :invalid, InvalidFormatter + Grape::Formatter.register :invalid, invalid_formatter Grape::ContentTypes.register :invalid, 'application/x-invalid' end diff --git a/spec/grape/path_spec.rb b/spec/grape/path_spec.rb index 43672146f..a222a2c40 100644 --- a/spec/grape/path_spec.rb +++ b/spec/grape/path_spec.rb @@ -184,8 +184,7 @@ module Grape context 'when using a specific format' do it 'accepts specified format' do path = described_class.new(nil, nil, {}) - allow(path).to receive(:uses_specific_format?).and_return(true) - allow(path).to receive(:settings).and_return({ format: :json }) + allow(path).to receive_messages(uses_specific_format?: true, settings: { format: :json }) expect(path.suffix).to eql('(.json)') end @@ -194,8 +193,7 @@ module Grape context 'when path versioning is used' do it "includes a '/'" do path = described_class.new(nil, nil, {}) - allow(path).to receive(:uses_specific_format?).and_return(false) - allow(path).to receive(:uses_path_versioning?).and_return(true) + allow(path).to receive_messages(uses_specific_format?: false, uses_path_versioning?: true) expect(path.suffix).to eql('(/.:format)') end @@ -204,24 +202,21 @@ module Grape context 'when path versioning is not used' do it "does not include a '/' when the path has a namespace" do path = described_class.new(nil, 'namespace', {}) - allow(path).to receive(:uses_specific_format?).and_return(false) - allow(path).to receive(:uses_path_versioning?).and_return(true) + allow(path).to receive_messages(uses_specific_format?: false, uses_path_versioning?: true) expect(path.suffix).to eql('(.:format)') end it "does not include a '/' when the path has a path" do path = described_class.new('/path', nil, {}) - allow(path).to receive(:uses_specific_format?).and_return(false) - allow(path).to receive(:uses_path_versioning?).and_return(true) + allow(path).to receive_messages(uses_specific_format?: false, uses_path_versioning?: true) expect(path.suffix).to eql('(.:format)') end it "includes a '/' otherwise" do path = described_class.new(nil, nil, {}) - allow(path).to receive(:uses_specific_format?).and_return(false) - allow(path).to receive(:uses_path_versioning?).and_return(true) + allow(path).to receive_messages(uses_specific_format?: false, uses_path_versioning?: true) expect(path.suffix).to eql('(/.:format)') end @@ -231,8 +226,7 @@ module Grape describe '#path_with_suffix' do it 'combines the path and suffix' do path = described_class.new(nil, nil, {}) - allow(path).to receive(:path).and_return('/the/path') - allow(path).to receive(:suffix).and_return('suffix') + allow(path).to receive_messages(path: '/the/path', suffix: 'suffix') expect(path.path_with_suffix).to eql('/the/pathsuffix') end @@ -240,9 +234,7 @@ module Grape context 'when using a specific format' do it 'might have a suffix with specified format' do path = described_class.new(nil, nil, {}) - allow(path).to receive(:path).and_return('/the/path') - allow(path).to receive(:uses_specific_format?).and_return(true) - allow(path).to receive(:settings).and_return({ format: :json }) + allow(path).to receive_messages(path: '/the/path', uses_specific_format?: true, settings: { format: :json }) expect(path.path_with_suffix).to eql('/the/path(.json)') end diff --git a/spec/grape/router/attribute_translator_spec.rb b/spec/grape/router/attribute_translator_spec.rb index 4ba5ffbce..54e22dd64 100644 --- a/spec/grape/router/attribute_translator_spec.rb +++ b/spec/grape/router/attribute_translator_spec.rb @@ -18,7 +18,7 @@ it "sets value for #{attribute}", :aggregate_failures do translator = described_class.new(attribute => 'value') expect do - translator.public_send("#{attribute}=", 'new_value') + translator.public_send(:"#{attribute}=", 'new_value') end.to change(translator, attribute).from('value').to('new_value') end end From 584546374e7afea2eeee8f0a1e1e274a6164f4e9 Mon Sep 17 00:00:00 2001 From: keita hino Date: Wed, 3 Jan 2024 13:33:31 +0900 Subject: [PATCH 196/304] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3d4f586f8..7fdae1459 100644 --- a/README.md +++ b/README.md @@ -1348,7 +1348,7 @@ class Color end def self.parse(value) - return new(value) if %w[blue red green]).include?(value) + return new(value) if %w[blue red green].include?(value) Grape::Types::InvalidValue.new('Unsupported color') end From 40e62bfccc11db2a6ca3d19c3931aa5514d13f3b Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Wed, 3 Jan 2024 22:54:58 +0100 Subject: [PATCH 197/304] Add ruby 3.3 to edge and test flows (#2397) * Add ruby 3.3 to edge and test flows Update actions/checkout to v4 Remove a spec not related to endpoint method missing * Add CHANGELOG.md entry * Revert docker-compose --- .github/workflows/danger.yml | 2 +- .github/workflows/edge.yml | 4 ++-- .github/workflows/test.yml | 8 ++++---- CHANGELOG.md | 3 ++- docker-compose.yml | 2 +- spec/grape/endpoint_spec.rb | 11 ----------- 6 files changed, 10 insertions(+), 20 deletions(-) diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml index dffe84d15..5e99cbf53 100644 --- a/.github/workflows/danger.yml +++ b/.github/workflows/danger.yml @@ -6,7 +6,7 @@ jobs: danger: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 100 - name: Set up Ruby diff --git a/.github/workflows/edge.yml b/.github/workflows/edge.yml index bea28ef24..c617da14e 100644 --- a/.github/workflows/edge.yml +++ b/.github/workflows/edge.yml @@ -6,14 +6,14 @@ jobs: strategy: fail-fast: false matrix: - ruby: ['2.7', '3.0', '3.1', '3.2', ruby-head, truffleruby-head, jruby-head] + ruby: ['2.7', '3.0', '3.1', '3.2', '3.3', ruby-head, truffleruby-head, jruby-head] gemfile: [rails_edge, rack_edge, rack_3_0] runs-on: ubuntu-latest continue-on-error: true env: BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index daf28bdd6..c3201f44a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,12 +7,12 @@ jobs: name: RuboCop runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: 3.2 + ruby-version: 3.3 bundler-cache: true rubygems: latest @@ -23,7 +23,7 @@ jobs: strategy: fail-fast: false matrix: - ruby: ['2.7', '3.0', '3.1', '3.2'] + ruby: ['2.7', '3.0', '3.1', '3.2', '3.3'] gemfile: [rack_2_0, rack_3_0, rails_6_0, rails_6_1, rails_7_0, rails_7_1] include: - ruby: '2.7' @@ -36,7 +36,7 @@ jobs: env: BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 diff --git a/CHANGELOG.md b/CHANGELOG.md index fe97db2ec..57237c82c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ * [#2390](https://github.com/ruby-grape/grape/pull/2390): Drop support for Ruby 2.6 and Rails 5 - [@ericproulx](https://github.com/ericproulx). * [#2393](https://github.com/ruby-grape/grape/pull/2393): Optimize AttributeTranslator - [@ericproulx](https://github.com/ericproulx). * [#2395](https://github.com/ruby-grape/grape/pull/2395): Set `max-age` to 0 when `cookies.delete` - [@ericproulx](https://github.com/ericproulx). +* [#2397](https://github.com/ruby-grape/grape/pull/2397): Add support for ruby 3.3 - [@ericproulx](https://github.com/ericproulx). * [#2399](https://github.com/ruby-grape/grape/pull/2399): Update `rubocop` to 1.59.0, `rubocop-performance` to 1.20.1 and `rubocop-rspec` to 2.25.0 - [@ericproulx](https://github.com/ericproulx). * Your contribution here. @@ -63,7 +64,7 @@ #### Features -* [#2288](https://github.com/ruby-grape/grape/pull/2288): Droped support for Ruby 2.5 - [@ericproulx](https://github.com/ericproulx). +* [#2288](https://github.com/ruby-grape/grape/pull/2288): Dropped support for Ruby 2.5 - [@ericproulx](https://github.com/ericproulx). * [#2288](https://github.com/ruby-grape/grape/pull/2288): Updated rubocop to 1.41.0 - [@ericproulx](https://github.com/ericproulx). * [#2296](https://github.com/ruby-grape/grape/pull/2296): Fix cops and enables some - [@ericproulx](https://github.com/ericproulx). * [#2302](https://github.com/ruby-grape/grape/pull/2302): Rack < 3 and update rack-test - [@ericproulx](https://github.com/ericproulx). diff --git a/docker-compose.yml b/docker-compose.yml index 0f83ee017..2b293708b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,4 +14,4 @@ services: tty: true volumes: - .:/var/grape - - gems:/usr/local/bundle \ No newline at end of file + - gems:/usr/local/bundle diff --git a/spec/grape/endpoint_spec.rb b/spec/grape/endpoint_spec.rb index 14d99c627..46a17a2b7 100644 --- a/spec/grape/endpoint_spec.rb +++ b/spec/grape/endpoint_spec.rb @@ -694,17 +694,6 @@ def app end.to raise_error(NoMethodError, %r{^undefined method `undefined_helper' for # in `/hey' endpoint}) end end - - context 'when performing an undefined method of an instance inside the API' do - it 'raises NoMethodError but stripping the internals of the Object class' do - subject.get('/hey') do - Object.new.x - end - expect do - get '/hey' - end.to raise_error(NoMethodError, /^undefined method `x' for #$/) - end - end end it 'does not persist params between calls' do From 3d85058d26b683b9ac5a9706e76b354d207a4c7a Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Thu, 4 Jan 2024 18:32:26 +0100 Subject: [PATCH 198/304] Change Grape::Deprecator's Behavior in test (#2402) * Grape::Deprecator's Behavior is set to raise in test Fix multiple specs * Add CHANGELOG.md Revert docker-compose.yml --- CHANGELOG.md | 1 + spec/grape/api_spec.rb | 2 +- spec/grape/dsl/inside_route_spec.rb | 18 ------------- spec/grape/endpoint_spec.rb | 2 +- spec/grape/validations/params_scope_spec.rb | 2 +- .../validations/validators/values_spec.rb | 26 +++++++++---------- spec/integration/multi_xml/xml_spec.rb | 2 +- spec/spec_helper.rb | 2 ++ 8 files changed, 20 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57237c82c..43ff91251 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ * [#2395](https://github.com/ruby-grape/grape/pull/2395): Set `max-age` to 0 when `cookies.delete` - [@ericproulx](https://github.com/ericproulx). * [#2397](https://github.com/ruby-grape/grape/pull/2397): Add support for ruby 3.3 - [@ericproulx](https://github.com/ericproulx). * [#2399](https://github.com/ruby-grape/grape/pull/2399): Update `rubocop` to 1.59.0, `rubocop-performance` to 1.20.1 and `rubocop-rspec` to 2.25.0 - [@ericproulx](https://github.com/ericproulx). +* [#2402](https://github.com/ruby-grape/grape/pull/2402): Grape::Deprecations will be raised when running specs - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index 8c9d87603..243c6cb9e 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -1239,7 +1239,7 @@ class DummyFormatClass test_file.write file_content test_file.rewind - subject.get('/file') { file test_file } + subject.get('/file') { stream test_file } get '/file' expect(last_response.headers[Rack::CONTENT_LENGTH]).to eq('25') expect(last_response.headers[Rack::CONTENT_TYPE]).to eq('text/plain') diff --git a/spec/grape/dsl/inside_route_spec.rb b/spec/grape/dsl/inside_route_spec.rb index 931382e48..7c6d75236 100644 --- a/spec/grape/dsl/inside_route_spec.rb +++ b/spec/grape/dsl/inside_route_spec.rb @@ -209,13 +209,7 @@ def initialize it 'emits a warning that this method is deprecated' do expect(Grape.deprecator).to receive(:warn).with(/Use sendfile or stream/) - - subject.file file_path - end - - it 'forwards the call to sendfile' do expect(subject).to receive(:sendfile).with(file_path) - subject.file file_path end end @@ -225,13 +219,7 @@ def initialize it 'emits a warning that this method is deprecated' do expect(Grape.deprecator).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 @@ -240,13 +228,7 @@ def initialize describe 'get' do it 'emits a warning that this method is deprecated' do expect(Grape.deprecator).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 diff --git a/spec/grape/endpoint_spec.rb b/spec/grape/endpoint_spec.rb index 46a17a2b7..94d0443c3 100644 --- a/spec/grape/endpoint_spec.rb +++ b/spec/grape/endpoint_spec.rb @@ -979,7 +979,7 @@ def memoized context 'binary' do before do subject.get do - file FileStreamer.new(__FILE__) + stream FileStreamer.new(__FILE__) end end diff --git a/spec/grape/validations/params_scope_spec.rb b/spec/grape/validations/params_scope_spec.rb index 05fa9c265..71caa267d 100644 --- a/spec/grape/validations/params_scope_spec.rb +++ b/spec/grape/validations/params_scope_spec.rb @@ -179,7 +179,7 @@ def initialize(value) end it 'allows the proc to pass validation without checking in except' do - subject.params { requires :numbers, type: Integer, values: { except: -> { [0, 1, 2] } } } + subject.params { requires :numbers, type: Integer, except_values: -> { [0, 1, 2] } } subject.post('/required') { 'coercion with proc works' } post '/required', numbers: '10' diff --git a/spec/grape/validations/validators/values_spec.rb b/spec/grape/validations/validators/values_spec.rb index 5d8daab91..4780829a1 100644 --- a/spec/grape/validations/validators/values_spec.rb +++ b/spec/grape/validations/validators/values_spec.rb @@ -63,17 +63,17 @@ def even?(value) end params do - requires :type, values: { except: ValuesModel.excepts, except_message: 'value is on exclusions list', message: 'default exclude message' } + requires :type, except_values: { value: ValuesModel.excepts, message: 'value is on exclusions list' }, default: 'default exclude message' end get '/exclude/exclude_message' params do - requires :type, values: { except: -> { ValuesModel.excepts }, except_message: 'value is on exclusions list' } + requires :type, except_values: { value: -> { ValuesModel.excepts }, message: 'value is on exclusions list' } end get '/exclude/lambda/exclude_message' params do - requires :type, values: { except: ValuesModel.excepts, message: 'default exclude message' } + requires :type, except_values: { value: ValuesModel.excepts, message: 'default exclude message' } end get '/exclude/fallback_message' end @@ -105,7 +105,7 @@ def even?(value) end params do - optional :type, values: { except: ValuesModel.excepts }, default: 'valid-type2' + optional :type, except_values: ValuesModel.excepts, default: 'valid-type2' end get '/default/except' do { type: params[:type] } @@ -187,42 +187,42 @@ def even?(value) get '/optional_with_required_values' params do - requires :type, values: { except: ValuesModel.excepts } + requires :type, except_values: ValuesModel.excepts end get '/except/exclusive' do { type: params[:type] } end params do - requires :type, type: String, values: { except: ValuesModel.excepts } + requires :type, type: String, except_values: ValuesModel.excepts end get '/except/exclusive/type' do { type: params[:type] } end params do - requires :type, values: { except: -> { ValuesModel.excepts } } + requires :type, except_values: ValuesModel.excepts end get '/except/exclusive/lambda' do { type: params[:type] } end params do - requires :type, type: String, values: { except: -> { ValuesModel.excepts } } + requires :type, type: String, except_values: -> { ValuesModel.excepts } end get '/except/exclusive/lambda/type' do { type: params[:type] } end params do - requires :type, type: Integer, values: { except: -> { [3, 4, 5] } } + requires :type, type: Integer, except_values: -> { [3, 4, 5] } end get '/except/exclusive/lambda/coercion' do { type: params[:type] } end params do - requires :type, type: Integer, values: { value: 1..5, except: [3] } + requires :type, type: Integer, values: 1..5, except_values: [3] end get '/mixed/value/except' do { type: params[:type] } @@ -234,14 +234,14 @@ def even?(value) put '/optional_with_array_of_string_values' params do - requires :type, values: { proc: ->(v) { ValuesModel.include? v } } + requires :type, values: ->(v) { ValuesModel.include? v } end get '/proc' do { type: params[:type] } end params do - requires :type, values: { proc: ->(v) { ValuesModel.include? v }, message: 'failed check' } + requires :type, values: { value: ->(v) { ValuesModel.include? v }, message: 'failed check' } end get '/proc/message' @@ -520,7 +520,7 @@ def even?(value) it 'raises IncompatibleOptionValues when except contains a value that is not a kind of the type' do subject = Class.new(Grape::API) expect do - subject.params { requires :type, values: { except: [10.5, 11] }, type: Integer } + subject.params { requires :type, except_values: [10.5, 11], type: Integer } end.to raise_error Grape::Exceptions::IncompatibleOptionValues end diff --git a/spec/integration/multi_xml/xml_spec.rb b/spec/integration/multi_xml/xml_spec.rb index 54d918e55..9dc4b5094 100644 --- a/spec/integration/multi_xml/xml_spec.rb +++ b/spec/integration/multi_xml/xml_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe Grape::Xml do +describe Grape::Xml, if: defined?(MultiXml) do it 'uses multi_xml' do expect(described_class).to eq(::MultiXml) end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 1a2581ae7..af3ff8256 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -10,6 +10,8 @@ require 'grape' +Grape.deprecator.behavior = :raise + %w[config support].each do |dir| Dir["#{File.dirname(__FILE__)}/#{dir}/**/*.rb"].sort.each do |file| require file From 10944de4cf81fb23bf0ea2abcf7e8bcfe59db5b3 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Fri, 5 Jan 2024 23:08:21 +0100 Subject: [PATCH 199/304] Remove mime-types dependency in specs (#2406) * Remove mime-types dependency in specs * Add CHANGELOG.md entry --- CHANGELOG.md | 1 + Gemfile | 1 - gemfiles/multi_json.gemfile | 1 - gemfiles/multi_xml.gemfile | 1 - gemfiles/rack_1_0.gemfile | 1 - gemfiles/rack_2_0.gemfile | 1 - gemfiles/rack_3_0.gemfile | 1 - gemfiles/rack_edge.gemfile | 1 - gemfiles/rails_6_0.gemfile | 1 - gemfiles/rails_6_1.gemfile | 1 - gemfiles/rails_7_0.gemfile | 1 - gemfiles/rails_7_1.gemfile | 1 - gemfiles/rails_edge.gemfile | 1 - spec/grape/api_spec.rb | 45 ++++++++++++++++++++++--------------- 14 files changed, 28 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43ff91251..d6ba50c99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ * [#2397](https://github.com/ruby-grape/grape/pull/2397): Add support for ruby 3.3 - [@ericproulx](https://github.com/ericproulx). * [#2399](https://github.com/ruby-grape/grape/pull/2399): Update `rubocop` to 1.59.0, `rubocop-performance` to 1.20.1 and `rubocop-rspec` to 2.25.0 - [@ericproulx](https://github.com/ericproulx). * [#2402](https://github.com/ruby-grape/grape/pull/2402): Grape::Deprecations will be raised when running specs - [@ericproulx](https://github.com/ericproulx). +* [#2406](https://github.com/ruby-grape/grape/pull/2406): Remove mime-types dependency in specs - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/Gemfile b/Gemfile index f845da35c..e7731ef6f 100644 --- a/Gemfile +++ b/Gemfile @@ -26,7 +26,6 @@ end group :test do gem 'grape-entity', '~> 0.6', require: false - gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' gem 'rspec', '< 4' diff --git a/gemfiles/multi_json.gemfile b/gemfiles/multi_json.gemfile index b7030feaf..5ed288a8d 100644 --- a/gemfiles/multi_json.gemfile +++ b/gemfiles/multi_json.gemfile @@ -26,7 +26,6 @@ end group :test do gem 'grape-entity', '~> 0.6', require: false - gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' gem 'rspec', '< 4' diff --git a/gemfiles/multi_xml.gemfile b/gemfiles/multi_xml.gemfile index b4690fe96..dfb2a8730 100644 --- a/gemfiles/multi_xml.gemfile +++ b/gemfiles/multi_xml.gemfile @@ -26,7 +26,6 @@ end group :test do gem 'grape-entity', '~> 0.6', require: false - gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' gem 'rspec', '< 4' diff --git a/gemfiles/rack_1_0.gemfile b/gemfiles/rack_1_0.gemfile index 0461250b4..41730950f 100644 --- a/gemfiles/rack_1_0.gemfile +++ b/gemfiles/rack_1_0.gemfile @@ -26,7 +26,6 @@ end group :test do gem 'grape-entity', '~> 0.6', require: false - gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' gem 'rspec', '< 4' diff --git a/gemfiles/rack_2_0.gemfile b/gemfiles/rack_2_0.gemfile index abda41200..8b9cced0e 100644 --- a/gemfiles/rack_2_0.gemfile +++ b/gemfiles/rack_2_0.gemfile @@ -26,7 +26,6 @@ end group :test do gem 'grape-entity', '~> 0.6', require: false - gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' gem 'rspec', '< 4' diff --git a/gemfiles/rack_3_0.gemfile b/gemfiles/rack_3_0.gemfile index 958b2dd9d..59fa6a9b9 100644 --- a/gemfiles/rack_3_0.gemfile +++ b/gemfiles/rack_3_0.gemfile @@ -26,7 +26,6 @@ end group :test do gem 'grape-entity', '~> 0.6', require: false - gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' gem 'rspec', '< 4' diff --git a/gemfiles/rack_edge.gemfile b/gemfiles/rack_edge.gemfile index f9bd6c4a8..c61fb1914 100644 --- a/gemfiles/rack_edge.gemfile +++ b/gemfiles/rack_edge.gemfile @@ -26,7 +26,6 @@ end group :test do gem 'grape-entity', '~> 0.6', require: false - gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' gem 'rspec', '< 4' diff --git a/gemfiles/rails_6_0.gemfile b/gemfiles/rails_6_0.gemfile index 357aa8a34..eec0b6074 100644 --- a/gemfiles/rails_6_0.gemfile +++ b/gemfiles/rails_6_0.gemfile @@ -26,7 +26,6 @@ end group :test do gem 'grape-entity', '~> 0.6', require: false - gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' gem 'rspec', '< 4' diff --git a/gemfiles/rails_6_1.gemfile b/gemfiles/rails_6_1.gemfile index 6cbd5d6fc..5daa55be9 100644 --- a/gemfiles/rails_6_1.gemfile +++ b/gemfiles/rails_6_1.gemfile @@ -26,7 +26,6 @@ end group :test do gem 'grape-entity', '~> 0.6', require: false - gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' gem 'rspec', '< 4' diff --git a/gemfiles/rails_7_0.gemfile b/gemfiles/rails_7_0.gemfile index 5d2beedab..9e16ddb55 100644 --- a/gemfiles/rails_7_0.gemfile +++ b/gemfiles/rails_7_0.gemfile @@ -26,7 +26,6 @@ end group :test do gem 'grape-entity', '~> 0.6', require: false - gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' gem 'rspec', '< 4' diff --git a/gemfiles/rails_7_1.gemfile b/gemfiles/rails_7_1.gemfile index 79131db63..940148931 100644 --- a/gemfiles/rails_7_1.gemfile +++ b/gemfiles/rails_7_1.gemfile @@ -27,7 +27,6 @@ end group :test do gem 'grape-entity', '~> 0.6', require: false - gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' gem 'rspec', '< 4' diff --git a/gemfiles/rails_edge.gemfile b/gemfiles/rails_edge.gemfile index cf0906d20..cac5c4970 100644 --- a/gemfiles/rails_edge.gemfile +++ b/gemfiles/rails_edge.gemfile @@ -26,7 +26,6 @@ end group :test do gem 'grape-entity', '~> 0.6', require: false - gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' gem 'rack-test', '< 2.1' gem 'rspec', '< 4' diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index 243c6cb9e..dc31094af 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -1324,36 +1324,45 @@ class DummyFormatClass context 'env["api.format"]' do before do + ct = content_type subject.post 'attachment' do filename = params[:file][:filename] - content_type MIME::Types.type_for(filename)[0].to_s + content_type ct env['api.format'] = :binary # there's no formatter for :binary, data will be returned "as is" header 'Content-Disposition', "attachment; filename*=UTF-8''#{CGI.escape(filename)}" params[:file][:tempfile].read end end - ['/attachment.png', 'attachment'].each do |url| - it "uploads and downloads a PNG file via #{url}" do - image_filename = 'grape.png' - post url, file: Rack::Test::UploadedFile.new(image_filename, 'image/png', true) - expect(last_response.status).to eq(201) - expect(last_response.headers[Rack::CONTENT_TYPE]).to eq('image/png') - expect(last_response.headers['Content-Disposition']).to eq("attachment; filename*=UTF-8''grape.png") - File.open(image_filename, 'rb') do |io| - expect(last_response.body).to eq io.read + context 'when image/png' do + let(:content_type) { 'image/png' } + + %w[/attachment.png attachment].each do |url| + it "uploads and downloads a PNG file via #{url}" do + image_filename = 'grape.png' + post url, file: Rack::Test::UploadedFile.new(image_filename, content_type, true) + expect(last_response.status).to eq(201) + expect(last_response.headers[Rack::CONTENT_TYPE]).to eq(content_type) + expect(last_response.headers['Content-Disposition']).to eq("attachment; filename*=UTF-8''grape.png") + File.open(image_filename, 'rb') do |io| + expect(last_response.body).to eq io.read + end end end end - it 'uploads and downloads a Ruby file' do - filename = __FILE__ - post '/attachment.rb', file: Rack::Test::UploadedFile.new(filename, 'application/x-ruby', true) - expect(last_response.status).to eq(201) - expect(last_response.headers[Rack::CONTENT_TYPE]).to eq('application/x-ruby') - expect(last_response.headers['Content-Disposition']).to eq("attachment; filename*=UTF-8''api_spec.rb") - File.open(filename, 'rb') do |io| - expect(last_response.body).to eq io.read + context 'when ruby file' do + let(:content_type) { 'application/x-ruby' } + + it 'uploads and downloads a Ruby file' do + filename = __FILE__ + post '/attachment.rb', file: Rack::Test::UploadedFile.new(filename, content_type, true) + expect(last_response.status).to eq(201) + expect(last_response.headers[Rack::CONTENT_TYPE]).to eq(content_type) + expect(last_response.headers['Content-Disposition']).to eq("attachment; filename*=UTF-8''api_spec.rb") + File.open(filename, 'rb') do |io| + expect(last_response.body).to eq io.read + end end end end From 6888ad6d0f9eca03f71ce12eb76ec5475c9f5276 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sat, 6 Jan 2024 23:34:25 +0100 Subject: [PATCH 200/304] Fix Rails Edge ruby 3.1 (#2405) * Fix #2403 * Fix #2404 Replace last_response.headers[Rack::CONTENT_TYPE] by last_response.content_type Replace last_response.headers['Location'] by last_response.content_type Replace last_response.headers[Rack::CONTENT_LENGTH] by last_response.content_type * Add CHANGELOG.md entry * Fix rubocop * Update CHANGELOG.md * Remove Rack::Chunked deprecation --- .github/workflows/edge.yml | 7 ++- CHANGELOG.md | 1 + spec/grape/api_spec.rb | 38 ++++++------ spec/grape/endpoint_spec.rb | 8 +-- spec/grape/entity_spec.rb | 6 +- spec/grape/middleware/base_spec.rb | 4 +- spec/support/basic_auth_encode_helpers.rb | 2 + spec/support/chunked_response.rb | 73 +++++++++++++++++++++++ 8 files changed, 110 insertions(+), 29 deletions(-) create mode 100644 spec/support/chunked_response.rb diff --git a/.github/workflows/edge.yml b/.github/workflows/edge.yml index c617da14e..cfa629b0b 100644 --- a/.github/workflows/edge.yml +++ b/.github/workflows/edge.yml @@ -7,7 +7,12 @@ jobs: fail-fast: false matrix: ruby: ['2.7', '3.0', '3.1', '3.2', '3.3', ruby-head, truffleruby-head, jruby-head] - gemfile: [rails_edge, rack_edge, rack_3_0] + gemfile: [rails_edge, rack_edge] + exclude: + - ruby: '2.7' + gemfile: rails_edge + - ruby: '3.0' + gemfile: rails_edge runs-on: ubuntu-latest continue-on-error: true env: diff --git a/CHANGELOG.md b/CHANGELOG.md index d6ba50c99..ac5cb93f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ * [#2373](https://github.com/ruby-grape/grape/pull/2373): Fix markdown files for following 1-line format - [@jcagarcia](https://github.com/jcagarcia). * [#2382](https://github.com/ruby-grape/grape/pull/2382): Fix values validator for params wrapped in `with` block - [@numbata](https://github.com/numbata). * [#2387](https://github.com/ruby-grape/grape/pull/2387): Fix rubygems version within workflows - [@ericproulx](https://github.com/ericproulx). +* [#2405](https://github.com/ruby-grape/grape/pull/2405): Fix edge workflow - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 2.0.0 (2023/11/11) diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index dc31094af..97c2dbff0 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -688,7 +688,7 @@ class DummyFormatClass 'example' end put '/example' - expect(last_response.headers[Rack::CONTENT_TYPE]).to eql 'text/plain' + expect(last_response.content_type).to eql 'text/plain' end describe 'adds an OPTIONS route that' do @@ -1195,7 +1195,7 @@ class DummyFormatClass it 'sets content type for txt format' do get '/foo' - expect(last_response.headers[Rack::CONTENT_TYPE]).to eq('text/plain') + expect(last_response.content_type).to eq('text/plain') end it 'does not set Cache-Control' do @@ -1205,22 +1205,22 @@ class DummyFormatClass it 'sets content type for xml' do get '/foo.xml' - expect(last_response.headers[Rack::CONTENT_TYPE]).to eq('application/xml') + expect(last_response.content_type).to eq('application/xml') end it 'sets content type for json' do get '/foo.json' - expect(last_response.headers[Rack::CONTENT_TYPE]).to eq('application/json') + expect(last_response.content_type).to eq('application/json') end it 'sets content type for serializable hash format' do get '/foo.serializable_hash' - expect(last_response.headers[Rack::CONTENT_TYPE]).to eq('application/json') + expect(last_response.content_type).to eq('application/json') end it 'sets content type for binary format' do get '/foo.binary' - expect(last_response.headers[Rack::CONTENT_TYPE]).to eq('application/octet-stream') + expect(last_response.content_type).to eq('application/octet-stream') end it 'returns raw data when content type binary' do @@ -1229,7 +1229,7 @@ class DummyFormatClass subject.format :binary subject.get('/binary_file') { File.binread(image_filename) } get '/binary_file' - expect(last_response.headers[Rack::CONTENT_TYPE]).to eq('application/octet-stream') + expect(last_response.content_type).to eq('application/octet-stream') expect(last_response.body).to eq(file) end @@ -1241,8 +1241,8 @@ class DummyFormatClass subject.get('/file') { stream test_file } get '/file' - expect(last_response.headers[Rack::CONTENT_LENGTH]).to eq('25') - expect(last_response.headers[Rack::CONTENT_TYPE]).to eq('text/plain') + expect(last_response.content_length).to eq(25) + expect(last_response.content_type).to eq('text/plain') expect(last_response.body).to eq(file_content) end @@ -1252,12 +1252,12 @@ class DummyFormatClass blk.yield ' file content' end - subject.use Rack::Chunked + subject.use Gem::Version.new(Rack.release) < Gem::Version.new('3') ? Rack::Chunked : ChunkedResponse subject.get('/stream') { stream test_stream } get '/stream', {}, 'HTTP_VERSION' => 'HTTP/1.1', 'SERVER_PROTOCOL' => 'HTTP/1.1' - expect(last_response.headers[Rack::CONTENT_TYPE]).to eq('text/plain') - expect(last_response.headers[Rack::CONTENT_LENGTH]).to be_nil + expect(last_response.content_type).to eq('text/plain') + expect(last_response.content_length).to be_nil expect(last_response.headers[Rack::CACHE_CONTROL]).to eq('no-cache') expect(last_response.headers[Grape::Http::Headers::TRANSFER_ENCODING]).to eq('chunked') @@ -1267,7 +1267,7 @@ class DummyFormatClass it 'sets content type for error' do subject.get('/error') { error!('error in plain text', 500) } get '/error' - expect(last_response.headers[Rack::CONTENT_TYPE]).to eql 'text/plain' + expect(last_response.content_type).to eql 'text/plain' end it 'sets content type for json error' do @@ -1275,7 +1275,7 @@ class DummyFormatClass subject.get('/error') { error!('error in json', 500) } get '/error.json' expect(last_response.status).to be 500 - expect(last_response.headers[Rack::CONTENT_TYPE]).to eql 'application/json' + expect(last_response.content_type).to eql 'application/json' end it 'sets content type for xml error' do @@ -1283,7 +1283,7 @@ class DummyFormatClass subject.get('/error') { error!('error in xml', 500) } get '/error' expect(last_response.status).to be 500 - expect(last_response.headers[Rack::CONTENT_TYPE]).to eql 'application/xml' + expect(last_response.content_type).to eql 'application/xml' end it 'includes extension in format' do @@ -1313,12 +1313,12 @@ class DummyFormatClass it 'sets content type' do get '/custom.custom' - expect(last_response.headers[Rack::CONTENT_TYPE]).to eql 'application/custom' + expect(last_response.content_type).to eql 'application/custom' end it 'sets content type for error' do get '/error.custom' - expect(last_response.headers[Rack::CONTENT_TYPE]).to eql 'application/custom' + expect(last_response.content_type).to eql 'application/custom' end end @@ -1342,7 +1342,7 @@ class DummyFormatClass image_filename = 'grape.png' post url, file: Rack::Test::UploadedFile.new(image_filename, content_type, true) expect(last_response.status).to eq(201) - expect(last_response.headers[Rack::CONTENT_TYPE]).to eq(content_type) + expect(last_response.content_type).to eq(content_type) expect(last_response.headers['Content-Disposition']).to eq("attachment; filename*=UTF-8''grape.png") File.open(image_filename, 'rb') do |io| expect(last_response.body).to eq io.read @@ -1358,7 +1358,7 @@ class DummyFormatClass filename = __FILE__ post '/attachment.rb', file: Rack::Test::UploadedFile.new(filename, content_type, true) expect(last_response.status).to eq(201) - expect(last_response.headers[Rack::CONTENT_TYPE]).to eq(content_type) + expect(last_response.content_type).to eq(content_type) expect(last_response.headers['Content-Disposition']).to eq("attachment; filename*=UTF-8''api_spec.rb") File.open(filename, 'rb') do |io| expect(last_response.body).to eq io.read diff --git a/spec/grape/endpoint_spec.rb b/spec/grape/endpoint_spec.rb index 94d0443c3..77dd116c0 100644 --- a/spec/grape/endpoint_spec.rb +++ b/spec/grape/endpoint_spec.rb @@ -490,7 +490,7 @@ def app end it 'responses with given content type in headers' do - expect(last_response.headers[Rack::CONTENT_TYPE]).to eq 'application/json; charset=utf-8' + expect(last_response.content_type).to eq 'application/json; charset=utf-8' end end @@ -650,7 +650,7 @@ def app end get '/hey' expect(last_response.status).to eq 302 - expect(last_response.headers['Location']).to eq '/ha' + expect(last_response.location).to eq '/ha' expect(last_response.body).to eq 'This resource has been moved temporarily to /ha.' end @@ -660,7 +660,7 @@ def app end post '/hey', {}, 'HTTP_VERSION' => 'HTTP/1.1' expect(last_response.status).to eq 303 - expect(last_response.headers['Location']).to eq '/ha' + expect(last_response.location).to eq '/ha' expect(last_response.body).to eq 'An alternate resource is located at /ha.' end @@ -670,7 +670,7 @@ def app end get '/hey' expect(last_response.status).to eq 301 - expect(last_response.headers['Location']).to eq '/ha' + expect(last_response.location).to eq '/ha' expect(last_response.body).to eq 'This resource has been moved permanently to /ha.' end diff --git a/spec/grape/entity_spec.rb b/spec/grape/entity_spec.rb index 425edf003..7eaa0fae8 100644 --- a/spec/grape/entity_spec.rb +++ b/spec/grape/entity_spec.rb @@ -236,7 +236,7 @@ def initialize(args) end get '/example' expect(last_response.status).to eq(200) - expect(last_response.headers['Content-type']).to eq('application/xml') + expect(last_response.content_type).to eq('application/xml') expect(last_response.body).to eq <<~XML @@ -266,7 +266,7 @@ def initialize(args) end get '/example' expect(last_response.status).to eq(200) - expect(last_response.headers['Content-type']).to eq('application/json') + expect(last_response.content_type).to eq('application/json') expect(last_response.body).to eq('{"example":{"name":"johnnyiller"}}') end @@ -298,7 +298,7 @@ def initialize(args) get '/example?callback=abcDef' expect(last_response.status).to eq(200) - expect(last_response.headers['Content-type']).to eq('application/javascript') + expect(last_response.content_type).to eq('application/javascript') expect(last_response.body).to include 'abcDef({"example":{"name":"johnnyiller"}})' end diff --git a/spec/grape/middleware/base_spec.rb b/spec/grape/middleware/base_spec.rb index cbba974e1..3be95be1f 100644 --- a/spec/grape/middleware/base_spec.rb +++ b/spec/grape/middleware/base_spec.rb @@ -93,7 +93,7 @@ end it 'header' do - expect(subject.response.header).to have_key(:abc) + expect(subject.response.headers).to have_key(:abc) end it 'returns the memoized Rack::Response instance' do @@ -115,7 +115,7 @@ end it 'header' do - expect(subject.response.header).to have_key(:abc) + expect(subject.response.headers).to have_key(:abc) end it 'returns the memoized Rack::Response instance' do diff --git a/spec/support/basic_auth_encode_helpers.rb b/spec/support/basic_auth_encode_helpers.rb index 78e21e6c8..00c3c6149 100644 --- a/spec/support/basic_auth_encode_helpers.rb +++ b/spec/support/basic_auth_encode_helpers.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'base64' + module Spec module Support module Helpers diff --git a/spec/support/chunked_response.rb b/spec/support/chunked_response.rb new file mode 100644 index 000000000..41defe87e --- /dev/null +++ b/spec/support/chunked_response.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +# this is a copy of Rack::Chunked which has been removed in rack > 3.0 + +class ChunkedResponse + class Body + TERM = "\r\n" + TAIL = "0#{TERM}" + + # Store the response body to be chunked. + def initialize(body) + @body = body + end + + # For each element yielded by the response body, yield + # the element in chunked encoding. + def each(&block) + term = TERM + @body.each do |chunk| + size = chunk.bytesize + next if size == 0 + + yield [size.to_s(16), term, chunk.b, term].join + end + yield TAIL + yield_trailers(&block) + yield term + end + + # Close the response body if the response body supports it. + def close + @body.close if @body.respond_to?(:close) + end + + private + + # Do nothing as this class does not support trailer headers. + def yield_trailers; end + end + + class TrailerBody < Body + private + + # Yield strings for each trailer header. + def yield_trailers + @body.trailers.each_pair do |k, v| + yield "#{k}: #{v}\r\n" + end + end + end + + def initialize(app) + @app = app + end + + def call(env) + status, headers, body = response = @app.call(env) + + if !Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) && + !headers[Rack::CONTENT_LENGTH] && + !headers[Rack::TRANSFER_ENCODING] + + headers[Rack::TRANSFER_ENCODING] = 'chunked' + response[2] = if headers['trailer'] + TrailerBody.new(body) + else + Body.new(body) + end + end + + response + end +end From 3a6aeac73f65d47180d74674701b3ca0b90d99d3 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sat, 6 Jan 2024 23:35:17 +0100 Subject: [PATCH 201/304] Fix params method redefined warnings (#2408) * Fix params method redefined warnings Activate warnings in specs * Add CHANGELOG.md entry * Add CHANGELOG.md entry * Bad entry --- .rspec | 1 + CHANGELOG.md | 1 + lib/grape/router/greedy_route.rb | 3 ++- lib/grape/router/route.rb | 3 ++- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.rspec b/.rspec index d4e55c387..27a9fb777 100644 --- a/.rspec +++ b/.rspec @@ -2,3 +2,4 @@ --color --format=documentation --order=rand +--warnings diff --git a/CHANGELOG.md b/CHANGELOG.md index ac5cb93f8..a25ea7b99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ * [#2399](https://github.com/ruby-grape/grape/pull/2399): Update `rubocop` to 1.59.0, `rubocop-performance` to 1.20.1 and `rubocop-rspec` to 2.25.0 - [@ericproulx](https://github.com/ericproulx). * [#2402](https://github.com/ruby-grape/grape/pull/2402): Grape::Deprecations will be raised when running specs - [@ericproulx](https://github.com/ericproulx). * [#2406](https://github.com/ruby-grape/grape/pull/2406): Remove mime-types dependency in specs - [@ericproulx](https://github.com/ericproulx). +* [#2408](https://github.com/ruby-grape/grape/pull/2408): Fix params method redefined warnings - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/lib/grape/router/greedy_route.rb b/lib/grape/router/greedy_route.rb index ac577eb56..765aa83de 100644 --- a/lib/grape/router/greedy_route.rb +++ b/lib/grape/router/greedy_route.rb @@ -13,7 +13,8 @@ class GreedyRoute attr_reader :index, :pattern, :options, :attributes - delegate Grape::Router::AttributeTranslator::ROUTE_ATTRIBUTES => :@attributes + # params must be handled in this class to avoid method redefined warning + delegate Grape::Router::AttributeTranslator::ROUTE_ATTRIBUTES - [:params] => :@attributes def initialize(index:, pattern:, **options) @index = index diff --git a/lib/grape/router/route.rb b/lib/grape/router/route.rb index 5a68af2a9..7d4a912a3 100644 --- a/lib/grape/router/route.rb +++ b/lib/grape/router/route.rb @@ -13,7 +13,8 @@ class Route attr_accessor :index def_delegators :pattern, :path, :origin - delegate Grape::Router::AttributeTranslator::ROUTE_ATTRIBUTES => :attributes + # params must be handled in this class to avoid method redefined warning + delegate Grape::Router::AttributeTranslator::ROUTE_ATTRIBUTES - [:params] => :attributes def initialize(method, pattern, **options) @options = options From 3674ae44df53c8d7a49f4613aa8450fac0695b39 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sun, 7 Jan 2024 14:17:02 +0100 Subject: [PATCH 202/304] Add DeprecationWarning to handle gems deprecations in a best effort (#2410) * Add DeprecationWarning to handle gems deprecations in a best effort * Change deprecation regex * Add CHANGELOG.md entry --- CHANGELOG.md | 1 + spec/support/deprecated_warning_handlers.rb | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 spec/support/deprecated_warning_handlers.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index a25ea7b99..b87d4bfd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ * [#2402](https://github.com/ruby-grape/grape/pull/2402): Grape::Deprecations will be raised when running specs - [@ericproulx](https://github.com/ericproulx). * [#2406](https://github.com/ruby-grape/grape/pull/2406): Remove mime-types dependency in specs - [@ericproulx](https://github.com/ericproulx). * [#2408](https://github.com/ruby-grape/grape/pull/2408): Fix params method redefined warnings - [@ericproulx](https://github.com/ericproulx). +* [#2410](https://github.com/ruby-grape/grape/pull/2410): Gem deprecations will raise a DeprecationWarning in specs - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/spec/support/deprecated_warning_handlers.rb b/spec/support/deprecated_warning_handlers.rb new file mode 100644 index 000000000..85c4bb78d --- /dev/null +++ b/spec/support/deprecated_warning_handlers.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +Warning[:deprecated] = true + +module DeprecatedWarningHandler + class DeprecationWarning < StandardError; end + + DEPRECATION_REGEX = /is deprecated/.freeze + + def warn(message) + return super(message) unless message.match?(DEPRECATION_REGEX) + + exception = DeprecationWarning.new(message) + exception.set_backtrace(caller) + raise exception + end +end + +Warning.singleton_class.prepend(DeprecatedWarningHandler) From 04c6a991a08eb70b4c9fc9632a6a79f89db7d0b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20L=C3=B6vmo?= Date: Thu, 18 Jan 2024 18:23:09 +0100 Subject: [PATCH 203/304] Remove outdated XML formatter documentation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7fdae1459..1ff6cd4fd 100644 --- a/README.md +++ b/README.md @@ -3100,7 +3100,7 @@ end Built-in formatters are the following. * `:json`: use object's `to_json` when available, otherwise call `MultiJson.dump` -* `:xml`: use object's `to_xml` when available, usually via `MultiXml`, otherwise call `to_s` +* `:xml`: use object's `to_xml` when available, usually via `MultiXml` * `:txt`: use object's `to_txt` when available, otherwise `to_s` * `:serializable_hash`: use object's `serializable_hash` when available, otherwise fallback to `:json` * `:binary`: data will be returned "as is" From e9aa45b033afe747a17db65e43446308563d7174 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sat, 20 Jan 2024 22:55:46 +0100 Subject: [PATCH 204/304] Remove old Rails <= 5.2 documentation (#2413) * Remove old Rails <= 5.2 documentation * Add zeitwerk --- README.md | 58 +++---------------------------------------------------- 1 file changed, 3 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 1ff6cd4fd..f6c8ddf22 100644 --- a/README.md +++ b/README.md @@ -19,13 +19,9 @@ - [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) + - [Zeitwerk](#zeitwerk) - [Modules](#modules) - [Remounting](#remounting) - [Mount Configuration](#mount-configuration) @@ -101,7 +97,6 @@ - [Rescuing exceptions inside namespaces](#rescuing-exceptions-inside-namespaces) - [Unrescuable Exceptions](#unrescuable-exceptions) - [Exceptions that should be rescued explicitly](#exceptions-that-should-be-rescued-explicitly) - - [Rails 3.x](#rails-3x) - [Logging](#logging) - [API Formats](#api-formats) - [JSONP](#jsonp) @@ -309,26 +304,6 @@ And would respond to the following routes: Grape will also automatically respond to HEAD and OPTIONS for all GET, and just OPTIONS for all other routes. -### ActiveRecord without Rails - -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 -``` - -#### Rails 5+ - -Use [otr-activerecord](https://github.com/jhollinger/otr-activerecord) as follows: - -```ruby -use OTR::ActiveRecord::ConnectionManagement -``` - ### Alongside Sinatra (or other frameworks) If you wish to mount Grape alongside another Rack framework such as Sinatra, you can do so easily using `Rack::Cascade`: @@ -367,21 +342,8 @@ Modify `config/routes`: ```ruby mount Twitter::API => '/' ``` - -#### Rails < 5.2 - -Modify `application.rb`: - -```ruby -config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb') -config.autoload_paths += Dir[Rails.root.join('app', 'api', '*')] -``` - -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: +#### Zeitwerk +Rails's default autoloader is `Zeitwerk`. By default, it inflects `api` as `Api` instead of `API`. To make our example work, you need to uncomment the lines at the bottom of `config/initializers/inflections.rb`, and add `API` as an acronym: ```ruby ActiveSupport::Inflector.inflections(:en) do |inflect| @@ -2883,20 +2845,6 @@ Any exception that is not subclass of `StandardError` should be rescued explicit Usually it is not a case for an application logic as such errors point to problems in Ruby runtime. This is following [standard recommendations for exceptions handling](https://ruby-doc.org/core/Exception.html). -### Rails 3.x - -When mounted inside containers, such as Rails 3.x, errors such as "404 Not Found" or "406 Not Acceptable" will likely be handled and rendered by Rails handlers. For instance, accessing a nonexistent route "/api/foo" raises a 404, which inside rails will ultimately be translated to an `ActionController::RoutingError`, which most likely will get rendered to a HTML error page. - -Most APIs will enjoy preventing downstream handlers from handling errors. You may set the `:cascade` option to `false` for the entire API or separately on specific `version` definitions, which will remove the `X-Cascade: true` header from API responses. - -```ruby -cascade false -``` - -```ruby -version 'v1', using: :header, vendor: 'twitter', cascade: false -``` - ## Logging `Grape::API` provides a `logger` method which by default will return an instance of the `Logger` class from Ruby's standard library. From c6ad84a4b793a21ec8189fd5121749fb20e81f6f Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Mon, 22 Jan 2024 15:51:38 +0100 Subject: [PATCH 205/304] Fix response headers from lint (#2414) * Only error! is public Minor refactor * Remove `rack_response` from inside_route Replace `rack_response` to `error!` * error! is now private call self.status once inside route * Fix rubocop * Add CHANGELOG.md * Add UPGRADING.md entry Revert rack_response in inside_route with deprecation Add spec * Fix escape_html * Fix UPGRADING.md Change deprecation msg --- CHANGELOG.md | 1 + UPGRADING.md | 6 ++ lib/grape/dsl/inside_route.rb | 9 +- lib/grape/middleware/error.rb | 93 +++++++++---------- spec/grape/api_spec.rb | 82 ++++++++-------- .../exceptions/body_parse_errors_spec.rb | 6 +- .../exceptions/invalid_accept_header_spec.rb | 8 +- .../validations/validators/values_spec.rb | 4 +- 8 files changed, 112 insertions(+), 97 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b87d4bfd7..f00754578 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ * [#2382](https://github.com/ruby-grape/grape/pull/2382): Fix values validator for params wrapped in `with` block - [@numbata](https://github.com/numbata). * [#2387](https://github.com/ruby-grape/grape/pull/2387): Fix rubygems version within workflows - [@ericproulx](https://github.com/ericproulx). * [#2405](https://github.com/ruby-grape/grape/pull/2405): Fix edge workflow - [@ericproulx](https://github.com/ericproulx). +* [#2414](https://github.com/ruby-grape/grape/pull/2414): Fix Rack::Lint missing content-type - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 2.0.0 (2023/11/11) diff --git a/UPGRADING.md b/UPGRADING.md index f8cdb93b3..900d0a340 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -3,6 +3,12 @@ Upgrading Grape ### Upgrading to >= 2.1.0 +#### Changes in rescue_from + +The `rack_response` method has been deprecated and the `error_response` method has been removed. Use `error!` instead. + +See [#2414](https://github.com/ruby-grape/grape/pull/2414) for more information. + #### Grape::Router::Route.route_xxx methods have been removed - `route_method` is accessible through `request_method` diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index 0286595b8..a5ed227ef 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -162,9 +162,9 @@ def configuration # @param status [Integer] the HTTP Status Code. Defaults to default_error_status, 500 if not set. # @param additional_headers [Hash] Addtional headers for the response. def error!(message, status = nil, additional_headers = nil) - self.status(status || namespace_inheritable(:default_error_status)) + status = self.status(status || namespace_inheritable(:default_error_status)) headers = additional_headers.present? ? header.merge(additional_headers) : header - throw :error, message: message, status: self.status, headers: headers + throw :error, message: message, status: status, headers: headers end # Creates a Rack response based on the provided message, status, and headers. @@ -180,8 +180,9 @@ def error!(message, status = nil, additional_headers = nil) # A Rack::Response object containing the specified message, status, and headers. # def rack_response(message, status = 200, headers = { Rack::CONTENT_TYPE => content_type }) - message = ERB::Util.html_escape(message) if headers[Rack::CONTENT_TYPE] == 'text/html' - Rack::Response.new([message], Rack::Utils.status_code(status), headers) + Grape.deprecator.warn('The rack_response method has been deprecated, use error! instead.') + message = Rack::Utils.escape_html(message) if headers[Rack::CONTENT_TYPE] == 'text/html' + Rack::Response.new(Array.wrap(message), Rack::Utils.status_code(status), headers) end # Redirect to a new url. diff --git a/lib/grape/middleware/error.rb b/lib/grape/middleware/error.rb index 02db9aae0..545519a1a 100644 --- a/lib/grape/middleware/error.rb +++ b/lib/grape/middleware/error.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require 'grape/middleware/base' -require 'active_support/core_ext/string/output_safety' module Grape module Middleware @@ -34,66 +33,59 @@ def initialize(app, *options) def call!(env) @env = env - begin - error_response(catch(:error) do - return @app.call(@env) - end) - rescue Exception => e # rubocop:disable Lint/RescueException - handler = - 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, e, @env[Grape::Env::API_ENDPOINT]) - end + error_response(catch(:error) { return @app.call(@env) }) + rescue Exception => e # rubocop:disable Lint/RescueException + run_rescue_handler(find_handler(e.class), e, @env[Grape::Env::API_ENDPOINT]) end - def error!(message, status = options[:default_status], headers = {}, backtrace = [], original_exception = nil) - headers = headers.reverse_merge(Rack::CONTENT_TYPE => content_type) - rack_response(format_message(message, backtrace, original_exception), status, headers) - end - - def default_rescue_handler(e) - error_response(message: e.message, backtrace: e.backtrace, original_exception: e) - end - - # TODO: This method is deprecated. Refactor out. - def error_response(error = {}) - status = error[:status] || options[:default_status] - message = error[:message] || options[:default_message] - headers = { Rack::CONTENT_TYPE => content_type } - headers.merge!(error[:headers]) if error[:headers].is_a?(Hash) - 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 + private - def rack_response(message, status = options[:default_status], headers = { Rack::CONTENT_TYPE => content_type }) - message = ERB::Util.html_escape(message) if headers[Rack::CONTENT_TYPE] == TEXT_HTML - Rack::Response.new([message], Rack::Utils.status_code(status), headers) + def rack_response(status, headers, message) + message = Rack::Utils.escape_html(message) if headers[Rack::CONTENT_TYPE] == TEXT_HTML + Rack::Response.new(Array.wrap(message), Rack::Utils.status_code(status), 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) + return formatter.call(message, backtrace, options, env, original_exception) if formatter + throw :error, status: 406, message: "The requested format '#{format}' is not supported.", backtrace: backtrace, - original_exception: original_exception unless formatter - formatter.call(message, backtrace, options, env, original_exception) + original_exception: original_exception end - private + def find_handler(klass) + rescue_handler_for_base_only_class(klass) || + rescue_handler_for_class_or_its_ancestor(klass) || + rescue_handler_for_grape_exception(klass) || + rescue_handler_for_any_class(klass) || + raise + end + + def error_response(error = {}) + status = error[:status] || options[:default_status] + message = error[:message] || options[:default_message] + headers = { Rack::CONTENT_TYPE => content_type }.tap do |h| + h.merge!(error[:headers]) if error[:headers].is_a?(Hash) + end + backtrace = error[:backtrace] || error[:original_exception]&.backtrace || [] + original_exception = error.is_a?(Exception) ? error : error[:original_exception] || nil + rack_response(status, headers, format_message(message, backtrace, original_exception)) + end + + def default_rescue_handler(e) + error_response(message: e.message, backtrace: e.backtrace, original_exception: e) + end def rescue_handler_for_base_only_class(klass) error, handler = options[:base_only_rescue_handlers].find { |err, _handler| klass == err } return unless error - handler || :default_rescue_handler + handler || method(:default_rescue_handler) end def rescue_handler_for_class_or_its_ancestor(klass) @@ -101,22 +93,22 @@ def rescue_handler_for_class_or_its_ancestor(klass) return unless error - handler || :default_rescue_handler + handler || method(:default_rescue_handler) end def rescue_handler_for_grape_exception(klass) return unless klass <= Grape::Exceptions::Base - return :error_response if klass == Grape::Exceptions::InvalidVersionHeader + return method(:error_response) if klass == Grape::Exceptions::InvalidVersionHeader return unless options[:rescue_grape_exceptions] || !options[:rescue_all] - options[:grape_exceptions_rescue_handler] || :error_response + options[:grape_exceptions_rescue_handler] || method(:error_response) end def rescue_handler_for_any_class(klass) return unless klass <= StandardError return unless options[:rescue_all] || options[:rescue_grape_exceptions] - options[:all_rescue_handler] || :default_rescue_handler + options[:all_rescue_handler] || method(:default_rescue_handler) end def run_rescue_handler(handler, error, endpoint) @@ -126,9 +118,9 @@ def run_rescue_handler(handler, error, endpoint) handler = public_method(handler) end - response = (catch(:error) do + response = catch(:error) do handler.arity.zero? ? endpoint.instance_exec(&handler) : endpoint.instance_exec(error, &handler) - end) + end response = error!(response[:message], response[:status], response[:headers]) if error?(response) @@ -139,6 +131,13 @@ def run_rescue_handler(handler, error, endpoint) end end + def error!(message, status = options[:default_status], headers = {}, backtrace = [], original_exception = nil) + rack_response( + status, headers.reverse_merge(Rack::CONTENT_TYPE => content_type), + format_message(message, backtrace, original_exception) + ) + end + def error?(response) response.is_a?(Hash) && response[:message] && response[:status] && response[:headers] end diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index 97c2dbff0..95b46079c 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -2102,7 +2102,7 @@ class CustomError < Grape::Exceptions::Base; end it 'rescues custom grape exceptions' do subject.rescue_from ApiSpec::CustomError do |e| - rack_response('New Error', e.status) + error!('New Error', e.status) end subject.get '/custom_error' do raise ApiSpec::CustomError.new(status: 400, message: 'Custom Error') @@ -2120,7 +2120,7 @@ class CustomError < Grape::Exceptions::Base; end allow(Grape::Formatter).to receive(:formatter_for) { formatter } subject.rescue_from :all do |_e| - rack_response('Formatter Error', 500) + error!('Formatter Error', 500) end subject.get('/formatter_exception') { 'Hello world' } @@ -2143,7 +2143,7 @@ class CustomError < Grape::Exceptions::Base; end describe '.rescue_from klass, block' do it 'rescues Exception' do subject.rescue_from RuntimeError do |e| - rack_response("rescued from #{e.message}", 202) + error!("rescued from #{e.message}", 202) end subject.get '/exception' do raise 'rain!' @@ -2164,7 +2164,7 @@ class CommunicationError < StandardError; end it 'rescues an error via rescue_from :all' do subject.rescue_from :all do |e| - rack_response("rescued from #{e.class.name}", 500) + error!("rescued from #{e.class.name}", 500) end subject.get '/exception' do raise ConnectionError @@ -2176,7 +2176,7 @@ class CommunicationError < StandardError; end it 'rescues a specific error' do subject.rescue_from ConnectionError do |e| - rack_response("rescued from #{e.class.name}", 500) + error!("rescued from #{e.class.name}", 500) end subject.get '/exception' do raise ConnectionError @@ -2188,7 +2188,7 @@ class CommunicationError < StandardError; end it 'rescues a subclass of an error by default' do subject.rescue_from RuntimeError do |e| - rack_response("rescued from #{e.class.name}", 500) + error!("rescued from #{e.class.name}", 500) end subject.get '/exception' do raise ConnectionError @@ -2200,10 +2200,10 @@ class CommunicationError < StandardError; end it 'rescues multiple specific errors' do subject.rescue_from ConnectionError do |e| - rack_response("rescued from #{e.class.name}", 500) + error!("rescued from #{e.class.name}", 500) end subject.rescue_from DatabaseError do |e| - rack_response("rescued from #{e.class.name}", 500) + error!("rescued from #{e.class.name}", 500) end subject.get '/connection' do raise ConnectionError @@ -2221,7 +2221,7 @@ class CommunicationError < StandardError; end it 'does not rescue a different error' do subject.rescue_from RuntimeError do |e| - rack_response("rescued from #{e.class.name}", 500) + error!("rescued from #{e.class.name}", 500) end subject.get '/uncaught' do raise CommunicationError @@ -2234,7 +2234,7 @@ class CommunicationError < StandardError; end describe '.rescue_from klass, lambda' do it 'rescues an error with the lambda' do subject.rescue_from ArgumentError, lambda { - rack_response('rescued with a lambda', 400) + error!('rescued with a lambda', 400) } subject.get('/rescue_lambda') { raise ArgumentError } @@ -2245,7 +2245,7 @@ class CommunicationError < StandardError; end it 'can execute the lambda with an argument' do subject.rescue_from ArgumentError, lambda { |e| - rack_response(e.message, 400) + error!(e.message, 400) } subject.get('/rescue_lambda') { raise ArgumentError, 'lambda takes an argument' } @@ -2326,7 +2326,7 @@ class ChildError < ParentError; end it 'rescues error as well as subclass errors with rescue_subclasses option set' do subject.rescue_from ApiSpec::APIErrors::ParentError, rescue_subclasses: true do |e| - rack_response("rescued from #{e.class.name}", 500) + error!("rescued from #{e.class.name}", 500) end subject.get '/caught_child' do raise ApiSpec::APIErrors::ChildError @@ -2347,7 +2347,7 @@ class ChildError < ParentError; end it 'sets rescue_subclasses to true by default' do subject.rescue_from ApiSpec::APIErrors::ParentError do |e| - rack_response("rescued from #{e.class.name}", 500) + error!("rescued from #{e.class.name}", 500) end subject.get '/caught_child' do raise ApiSpec::APIErrors::ChildError @@ -2359,7 +2359,7 @@ class ChildError < ParentError; end it 'does not rescue child errors if rescue_subclasses is false' do subject.rescue_from ApiSpec::APIErrors::ParentError, rescue_subclasses: false do |e| - rack_response("rescued from #{e.class.name}", 500) + error!("rescued from #{e.class.name}", 500) end subject.get '/uncaught' do raise ApiSpec::APIErrors::ChildError @@ -2389,7 +2389,7 @@ class ChildError < ParentError; end it 'rescues grape exceptions with a user-defined handler' do subject.rescue_from grape_exception.class do |_error| - rack_response('Redefined Error', 403) + error!('Redefined Error', 403) end exception = grape_exception @@ -2572,11 +2572,7 @@ def self.call(message, _backtrace, _option, _env, _original_exception) end get '/excel.json' expect(last_response.status).to eq(406) - if ActiveSupport::VERSION::MAJOR == 3 - expect(last_response.body).to eq('The requested format 'txt' is not supported.') - else - expect(last_response.body).to eq('The requested format 'txt' is not supported.') - end + expect(last_response.body).to eq(Rack::Utils.escape_html("The requested format 'txt' is not supported.")) end end @@ -3375,7 +3371,7 @@ def static context 'when some rescues are defined by mounted' do it 'inherits parent rescues' do subject.rescue_from :all do |e| - rack_response("rescued from #{e.message}", 202) + error!("rescued from #{e.message}", 202) end app = Class.new(described_class) @@ -3393,14 +3389,14 @@ def static it 'prefers rescues defined by mounted if they rescue similar error class' do subject.rescue_from StandardError do - rack_response('outer rescue') + error!('outer rescue') end app = Class.new(described_class) subject.namespace :mounted do rescue_from StandardError do - rack_response('inner rescue') + error!('inner rescue') end app.get('/fail') { raise 'doh!' } mount app @@ -3412,14 +3408,14 @@ def static it 'prefers rescues defined by mounted even if outer is more specific' do subject.rescue_from ArgumentError do - rack_response('outer rescue') + error!('outer rescue') end app = Class.new(described_class) subject.namespace :mounted do rescue_from StandardError do - rack_response('inner rescue') + error!('inner rescue') end app.get('/fail') { raise ArgumentError.new } mount app @@ -3431,14 +3427,14 @@ def static it 'prefers more specific rescues defined by mounted' do subject.rescue_from StandardError do - rack_response('outer rescue') + error!('outer rescue') end app = Class.new(described_class) subject.namespace :mounted do rescue_from ArgumentError do - rack_response('inner rescue') + error!('inner rescue') end app.get('/fail') { raise ArgumentError.new } mount app @@ -4125,11 +4121,7 @@ def before end get '/something' expect(last_response.status).to eq(406) - if ActiveSupport::VERSION::MAJOR == 3 - expect(last_response.body).to eq('{"error":"The requested format 'txt' is not supported."}') - else - expect(last_response.body).to eq('{"error":"The requested format 'txt' is not supported."}') - end + expect(last_response.body).to eq(Rack::Utils.escape_html({ error: "The requested format 'txt' is not supported." }.to_json)) end end @@ -4141,11 +4133,7 @@ def before end get '/something?format=' expect(last_response.status).to eq(406) - if ActiveSupport::VERSION::MAJOR == 3 - expect(last_response.body).to eq('The requested format '<script>blah</script>' is not supported.') - else - expect(last_response.body).to eq('The requested format '<script>blah</script>' is not supported.') - end + expect(last_response.body).to eq(Rack::Utils.escape_html("The requested format '' is not supported.")) end end @@ -4434,4 +4422,24 @@ def uniqe_id_route end end end + + context 'rack_response deprecated' do + let(:app) do + Class.new(described_class) do + rescue_from :all do + rack_response('deprecated', 500) + end + + get 'test' do + raise ArgumentError + end + end + end + + it 'raises a deprecation' do + expect(Grape.deprecator).to receive(:warn).with('The rack_response method has been deprecated, use error! instead.') + get 'test' + expect(last_response.body).to eq('deprecated') + end + end end diff --git a/spec/grape/exceptions/body_parse_errors_spec.rb b/spec/grape/exceptions/body_parse_errors_spec.rb index 7017400f6..06866b7b3 100644 --- a/spec/grape/exceptions/body_parse_errors_spec.rb +++ b/spec/grape/exceptions/body_parse_errors_spec.rb @@ -6,7 +6,7 @@ before do subject.rescue_from :all do |_e| - rack_response 'message was processed', 400 + error! 'message was processed', 400 end subject.params do requires :beer @@ -58,7 +58,7 @@ def app before do subject.rescue_from :all do |_e| - rack_response 'message was processed', 400 + error! 'message was processed', 400 end subject.rescue_from :grape_exceptions @@ -96,7 +96,7 @@ def app before do subject.rescue_from :grape_exceptions do |e| - rack_response "Custom Error Contents, Original Message: #{e.message}", 400 + error! "Custom Error Contents, Original Message: #{e.message}", 400 end subject.params do diff --git a/spec/grape/exceptions/invalid_accept_header_spec.rb b/spec/grape/exceptions/invalid_accept_header_spec.rb index ca4aec5ec..42edfb526 100644 --- a/spec/grape/exceptions/invalid_accept_header_spec.rb +++ b/spec/grape/exceptions/invalid_accept_header_spec.rb @@ -44,7 +44,7 @@ before do subject.version 'v99', using: :header, vendor: 'vendorname', format: :json, cascade: false subject.rescue_from :all do |e| - rack_response 'message was processed', 400, e[:headers] + error! 'message was processed', 400, e[:headers] end subject.get '/beer' do 'beer received' @@ -114,7 +114,7 @@ def app before do subject.version 'v99', using: :header, vendor: 'vendorname', format: :json, cascade: false subject.rescue_from :all do |e| - rack_response 'message was processed', 400, e[:headers] + error! 'message was processed', 400, e[:headers] end subject.desc 'Get beer' do failure [[400, 'Bad Request'], [401, 'Unauthorized'], [403, 'Forbidden'], @@ -194,7 +194,7 @@ def app before do subject.version 'v99', using: :header, vendor: 'vendorname', format: :json, cascade: true subject.rescue_from :all do |e| - rack_response 'message was processed', 400, e[:headers] + error! 'message was processed', 400, e[:headers] end subject.get '/beer' do 'beer received' @@ -273,7 +273,7 @@ def app before do subject.version 'v99', using: :header, vendor: 'vendorname', format: :json, cascade: true subject.rescue_from :all do |e| - rack_response 'message was processed', 400, e[:headers] + error! 'message was processed', 400, e[:headers] end subject.desc 'Get beer' do failure [[400, 'Bad Request'], [401, 'Unauthorized'], [403, 'Forbidden'], diff --git a/spec/grape/validations/validators/values_spec.rb b/spec/grape/validations/validators/values_spec.rb index 4780829a1..c66bf42e5 100644 --- a/spec/grape/validations/validators/values_spec.rb +++ b/spec/grape/validations/validators/values_spec.rb @@ -404,13 +404,13 @@ def even?(value) expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end - it 'validates against values in an endless range', if: ActiveSupport::VERSION::MAJOR >= 6 do + it 'validates against values in an endless range' do get('/endless', type: 10) expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: 10 }.to_json) end - it 'does not allow an invalid value for a parameter using an endless range', if: ActiveSupport::VERSION::MAJOR >= 6 do + it 'does not allow an invalid value for a parameter using an endless range' do get('/endless', type: 0) expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) From 7ec1f510dc091639aaa45a98cfdd583fad49e6ee Mon Sep 17 00:00:00 2001 From: Dmitry Gutov Date: Sun, 24 Mar 2024 03:04:33 +0200 Subject: [PATCH 206/304] Add the `contract` DSL (#2419) Add the `contract` DSL Resolves #2386 --- .github/workflows/test.yml | 9 + .rubocop_todo.yml | 10 +- Appraisals | 33 +++- CHANGELOG.md | 1 + Gemfile | 1 + README.md | 35 ++++ gemfiles/multi_json.gemfile | 1 + gemfiles/multi_xml.gemfile | 1 + gemfiles/no_dry_validation.gemfile | 40 ++++ gemfiles/rack_1_0.gemfile | 1 + gemfiles/rack_2_0.gemfile | 1 + gemfiles/rack_3_0.gemfile | 1 + gemfiles/rack_edge.gemfile | 1 + gemfiles/rails_6_0.gemfile | 1 + gemfiles/rails_6_1.gemfile | 1 + gemfiles/rails_7_0.gemfile | 1 + gemfiles/rails_7_1.gemfile | 2 +- gemfiles/rails_edge.gemfile | 1 + lib/grape.rb | 1 + lib/grape/dsl/inside_route.rb | 14 +- lib/grape/dsl/validations.rb | 13 ++ lib/grape/validations/contract_scope.rb | 71 +++++++ spec/grape/dsl/validations_spec.rb | 10 + spec/grape/validations/contract_scope_spec.rb | 179 ++++++++++++++++++ .../no_dry_validation_spec.rb | 28 +++ 25 files changed, 440 insertions(+), 17 deletions(-) create mode 100644 gemfiles/no_dry_validation.gemfile create mode 100644 lib/grape/validations/contract_scope.rb create mode 100644 spec/grape/validations/contract_scope_spec.rb create mode 100644 spec/integration/no_dry_validation/no_dry_validation_spec.rb diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c3201f44a..81352673e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,6 +25,7 @@ jobs: matrix: ruby: ['2.7', '3.0', '3.1', '3.2', '3.3'] gemfile: [rack_2_0, rack_3_0, rails_6_0, rails_6_1, rails_7_0, rails_7_1] + integration_only: [false] include: - ruby: '2.7' gemfile: rack_1_0 @@ -32,6 +33,9 @@ jobs: gemfile: multi_json - ruby: '2.7' gemfile: multi_xml + - ruby: '3.3' + gemfile: no_dry_validation + integration_only: true runs-on: ubuntu-latest env: BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile @@ -45,6 +49,7 @@ jobs: bundler-cache: true - name: Run tests + if: ${{ matrix.integration_only == false }} run: bundle exec rake spec - name: Run tests (spec/integration/eager_load) @@ -70,6 +75,10 @@ jobs: if: ${{ matrix.gemfile == 'rack_3_0' }} run: bundle exec rspec spec/integration/rack/v3 + - name: Run tests (spec/integration/no_dry_validation) + if: ${{ matrix.gemfile == 'no_dry_validation' }} + run: bundle exec rspec spec/integration/no_dry_validation + - name: Coveralls uses: coverallsapp/github-action@master with: diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 370f887e4..be369f103 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -228,7 +228,7 @@ RSpec/ExpectInHook: - 'spec/grape/api_spec.rb' - 'spec/grape/validations/validators/values_spec.rb' -# Offense count: 47 +# Offense count: 48 # Configuration parameters: Include, CustomTransform, IgnoreMethods, SpecSuffixOnly. # Include: **/*_spec*rb*, **/spec/**/* RSpec/FilePath: @@ -278,6 +278,7 @@ RSpec/FilePath: - 'spec/integration/eager_load/eager_load_spec.rb' - 'spec/integration/multi_json/json_spec.rb' - 'spec/integration/multi_xml/xml_spec.rb' + - 'spec/integration/no_dry_validation/no_dry_validation_spec.rb' - 'spec/integration/rack/v2/headers_spec.rb' - 'spec/integration/rack/v3/headers_spec.rb' @@ -341,7 +342,7 @@ RSpec/MissingExampleGroupArgument: Exclude: - 'spec/grape/middleware/exception_spec.rb' -# Offense count: 788 +# Offense count: 804 # Configuration parameters: Max. RSpec/MultipleExpectations: Exclude: @@ -392,6 +393,7 @@ RSpec/MultipleExpectations: - 'spec/grape/util/reverse_stackable_values_spec.rb' - 'spec/grape/util/stackable_values_spec.rb' - 'spec/grape/validations/attributes_doc_spec.rb' + - 'spec/grape/validations/contract_scope_spec.rb' - 'spec/grape/validations/instance_behaivour_spec.rb' - 'spec/grape/validations/params_scope_spec.rb' - 'spec/grape/validations/types/array_coercer_spec.rb' @@ -411,6 +413,7 @@ RSpec/MultipleExpectations: - 'spec/grape/validations/validators/same_as_spec.rb' - 'spec/grape/validations/validators/values_spec.rb' - 'spec/grape/validations_spec.rb' + - 'spec/integration/no_dry_validation/no_dry_validation_spec.rb' - 'spec/shared/versioning_examples.rb' # Offense count: 38 @@ -557,7 +560,7 @@ RSpec/ScatteredSetup: - 'spec/grape/util/inheritable_setting_spec.rb' - 'spec/grape/validations_spec.rb' -# Offense count: 47 +# Offense count: 48 # Configuration parameters: Include, CustomTransform, IgnoreMethods, IgnoreMetadata. # Include: **/*_spec.rb RSpec/SpecFilePathFormat: @@ -608,6 +611,7 @@ RSpec/SpecFilePathFormat: - 'spec/integration/eager_load/eager_load_spec.rb' - 'spec/integration/multi_json/json_spec.rb' - 'spec/integration/multi_xml/xml_spec.rb' + - 'spec/integration/no_dry_validation/no_dry_validation_spec.rb' - 'spec/integration/rack/v2/headers_spec.rb' - 'spec/integration/rack/v3/headers_spec.rb' diff --git a/Appraisals b/Appraisals index a9d68682d..ad4ce12c0 100644 --- a/Appraisals +++ b/Appraisals @@ -1,10 +1,15 @@ # frozen_string_literal: true -appraise 'rails-5' do - gem 'rails', '~> 5.2' +customize_gemfiles do + { + single_quotes: true, + heading: "frozen_string_literal: true + +This file was generated by Appraisal" + } end -appraise 'rails-6' do +appraise 'rails-6-0' do gem 'rails', '~> 6.0.0' end @@ -12,8 +17,12 @@ appraise 'rails-6-1' do gem 'rails', '~> 6.1' end -appraise 'rails-7' do - gem 'rails', '~> 7.0' +appraise 'rails-7-0' do + gem 'rails', '~> 7.0.0' +end + +appraise 'rails-7-1' do + gem 'rails', '~> 7.1.0' end appraise 'rails-edge' do @@ -32,14 +41,20 @@ appraise 'multi_xml' do gem 'multi_xml', require: 'multi_xml' end -appraise 'rack1' do +appraise 'rack_1_0' do gem 'rack', '~> 1.0' end -appraise 'rack2' do - gem 'rack', '~> 2.0.0' +appraise 'rack_2_0' do + gem 'rack', '~> 2.0' end -appraise 'rack3' do +appraise 'rack_3_0' do gem 'rack', '~> 3.0.0' end + +appraise 'no_dry_validation' do + group :development, :test do + remove_gem 'dry-validation' + end +end diff --git a/CHANGELOG.md b/CHANGELOG.md index f00754578..4d6df9feb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ #### Features +* [#2419](https://github.com/ruby-grape/grape/pull/2419): Add the `contract` DSL - [@dgutov](https://github.com/dgutov). * [#2371](https://github.com/ruby-grape/grape/pull/2371): Use a param value as the `default` value of other param - [@jcagarcia](https://github.com/jcagarcia). * [#2377](https://github.com/ruby-grape/grape/pull/2377): Allow to use instance variables values inside `rescue_from` - [@jcagarcia](https://github.com/jcagarcia). * [#2379](https://github.com/ruby-grape/grape/pull/2379): Take into account the `route_param` type in `recognize_path` - [@jcagarcia](https://github.com/jcagarcia). diff --git a/Gemfile b/Gemfile index e7731ef6f..544c59d3d 100644 --- a/Gemfile +++ b/Gemfile @@ -8,6 +8,7 @@ gemspec group :development, :test do gem 'bundler' + gem 'dry-validation' gem 'hashie' gem 'rake' gem 'rubocop', '1.59.0', require: false diff --git a/README.md b/README.md index f6c8ddf22..f0ee3160f 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ - [Pass symbols for i18n translations](#pass-symbols-for-i18n-translations) - [Overriding Attribute Names](#overriding-attribute-names) - [With Default](#with-default) + - [Using dry-validation or dry-schema](#using-dry-validation-or-dry-schema) - [Headers](#headers) - [Request](#request) - [Header Case Handling](#header-case-handling) @@ -2086,6 +2087,40 @@ params do end ``` +### Using `dry-validation` or `dry-schema` + +As an alternative to the `params` DSL described above, you can use a schema or `dry-validation` contract to describe an endpoint's parameters. This can be especially useful if you use the above already in some other parts of your application. If not, you'll need to add `dry-validation` or `dry-schema` to your `Gemfile`. + +Then call `contract` with a contract or schema defined previously: + +```rb +CreateOrdersSchema = Dry::Schema.Params do + required(:orders).array(:hash) do + required(:name).filled(:string) + optional(:volume).maybe(:integer, lt?: 9) + end +end + +# ... + +contract CreateOrdersSchema +``` + +or with a block, using the [schema definition syntax](https://dry-rb.org/gems/dry-schema/1.13/#quick-start): + +```rb +contract do + required(:orders).array(:hash) do + required(:name).filled(:string) + optional(:volume).maybe(:integer, lt?: 9) + end +end +``` + +The latter will define a coercing schema (`Dry::Schema.Params`). When using the former approach, it's up to you to decide whether the input will need coercing. + +The `params` and `contract` declarations can also be used together in the same API, e.g. to describe different parts of a nested namespace for an endpoint. + ## Headers ### Request diff --git a/gemfiles/multi_json.gemfile b/gemfiles/multi_json.gemfile index 5ed288a8d..b2b48aa27 100644 --- a/gemfiles/multi_json.gemfile +++ b/gemfiles/multi_json.gemfile @@ -8,6 +8,7 @@ gem 'multi_json', require: 'multi_json' group :development, :test do gem 'bundler' + gem 'dry-validation' gem 'hashie' gem 'rake' gem 'rubocop', '1.59.0', require: false diff --git a/gemfiles/multi_xml.gemfile b/gemfiles/multi_xml.gemfile index dfb2a8730..26cd081fd 100644 --- a/gemfiles/multi_xml.gemfile +++ b/gemfiles/multi_xml.gemfile @@ -8,6 +8,7 @@ gem 'multi_xml', require: 'multi_xml' group :development, :test do gem 'bundler' + gem 'dry-validation' gem 'hashie' gem 'rake' gem 'rubocop', '1.59.0', require: false diff --git a/gemfiles/no_dry_validation.gemfile b/gemfiles/no_dry_validation.gemfile new file mode 100644 index 000000000..46f2c2c0b --- /dev/null +++ b/gemfiles/no_dry_validation.gemfile @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +# This file was generated by Appraisal + +source 'https://rubygems.org' + +group :development, :test do + gem 'bundler' + gem 'hashie' + gem 'rake' + gem 'rubocop', '1.59.0', require: false + gem 'rubocop-performance', '1.20.1', require: false + gem 'rubocop-rspec', '2.25.0', 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 'grape-entity', '~> 0.6', require: false + gem 'rack-jsonp', require: 'rack/jsonp' + gem 'rack-test', '< 2.1' + gem 'rspec', '< 4' + gem 'ruby-grape-danger', '~> 0.2.0', require: false + gem 'simplecov', '~> 0.21.2' + gem 'simplecov-lcov', '~> 0.8.0' + gem 'test-prof', require: false +end + +platforms :jruby do + gem 'racc' +end + +gemspec path: '../' diff --git a/gemfiles/rack_1_0.gemfile b/gemfiles/rack_1_0.gemfile index 41730950f..c2ace2218 100644 --- a/gemfiles/rack_1_0.gemfile +++ b/gemfiles/rack_1_0.gemfile @@ -8,6 +8,7 @@ gem 'rack', '~> 1.0' group :development, :test do gem 'bundler' + gem 'dry-validation' gem 'hashie' gem 'rake' gem 'rubocop', '1.59.0', require: false diff --git a/gemfiles/rack_2_0.gemfile b/gemfiles/rack_2_0.gemfile index 8b9cced0e..04f3fe947 100644 --- a/gemfiles/rack_2_0.gemfile +++ b/gemfiles/rack_2_0.gemfile @@ -8,6 +8,7 @@ gem 'rack', '~> 2.0' group :development, :test do gem 'bundler' + gem 'dry-validation' gem 'hashie' gem 'rake' gem 'rubocop', '1.59.0', require: false diff --git a/gemfiles/rack_3_0.gemfile b/gemfiles/rack_3_0.gemfile index 59fa6a9b9..6b4712c22 100644 --- a/gemfiles/rack_3_0.gemfile +++ b/gemfiles/rack_3_0.gemfile @@ -8,6 +8,7 @@ gem 'rack', '~> 3.0.0' group :development, :test do gem 'bundler' + gem 'dry-validation' gem 'hashie' gem 'rake' gem 'rubocop', '1.59.0', require: false diff --git a/gemfiles/rack_edge.gemfile b/gemfiles/rack_edge.gemfile index c61fb1914..a58b7238f 100644 --- a/gemfiles/rack_edge.gemfile +++ b/gemfiles/rack_edge.gemfile @@ -8,6 +8,7 @@ gem 'rack', github: 'rack/rack' group :development, :test do gem 'bundler' + gem 'dry-validation' gem 'hashie' gem 'rake' gem 'rubocop', '1.59.0', require: false diff --git a/gemfiles/rails_6_0.gemfile b/gemfiles/rails_6_0.gemfile index eec0b6074..0a9d4ffdc 100644 --- a/gemfiles/rails_6_0.gemfile +++ b/gemfiles/rails_6_0.gemfile @@ -8,6 +8,7 @@ gem 'rails', '~> 6.0.0' group :development, :test do gem 'bundler' + gem 'dry-validation' gem 'hashie' gem 'rake' gem 'rubocop', '1.59.0', require: false diff --git a/gemfiles/rails_6_1.gemfile b/gemfiles/rails_6_1.gemfile index 5daa55be9..c1121bf80 100644 --- a/gemfiles/rails_6_1.gemfile +++ b/gemfiles/rails_6_1.gemfile @@ -8,6 +8,7 @@ gem 'rails', '~> 6.1' group :development, :test do gem 'bundler' + gem 'dry-validation' gem 'hashie' gem 'rake' gem 'rubocop', '1.59.0', require: false diff --git a/gemfiles/rails_7_0.gemfile b/gemfiles/rails_7_0.gemfile index 9e16ddb55..17b375975 100644 --- a/gemfiles/rails_7_0.gemfile +++ b/gemfiles/rails_7_0.gemfile @@ -8,6 +8,7 @@ gem 'rails', '~> 7.0.0' group :development, :test do gem 'bundler' + gem 'dry-validation' gem 'hashie' gem 'rake' gem 'rubocop', '1.59.0', require: false diff --git a/gemfiles/rails_7_1.gemfile b/gemfiles/rails_7_1.gemfile index 940148931..5f3452444 100644 --- a/gemfiles/rails_7_1.gemfile +++ b/gemfiles/rails_7_1.gemfile @@ -5,10 +5,10 @@ source 'https://rubygems.org' gem 'rails', '~> 7.1.0' -gem 'tzinfo-data', require: false group :development, :test do gem 'bundler' + gem 'dry-validation' gem 'hashie' gem 'rake' gem 'rubocop', '1.59.0', require: false diff --git a/gemfiles/rails_edge.gemfile b/gemfiles/rails_edge.gemfile index cac5c4970..4462cd734 100644 --- a/gemfiles/rails_edge.gemfile +++ b/gemfiles/rails_edge.gemfile @@ -8,6 +8,7 @@ gem 'rails', github: 'rails/rails' group :development, :test do gem 'bundler' + gem 'dry-validation' gem 'hashie' gem 'rake' gem 'rubocop', '1.59.0', require: false diff --git a/lib/grape.rb b/lib/grape.rb index 6d4d90e1c..7d3134235 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -249,6 +249,7 @@ module Validations autoload :SingleAttributeIterator autoload :Types autoload :ParamsScope + autoload :ContractScope autoload :ValidatorFactory autoload :Base, 'grape/validations/validators/base' end diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index a5ed227ef..ef3bdec08 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -31,11 +31,17 @@ def declared(passed_params, options = {}, declared_params = nil, params_nested_p options = options.reverse_merge(include_missing: true, include_parent_namespaces: true, evaluate_given: false) declared_params ||= optioned_declared_params(**options) - if passed_params.is_a?(Array) - declared_array(passed_params, options, declared_params, params_nested_path) - else - declared_hash(passed_params, options, declared_params, params_nested_path) + res = if passed_params.is_a?(Array) + declared_array(passed_params, options, declared_params, params_nested_path) + else + declared_hash(passed_params, options, declared_params, params_nested_path) + end + + if (key_maps = namespace_stackable(:contract_key_map)) + key_maps.each { |key_map| key_map.write(passed_params, res) } end + + res end private diff --git a/lib/grape/dsl/validations.rb b/lib/grape/dsl/validations.rb index 66aff55ef..81d71bfeb 100644 --- a/lib/grape/dsl/validations.rb +++ b/lib/grape/dsl/validations.rb @@ -38,6 +38,19 @@ def reset_validations! def params(&block) Grape::Validations::ParamsScope.new(api: self, type: Hash, &block) end + + # Declare the contract to be used for the endpoint's parameters. + # @param contract [Class | Dry::Schema::Processor] + # The contract or schema to be used for validation. Optional. + # @yield a block yielding a new instance of Dry::Schema::Params + # subclass, allowing to define the schema inline. When the + # +contract+ parameter is a schema, it will be used as a parent. Optional. + def contract(contract = nil, &block) + raise ArgumentError, 'Either contract or block must be provided' unless contract || block + raise ArgumentError, 'Cannot inherit from contract, only schema' if block && contract.respond_to?(:schema) + + Grape::Validations::ContractScope.new(self, contract, &block) + end end end end diff --git a/lib/grape/validations/contract_scope.rb b/lib/grape/validations/contract_scope.rb new file mode 100644 index 000000000..0255051b4 --- /dev/null +++ b/lib/grape/validations/contract_scope.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +module Grape + module Validations + class ContractScope + # Declare the contract to be used for the endpoint's parameters. + # @param api [API] the API endpoint to modify. + # @param contract the contract or schema to be used for validation. Optional. + # @yield a block yielding a new schema class. Optional. + def initialize(api, contract = nil, &block) + # When block is passed, the first arg is either schema or nil. + contract = Dry::Schema.Params(parent: contract, &block) if block + + if contract.respond_to?(:schema) + # It's a Dry::Validation::Contract, then. + contract = contract.new + key_map = contract.schema.key_map + else + # Dry::Schema::Processor, hopefully. + key_map = contract.key_map + end + + api.namespace_stackable(:contract_key_map, key_map) + + validator_options = { + validator_class: Validator, + opts: { schema: contract } + } + + api.namespace_stackable(:validations, validator_options) + end + + class Validator + attr_reader :schema + + def initialize(*_args, schema:) + @schema = schema + end + + # Validates a given request. + # @param request [Grape::Request] the request currently being handled + # @raise [Grape::Exceptions::ValidationArrayErrors] if validation failed + # @return [void] + def validate(request) + res = schema.call(request.params) + + if res.success? + request.params.deep_merge!(res.to_h) + return + end + + errors = [] + + res.errors.messages.each do |message| + full_name = message.path.first.to_s + + full_name += "[#{message.path[1..].join('][')}]" if message.path.size > 1 + + errors << Grape::Exceptions::Validation.new(params: [full_name], message: message.text) + end + + raise Grape::Exceptions::ValidationArrayErrors.new(errors) + end + + def fail_fast? + false + end + end + end + end +end diff --git a/spec/grape/dsl/validations_spec.rb b/spec/grape/dsl/validations_spec.rb index 66db7645a..3bb63cc97 100644 --- a/spec/grape/dsl/validations_spec.rb +++ b/spec/grape/dsl/validations_spec.rb @@ -50,6 +50,16 @@ class Dummy expect { subject.params { raise 'foo' } }.to raise_error RuntimeError, 'foo' end end + + describe '.contract' do + it 'saves the schema instance' do + expect(subject.contract(Dry::Schema.Params)).to be_a Grape::Validations::ContractScope + end + + it 'errors without params or block' do + expect { subject.contract }.to raise_error(ArgumentError) + end + end end end end diff --git a/spec/grape/validations/contract_scope_spec.rb b/spec/grape/validations/contract_scope_spec.rb new file mode 100644 index 000000000..c17378bc9 --- /dev/null +++ b/spec/grape/validations/contract_scope_spec.rb @@ -0,0 +1,179 @@ +# frozen_string_literal: true + +require 'pry' + +describe Grape::Validations::ContractScope do + let(:validated_params) { {} } + let(:app) do + vp = validated_params + + Class.new(Grape::API) do + after_validation do + vp.replace(params) + end + end + end + + context 'with simple schema, pre-defined' do + let(:contract) do + Dry::Schema.Params do + required(:number).filled(:integer) + end + end + + before do + app.contract(contract) + app.post('/required') + end + + it 'coerces the parameter value one level deep' do + post '/required', number: '1' + expect(last_response.status).to eq(201) + expect(validated_params).to eq('number' => 1) + end + + it 'shows expected validation error' do + post '/required' + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('number is missing') + end + end + + context 'with contract class' do + let(:contract) do + Class.new(Dry::Validation::Contract) do + params do + required(:number).filled(:integer) + required(:name).filled(:string) + end + + rule(:number) do + key.failure('is too high') if value > 5 + end + end + end + + before do + app.contract(contract) + app.post('/required') + end + + it 'coerces the parameter' do + post '/required', number: '1', name: '2' + expect(last_response.status).to eq(201) + expect(validated_params).to eq('number' => 1, 'name' => '2') + end + + it 'shows expected validation error' do + post '/required', number: '6' + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('name is missing, number is too high') + end + end + + context 'with nested schema' do + before do + app.contract do + required(:home).hash do + required(:address).hash do + required(:number).filled(:integer) + end + end + required(:turns).array(:integer) + end + + app.post('/required') + end + + it 'keeps unknown parameters' do + post '/required', home: { address: { number: '1', street: 'Baker' } }, turns: %w[2 3] + expect(last_response.status).to eq(201) + expected = { 'home' => { 'address' => { 'number' => 1, 'street' => 'Baker' } }, 'turns' => [2, 3] } + expect(validated_params).to eq(expected) + end + + it 'shows expected validation error' do + post '/required', home: { address: { something: 'else' } } + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('home[address][number] is missing, turns is missing') + end + end + + context 'with mixed validation sources' do + before do + app.resource :foos do + route_param :foo_id, type: Integer do + contract do + required(:number).filled(:integer) + end + post('/required') + end + end + end + + it 'combines the coercions' do + post '/foos/123/required', number: '1' + expect(last_response.status).to eq(201) + expected = { 'foo_id' => 123, 'number' => 1 } + expect(validated_params).to eq(expected) + end + + it 'shows validation error for missing' do + post '/foos/123/required' + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('number is missing') + end + + it 'includes keys from all sources into declared' do + declared_params = nil + + app.after_validation do + declared_params = declared(params) + end + + post '/foos/123/required', number: '1', string: '2' + expect(last_response.status).to eq(201) + expected = { 'foo_id' => 123, 'number' => 1 } + expect(validated_params).to eq(expected.merge('string' => '2')) + expect(declared_params).to eq(expected) + end + end + + context 'with schema config validate_keys=true' do + it 'validates the whole params hash' do + app.resource :foos do + route_param :foo_id do + contract do + config.validate_keys = true + + required(:number).filled(:integer) + required(:foo_id).filled(:integer) + end + post('/required') + end + end + + post '/foos/123/required', number: '1' + expect(last_response.status).to eq(201) + expected = { 'foo_id' => 123, 'number' => 1 } + expect(validated_params).to eq(expected) + end + + it 'fails validation for any parameters not in schema' do + app.resource :foos do + route_param :foo_id, type: Integer do + contract do + config.validate_keys = true + + required(:number).filled(:integer) + end + post('/required') + end + end + + post '/foos/123/required', number: '1' + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('foo_id is not allowed') + end + end +end diff --git a/spec/integration/no_dry_validation/no_dry_validation_spec.rb b/spec/integration/no_dry_validation/no_dry_validation_spec.rb new file mode 100644 index 000000000..3954ad07a --- /dev/null +++ b/spec/integration/no_dry_validation/no_dry_validation_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', '..', 'lib')) +require 'grape' + +describe Grape do + let(:app) do + Class.new(Grape::API) do + resource :foos do + params do + requires :type, type: String + optional :limit, type: Integer + end + get do + declared(params).to_json + end + end + end + end + + it 'executes request normally' do + get '/foos', type: 'bar', limit: 4, qux: 'tee' + + expect(last_response.status).to eq(200) + result = JSON.parse(last_response.body) + expect(result).to eq({ 'type' => 'bar', 'limit' => 4 }) + end +end From 250200d06ceafbb4bdc11370984f45e645771ecc Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sun, 24 Mar 2024 02:05:22 +0100 Subject: [PATCH 207/304] Remove rack-accept dependency (#2389) * Remove rack-accept dependency Create Grape::Util::MediaType Use Rack::Util functions --- .rubocop.yml | 3 + CHANGELOG.md | 1 + grape.gemspec | 1 - lib/grape.rb | 1 - lib/grape/middleware/versioner/header.rb | 178 ++---------------- .../versioner/parse_media_type_patch.rb | 24 --- lib/grape/util/accept_header_handler.rb | 107 +++++++++++ lib/grape/util/media_type.rb | 70 +++++++ spec/grape/endpoint_spec.rb | 2 +- spec/grape/util/accept_header_handler_spec.rb | 114 +++++++++++ spec/grape/util/media_type_spec.rb | 118 ++++++++++++ 11 files changed, 431 insertions(+), 188 deletions(-) delete mode 100644 lib/grape/middleware/versioner/parse_media_type_patch.rb create mode 100644 lib/grape/util/accept_header_handler.rb create mode 100644 lib/grape/util/media_type.rb create mode 100644 spec/grape/util/accept_header_handler_spec.rb create mode 100644 spec/grape/util/media_type_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index 459c29c9b..120d1d5cf 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -58,3 +58,6 @@ RSpec/Capybara/FeatureMethods: RSpec/ExampleLength: Max: 60 + +RSpec/NestedGroups: + Max: 4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d6df9feb..3eb107014 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ * [#2406](https://github.com/ruby-grape/grape/pull/2406): Remove mime-types dependency in specs - [@ericproulx](https://github.com/ericproulx). * [#2408](https://github.com/ruby-grape/grape/pull/2408): Fix params method redefined warnings - [@ericproulx](https://github.com/ericproulx). * [#2410](https://github.com/ruby-grape/grape/pull/2410): Gem deprecations will raise a DeprecationWarning in specs - [@ericproulx](https://github.com/ericproulx). +* [#2389](https://github.com/ruby-grape/grape/pull/2389): Remove rack-accept dependency - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/grape.gemspec b/grape.gemspec index 3ea7193b7..a08547175 100644 --- a/grape.gemspec +++ b/grape.gemspec @@ -25,7 +25,6 @@ Gem::Specification.new do |s| s.add_runtime_dependency 'dry-types', '>= 1.1' s.add_runtime_dependency 'mustermann-grape', '~> 1.1.0' s.add_runtime_dependency 'rack', '>= 1.3.0' - s.add_runtime_dependency 'rack-accept' s.files = Dir['lib/**/*', 'CHANGELOG.md', 'CONTRIBUTING.md', 'README.md', 'grape.png', 'UPGRADING.md', 'LICENSE', 'grape.gemspec'] s.require_paths = ['lib'] diff --git a/lib/grape.rb b/lib/grape.rb index 7d3134235..4bfecdcb0 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -3,7 +3,6 @@ require 'logger' require 'rack' require 'rack/builder' -require 'rack/accept' require 'rack/auth/basic' require 'set' require 'bigdecimal' diff --git a/lib/grape/middleware/versioner/header.rb b/lib/grape/middleware/versioner/header.rb index 785f392e4..f8c9f2823 100644 --- a/lib/grape/middleware/versioner/header.rb +++ b/lib/grape/middleware/versioner/header.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true require 'grape/middleware/base' -require 'grape/middleware/versioner/parse_media_type_patch' +require 'grape/util/media_type' +require 'grape/util/accept_header_handler' module Grape module Middleware @@ -25,169 +26,24 @@ module Versioner # X-Cascade header to alert Grape::Router to attempt the next matched # route. class Header < Base - VENDOR_VERSION_HEADER_REGEX = - /\Avnd\.([a-z0-9.\-_!#{Regexp.last_match(0)}\^]+?)(?:-([a-z0-9*.]+))?(?:\+([a-z0-9*\-.]+))?\z/.freeze - - HAS_VENDOR_REGEX = /\Avnd\.[a-z0-9.\-_!#{Regexp.last_match(0)}\^]+/.freeze - HAS_VERSION_REGEX = /\Avnd\.([a-z0-9.\-_!#{Regexp.last_match(0)}\^]+?)(?:-([a-z0-9*.]+))+/.freeze - def before - strict_header_checks if strict? - - if media_type || env[Grape::Env::GRAPE_ALLOWED_METHODS] - media_type_header_handler - elsif headers_contain_wrong_vendor? - fail_with_invalid_accept_header!('API vendor not found.') - elsif headers_contain_wrong_version? - fail_with_invalid_version_header!('API version not found.') - end - end - - private - - def strict_header_checks - strict_accept_header_presence_check - strict_version_vendor_accept_header_presence_check - end - - def strict_accept_header_presence_check - return unless header.qvalues.empty? - - fail_with_invalid_accept_header!('Accept header must be set.') - end - - def strict_version_vendor_accept_header_presence_check - return if versions.blank? || an_accept_header_with_version_and_vendor_is_present? - - fail_with_invalid_accept_header!('API vendor or version not found.') - end - - def an_accept_header_with_version_and_vendor_is_present? - header.qvalues.keys.any? do |h| - VENDOR_VERSION_HEADER_REGEX.match?(h.sub('application/', '')) - end - end - - def header - @header ||= rack_accept_header - end - - def media_type - @media_type ||= header.best_of(available_media_types) - end - - def media_type_header_handler - type, subtype = Rack::Accept::Header.parse_media_type(media_type) - env[Grape::Env::API_TYPE] = type - env[Grape::Env::API_SUBTYPE] = subtype - - return unless VENDOR_VERSION_HEADER_REGEX =~ subtype - - env[Grape::Env::API_VENDOR] = Regexp.last_match[1] - env[Grape::Env::API_VERSION] = Regexp.last_match[2] - # weird that Grape::Middleware::Formatter also does this - env[Grape::Env::API_FORMAT] = Regexp.last_match[3] - end - - def fail_with_invalid_accept_header!(message) - raise Grape::Exceptions::InvalidAcceptHeader - .new(message, error_headers) - end - - def fail_with_invalid_version_header!(message) - raise Grape::Exceptions::InvalidVersionHeader - .new(message, error_headers) - end - - def available_media_types - [].tap do |available_media_types| - content_types.each_key do |extension| - versions.reverse_each do |version| - available_media_types << "application/vnd.#{vendor}-#{version}+#{extension}" - available_media_types << "application/vnd.#{vendor}-#{version}" - end - available_media_types << "application/vnd.#{vendor}+#{extension}" - end - - available_media_types << "application/vnd.#{vendor}" - available_media_types.concat(content_types.values.flatten) - end - end - - def headers_contain_wrong_vendor? - header.values.all? do |header_value| - vendor?(header_value) && request_vendor(header_value) != vendor - end - end - - def headers_contain_wrong_version? - header.values.all? do |header_value| - version?(header_value) && versions.exclude?(request_version(header_value)) - end - end - - def rack_accept_header - Rack::Accept::MediaType.new env[Grape::Http::Headers::HTTP_ACCEPT] - rescue RuntimeError => e - fail_with_invalid_accept_header!(e.message) - end - - def versions - options[:versions] || [] - end - - def vendor - version_options && version_options[:vendor] - end - - def strict? - version_options && version_options[:strict] - end - - def version_options - options[:version_options] - end - - # By default those errors contain an `X-Cascade` header set to `pass`, - # which allows nesting and stacking of routes - # (see Grape::Router for more - # information). To prevent # this behavior, and not add the `X-Cascade` - # header, one can set the `:cascade` option to `false`. - def cascade? - if version_options&.key?(:cascade) - version_options[:cascade] - else - true + handler = Grape::Util::AcceptHeaderHandler.new( + accept_header: env[Grape::Http::Headers::HTTP_ACCEPT], + versions: options[:versions], + **options.fetch(:version_options) { {} } + ) + + handler.match_best_quality_media_type!( + content_types: content_types, + allowed_methods: env[Grape::Env::GRAPE_ALLOWED_METHODS] + ) do |media_type| + env[Grape::Env::API_TYPE] = media_type.type + env[Grape::Env::API_SUBTYPE] = media_type.subtype + env[Grape::Env::API_VENDOR] = media_type.vendor + env[Grape::Env::API_VERSION] = media_type.version + env[Grape::Env::API_FORMAT] = media_type.format end end - - def error_headers - cascade? ? { Grape::Http::Headers::X_CASCADE => 'pass' } : {} - end - - # @param [String] media_type a content type - # @return [Boolean] whether the content type sets a vendor - def vendor?(media_type) - _, subtype = Rack::Accept::Header.parse_media_type(media_type) - subtype.present? && subtype[HAS_VENDOR_REGEX] - end - - def request_vendor(media_type) - _, subtype = Rack::Accept::Header.parse_media_type(media_type) - subtype.match(VENDOR_VERSION_HEADER_REGEX)[1] - end - - def request_version(media_type) - _, subtype = Rack::Accept::Header.parse_media_type(media_type) - subtype.match(VENDOR_VERSION_HEADER_REGEX)[2] - end - - # @param [String] media_type a content 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.present? && subtype[HAS_VERSION_REGEX] - end end end end diff --git a/lib/grape/middleware/versioner/parse_media_type_patch.rb b/lib/grape/middleware/versioner/parse_media_type_patch.rb deleted file mode 100644 index 798068ff8..000000000 --- a/lib/grape/middleware/versioner/parse_media_type_patch.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -require 'English' -module Rack - module Accept - module Header - ALLOWED_CHARACTERS = %r{^([a-z*]+)/([a-z0-9*&\^\-_#{$ERROR_INFO}.+]+)(?:;([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&.match(ALLOWED_CHARACTERS) - m ? [m[1], m[2], m[3] || ''] : [] - end - end - end - - class MediaType - def parse_media_type(media_type) - Header.parse_media_type(media_type) - end - end - end -end diff --git a/lib/grape/util/accept_header_handler.rb b/lib/grape/util/accept_header_handler.rb new file mode 100644 index 000000000..25d703631 --- /dev/null +++ b/lib/grape/util/accept_header_handler.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +require 'grape/util/media_type' + +module Grape + module Util + class AcceptHeaderHandler + attr_reader :accept_header, :versions, :vendor, :strict, :cascade + + def initialize(accept_header:, versions:, **options) + @accept_header = accept_header + @versions = versions + @vendor = options.fetch(:vendor, nil) + @strict = options.fetch(:strict, false) + @cascade = options.fetch(:cascade, true) + end + + def match_best_quality_media_type!(content_types: Grape::ContentTypes::CONTENT_TYPES, allowed_methods: nil) + return unless vendor + + strict_header_checks! + media_type = Grape::Util::MediaType.best_quality(accept_header, available_media_types(content_types)) + if media_type + yield media_type + else + fail!(allowed_methods) + end + end + + private + + def strict_header_checks! + return unless strict + + accept_header_check! + version_and_vendor_check! + end + + def accept_header_check! + return if accept_header.present? + + invalid_accept_header!('Accept header must be set.') + end + + def version_and_vendor_check! + return if versions.blank? || version_and_vendor? + + invalid_accept_header!('API vendor or version not found.') + end + + def q_values_mime_types + @q_values_mime_types ||= Rack::Utils.q_values(accept_header).map(&:first) + end + + def version_and_vendor? + q_values_mime_types.any? { |mime_type| Grape::Util::MediaType.match?(mime_type) } + end + + def invalid_accept_header!(message) + raise Grape::Exceptions::InvalidAcceptHeader.new(message, error_headers) + end + + def invalid_version_header!(message) + raise Grape::Exceptions::InvalidVersionHeader.new(message, error_headers) + end + + def fail!(grape_allowed_methods) + return grape_allowed_methods if grape_allowed_methods.present? + + media_types = q_values_mime_types.map { |mime_type| Grape::Util::MediaType.parse(mime_type) } + vendor_not_found!(media_types) || version_not_found!(media_types) + end + + def vendor_not_found!(media_types) + return unless media_types.all? { |media_type| media_type&.vendor && media_type.vendor != vendor } + + invalid_accept_header!('API vendor not found.') + end + + def version_not_found!(media_types) + return unless media_types.all? { |media_type| media_type&.version && versions.exclude?(media_type.version) } + + invalid_version_header!('API version not found.') + end + + def error_headers + cascade ? { Grape::Http::Headers::X_CASCADE => 'pass' } : {} + end + + def available_media_types(content_types) + [].tap do |available_media_types| + base_media_type = "application/vnd.#{vendor}" + content_types.each_key do |extension| + versions&.reverse_each do |version| + available_media_types << "#{base_media_type}-#{version}+#{extension}" + available_media_types << "#{base_media_type}-#{version}" + end + available_media_types << "#{base_media_type}+#{extension}" + end + + available_media_types << base_media_type + available_media_types.concat(content_types.values.flatten) + end + end + end + end +end diff --git a/lib/grape/util/media_type.rb b/lib/grape/util/media_type.rb new file mode 100644 index 000000000..1812fafa6 --- /dev/null +++ b/lib/grape/util/media_type.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +module Grape + module Util + class MediaType + attr_reader :type, :subtype, :vendor, :version, :format + + # based on the HTTP Accept header with the pattern: + # application/vnd.:vendor-:version+:format + VENDOR_VERSION_HEADER_REGEX = /\Avnd\.(?[a-z0-9.\-_!^]+?)(?:-(?[a-z0-9*.]+))?(?:\+(?[a-z0-9*\-.]+))?\z/.freeze + + def initialize(type:, subtype:) + @type = type + @subtype = subtype + VENDOR_VERSION_HEADER_REGEX.match(subtype) do |m| + @vendor = m[:vendor] + @version = m[:version] + @format = m[:format] + end + end + + def ==(other) + eql?(other) + end + + def eql?(other) + self.class == other.class && + other.type == type && + other.subtype == subtype && + other.vendor == vendor && + other.version == version && + other.format == format + end + + def hash + [self.class, type, subtype, vendor, version, format].hash + end + + class << self + def best_quality(header, available_media_types) + parse(best_quality_media_type(header, available_media_types)) + end + + def parse(media_type) + return if media_type.blank? + + type, subtype = media_type.split('/', 2) + return if type.blank? || subtype.blank? + + new(type: type, subtype: subtype) + end + + def match?(media_type) + return false if media_type.blank? + + subtype = media_type.split('/', 2).last + return false if subtype.blank? + + VENDOR_VERSION_HEADER_REGEX.match?(subtype) + end + + def best_quality_media_type(header, available_media_types) + header.blank? ? available_media_types.first : Rack::Utils.best_q_match(header, available_media_types) + end + end + + private_class_method :best_quality_media_type + end + end +end diff --git a/spec/grape/endpoint_spec.rb b/spec/grape/endpoint_spec.rb index 77dd116c0..d8e88aeb1 100644 --- a/spec/grape/endpoint_spec.rb +++ b/spec/grape/endpoint_spec.rb @@ -970,7 +970,7 @@ def memoized expect(last_response.status).to eq(406) end - it 'result in a 406 response if they cannot be parsed by rack-accept' do + it 'result in a 406 response if they cannot be parsed' do get '/test', {}, 'HTTP_ACCEPT' => 'application/vnd.ohanapi.v1+json; version=1' expect(last_response.status).to eq(406) end diff --git a/spec/grape/util/accept_header_handler_spec.rb b/spec/grape/util/accept_header_handler_spec.rb new file mode 100644 index 000000000..ac4ff3f46 --- /dev/null +++ b/spec/grape/util/accept_header_handler_spec.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +require 'grape/util/accept_header_handler' + +RSpec.describe Grape::Util::AcceptHeaderHandler do + subject(:match_best_quality_media_type!) { instance.match_best_quality_media_type! } + + let(:instance) do + described_class.new( + accept_header: accept_header, + versions: versions, + **options + ) + end + let(:accept_header) { '*/*' } + let(:versions) { ['v1'] } + let(:options) { {} } + + shared_examples 'an invalid accept header exception' do |message| + before do + allow(Grape::Exceptions::InvalidAcceptHeader).to receive(:new) + .with(message, { Grape::Http::Headers::X_CASCADE => 'pass' }) + .and_call_original + end + + it 'raises a Grape::Exceptions::InvalidAcceptHeader' do + expect { match_best_quality_media_type! }.to raise_error(Grape::Exceptions::InvalidAcceptHeader) + end + end + + describe '#match_best_quality_media_type!' do + context 'when no vendor set' do + let(:options) do + { + vendor: nil + } + end + + it { is_expected.to be_nil } + end + + context 'when strict header check' do + let(:options) do + { + vendor: 'vendor', + strict: true + } + end + + context 'when accept_header blank' do + let(:accept_header) { nil } + + it_behaves_like 'an invalid accept header exception', 'Accept header must be set.' + end + + context 'when vendor not found' do + let(:accept_header) { '*/*' } + + it_behaves_like 'an invalid accept header exception', 'API vendor or version not found.' + end + end + + context 'when media_type found' do + let(:options) do + { + vendor: 'vendor' + } + end + + let(:accept_header) { 'application/vnd.vendor-v1+json' } + + it 'yields a media type' do + expect { |b| instance.match_best_quality_media_type!(&b) }.to yield_with_args(Grape::Util::MediaType.new(type: 'application', subtype: 'vnd.vendor-v1+json')) + end + end + + context 'when media_type is not found' do + let(:options) do + { + vendor: 'vendor' + } + end + + let(:accept_header) { 'application/vnd.another_vendor-v1+json' } + + context 'when allowed_methods present' do + subject { instance.match_best_quality_media_type!(allowed_methods: allowed_methods) } + + let(:allowed_methods) { ['OPTIONS'] } + + it { is_expected.to match_array(allowed_methods) } + end + + context 'when vendor not found' do + it_behaves_like 'an invalid accept header exception', 'API vendor not found.' + end + + context 'when version not found' do + let(:versions) { ['v2'] } + let(:accept_header) { 'application/vnd.vendor-v1+json' } + + before do + allow(Grape::Exceptions::InvalidVersionHeader).to receive(:new) + .with('API version not found.', { Grape::Http::Headers::X_CASCADE => 'pass' }) + .and_call_original + end + + it 'raises a Grape::Exceptions::InvalidAcceptHeader' do + expect { match_best_quality_media_type! }.to raise_error(Grape::Exceptions::InvalidVersionHeader) + end + end + end + end +end diff --git a/spec/grape/util/media_type_spec.rb b/spec/grape/util/media_type_spec.rb new file mode 100644 index 000000000..2905c3f9e --- /dev/null +++ b/spec/grape/util/media_type_spec.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +RSpec.describe Grape::Util::MediaType do + shared_examples 'MediaType' do + it { is_expected.to eq(described_class.new(type: type, subtype: subtype)) } + end + + describe '.parse' do + subject(:media_type) { described_class.parse(header) } + + context 'when header blank?' do + let(:header) { nil } + + it { is_expected.to be_nil } + end + + context 'when header is not a mime type' do + let(:header) { 'abc' } + + it { is_expected.to be_nil } + end + + context 'when header is a valid mime type' do + let(:header) { [type, subtype].join('/') } + let(:type) { 'text' } + let(:subtype) { 'html' } + + it_behaves_like 'MediaType' + + context 'when header is a vendor mime type' do + let(:type) { 'application' } + let(:subtype) { 'vnd.test-v1+json' } + + it_behaves_like 'MediaType' + end + + context 'when header is a vendor mime type without version' do + let(:type) { 'application' } + let(:subtype) { 'vnd.ms-word' } + + it_behaves_like 'MediaType' + end + end + end + + describe '.match?' do + subject { described_class.match?(media_type) } + + context 'when media_type is blank?' do + let(:media_type) { nil } + + it { is_expected.to be_falsey } + end + + context 'when header is not a mime type' do + let(:media_type) { 'abc' } + + it { is_expected.to be_falsey } + end + + context 'when header is a valid mime type but not vendor' do + let(:media_type) { 'text/html' } + + it { is_expected.to be_falsey } + end + + context 'when header is a vendor mime type' do + let(:media_type) { 'application/vnd.test-v1+json' } + + it { is_expected.to be_truthy } + end + end + + describe '.best_quality' do + subject(:media_type) { described_class.best_quality(header, available_media_types) } + + let(:available_media_types) { %w[application/json text/html] } + + context 'when header is blank?' do + let(:header) { nil } + let(:type) { 'application' } + let(:subtype) { 'json' } + + it_behaves_like 'MediaType' + end + + context 'when header is not blank' do + let(:header) { [type, subtype].join('/') } + let(:type) { 'text' } + let(:subtype) { 'html' } + + it 'calls Rack::Utils.best_q_match' do + allow(Rack::Utils).to receive(:best_q_match).and_call_original + expect(media_type).to eq(described_class.new(type: type, subtype: subtype)) + end + end + end + + describe '.==' do + subject { described_class.new(type: type, subtype: subtype) } + + let(:type) { 'application' } + let(:subtype) { 'vnd.test-v1+json' } + let(:other_media_type_class) { Class.new(Struct.new(:type, :subtype, :vendor, :version, :format)) } + let(:other_media_type_instance) { other_media_type_class.new(type, subtype, 'test', 'v1', 'json') } + + it { is_expected.not_to eq(other_media_type_class.new(type, subtype, 'test', 'v1', 'json')) } + end + + describe '.hash' do + subject { Set.new([described_class.new(type: type, subtype: subtype)]) } + + let(:type) { 'text' } + let(:subtype) { 'html' } + + it { is_expected.to include(described_class.new(type: type, subtype: subtype)) } + end +end From f36011b26aba8751eaf914988b91d2e4fe8b97a6 Mon Sep 17 00:00:00 2001 From: Dmitry Gutov Date: Sun, 24 Mar 2024 05:09:05 +0200 Subject: [PATCH 208/304] Declare integration tests with less repetition (#2420) * Declare integration tests with less repetition * Use 'not' --- .github/workflows/test.yml | 46 ++++++------------- .rubocop_todo.yml | 8 ++-- .../{rack/v2 => rack_2_0}/headers_spec.rb | 0 .../{rack/v3 => rack_3_0}/headers_spec.rb | 0 4 files changed, 17 insertions(+), 37 deletions(-) rename spec/integration/{rack/v2 => rack_2_0}/headers_spec.rb (100%) rename spec/integration/{rack/v3 => rack_3_0}/headers_spec.rb (100%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 81352673e..24fee4887 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,20 +25,23 @@ jobs: matrix: ruby: ['2.7', '3.0', '3.1', '3.2', '3.3'] gemfile: [rack_2_0, rack_3_0, rails_6_0, rails_6_1, rails_7_0, rails_7_1] - integration_only: [false] + integration: [false] include: - ruby: '2.7' gemfile: rack_1_0 - ruby: '2.7' - gemfile: multi_json + integration: multi_json - ruby: '2.7' - gemfile: multi_xml + integration: multi_xml + - ruby: '2.7' + integration: rack_2_0 + - ruby: '2.7' + integration: rack_3_0 - ruby: '3.3' - gemfile: no_dry_validation - integration_only: true + integration: no_dry_validation runs-on: ubuntu-latest env: - BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile + BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.integration || matrix.gemfile }}.gemfile steps: - uses: actions/checkout@v4 @@ -49,35 +52,12 @@ jobs: bundler-cache: true - name: Run tests - if: ${{ matrix.integration_only == false }} + if: ${{ !matrix.integration }} run: bundle exec rake spec - - name: Run tests (spec/integration/eager_load) - # rack_2_0.gemfile is equals to Gemfile - if: ${{ matrix.gemfile == 'rack_2_0' }} - run: bundle exec rspec spec/integration/eager_load - - - name: Run tests (spec/integration/multi_json) - if: ${{ matrix.gemfile == 'multi_json' }} - run: bundle exec rspec spec/integration/multi_json - - - name: Run tests (spec/integration/multi_xml) - if: ${{ matrix.gemfile == 'multi_xml' }} - run: bundle exec rspec spec/integration/multi_xml - - - name: Run tests (spec/integration/rack/v2) - # rack_2_0.gemfile is equals to Gemfile - if: ${{ matrix.gemfile == 'rack_2_0' }} - run: bundle exec rspec spec/integration/rack/v2 - - - name: Run tests (spec/integration/rack/v3) - # rack_2_0.gemfile is equals to Gemfile - if: ${{ matrix.gemfile == 'rack_3_0' }} - run: bundle exec rspec spec/integration/rack/v3 - - - name: Run tests (spec/integration/no_dry_validation) - if: ${{ matrix.gemfile == 'no_dry_validation' }} - run: bundle exec rspec spec/integration/no_dry_validation + - name: Run integration tests (spec/integration/${{ matrix.integration }}) + if: ${{ matrix.integration }} + run: bundle exec rspec spec/integration/${{ matrix.integration }} - name: Coveralls uses: coverallsapp/github-action@master diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index be369f103..6f5f76b8e 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -279,8 +279,8 @@ RSpec/FilePath: - 'spec/integration/multi_json/json_spec.rb' - 'spec/integration/multi_xml/xml_spec.rb' - 'spec/integration/no_dry_validation/no_dry_validation_spec.rb' - - 'spec/integration/rack/v2/headers_spec.rb' - - 'spec/integration/rack/v3/headers_spec.rb' + - 'spec/integration/rack_2_0/headers_spec.rb' + - 'spec/integration/rack_3_0/headers_spec.rb' # Offense count: 6 # Configuration parameters: Max, AllowedIdentifiers, AllowedPatterns. @@ -612,8 +612,8 @@ RSpec/SpecFilePathFormat: - 'spec/integration/multi_json/json_spec.rb' - 'spec/integration/multi_xml/xml_spec.rb' - 'spec/integration/no_dry_validation/no_dry_validation_spec.rb' - - 'spec/integration/rack/v2/headers_spec.rb' - - 'spec/integration/rack/v3/headers_spec.rb' + - 'spec/integration/rack_2_0/headers_spec.rb' + - 'spec/integration/rack_3_0/headers_spec.rb' # Offense count: 9 RSpec/StubbedMock: diff --git a/spec/integration/rack/v2/headers_spec.rb b/spec/integration/rack_2_0/headers_spec.rb similarity index 100% rename from spec/integration/rack/v2/headers_spec.rb rename to spec/integration/rack_2_0/headers_spec.rb diff --git a/spec/integration/rack/v3/headers_spec.rb b/spec/integration/rack_3_0/headers_spec.rb similarity index 100% rename from spec/integration/rack/v3/headers_spec.rb rename to spec/integration/rack_3_0/headers_spec.rb From 048c649ad9ed148ad9a144db0c261f9623f71120 Mon Sep 17 00:00:00 2001 From: Dmitry Gutov Date: Mon, 25 Mar 2024 03:19:20 +0200 Subject: [PATCH 209/304] Tweak Rubocop rules; add more exceptions to the main config --- .rubocop.yml | 13 ++++ .rubocop_todo.yml | 180 +++------------------------------------------- 2 files changed, 24 insertions(+), 169 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 120d1d5cf..407390031 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -61,3 +61,16 @@ RSpec/ExampleLength: RSpec/NestedGroups: Max: 4 + +RSpec/FilePath: + SpecSuffixOnly: true + +RSpec/SpecFilePathFormat: + Exclude: + - '**/spec/routing/**/*' + - '**/spec/grape/api/*' + - '**/spec/integration/**/*' + - '**/spec/grape/validations/validators/*' + +RSpec/MultipleExpectations: + Max: 4 diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 6f5f76b8e..41bcd4b85 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 5000` -# on 2024-01-01 22:17:14 UTC using RuboCop version 1.59.0. +# on 2024-03-25 01:13:48 UTC using RuboCop version 1.59.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 @@ -136,7 +136,7 @@ RSpec/AnyInstance: - 'spec/grape/api_spec.rb' - 'spec/grape/middleware/base_spec.rb' -# Offense count: 344 +# Offense count: 345 # Configuration parameters: Prefixes, AllowedPatterns. # Prefixes: when, with, without RSpec/ContextWording: @@ -228,60 +228,6 @@ RSpec/ExpectInHook: - 'spec/grape/api_spec.rb' - 'spec/grape/validations/validators/values_spec.rb' -# Offense count: 48 -# Configuration parameters: Include, CustomTransform, IgnoreMethods, SpecSuffixOnly. -# Include: **/*_spec*rb*, **/spec/**/* -RSpec/FilePath: - Exclude: - - 'spec/grape/api/custom_validations_spec.rb' - - 'spec/grape/api/deeply_included_options_spec.rb' - - 'spec/grape/api/defines_boolean_in_params_spec.rb' - - 'spec/grape/api/documentation_spec.rb' - - 'spec/grape/api/inherited_helpers_spec.rb' - - 'spec/grape/api/invalid_format_spec.rb' - - 'spec/grape/api/mount_and_helpers_order_spec.rb' - - 'spec/grape/api/mount_and_rescue_from_spec.rb' - - 'spec/grape/api/namespace_parameters_in_route_spec.rb' - - 'spec/grape/api/nested_helpers_spec.rb' - - 'spec/grape/api/optional_parameters_in_route_spec.rb' - - 'spec/grape/api/parameters_modification_spec.rb' - - 'spec/grape/api/patch_method_helpers_spec.rb' - - 'spec/grape/api/recognize_path_spec.rb' - - 'spec/grape/api/required_parameters_in_route_spec.rb' - - 'spec/grape/api/required_parameters_with_invalid_method_spec.rb' - - 'spec/grape/api/routes_with_requirements_spec.rb' - - 'spec/grape/api/shared_helpers_exactly_one_of_spec.rb' - - 'spec/grape/api/shared_helpers_spec.rb' - - 'spec/grape/dsl/inside_route_spec.rb' - - 'spec/grape/endpoint/declared_spec.rb' - - 'spec/grape/exceptions/body_parse_errors_spec.rb' - - 'spec/grape/extensions/param_builders/hash_spec.rb' - - 'spec/grape/extensions/param_builders/hash_with_indifferent_access_spec.rb' - - 'spec/grape/extensions/param_builders/hashie/mash_spec.rb' - - 'spec/grape/integration/global_namespace_function_spec.rb' - - 'spec/grape/integration/rack_sendfile_spec.rb' - - 'spec/grape/loading_spec.rb' - - 'spec/grape/middleware/exception_spec.rb' - - 'spec/grape/validations/attributes_doc_spec.rb' - - 'spec/grape/validations/validators/all_or_none_spec.rb' - - 'spec/grape/validations/validators/allow_blank_spec.rb' - - 'spec/grape/validations/validators/at_least_one_of_spec.rb' - - 'spec/grape/validations/validators/coerce_spec.rb' - - 'spec/grape/validations/validators/default_spec.rb' - - 'spec/grape/validations/validators/exactly_one_of_spec.rb' - - 'spec/grape/validations/validators/except_values_spec.rb' - - 'spec/grape/validations/validators/mutual_exclusion_spec.rb' - - 'spec/grape/validations/validators/presence_spec.rb' - - 'spec/grape/validations/validators/regexp_spec.rb' - - 'spec/grape/validations/validators/same_as_spec.rb' - - 'spec/grape/validations/validators/values_spec.rb' - - 'spec/integration/eager_load/eager_load_spec.rb' - - 'spec/integration/multi_json/json_spec.rb' - - 'spec/integration/multi_xml/xml_spec.rb' - - 'spec/integration/no_dry_validation/no_dry_validation_spec.rb' - - 'spec/integration/rack_2_0/headers_spec.rb' - - 'spec/integration/rack_3_0/headers_spec.rb' - # Offense count: 6 # Configuration parameters: Max, AllowedIdentifiers, AllowedPatterns. RSpec/IndexedLet: @@ -331,7 +277,7 @@ RSpec/MessageChain: Exclude: - 'spec/grape/middleware/formatter_spec.rb' -# Offense count: 147 +# Offense count: 148 # Configuration parameters: . # SupportedStyles: have_received, receive RSpec/MessageSpies: @@ -342,79 +288,23 @@ RSpec/MissingExampleGroupArgument: Exclude: - 'spec/grape/middleware/exception_spec.rb' -# Offense count: 804 +# Offense count: 57 # Configuration parameters: Max. RSpec/MultipleExpectations: Exclude: - - 'spec/grape/api/custom_validations_spec.rb' - - 'spec/grape/api/deeply_included_options_spec.rb' - - 'spec/grape/api/defines_boolean_in_params_spec.rb' - - 'spec/grape/api/invalid_format_spec.rb' - - 'spec/grape/api/mount_and_helpers_order_spec.rb' - 'spec/grape/api/mount_and_rescue_from_spec.rb' - - 'spec/grape/api/namespace_parameters_in_route_spec.rb' - - 'spec/grape/api/optional_parameters_in_route_spec.rb' - 'spec/grape/api/parameters_modification_spec.rb' - - 'spec/grape/api/patch_method_helpers_spec.rb' - - 'spec/grape/api/required_parameters_in_route_spec.rb' - - 'spec/grape/api/routes_with_requirements_spec.rb' - - 'spec/grape/api/shared_helpers_exactly_one_of_spec.rb' - - 'spec/grape/api/shared_helpers_spec.rb' - - 'spec/grape/api_remount_spec.rb' - 'spec/grape/api_spec.rb' - - 'spec/grape/dsl/desc_spec.rb' - - 'spec/grape/dsl/headers_spec.rb' - 'spec/grape/dsl/helpers_spec.rb' - - 'spec/grape/dsl/inside_route_spec.rb' - - 'spec/grape/dsl/parameters_spec.rb' - - 'spec/grape/dsl/request_response_spec.rb' - 'spec/grape/dsl/routing_spec.rb' - 'spec/grape/dsl/settings_spec.rb' - 'spec/grape/endpoint/declared_spec.rb' - - 'spec/grape/endpoint_spec.rb' - - 'spec/grape/entity_spec.rb' - - 'spec/grape/exceptions/body_parse_errors_spec.rb' - - 'spec/grape/exceptions/invalid_accept_header_spec.rb' - - 'spec/grape/exceptions/validation_errors_spec.rb' - - 'spec/grape/extensions/param_builders/hash_spec.rb' - - 'spec/grape/extensions/param_builders/hash_with_indifferent_access_spec.rb' - - 'spec/grape/extensions/param_builders/hashie/mash_spec.rb' - - 'spec/grape/middleware/auth/base_spec.rb' - - 'spec/grape/middleware/auth/dsl_spec.rb' - - 'spec/grape/middleware/base_spec.rb' - - 'spec/grape/middleware/exception_spec.rb' - - 'spec/grape/middleware/formatter_spec.rb' - - 'spec/grape/middleware/stack_spec.rb' - - 'spec/grape/middleware/versioner/accept_version_header_spec.rb' - 'spec/grape/middleware/versioner/header_spec.rb' - - 'spec/grape/middleware/versioner/param_spec.rb' - - 'spec/grape/presenters/presenter_spec.rb' - 'spec/grape/util/inheritable_setting_spec.rb' - - 'spec/grape/util/reverse_stackable_values_spec.rb' - - 'spec/grape/util/stackable_values_spec.rb' - - 'spec/grape/validations/attributes_doc_spec.rb' - - 'spec/grape/validations/contract_scope_spec.rb' - - 'spec/grape/validations/instance_behaivour_spec.rb' - 'spec/grape/validations/params_scope_spec.rb' - - 'spec/grape/validations/types/array_coercer_spec.rb' - - 'spec/grape/validations/types/primitive_coercer_spec.rb' - - 'spec/grape/validations/types/set_coercer_spec.rb' - - 'spec/grape/validations/types_spec.rb' - - 'spec/grape/validations/validators/all_or_none_spec.rb' - - 'spec/grape/validations/validators/allow_blank_spec.rb' - - 'spec/grape/validations/validators/at_least_one_of_spec.rb' - 'spec/grape/validations/validators/coerce_spec.rb' - - 'spec/grape/validations/validators/default_spec.rb' - - 'spec/grape/validations/validators/exactly_one_of_spec.rb' - - 'spec/grape/validations/validators/except_values_spec.rb' - - 'spec/grape/validations/validators/mutual_exclusion_spec.rb' - 'spec/grape/validations/validators/presence_spec.rb' - - 'spec/grape/validations/validators/regexp_spec.rb' - - 'spec/grape/validations/validators/same_as_spec.rb' - - 'spec/grape/validations/validators/values_spec.rb' - 'spec/grape/validations_spec.rb' - - 'spec/integration/no_dry_validation/no_dry_validation_spec.rb' - - 'spec/shared/versioning_examples.rb' # Offense count: 38 # Configuration parameters: AllowSubject, Max. @@ -424,7 +314,7 @@ RSpec/MultipleMemoizedHelpers: - 'spec/grape/request_spec.rb' - 'spec/grape/validations/attributes_doc_spec.rb' -# Offense count: 2182 +# Offense count: 2180 # Configuration parameters: EnforcedStyle, IgnoreSharedExamples. # SupportedStyles: always, named_only RSpec/NamedSubject: @@ -489,29 +379,14 @@ RSpec/NamedSubject: - 'spec/grape/validations/validators/presence_spec.rb' - 'spec/grape/validations_spec.rb' -# Offense count: 174 +# Offense count: 28 # Configuration parameters: Max, AllowedGroups. RSpec/NestedGroups: Exclude: - 'spec/grape/api_remount_spec.rb' - 'spec/grape/api_spec.rb' - - 'spec/grape/dsl/headers_spec.rb' - 'spec/grape/dsl/inside_route_spec.rb' - - 'spec/grape/endpoint_spec.rb' - - 'spec/grape/exceptions/base_spec.rb' - - 'spec/grape/exceptions/invalid_accept_header_spec.rb' - - 'spec/grape/middleware/formatter_spec.rb' - - 'spec/grape/presenters/presenter_spec.rb' - - 'spec/grape/validations/attributes_doc_spec.rb' - - 'spec/grape/validations/params_scope_spec.rb' - - 'spec/grape/validations/single_attribute_iterator_spec.rb' - - 'spec/grape/validations/types/primitive_coercer_spec.rb' - - 'spec/grape/validations/validators/all_or_none_spec.rb' - - 'spec/grape/validations/validators/allow_blank_spec.rb' - - 'spec/grape/validations/validators/at_least_one_of_spec.rb' - 'spec/grape/validations/validators/coerce_spec.rb' - - 'spec/grape/validations/validators/exactly_one_of_spec.rb' - - 'spec/grape/validations/validators/mutual_exclusion_spec.rb' - 'spec/grape/validations_spec.rb' # Offense count: 18 @@ -560,31 +435,15 @@ RSpec/ScatteredSetup: - 'spec/grape/util/inheritable_setting_spec.rb' - 'spec/grape/validations_spec.rb' -# Offense count: 48 +# Offense count: 11 # Configuration parameters: Include, CustomTransform, IgnoreMethods, IgnoreMetadata. # Include: **/*_spec.rb RSpec/SpecFilePathFormat: Exclude: - '**/spec/routing/**/*' - - 'spec/grape/api/custom_validations_spec.rb' - - 'spec/grape/api/deeply_included_options_spec.rb' - - 'spec/grape/api/defines_boolean_in_params_spec.rb' - - 'spec/grape/api/documentation_spec.rb' - - 'spec/grape/api/inherited_helpers_spec.rb' - - 'spec/grape/api/invalid_format_spec.rb' - - 'spec/grape/api/mount_and_helpers_order_spec.rb' - - 'spec/grape/api/mount_and_rescue_from_spec.rb' - - 'spec/grape/api/namespace_parameters_in_route_spec.rb' - - 'spec/grape/api/nested_helpers_spec.rb' - - 'spec/grape/api/optional_parameters_in_route_spec.rb' - - 'spec/grape/api/parameters_modification_spec.rb' - - 'spec/grape/api/patch_method_helpers_spec.rb' - - 'spec/grape/api/recognize_path_spec.rb' - - 'spec/grape/api/required_parameters_in_route_spec.rb' - - 'spec/grape/api/required_parameters_with_invalid_method_spec.rb' - - 'spec/grape/api/routes_with_requirements_spec.rb' - - 'spec/grape/api/shared_helpers_exactly_one_of_spec.rb' - - 'spec/grape/api/shared_helpers_spec.rb' + - '**/spec/grape/api/*' + - '**/spec/integration/**/*' + - '**/spec/grape/validations/validators/*' - 'spec/grape/dsl/inside_route_spec.rb' - 'spec/grape/endpoint/declared_spec.rb' - 'spec/grape/exceptions/body_parse_errors_spec.rb' @@ -596,24 +455,6 @@ RSpec/SpecFilePathFormat: - 'spec/grape/loading_spec.rb' - 'spec/grape/middleware/exception_spec.rb' - 'spec/grape/validations/attributes_doc_spec.rb' - - 'spec/grape/validations/validators/all_or_none_spec.rb' - - 'spec/grape/validations/validators/allow_blank_spec.rb' - - 'spec/grape/validations/validators/at_least_one_of_spec.rb' - - 'spec/grape/validations/validators/coerce_spec.rb' - - 'spec/grape/validations/validators/default_spec.rb' - - 'spec/grape/validations/validators/exactly_one_of_spec.rb' - - 'spec/grape/validations/validators/except_values_spec.rb' - - 'spec/grape/validations/validators/mutual_exclusion_spec.rb' - - 'spec/grape/validations/validators/presence_spec.rb' - - 'spec/grape/validations/validators/regexp_spec.rb' - - 'spec/grape/validations/validators/same_as_spec.rb' - - 'spec/grape/validations/validators/values_spec.rb' - - 'spec/integration/eager_load/eager_load_spec.rb' - - 'spec/integration/multi_json/json_spec.rb' - - 'spec/integration/multi_xml/xml_spec.rb' - - 'spec/integration/no_dry_validation/no_dry_validation_spec.rb' - - 'spec/integration/rack_2_0/headers_spec.rb' - - 'spec/integration/rack_3_0/headers_spec.rb' # Offense count: 9 RSpec/StubbedMock: @@ -660,6 +501,7 @@ RSpec/VoidExpect: - 'spec/grape/dsl/headers_spec.rb' # Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). Style/CombinableLoops: Exclude: - 'spec/grape/endpoint_spec.rb' From 03730f1c8acda0512a7eaf258e155fa05ca4236a Mon Sep 17 00:00:00 2001 From: Dmitry Gutov Date: Mon, 25 Mar 2024 03:40:12 +0200 Subject: [PATCH 210/304] Simply disable RSpec/SpecFilePathFormat The alternative would be to use the more advanced ```yaml inherit_mode: merge: - Exclude ``` --- .rubocop.yml | 6 +----- .rubocop_todo.yml | 23 +---------------------- 2 files changed, 2 insertions(+), 27 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 407390031..413d0d4b4 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -66,11 +66,7 @@ RSpec/FilePath: SpecSuffixOnly: true RSpec/SpecFilePathFormat: - Exclude: - - '**/spec/routing/**/*' - - '**/spec/grape/api/*' - - '**/spec/integration/**/*' - - '**/spec/grape/validations/validators/*' + Enabled: false RSpec/MultipleExpectations: Max: 4 diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 41bcd4b85..e296e585a 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 5000` -# on 2024-03-25 01:13:48 UTC using RuboCop version 1.59.0. +# on 2024-03-25 01:40:39 UTC using RuboCop version 1.59.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 @@ -435,27 +435,6 @@ RSpec/ScatteredSetup: - 'spec/grape/util/inheritable_setting_spec.rb' - 'spec/grape/validations_spec.rb' -# Offense count: 11 -# Configuration parameters: Include, CustomTransform, IgnoreMethods, IgnoreMetadata. -# Include: **/*_spec.rb -RSpec/SpecFilePathFormat: - Exclude: - - '**/spec/routing/**/*' - - '**/spec/grape/api/*' - - '**/spec/integration/**/*' - - '**/spec/grape/validations/validators/*' - - 'spec/grape/dsl/inside_route_spec.rb' - - 'spec/grape/endpoint/declared_spec.rb' - - 'spec/grape/exceptions/body_parse_errors_spec.rb' - - 'spec/grape/extensions/param_builders/hash_spec.rb' - - 'spec/grape/extensions/param_builders/hash_with_indifferent_access_spec.rb' - - 'spec/grape/extensions/param_builders/hashie/mash_spec.rb' - - 'spec/grape/integration/global_namespace_function_spec.rb' - - 'spec/grape/integration/rack_sendfile_spec.rb' - - 'spec/grape/loading_spec.rb' - - 'spec/grape/middleware/exception_spec.rb' - - 'spec/grape/validations/attributes_doc_spec.rb' - # Offense count: 9 RSpec/StubbedMock: Exclude: From 3e9c2c8bf38816e7fa6e8017a3fd2b2ae007b2e0 Mon Sep 17 00:00:00 2001 From: Dmitry Gutov Date: Tue, 26 Mar 2024 02:08:28 +0200 Subject: [PATCH 211/304] Add more cop settings tweaks, exclusions and disablings --- .rubocop.yml | 17 ++- .rubocop_todo.yml | 183 +-------------------------------- spec/support/endpoint_faker.rb | 3 +- 3 files changed, 17 insertions(+), 186 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 413d0d4b4..a58cedd15 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -15,6 +15,10 @@ inherit_from: .rubocop_todo.yml Layout/LineLength: Max: 215 +Lint/EmptyBlock: + Exclude: + - spec/**/*_spec.rb + Style/Documentation: Enabled: false @@ -60,7 +64,7 @@ RSpec/ExampleLength: Max: 60 RSpec/NestedGroups: - Max: 4 + Max: 6 RSpec/FilePath: SpecSuffixOnly: true @@ -69,4 +73,13 @@ RSpec/SpecFilePathFormat: Enabled: false RSpec/MultipleExpectations: - Max: 4 + Enabled: false + +RSpec/NamedSubject: + Enabled: false + +RSpec/MultipleMemoizedHelpers: + Max: 11 + +RSpec/ContextWording: + Enabled: false diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index e296e585a..afdefec25 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 5000` -# on 2024-03-25 01:40:39 UTC using RuboCop version 1.59.0. +# on 2024-03-26 00:06:40 UTC using RuboCop version 1.59.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 @@ -52,29 +52,6 @@ Lint/DuplicateBranch: Exclude: - 'spec/support/versioned_helpers.rb' -# Offense count: 75 -# Configuration parameters: AllowComments, AllowEmptyLambdas. -Lint/EmptyBlock: - Exclude: - - 'spec/grape/api/recognize_path_spec.rb' - - 'spec/grape/api/required_parameters_with_invalid_method_spec.rb' - - 'spec/grape/api_spec.rb' - - 'spec/grape/dsl/routing_spec.rb' - - 'spec/grape/dsl/settings_spec.rb' - - 'spec/grape/endpoint/declared_spec.rb' - - 'spec/grape/endpoint_spec.rb' - - 'spec/grape/loading_spec.rb' - - 'spec/grape/validations/params_scope_spec.rb' - - 'spec/grape/validations/validators/all_or_none_spec.rb' - - 'spec/grape/validations/validators/at_least_one_of_spec.rb' - - 'spec/grape/validations/validators/coerce_spec.rb' - - 'spec/grape/validations/validators/exactly_one_of_spec.rb' - - 'spec/grape/validations/validators/mutual_exclusion_spec.rb' - - 'spec/grape/validations/validators/regexp_spec.rb' - - 'spec/grape/validations/validators/same_as_spec.rb' - - 'spec/grape/validations_spec.rb' - - 'spec/support/endpoint_faker.rb' - # Offense count: 5 # Configuration parameters: AllowComments. Lint/EmptyClass: @@ -136,63 +113,6 @@ RSpec/AnyInstance: - 'spec/grape/api_spec.rb' - 'spec/grape/middleware/base_spec.rb' -# Offense count: 345 -# Configuration parameters: Prefixes, AllowedPatterns. -# Prefixes: when, with, without -RSpec/ContextWording: - Exclude: - - 'spec/grape/api/defines_boolean_in_params_spec.rb' - - 'spec/grape/api/documentation_spec.rb' - - 'spec/grape/api/inherited_helpers_spec.rb' - - 'spec/grape/api/instance_spec.rb' - - 'spec/grape/api/invalid_format_spec.rb' - - 'spec/grape/api/namespace_parameters_in_route_spec.rb' - - 'spec/grape/api/optional_parameters_in_route_spec.rb' - - 'spec/grape/api/patch_method_helpers_spec.rb' - - 'spec/grape/api/required_parameters_in_route_spec.rb' - - 'spec/grape/api/required_parameters_with_invalid_method_spec.rb' - - 'spec/grape/api/routes_with_requirements_spec.rb' - - 'spec/grape/api_remount_spec.rb' - - 'spec/grape/api_spec.rb' - - 'spec/grape/dsl/helpers_spec.rb' - - 'spec/grape/dsl/inside_route_spec.rb' - - 'spec/grape/endpoint_spec.rb' - - 'spec/grape/entity_spec.rb' - - 'spec/grape/exceptions/body_parse_errors_spec.rb' - - 'spec/grape/exceptions/invalid_accept_header_spec.rb' - - 'spec/grape/exceptions/validation_errors_spec.rb' - - 'spec/grape/extensions/param_builders/hashie/mash_spec.rb' - - 'spec/grape/middleware/auth/strategies_spec.rb' - - 'spec/grape/middleware/base_spec.rb' - - 'spec/grape/middleware/exception_spec.rb' - - 'spec/grape/middleware/formatter_spec.rb' - - 'spec/grape/middleware/globals_spec.rb' - - 'spec/grape/middleware/stack_spec.rb' - - 'spec/grape/middleware/versioner/accept_version_header_spec.rb' - - 'spec/grape/middleware/versioner/header_spec.rb' - - 'spec/grape/path_spec.rb' - - 'spec/grape/util/inheritable_values_spec.rb' - - 'spec/grape/util/reverse_stackable_values_spec.rb' - - 'spec/grape/util/stackable_values_spec.rb' - - 'spec/grape/validations/attributes_doc_spec.rb' - - 'spec/grape/validations/params_scope_spec.rb' - - 'spec/grape/validations/single_attribute_iterator_spec.rb' - - 'spec/grape/validations/types/array_coercer_spec.rb' - - 'spec/grape/validations/types/primitive_coercer_spec.rb' - - 'spec/grape/validations/types/set_coercer_spec.rb' - - 'spec/grape/validations/validators/all_or_none_spec.rb' - - 'spec/grape/validations/validators/allow_blank_spec.rb' - - 'spec/grape/validations/validators/at_least_one_of_spec.rb' - - 'spec/grape/validations/validators/coerce_spec.rb' - - 'spec/grape/validations/validators/default_spec.rb' - - 'spec/grape/validations/validators/exactly_one_of_spec.rb' - - 'spec/grape/validations/validators/mutual_exclusion_spec.rb' - - 'spec/grape/validations/validators/regexp_spec.rb' - - 'spec/grape/validations/validators/same_as_spec.rb' - - 'spec/grape/validations/validators/values_spec.rb' - - 'spec/grape/validations_spec.rb' - - 'spec/shared/versioning_examples.rb' - # Offense count: 2 # Configuration parameters: IgnoredMetadata. RSpec/DescribeClass: @@ -288,107 +208,6 @@ RSpec/MissingExampleGroupArgument: Exclude: - 'spec/grape/middleware/exception_spec.rb' -# Offense count: 57 -# Configuration parameters: Max. -RSpec/MultipleExpectations: - Exclude: - - 'spec/grape/api/mount_and_rescue_from_spec.rb' - - 'spec/grape/api/parameters_modification_spec.rb' - - 'spec/grape/api_spec.rb' - - 'spec/grape/dsl/helpers_spec.rb' - - 'spec/grape/dsl/routing_spec.rb' - - 'spec/grape/dsl/settings_spec.rb' - - 'spec/grape/endpoint/declared_spec.rb' - - 'spec/grape/middleware/versioner/header_spec.rb' - - 'spec/grape/util/inheritable_setting_spec.rb' - - 'spec/grape/validations/params_scope_spec.rb' - - 'spec/grape/validations/validators/coerce_spec.rb' - - 'spec/grape/validations/validators/presence_spec.rb' - - 'spec/grape/validations_spec.rb' - -# Offense count: 38 -# Configuration parameters: AllowSubject, Max. -RSpec/MultipleMemoizedHelpers: - Exclude: - - 'spec/grape/middleware/exception_spec.rb' - - 'spec/grape/request_spec.rb' - - 'spec/grape/validations/attributes_doc_spec.rb' - -# Offense count: 2180 -# Configuration parameters: EnforcedStyle, IgnoreSharedExamples. -# SupportedStyles: always, named_only -RSpec/NamedSubject: - Exclude: - - 'spec/grape/api/defines_boolean_in_params_spec.rb' - - 'spec/grape/api/documentation_spec.rb' - - 'spec/grape/api/invalid_format_spec.rb' - - 'spec/grape/api/namespace_parameters_in_route_spec.rb' - - 'spec/grape/api/optional_parameters_in_route_spec.rb' - - 'spec/grape/api/parameters_modification_spec.rb' - - 'spec/grape/api/recognize_path_spec.rb' - - 'spec/grape/api/required_parameters_in_route_spec.rb' - - 'spec/grape/api/required_parameters_with_invalid_method_spec.rb' - - 'spec/grape/api/routes_with_requirements_spec.rb' - - 'spec/grape/api_spec.rb' - - 'spec/grape/dsl/callbacks_spec.rb' - - 'spec/grape/dsl/desc_spec.rb' - - 'spec/grape/dsl/headers_spec.rb' - - 'spec/grape/dsl/helpers_spec.rb' - - 'spec/grape/dsl/inside_route_spec.rb' - - 'spec/grape/dsl/logger_spec.rb' - - 'spec/grape/dsl/middleware_spec.rb' - - 'spec/grape/dsl/parameters_spec.rb' - - 'spec/grape/dsl/request_response_spec.rb' - - 'spec/grape/dsl/routing_spec.rb' - - 'spec/grape/dsl/settings_spec.rb' - - 'spec/grape/dsl/validations_spec.rb' - - 'spec/grape/endpoint/declared_spec.rb' - - 'spec/grape/endpoint_spec.rb' - - 'spec/grape/entity_spec.rb' - - 'spec/grape/exceptions/base_spec.rb' - - 'spec/grape/exceptions/body_parse_errors_spec.rb' - - 'spec/grape/exceptions/invalid_accept_header_spec.rb' - - 'spec/grape/exceptions/validation_errors_spec.rb' - - 'spec/grape/extensions/param_builders/hash_spec.rb' - - 'spec/grape/extensions/param_builders/hash_with_indifferent_access_spec.rb' - - 'spec/grape/extensions/param_builders/hashie/mash_spec.rb' - - 'spec/grape/integration/rack_sendfile_spec.rb' - - 'spec/grape/middleware/auth/dsl_spec.rb' - - 'spec/grape/middleware/base_spec.rb' - - 'spec/grape/middleware/formatter_spec.rb' - - 'spec/grape/middleware/globals_spec.rb' - - 'spec/grape/middleware/stack_spec.rb' - - 'spec/grape/middleware/versioner/accept_version_header_spec.rb' - - 'spec/grape/middleware/versioner/header_spec.rb' - - 'spec/grape/middleware/versioner/param_spec.rb' - - 'spec/grape/middleware/versioner/path_spec.rb' - - 'spec/grape/parser_spec.rb' - - 'spec/grape/presenters/presenter_spec.rb' - - 'spec/grape/util/inheritable_setting_spec.rb' - - 'spec/grape/util/inheritable_values_spec.rb' - - 'spec/grape/util/reverse_stackable_values_spec.rb' - - 'spec/grape/util/stackable_values_spec.rb' - - 'spec/grape/util/strict_hash_configuration_spec.rb' - - 'spec/grape/validations/attributes_doc_spec.rb' - - 'spec/grape/validations/params_scope_spec.rb' - - 'spec/grape/validations/types/array_coercer_spec.rb' - - 'spec/grape/validations/types/primitive_coercer_spec.rb' - - 'spec/grape/validations/types/set_coercer_spec.rb' - - 'spec/grape/validations/validators/coerce_spec.rb' - - 'spec/grape/validations/validators/default_spec.rb' - - 'spec/grape/validations/validators/presence_spec.rb' - - 'spec/grape/validations_spec.rb' - -# Offense count: 28 -# Configuration parameters: Max, AllowedGroups. -RSpec/NestedGroups: - Exclude: - - 'spec/grape/api_remount_spec.rb' - - 'spec/grape/api_spec.rb' - - 'spec/grape/dsl/inside_route_spec.rb' - - 'spec/grape/validations/validators/coerce_spec.rb' - - 'spec/grape/validations_spec.rb' - # Offense count: 18 # Configuration parameters: AllowedPatterns. # AllowedPatterns: ^expect_, ^assert_ diff --git a/spec/support/endpoint_faker.rb b/spec/support/endpoint_faker.rb index 1cf02ef1d..773eb85a3 100644 --- a/spec/support/endpoint_faker.rb +++ b/spec/support/endpoint_faker.rb @@ -4,8 +4,7 @@ module Spec module Support class EndpointFaker class FakerAPI < Grape::API - get '/' do - end + get('/') end def initialize(app, endpoint = FakerAPI.endpoints.first) From 90687444421c3ff12d4574caff5aa992af91bb23 Mon Sep 17 00:00:00 2001 From: Dmitry Gutov Date: Tue, 26 Mar 2024 05:56:36 +0200 Subject: [PATCH 212/304] Fix most Lint/ConstantDefinitionInBlock and RSpec/LeakyConstantDeclaration offenses --- .rubocop_todo.yml | 53 +--- .../api/defines_boolean_in_params_spec.rb | 12 +- spec/grape/api/inherited_helpers_spec.rb | 30 ++- .../grape/api/mount_and_helpers_order_spec.rb | 165 ++++++++----- spec/grape/api/mount_and_rescue_from_spec.rb | 67 ++--- spec/grape/api/nested_helpers_spec.rb | 24 +- spec/grape/api/patch_method_helpers_spec.rb | 27 +- spec/grape/api_spec.rb | 233 +++++++++--------- spec/grape/entity_spec.rb | 18 +- spec/grape/loading_spec.rb | 11 +- spec/grape/middleware/base_spec.rb | 31 ++- spec/grape/middleware/error_spec.rb | 30 +-- spec/grape/middleware/exception_spec.rb | 10 +- spec/grape/middleware/formatter_spec.rb | 4 +- spec/grape/middleware/stack_spec.rb | 87 ++++--- spec/grape/validations/params_scope_spec.rb | 7 +- spec/grape/validations/types_spec.rb | 15 +- .../validations/validators/coerce_spec.rb | 20 +- .../validations/validators/presence_spec.rb | 4 +- .../validations/validators/values_spec.rb | 31 ++- 20 files changed, 450 insertions(+), 429 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index afdefec25..3a11f910b 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 5000` -# on 2024-03-26 00:06:40 UTC using RuboCop version 1.59.0. +# on 2024-03-26 03:54:44 UTC using RuboCop version 1.59.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 @@ -21,30 +21,12 @@ Lint/AmbiguousBlockAssociation: Exclude: - 'spec/grape/dsl/routing_spec.rb' -# Offense count: 55 +# Offense count: 1 # Configuration parameters: AllowedMethods. # AllowedMethods: enums Lint/ConstantDefinitionInBlock: Exclude: - - 'spec/grape/api/defines_boolean_in_params_spec.rb' - - 'spec/grape/api/inherited_helpers_spec.rb' - - 'spec/grape/api/mount_and_helpers_order_spec.rb' - - 'spec/grape/api/mount_and_rescue_from_spec.rb' - - 'spec/grape/api/nested_helpers_spec.rb' - - 'spec/grape/api/patch_method_helpers_spec.rb' - - 'spec/grape/api_spec.rb' - - 'spec/grape/entity_spec.rb' - - 'spec/grape/loading_spec.rb' - - 'spec/grape/middleware/base_spec.rb' - - 'spec/grape/middleware/error_spec.rb' - - 'spec/grape/middleware/formatter_spec.rb' - - 'spec/grape/middleware/stack_spec.rb' - - 'spec/grape/validations/params_scope_spec.rb' - - 'spec/grape/validations/types_spec.rb' - - 'spec/grape/validations/validators/coerce_spec.rb' - 'spec/grape/validations/validators/except_values_spec.rb' - - 'spec/grape/validations/validators/presence_spec.rb' - - 'spec/grape/validations/validators/values_spec.rb' # Offense count: 3 # Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches. @@ -52,14 +34,11 @@ Lint/DuplicateBranch: Exclude: - 'spec/support/versioned_helpers.rb' -# Offense count: 5 +# Offense count: 1 # Configuration parameters: AllowComments. Lint/EmptyClass: Exclude: - 'lib/grape/dsl/parameters.rb' - - 'spec/grape/api_spec.rb' - - 'spec/grape/entity_spec.rb' - - 'spec/grape/middleware/stack_spec.rb' # Offense count: 6 # Configuration parameters: AllowedParentClasses. @@ -107,7 +86,7 @@ Naming/VariableNumber: - 'spec/grape/exceptions/validation_errors_spec.rb' - 'spec/grape/validations_spec.rb' -# Offense count: 4 +# Offense count: 2 RSpec/AnyInstance: Exclude: - 'spec/grape/api_spec.rb' @@ -156,11 +135,10 @@ RSpec/IndexedLet: - 'spec/grape/presenters/presenter_spec.rb' - 'spec/shared/versioning_examples.rb' -# Offense count: 44 +# Offense count: 39 # Configuration parameters: AssignmentOnly. RSpec/InstanceVariable: Exclude: - - 'spec/grape/api/mount_and_helpers_order_spec.rb' - 'spec/grape/api_spec.rb' - 'spec/grape/endpoint_spec.rb' - 'spec/grape/middleware/base_spec.rb' @@ -168,29 +146,10 @@ RSpec/InstanceVariable: - 'spec/grape/middleware/versioner/header_spec.rb' - 'spec/grape/validations/validators/except_values_spec.rb' -# Offense count: 97 +# Offense count: 6 RSpec/LeakyConstantDeclaration: Exclude: - - 'spec/grape/api/defines_boolean_in_params_spec.rb' - - 'spec/grape/api/inherited_helpers_spec.rb' - - 'spec/grape/api/mount_and_helpers_order_spec.rb' - - 'spec/grape/api/mount_and_rescue_from_spec.rb' - - 'spec/grape/api/nested_helpers_spec.rb' - - 'spec/grape/api/patch_method_helpers_spec.rb' - - 'spec/grape/api_spec.rb' - - 'spec/grape/entity_spec.rb' - - 'spec/grape/loading_spec.rb' - - 'spec/grape/middleware/base_spec.rb' - - 'spec/grape/middleware/error_spec.rb' - - 'spec/grape/middleware/exception_spec.rb' - - 'spec/grape/middleware/formatter_spec.rb' - - 'spec/grape/middleware/stack_spec.rb' - - 'spec/grape/validations/params_scope_spec.rb' - - 'spec/grape/validations/types_spec.rb' - - 'spec/grape/validations/validators/coerce_spec.rb' - 'spec/grape/validations/validators/except_values_spec.rb' - - 'spec/grape/validations/validators/presence_spec.rb' - - 'spec/grape/validations/validators/values_spec.rb' # Offense count: 2 RSpec/MessageChain: diff --git a/spec/grape/api/defines_boolean_in_params_spec.rb b/spec/grape/api/defines_boolean_in_params_spec.rb index e1f31332d..6d35049cf 100644 --- a/spec/grape/api/defines_boolean_in_params_spec.rb +++ b/spec/grape/api/defines_boolean_in_params_spec.rb @@ -2,10 +2,10 @@ describe Grape::API::Instance do describe 'boolean constant' do - module DefinesBooleanInstanceSpec - class API < Grape::API + let(:app) do + Class.new(Grape::API) do params do - requires :message, type: Boolean + requires :message, type: Grape::API::Boolean end post :echo do { class: params[:message].class.name, value: params[:message] } @@ -13,10 +13,6 @@ class API < Grape::API end end - def app - DefinesBooleanInstanceSpec::API - end - let(:expected_body) do { class: 'TrueClass', value: true }.to_s end @@ -28,7 +24,7 @@ def app end context 'Params endpoint type' do - subject { DefinesBooleanInstanceSpec::API.new.router.map['POST'].first.options[:params]['message'][:type] } + subject { app.new.router.map['POST'].first.options[:params]['message'][:type] } it 'params type is a boolean' do expect(subject).to eq 'Grape::API::Boolean' diff --git a/spec/grape/api/inherited_helpers_spec.rb b/spec/grape/api/inherited_helpers_spec.rb index 0b7933cb9..49597bc5b 100644 --- a/spec/grape/api/inherited_helpers_spec.rb +++ b/spec/grape/api/inherited_helpers_spec.rb @@ -3,9 +3,8 @@ describe Grape::API::Helpers do let(:user) { 'Miguel Caneo' } let(:id) { '42' } - - module InheritedHelpersSpec - class SuperClass < Grape::API + let(:api_super_class) do + Class.new(Grape::API) do helpers do params(:superclass_params) { requires :id, type: String } @@ -14,8 +13,9 @@ def current_user end end end - - class OverriddenSubClass < SuperClass + end + let(:api_overridden_sub_class) do + Class.new(api_super_class) do params { use :superclass_params } helpers do @@ -28,16 +28,18 @@ def current_user "#{current_user}: #{params['id']}" end end - - class SubClass < SuperClass + end + let(:api_sub_class) do + Class.new(api_super_class) do params { use :superclass_params } get 'resource' do "#{current_user}: #{params['id']}" end end - - class Example < SubClass + end + let(:api_example) do + Class.new(api_sub_class) do params { use :superclass_params } get 'resource' do @@ -47,7 +49,7 @@ class Example < SubClass end context 'non overriding subclass' do - subject { InheritedHelpersSpec::SubClass } + subject { api_sub_class } def app subject @@ -69,10 +71,8 @@ def app end context 'overriding subclass' do - subject { InheritedHelpersSpec::OverriddenSubClass } - def app - subject + api_overridden_sub_class end context 'given expected params' do @@ -91,10 +91,8 @@ def app end context 'example subclass' do - subject { InheritedHelpersSpec::Example } - def app - subject + api_example end context 'given expected params' do diff --git a/spec/grape/api/mount_and_helpers_order_spec.rb b/spec/grape/api/mount_and_helpers_order_spec.rb index a00423a14..3485a820e 100644 --- a/spec/grape/api/mount_and_helpers_order_spec.rb +++ b/spec/grape/api/mount_and_helpers_order_spec.rb @@ -1,31 +1,35 @@ # frozen_string_literal: true describe Grape::API do - def app - subject - end - describe 'rescue_from' do context 'when the API is mounted AFTER defining the class rescue_from handler' do - class APIRescueFrom < Grape::API - rescue_from :all do - error!({ type: 'all' }, 404) - end - - get do - { count: 1 / 0 } + let(:api_rescue_from) do + Class.new(Grape::API) do + rescue_from :all do + error!({ type: 'all' }, 404) + end + + get do + { count: 1 / 0 } + end end end - class MainRescueFromAfter < Grape::API - rescue_from ZeroDivisionError do - error!({ type: 'zero' }, 500) - end + let(:main_rescue_from_after) do + context = self - mount APIRescueFrom + Class.new(Grape::API) do + rescue_from ZeroDivisionError do + error!({ type: 'zero' }, 500) + end + + mount context.api_rescue_from + end end - subject { MainRescueFromAfter } + def app + main_rescue_from_after + end it 'is rescued by the rescue_from ZeroDivisionError handler from Main class' do get '/' @@ -36,25 +40,32 @@ class MainRescueFromAfter < Grape::API end context 'when the API is mounted BEFORE defining the class rescue_from handler' do - class APIRescueFrom < Grape::API - rescue_from :all do - error!({ type: 'all' }, 404) - end - - get do - { count: 1 / 0 } + let(:api_rescue_from) do + Class.new(Grape::API) do + rescue_from :all do + error!({ type: 'all' }, 404) + end + + get do + { count: 1 / 0 } + end end end + let(:main_rescue_from_before) do + context = self - class MainRescueFromBefore < Grape::API - mount APIRescueFrom + Class.new(Grape::API) do + mount context.api_rescue_from - rescue_from ZeroDivisionError do - error!({ type: 'zero' }, 500) + rescue_from ZeroDivisionError do + error!({ type: 'zero' }, 500) + end end end - subject { MainRescueFromBefore } + def app + main_rescue_from_before + end it 'is rescued by the rescue_from ZeroDivisionError handler from Main class' do get '/' @@ -67,21 +78,28 @@ class MainRescueFromBefore < Grape::API describe 'before' do context 'when the API is mounted AFTER defining the before helper' do - class APIBeforeHandler < Grape::API - get do - { count: @count }.to_json + let(:api_before_handler) do + Class.new(Grape::API) do + get do + { count: @count }.to_json + end end end + let(:main_before_handler_after) do + context = self - class MainBeforeHandlerAfter < Grape::API - before do - @count = 1 - end + Class.new(Grape::API) do + before do + @count = 1 + end - mount APIBeforeHandler + mount context.api_before_handler + end end - subject { MainBeforeHandlerAfter } + def app + main_before_handler_after + end it 'is able to access the variables defined in the before helper' do get '/' @@ -92,21 +110,28 @@ class MainBeforeHandlerAfter < Grape::API end context 'when the API is mounted BEFORE defining the before helper' do - class APIBeforeHandler < Grape::API - get do - { count: @count }.to_json + let(:api_before_handler) do + Class.new(Grape::API) do + get do + { count: @count }.to_json + end end end + let(:main_before_handler_before) do + context = self - class MainBeforeHandlerBefore < Grape::API - mount APIBeforeHandler + Class.new(Grape::API) do + mount context.api_before_handler - before do - @count = 1 + before do + @count = 1 + end end end - subject { MainBeforeHandlerBefore } + def app + main_before_handler_before + end it 'is able to access the variables defined in the before helper' do get '/' @@ -119,21 +144,28 @@ class MainBeforeHandlerBefore < Grape::API describe 'after' do context 'when the API is mounted AFTER defining the after handler' do - class APIAfterHandler < Grape::API - get do - { count: 1 }.to_json + let(:api_after_handler) do + Class.new(Grape::API) do + get do + { count: 1 }.to_json + end end end + let(:main_after_handler_after) do + context = self - class MainAfterHandlerAfter < Grape::API - after do - error!({ type: 'after' }, 500) - end + Class.new(Grape::API) do + after do + error!({ type: 'after' }, 500) + end - mount APIAfterHandler + mount context.api_after_handler + end end - subject { MainAfterHandlerAfter } + def app + main_after_handler_after + end it 'is able to access the variables defined in the after helper' do get '/' @@ -144,21 +176,28 @@ class MainAfterHandlerAfter < Grape::API end context 'when the API is mounted BEFORE defining the after helper' do - class APIAfterHandler < Grape::API - get do - { count: 1 }.to_json + let(:api_after_handler) do + Class.new(Grape::API) do + get do + { count: 1 }.to_json + end end end + let(:main_after_handler_before) do + context = self - class MainAfterHandlerBefore < Grape::API - mount APIAfterHandler + Class.new(Grape::API) do + mount context.api_after_handler - after do - error!({ type: 'after' }, 500) + after do + error!({ type: 'after' }, 500) + end end end - subject { MainAfterHandlerBefore } + def app + main_after_handler_before + end it 'is able to access the variables defined in the after helper' do get '/' diff --git a/spec/grape/api/mount_and_rescue_from_spec.rb b/spec/grape/api/mount_and_rescue_from_spec.rb index 0dfc4a35d..1289a7134 100644 --- a/spec/grape/api/mount_and_rescue_from_spec.rb +++ b/spec/grape/api/mount_and_rescue_from_spec.rb @@ -1,38 +1,42 @@ # frozen_string_literal: true describe Grape::API do - def app - subject - end - context 'when multiple classes defines the same rescue_from' do - class AnAPI < Grape::API - rescue_from ZeroDivisionError do - error!({ type: 'an-api-zero' }, 404) - end + let(:an_api) do + Class.new(Grape::API) do + rescue_from ZeroDivisionError do + error!({ type: 'an-api-zero' }, 404) + end - get '/an-api' do - { count: 1 / 0 } + get '/an-api' do + { count: 1 / 0 } + end end end + let(:another_api) do + Class.new(Grape::API) do + rescue_from ZeroDivisionError do + error!({ type: 'another-api-zero' }, 322) + end - class AnotherAPI < Grape::API - rescue_from ZeroDivisionError do - error!({ type: 'another-api-zero' }, 322) + get '/another-api' do + { count: 1 / 0 } + end end + end + let(:other_main) do + context = self - get '/another-api' do - { count: 1 / 0 } + Class.new(Grape::API) do + mount context.an_api + mount context.another_api end end - class OtherMain < Grape::API - mount AnAPI - mount AnotherAPI + def app + other_main end - subject { OtherMain } - it 'is rescued by the rescue_from ZeroDivisionError handler defined inside each of the classes' do get '/an-api' @@ -46,19 +50,26 @@ class OtherMain < Grape::API end context 'when some class does not define a rescue_from but it was defined in a previous mounted endpoint' do - class AnAPIWithoutDefinedRescueFrom < Grape::API - get '/another-api-without-defined-rescue-from' do - { count: 1 / 0 } + let(:an_api_without_defined_rescue_from) do + Class.new(Grape::API) do + get '/another-api-without-defined-rescue-from' do + { count: 1 / 0 } + end end end + let(:other_main_with_not_defined_rescue_from) do + context = self - class OtherMainWithNotDefinedRescueFrom < Grape::API - mount AnAPI - mount AnotherAPI - mount AnAPIWithoutDefinedRescueFrom + Class.new(Grape::API) do + mount context.an_api + mount context.another_api + mount context.an_api_without_defined_rescue_from + end end - subject { OtherMainWithNotDefinedRescueFrom } + def app + other_main_with_not_defined_rescue_from + end it 'is not rescued by any of the previous defined rescue_from ZeroDivisionError handlers' do get '/an-api' diff --git a/spec/grape/api/nested_helpers_spec.rb b/spec/grape/api/nested_helpers_spec.rb index 2acddbcf0..77975f434 100644 --- a/spec/grape/api/nested_helpers_spec.rb +++ b/spec/grape/api/nested_helpers_spec.rb @@ -1,17 +1,20 @@ # frozen_string_literal: true describe Grape::API::Helpers do - module NestedHelpersSpec - module HelperMethods + let(:helper_methods) do + Module.new do extend Grape::API::Helpers def current_user @current_user ||= params[:current_user] end end + end + let(:nested) do + context = self - class Nested < Grape::API + Class.new(Grape::API) do resource :level1 do - helpers HelperMethods + helpers context.helper_methods get do current_user @@ -24,18 +27,17 @@ class Nested < Grape::API end end end - - class Main < Grape::API - mount Nested - end end + let(:main) do + context = self - subject do - NestedHelpersSpec::Main + Class.new(Grape::API) do + mount context.nested + end end def app - subject + main end it 'can access helpers from a mounted resource' do diff --git a/spec/grape/api/patch_method_helpers_spec.rb b/spec/grape/api/patch_method_helpers_spec.rb index 23b4338e7..ad0a162ba 100644 --- a/spec/grape/api/patch_method_helpers_spec.rb +++ b/spec/grape/api/patch_method_helpers_spec.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true describe Grape::API::Helpers do - module PatchHelpersSpec - class PatchPublic < Grape::API + let(:patch_public) do + Class.new(Grape::API) do format :json version 'public-v1', using: :header, vendor: 'grape' @@ -10,16 +10,20 @@ class PatchPublic < Grape::API { ok: 'public' } end end - - module AuthMethods + end + let(:auth_methods) do + Module.new do def authenticate!; end end + end + let(:patch_private) do + context = self - class PatchPrivate < Grape::API + Class.new(Grape::API) do format :json version 'private-v1', using: :header, vendor: 'grape' - helpers AuthMethods + helpers context.auth_methods before do authenticate! @@ -29,15 +33,18 @@ class PatchPrivate < Grape::API { ok: 'private' } end end + end + let(:main) do + context = self - class Main < Grape::API - mount PatchPublic - mount PatchPrivate + Class.new(Grape::API) do + mount context.patch_public + mount context.patch_private end end def app - PatchHelpersSpec::Main + main end context 'patch' do diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index 95b46079c..f8bb785ff 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'shared/versioning_examples' +require 'grape-entity' describe Grape::API do subject do @@ -369,17 +370,19 @@ def subject.enable_root_route! end context 'format' do - module ApiSpec - class DummyFormatClass - end - end - before do - allow_any_instance_of(ApiSpec::DummyFormatClass).to receive(:to_json).and_return('abc') - allow_any_instance_of(ApiSpec::DummyFormatClass).to receive(:to_txt).and_return('def') + dummy_class = Class.new do + def to_json(*_rest) + 'abc' + end + + def to_txt + 'def' + end + end subject.get('/abc') do - ApiSpec::DummyFormatClass.new + dummy_class.new end end @@ -1369,8 +1372,8 @@ class DummyFormatClass end context 'custom middleware' do - module ApiSpec - class PhonyMiddleware + let(:phony_middleware) do + Class.new do def initialize(app, *args) @args = args @app = app @@ -1388,43 +1391,44 @@ def call(env) describe '.middleware' do it 'includes middleware arguments from settings' do - subject.use ApiSpec::PhonyMiddleware, 'abc', 123 - expect(subject.middleware).to eql [[:use, ApiSpec::PhonyMiddleware, 'abc', 123]] + subject.use phony_middleware, 'abc', 123 + expect(subject.middleware).to eql [[:use, phony_middleware, 'abc', 123]] end it 'includes all middleware from stacked settings' do - subject.use ApiSpec::PhonyMiddleware, 123 - subject.use ApiSpec::PhonyMiddleware, 'abc' - subject.use ApiSpec::PhonyMiddleware, 'foo' + subject.use phony_middleware, 123 + subject.use phony_middleware, 'abc' + subject.use phony_middleware, 'foo' expect(subject.middleware).to eql [ - [:use, ApiSpec::PhonyMiddleware, 123], - [:use, ApiSpec::PhonyMiddleware, 'abc'], - [:use, ApiSpec::PhonyMiddleware, 'foo'] + [:use, phony_middleware, 123], + [:use, phony_middleware, 'abc'], + [:use, phony_middleware, 'foo'] ] end end describe '.use' do it 'adds middleware' do - subject.use ApiSpec::PhonyMiddleware, 123 - expect(subject.middleware).to eql [[:use, ApiSpec::PhonyMiddleware, 123]] + subject.use phony_middleware, 123 + expect(subject.middleware).to eql [[:use, phony_middleware, 123]] end it 'does not show up outside the namespace' do + example = self inner_middleware = nil - subject.use ApiSpec::PhonyMiddleware, 123 + subject.use phony_middleware, 123 subject.namespace :awesome do - use ApiSpec::PhonyMiddleware, 'abc' + use example.phony_middleware, 'abc' inner_middleware = middleware end - expect(subject.middleware).to eql [[:use, ApiSpec::PhonyMiddleware, 123]] - expect(inner_middleware).to eql [[:use, ApiSpec::PhonyMiddleware, 123], [:use, ApiSpec::PhonyMiddleware, 'abc']] + expect(subject.middleware).to eql [[:use, phony_middleware, 123]] + expect(inner_middleware).to eql [[:use, phony_middleware, 123], [:use, phony_middleware, 'abc']] end it 'calls the middleware' do - subject.use ApiSpec::PhonyMiddleware, 'hello' + subject.use phony_middleware, 'hello' subject.get '/' do env['phony.args'].first.first end @@ -1435,13 +1439,13 @@ def call(env) it 'adds a block if one is given' do block = -> {} - subject.use ApiSpec::PhonyMiddleware, &block - expect(subject.middleware).to eql [[:use, ApiSpec::PhonyMiddleware, block]] + subject.use phony_middleware, &block + expect(subject.middleware).to eql [[:use, phony_middleware, block]] end it 'uses a block if one is given' do block = -> {} - subject.use ApiSpec::PhonyMiddleware, &block + subject.use phony_middleware, &block subject.get '/' do env['phony.block'].inspect end @@ -1452,7 +1456,7 @@ def call(env) it 'does not destroy the middleware settings on multiple runs' do block = -> {} - subject.use ApiSpec::PhonyMiddleware, &block + subject.use phony_middleware, &block subject.get '/' do env['phony.block'].inspect end @@ -1488,8 +1492,8 @@ def call(env) end end - subject.use ApiSpec::PhonyMiddleware, 'hello' - subject.insert_before ApiSpec::PhonyMiddleware, m, message: 'bye' + subject.use phony_middleware, 'hello' + subject.insert_before phony_middleware, m, message: 'bye' subject.get '/' do env['phony.args'].join(' ') end @@ -1509,8 +1513,8 @@ def call(env) end end - subject.use ApiSpec::PhonyMiddleware, 'hello' - subject.insert_after ApiSpec::PhonyMiddleware, m, message: 'bye' + subject.use phony_middleware, 'hello' + subject.insert_after phony_middleware, m, message: 'bye' subject.get '/' do env['phony.args'].join(' ') end @@ -1519,27 +1523,27 @@ def call(env) expect(last_response.body).to eql 'hello bye' end 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) + 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 - 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 + subject.use phony_middleware, 'bye' + subject.insert 0, m, message: 'good' + subject.insert 0, m, message: 'hello' + subject.get '/' do + env['phony.args'].join(' ') + end - get '/' - expect(last_response.body).to eql 'hello good bye' + get '/' + expect(last_response.body).to eql 'hello good bye' + end end end @@ -2089,9 +2093,7 @@ def foo context 'CustomError subclass of Grape::Exceptions::Base' do before do - module ApiSpec - class CustomError < Grape::Exceptions::Base; end - end + stub_const('ApiSpec::CustomError', Class.new(Grape::Exceptions::Base)) end it 'does not re-raise exceptions of type Grape::Exceptions::Base' do @@ -2155,11 +2157,9 @@ class CustomError < Grape::Exceptions::Base; end context 'custom errors' do before do - class ConnectionError < RuntimeError; end - - class DatabaseError < RuntimeError; end - - class CommunicationError < StandardError; end + stub_const('ConnectionError', Class.new(RuntimeError)) + stub_const('DatabaseError', Class.new(RuntimeError)) + stub_const('CommunicationError', Class.new(StandardError)) end it 'rescues an error via rescue_from :all' do @@ -2315,13 +2315,9 @@ def rescue_all_errors describe '.rescue_from klass, rescue_subclasses: boolean' do before do - module ApiSpec - module APIErrors - class ParentError < StandardError; end - - class ChildError < ParentError; end - end - end + parent_error = Class.new(StandardError) + stub_const('ApiSpec::APIErrors::ParentError', parent_error) + stub_const('ApiSpec::APIErrors::ChildError', Class.new(parent_error)) end it 'rescues error as well as subclass errors with rescue_subclasses option set' do @@ -2449,47 +2445,30 @@ class ChildError < ParentError; end end context 'class' do - before do - module ApiSpec - class CustomErrorFormatter - def self.call(message, _backtrace, _options, _env, _original_exception) - "message: #{message} @backtrace" - end + let(:custom_error_formatter) do + Class.new do + def self.call(message, _backtrace, _options, _env, _original_exception) + "message: #{message} @backtrace" end end end it 'returns a custom error format' do subject.rescue_from :all, backtrace: true - subject.error_formatter :txt, ApiSpec::CustomErrorFormatter - subject.get '/exception' do - raise 'rain!' - end + subject.error_formatter :txt, custom_error_formatter + subject.get('/exception') { raise 'rain!' } + get '/exception' expect(last_response.body).to eq('message: rain! @backtrace') end - end - - describe 'with' do - context 'class' do - before do - module ApiSpec - class CustomErrorFormatter - def self.call(message, _backtrace, _option, _env, _original_exception) - "message: #{message} @backtrace" - end - end - end - end - it 'returns a custom error format' do - subject.rescue_from :all, backtrace: true - subject.error_formatter :txt, with: ApiSpec::CustomErrorFormatter - subject.get('/exception') { raise 'rain!' } + it 'returns a custom error format (using keyword :with)' do + subject.rescue_from :all, backtrace: true + subject.error_formatter :txt, with: custom_error_formatter + subject.get('/exception') { raise 'rain!' } - get '/exception' - expect(last_response.body).to eq('message: rain! @backtrace') - end + get '/exception' + expect(last_response.body).to eq('message: rain! @backtrace') end end @@ -2619,17 +2598,18 @@ def self.call(message, _backtrace, _option, _env, _original_exception) end context 'custom formatter class' do - module ApiSpec - module CustomFormatter + let(:custom_formatter) do + Module.new do def self.call(object, _env) "{\"custom_formatter\":\"#{object[:some]}\"}" end end end + before do subject.content_type :json, 'application/json' subject.content_type :custom, 'application/custom' - subject.formatter :custom, ApiSpec::CustomFormatter + subject.formatter :custom, custom_formatter subject.get :simple do { some: 'hash' } end @@ -2678,17 +2658,18 @@ def self.call(object, _env) end context 'custom parser class' do - module ApiSpec - module CustomParser + let(:custom_parser) do + Module.new do def self.call(object, _env) { object.to_sym => object.to_s.reverse } end end end + before do subject.content_type :txt, 'text/plain' subject.content_type :custom, 'text/custom' - subject.parser :custom, ApiSpec::CustomParser + subject.parser :custom, custom_parser subject.put :simple do params[:simple] end @@ -3471,15 +3452,15 @@ def static end it 'mounts on a nested path' do - APP1 = Class.new(described_class) - APP2 = Class.new(described_class) - APP2.get '/nice' do + app1 = Class.new(described_class) + app2 = Class.new(described_class) + app2.get '/nice' do 'play' end # NOTE: that the reverse won't work, mount from outside-in - APP3 = subject - APP3.mount APP1 => '/app1' - APP1.mount APP2 => '/app2' + app3 = subject + app3.mount app1 => '/app1' + app1.mount app2 => '/app2' get '/app1/app2/nice' expect(last_response.status).to eq(200) expect(last_response.body).to eq('play') @@ -3667,13 +3648,18 @@ def static def self.included(base) base.extend(ClassMethods) end + end + end - module ClassMethods + before do + stub_const( + 'ClassMethods', + Module.new do def my_method @test = true end end - end + ) end it 'correctlies include module in nested mount' do @@ -3892,13 +3878,16 @@ def before end context ':serializable_hash' do - class SerializableHashExample - def serializable_hash - { abc: 'def' } - end - end - before do + stub_const( + 'SerializableHashExample', + Class.new do + def serializable_hash + { abc: 'def' } + end + end + ) + subject.format :serializable_hash end @@ -4231,17 +4220,21 @@ def self.inherited(child_api) end context 'overriding via composition' do - module Inherited - def inherited(api) - super - api.instance_variable_set(:@foo, @bar.dup) + let(:inherited) do + Module.new do + def inherited(api) + super + api.instance_variable_set(:@foo, @bar.dup) + end end end let(:root_api) do + context = self + Class.new(described_class) do @bar = 'Hello, world' - extend Inherited + extend context.inherited end end diff --git a/spec/grape/entity_spec.rb b/spec/grape/entity_spec.rb index 7eaa0fae8..d5eaf367f 100644 --- a/spec/grape/entity_spec.rb +++ b/spec/grape/entity_spec.rb @@ -46,24 +46,20 @@ def app entity = Class.new(described_class) allow(entity).to receive(:represent).and_return('Hiya') - module EntitySpec - class TestObject - end - - class FakeCollection - def first - TestObject.new - end + test_object_class = Class.new + fake_collection_class = Class.new do + define_method(:first) do + test_object_class.new end end - subject.represent EntitySpec::TestObject, with: entity + subject.represent test_object_class, with: entity subject.get '/example' do - present [EntitySpec::TestObject.new] + present [test_object_class.new] end subject.get '/example2' do - present EntitySpec::FakeCollection.new + present fake_collection_class.new end get '/example' diff --git a/spec/grape/loading_spec.rb b/spec/grape/loading_spec.rb index 767a0b793..f091fef85 100644 --- a/spec/grape/loading_spec.rb +++ b/spec/grape/loading_spec.rb @@ -2,10 +2,11 @@ describe Grape::API do subject do - CombinedApi = combined_api + context = self + Class.new(Grape::API) do format :json - mount CombinedApi => '/' + mount context.combined_api => '/' end end @@ -16,6 +17,7 @@ namespace :three do get :one do end + get :two do end end @@ -25,10 +27,11 @@ end let(:combined_api) do - JobsApi = jobs_api + context = self + Class.new(Grape::API) do version :v1, using: :accept_version_header, cascade: true - mount JobsApi + mount context.jobs_api end end diff --git a/spec/grape/middleware/base_spec.rb b/spec/grape/middleware/base_spec.rb index 3be95be1f..bb3b2d6b8 100644 --- a/spec/grape/middleware/base_spec.rb +++ b/spec/grape/middleware/base_spec.rb @@ -139,8 +139,8 @@ end context 'defaults' do - module BaseSpec - class ExampleWare < Grape::Middleware::Base + let(:example_ware) do + Class.new(Grape::Middleware::Base) do def default_options { monkey: true } end @@ -148,18 +148,18 @@ def default_options end it 'persists the default options' do - expect(BaseSpec::ExampleWare.new(blank_app).options[:monkey]).to be true + expect(example_ware.new(blank_app).options[:monkey]).to be true end it 'overrides default options when provided' do - expect(BaseSpec::ExampleWare.new(blank_app, monkey: false).options[:monkey]).to be false + expect(example_ware.new(blank_app, monkey: false).options[:monkey]).to be false end end end context 'header' do - module HeaderSpec - class ExampleWare < Grape::Middleware::Base + let(:example_ware) do + Class.new(Grape::Middleware::Base) do def before header 'X-Test-Before', 'Hi' end @@ -172,8 +172,10 @@ def after end def app + context = self + Rack::Builder.app do - use HeaderSpec::ExampleWare + use context.example_ware run ->(_) { [200, {}, ['Yeah']] } end end @@ -186,8 +188,8 @@ def app end context 'header overwrite' do - module HeaderOverwritingSpec - class ExampleWare < Grape::Middleware::Base + let(:example_ware) do + Class.new(Grape::Middleware::Base) do def before header 'X-Test-Overwriting', 'Hi' end @@ -197,8 +199,9 @@ def after nil end end - - class API < Grape::API + end + let(:api) do + Class.new(Grape::API) do get('/') do header 'X-Test-Overwriting', 'Yeah' 'Hello' @@ -207,9 +210,11 @@ class API < Grape::API end def app + context = self + Rack::Builder.app do - use HeaderOverwritingSpec::ExampleWare - run HeaderOverwritingSpec::API.new + use context.example_ware + run context.api.new end end diff --git a/spec/grape/middleware/error_spec.rb b/spec/grape/middleware/error_spec.rb index d056ca7e2..08ae7d460 100644 --- a/spec/grape/middleware/error_spec.rb +++ b/spec/grape/middleware/error_spec.rb @@ -3,8 +3,8 @@ require 'grape-entity' describe Grape::Middleware::Error do - module ErrorSpec - class ErrorEntity < Grape::Entity + let(:error_entity) do + Class.new(Grape::Entity) do expose :code expose :static @@ -12,8 +12,9 @@ def static 'static text' end end - - class ErrApp + end + let(:err_app) do + Class.new do class << self attr_accessor :error, :format @@ -23,44 +24,44 @@ def call(_env) end end end + let(:options) { { default_message: 'Aww, hamburgers.' } } def app opts = options + context = self Rack::Builder.app do use Spec::Support::EndpointFaker use Grape::Middleware::Error, **opts - run ErrorSpec::ErrApp + run context.err_app end end - let(:options) { { default_message: 'Aww, hamburgers.' } } - it 'sets the status code appropriately' do - ErrorSpec::ErrApp.error = { status: 410 } + err_app.error = { status: 410 } get '/' expect(last_response.status).to eq(410) end it 'sets the status code based on the rack util status code symbol' do - ErrorSpec::ErrApp.error = { status: :gone } + err_app.error = { status: :gone } get '/' expect(last_response.status).to eq(410) end it 'sets the error message appropriately' do - ErrorSpec::ErrApp.error = { message: 'Awesome stuff.' } + err_app.error = { message: 'Awesome stuff.' } get '/' expect(last_response.body).to eq('Awesome stuff.') end it 'defaults to a 500 status' do - ErrorSpec::ErrApp.error = {} + err_app.error = {} get '/' expect(last_response.status).to eq(500) end it 'has a default message' do - ErrorSpec::ErrApp.error = {} + err_app.error = {} get '/' expect(last_response.body).to eq('Aww, hamburgers.') end @@ -69,14 +70,15 @@ def app let(:options) { { default_message: 'Aww, hamburgers.' } } it 'adds the status code if wanted' do - ErrorSpec::ErrApp.error = { message: { code: 200 } } + err_app.error = { message: { code: 200 } } get '/' expect(last_response.body).to eq({ code: 200 }.to_json) end it 'presents an error message' do - ErrorSpec::ErrApp.error = { message: { code: 200, with: ErrorSpec::ErrorEntity } } + entity = error_entity + err_app.error = { message: { code: 200, with: entity } } get '/' expect(last_response.body).to eq({ code: 200, static: 'static text' }.to_json) diff --git a/spec/grape/middleware/exception_spec.rb b/spec/grape/middleware/exception_spec.rb index 18d8bff8b..bc28fa562 100644 --- a/spec/grape/middleware/exception_spec.rb +++ b/spec/grape/middleware/exception_spec.rb @@ -22,13 +22,11 @@ def call(_env) end let(:custom_error_app) do - Class.new do - class << self - class CustomError < Grape::Exceptions::Base; end + custom_error = Class.new(Grape::Exceptions::Base) - def call(_env) - raise CustomError.new(status: 400, message: 'failed validation') - end + Class.new do + define_singleton_method(:call) do |_env| + raise custom_error.new(status: 400, message: 'failed validation') end end end diff --git a/spec/grape/middleware/formatter_spec.rb b/spec/grape/middleware/formatter_spec.rb index 025ab42a5..20600f9d4 100644 --- a/spec/grape/middleware/formatter_spec.rb +++ b/spec/grape/middleware/formatter_spec.rb @@ -257,13 +257,13 @@ def to_xml context 'no content responses' do let(:no_content_response) { ->(status) { [status, {}, ['']] } } - STATUSES_WITHOUT_BODY = if Gem::Version.new(Rack.release) >= Gem::Version.new('2.1.0') + 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| + 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) diff --git a/spec/grape/middleware/stack_spec.rb b/spec/grape/middleware/stack_spec.rb index 3325469fd..d5d503a17 100644 --- a/spec/grape/middleware/stack_spec.rb +++ b/spec/grape/middleware/stack_spec.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true describe Grape::Middleware::Stack do - module StackSpec - class FooMiddleware; end - - class BarMiddleware; end + subject { described_class.new } - class BlockMiddleware + let(:foo_middleware) { Class.new } + let(:bar_middleware) { Class.new } + let(:block_middleware) do + Class.new do attr_reader :block def initialize(&block) @@ -14,34 +14,31 @@ def initialize(&block) end end end - - subject { described_class.new } - let(:proc) { -> {} } - let(:others) { [[:use, StackSpec::BarMiddleware], [:insert_before, StackSpec::BarMiddleware, StackSpec::BlockMiddleware, proc]] } + let(:others) { [[:use, bar_middleware], [:insert_before, bar_middleware, block_middleware, proc]] } before do - subject.use StackSpec::FooMiddleware + subject.use foo_middleware end describe '#use' do it 'pushes a middleware class onto the stack' do - expect { subject.use StackSpec::BarMiddleware } + expect { subject.use bar_middleware } .to change(subject, :size).by(1) - expect(subject.last).to eq(StackSpec::BarMiddleware) + expect(subject.last).to eq(bar_middleware) end it 'pushes a middleware class with arguments onto the stack' do - expect { subject.use StackSpec::BarMiddleware, false, my_arg: 42 } + expect { subject.use bar_middleware, false, my_arg: 42 } .to change(subject, :size).by(1) - expect(subject.last).to eq(StackSpec::BarMiddleware) + expect(subject.last).to eq(bar_middleware) expect(subject.last.args).to eq([false, { my_arg: 42 }]) end it 'pushes a middleware class with block arguments onto the stack' do - expect { subject.use StackSpec::BlockMiddleware, &proc } + expect { subject.use block_middleware, &proc } .to change(subject, :size).by(1) - expect(subject.last).to eq(StackSpec::BlockMiddleware) + expect(subject.last).to eq(block_middleware) expect(subject.last.args).to eq([]) expect(subject.last.block).to eq(proc) end @@ -49,57 +46,59 @@ def initialize(&block) describe '#insert' do it 'inserts a middleware class at the integer index' do - expect { subject.insert 0, StackSpec::BarMiddleware } + expect { subject.insert 0, bar_middleware } .to change(subject, :size).by(1) - expect(subject[0]).to eq(StackSpec::BarMiddleware) - expect(subject[1]).to eq(StackSpec::FooMiddleware) + expect(subject[0]).to eq(bar_middleware) + expect(subject[1]).to eq(foo_middleware) end end describe '#insert_before' do it 'inserts a middleware before another middleware class' do - expect { subject.insert_before StackSpec::FooMiddleware, StackSpec::BarMiddleware } + expect { subject.insert_before foo_middleware, bar_middleware } .to change(subject, :size).by(1) - expect(subject[0]).to eq(StackSpec::BarMiddleware) - expect(subject[1]).to eq(StackSpec::FooMiddleware) + expect(subject[0]).to eq(bar_middleware) + expect(subject[1]).to eq(foo_middleware) end it 'inserts a middleware before an anonymous class given by its superclass' do - subject.use Class.new(StackSpec::BlockMiddleware) + subject.use Class.new(block_middleware) - expect { subject.insert_before StackSpec::BlockMiddleware, StackSpec::BarMiddleware } + expect { subject.insert_before block_middleware, bar_middleware } .to change(subject, :size).by(1) - expect(subject[1]).to eq(StackSpec::BarMiddleware) - expect(subject[2]).to eq(StackSpec::BlockMiddleware) + expect(subject[1]).to eq(bar_middleware) + expect(subject[2]).to eq(block_middleware) end it 'raises an error on an invalid index' do - expect { subject.insert_before StackSpec::BlockMiddleware, StackSpec::BarMiddleware } + stub_const('StackSpec::BlockMiddleware', block_middleware) + expect { subject.insert_before block_middleware, bar_middleware } .to raise_error(RuntimeError, 'No such middleware to insert before: StackSpec::BlockMiddleware') end end describe '#insert_after' do it 'inserts a middleware after another middleware class' do - expect { subject.insert_after StackSpec::FooMiddleware, StackSpec::BarMiddleware } + expect { subject.insert_after foo_middleware, bar_middleware } .to change(subject, :size).by(1) - expect(subject[1]).to eq(StackSpec::BarMiddleware) - expect(subject[0]).to eq(StackSpec::FooMiddleware) + expect(subject[1]).to eq(bar_middleware) + expect(subject[0]).to eq(foo_middleware) end it 'inserts a middleware after an anonymous class given by its superclass' do - subject.use Class.new(StackSpec::BlockMiddleware) + subject.use Class.new(block_middleware) - expect { subject.insert_after StackSpec::BlockMiddleware, StackSpec::BarMiddleware } + expect { subject.insert_after block_middleware, bar_middleware } .to change(subject, :size).by(1) - expect(subject[1]).to eq(StackSpec::BlockMiddleware) - expect(subject[2]).to eq(StackSpec::BarMiddleware) + expect(subject[1]).to eq(block_middleware) + expect(subject[2]).to eq(bar_middleware) end it 'raises an error on an invalid index' do - expect { subject.insert_after StackSpec::BlockMiddleware, StackSpec::BarMiddleware } + stub_const('StackSpec::BlockMiddleware', block_middleware) + expect { subject.insert_after block_middleware, bar_middleware } .to raise_error(RuntimeError, 'No such middleware to insert after: StackSpec::BlockMiddleware') end end @@ -108,16 +107,16 @@ def initialize(&block) it 'applies a collection of operations and middlewares' do expect { subject.merge_with(others) } .to change(subject, :size).by(2) - expect(subject[0]).to eq(StackSpec::FooMiddleware) - expect(subject[1]).to eq(StackSpec::BlockMiddleware) - expect(subject[2]).to eq(StackSpec::BarMiddleware) + expect(subject[0]).to eq(foo_middleware) + expect(subject[1]).to eq(block_middleware) + expect(subject[2]).to eq(bar_middleware) end context 'middleware spec with proc declaration exists' do - let(:middleware_spec_with_proc) { [:use, StackSpec::FooMiddleware, proc] } + let(:middleware_spec_with_proc) { [:use, foo_middleware, proc] } it 'properly forwards spec arguments' do - expect(subject).to receive(:use).with(StackSpec::FooMiddleware) + expect(subject).to receive(:use).with(foo_middleware) subject.merge_with([middleware_spec_with_proc]) end end @@ -129,15 +128,15 @@ def initialize(&block) end context 'when @others are present' do - let(:others) { [[:insert_after, Grape::Middleware::Formatter, StackSpec::BarMiddleware]] } + let(:others) { [[:insert_after, Grape::Middleware::Formatter, bar_middleware]] } it 'applies the middleware specs stored in @others' do subject.concat others subject.use Grape::Middleware::Formatter subject.build - expect(subject[0]).to eq StackSpec::FooMiddleware + expect(subject[0]).to eq foo_middleware expect(subject[1]).to eq Grape::Middleware::Formatter - expect(subject[2]).to eq StackSpec::BarMiddleware + expect(subject[2]).to eq bar_middleware end end end @@ -148,7 +147,7 @@ def initialize(&block) end it 'calls +merge_with+ with the :use specs' do - expect(subject).to receive(:merge_with).with [[:use, StackSpec::BarMiddleware]] + expect(subject).to receive(:merge_with).with [[:use, bar_middleware]] subject.concat others end end diff --git a/spec/grape/validations/params_scope_spec.rb b/spec/grape/validations/params_scope_spec.rb index 71caa267d..9ef7ad7db 100644 --- a/spec/grape/validations/params_scope_spec.rb +++ b/spec/grape/validations/params_scope_spec.rb @@ -10,8 +10,8 @@ def app end context 'when using custom types' do - module ParamsScopeSpec - class CustomType + let(:custom_type) do + Class.new do attr_reader :value def self.parse(value) @@ -27,8 +27,9 @@ def initialize(value) end it 'coerces the parameter via the type\'s parse method' do + context = self subject.params do - requires :foo, type: ParamsScopeSpec::CustomType + requires :foo, type: context.custom_type end subject.get('/types') { params[:foo].value } diff --git a/spec/grape/validations/types_spec.rb b/spec/grape/validations/types_spec.rb index 402c3b6db..003088a43 100644 --- a/spec/grape/validations/types_spec.rb +++ b/spec/grape/validations/types_spec.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true describe Grape::Validations::Types do - module TypesSpec - class FooType + let(:foo_type) do + Class.new do def self.parse(_); end end - - class BarType + end + let(:bar_type) do + Class.new do def self.parse; end end end @@ -24,7 +25,7 @@ def self.parse; end it 'identifies unknown types' do expect(described_class).not_to be_primitive(Object) - expect(described_class).not_to be_primitive(TypesSpec::FooType) + expect(described_class).not_to be_primitive(foo_type) end end @@ -82,11 +83,11 @@ def self.parse; end end it 'returns true if the type responds to :parse with one argument' do - expect(described_class).to be_custom(TypesSpec::FooType) + expect(described_class).to be_custom(foo_type) end it 'returns false if the type\'s #parse method takes other than one argument' do - expect(described_class).not_to be_custom(TypesSpec::BarType) + expect(described_class).not_to be_custom(bar_type) end end diff --git a/spec/grape/validations/validators/coerce_spec.rb b/spec/grape/validations/validators/coerce_spec.rb index cd1d9dcbd..0afe720e5 100644 --- a/spec/grape/validations/validators/coerce_spec.rb +++ b/spec/grape/validations/validators/coerce_spec.rb @@ -10,13 +10,15 @@ def app end describe 'coerce' do - class SecureURIOnly - def self.parse(value) - URI.parse(value) - end + let(:secure_uri_only) do + Class.new do + def self.parse(value) + URI.parse(value) + end - def self.parsed?(value) - value.is_a? URI::HTTPS + def self.parsed?(value) + value.is_a? URI::HTTPS + end end end @@ -228,8 +230,9 @@ def self.parsed?(value) context 'a custom type' do it 'coerces the given value' do + context = self subject.params do - requires :uri, coerce: SecureURIOnly + requires :uri, coerce: context.secure_uri_only end subject.get '/secure_uri' do params[:uri].class @@ -325,8 +328,9 @@ def self.parse(_val) end it 'Array of a custom type' do + context = self subject.params do - requires :uri, type: Array[SecureURIOnly] + requires :uri, type: Array[context.secure_uri_only] end subject.get '/secure_uris' do params[:uri].first.class diff --git a/spec/grape/validations/validators/presence_spec.rb b/spec/grape/validations/validators/presence_spec.rb index bbe9865bd..349fcf8d0 100644 --- a/spec/grape/validations/validators/presence_spec.rb +++ b/spec/grape/validations/validators/presence_spec.rb @@ -287,7 +287,7 @@ def app context 'with a custom type' do it 'does not validate their type when it is missing' do - class CustomType + custom_type = Class.new do def self.parse(value) return if value.blank? @@ -296,7 +296,7 @@ def self.parse(value) end subject.params do - requires :custom, type: CustomType + requires :custom, type: custom_type end subject.get '/custom' do 'custom' diff --git a/spec/grape/validations/validators/values_spec.rb b/spec/grape/validations/validators/values_spec.rb index c66bf42e5..f78f63348 100644 --- a/spec/grape/validations/validators/values_spec.rb +++ b/spec/grape/validations/validators/values_spec.rb @@ -1,15 +1,12 @@ # frozen_string_literal: true describe Grape::Validations::Validators::ValuesValidator do - let_it_be(:values_model) do + let(:values_model) do Class.new do - DEFAULT_VALUES = %w[valid-type1 valid-type2 valid-type3].freeze - DEFAULT_EXCEPTS = %w[invalid-type1 invalid-type2 invalid-type3].freeze - class << self def values @values ||= [] - [DEFAULT_VALUES + @values].flatten.uniq + [default_values + @values].flatten.uniq end def add_value(value) @@ -19,7 +16,7 @@ def add_value(value) def excepts @excepts ||= [] - [DEFAULT_EXCEPTS + @excepts].flatten.uniq + [default_excepts + @excepts].flatten.uniq end def add_except(except) @@ -34,16 +31,21 @@ def include?(value) def even?(value) value.to_i.even? end + + private + + def default_values + %w[valid-type1 valid-type2 valid-type3].freeze + end + + def default_excepts + %w[invalid-type1 invalid-type2 invalid-type3].freeze + end end end end - before do - stub_const('ValuesModel', values_model) - end - - let_it_be(:app) do - ValuesModel = values_model + let(:app) do Class.new(Grape::API) do default_format :json @@ -271,6 +273,10 @@ def even?(value) end end + before do + stub_const('ValuesModel', values_model) + end + context 'with a custom validation message' do it 'allows a valid value for a parameter' do get('/custom_message', type: 'valid-type1') @@ -384,6 +390,7 @@ def even?(value) end it 'does not validate updated values without proc' do + app # Instantiate with the existing values. ValuesModel.add_value('valid-type4') get('/', type: 'valid-type4') expect(last_response.status).to eq 400 From 8cdcf0614a4fbb9cb4a4e338b51a41e32a53148e Mon Sep 17 00:00:00 2001 From: Andrii Gladkyi Date: Thu, 23 Nov 2023 11:13:23 +0100 Subject: [PATCH 213/304] Do not overwrite route_param with a regular one if they share same name --- CHANGELOG.md | 1 + README.md | 30 +++++++++++ UPGRADING.md | 53 +++++++++++++++++++ lib/grape/extensions/hash.rb | 6 ++- .../extensions/param_builders/hash_spec.rb | 29 ++++++++++ .../hash_with_indifferent_access_spec.rb | 29 ++++++++++ .../param_builders/hashie/mash_spec.rb | 30 +++++++++++ 7 files changed, 177 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f00754578..381d14d2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ * [#2387](https://github.com/ruby-grape/grape/pull/2387): Fix rubygems version within workflows - [@ericproulx](https://github.com/ericproulx). * [#2405](https://github.com/ruby-grape/grape/pull/2405): Fix edge workflow - [@ericproulx](https://github.com/ericproulx). * [#2414](https://github.com/ruby-grape/grape/pull/2414): Fix Rack::Lint missing content-type - [@ericproulx](https://github.com/ericproulx). +* [#2378](https://github.com/ruby-grape/grape/pull/2378): Do not overwrite `route_param` with a regular one if they share same name - [@arg](https://github.com/arg). * Your contribution here. ### 2.0.0 (2023/11/11) diff --git a/README.md b/README.md index f6c8ddf22..c15c5f134 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ - [Include Parent Namespaces](#include-parent-namespaces) - [Include Missing](#include-missing) - [Evaluate Given](#evaluate-given) + - [Parameter Precedence](#parameter-precedence) - [Parameter Validation and Coercion](#parameter-validation-and-coercion) - [Supported Parameter Types](#supported-parameter-types) - [Integer/Fixnum and Coercions](#integerfixnum-and-coercions) @@ -1198,6 +1199,35 @@ curl -X POST -H "Content-Type: application/json" localhost:9292/child -d '{"chil } ```` +### Parameter Precedence + +Using `route_param` takes higher precedence over a regular parameter defined with same name: + +```ruby +params do + requires :foo, type: String +end +route_param :foo do + get do + { value: params[:foo] } + end +end +``` + +**Request** + +```bash +curl -X POST -H "Content-Type: application/json" localhost:9292/bar -d '{"foo": "baz"}' +``` + +**Response** + +```json +{ + "value": "bar" +} +``` + ## Parameter Validation and Coercion You can define validations and coercion options for your parameters using a `params` block. diff --git a/UPGRADING.md b/UPGRADING.md index 900d0a340..d7ef9248b 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -9,6 +9,59 @@ The `rack_response` method has been deprecated and the `error_response` method h See [#2414](https://github.com/ruby-grape/grape/pull/2414) for more information. +#### Change in parameters precedence + +When using together with `Grape::Extensions::Hash::ParamBuilder`, `route_param` takes higher precedence over a regular parameter defined with same name, which now matches the default param builder behavior. + +This was a regression introduced by [#2326](https://github.com/ruby-grape/grape/pull/2326) in Grape v1.8.0. + +```ruby +grape.configure do |config| + config.param_builder = Grape::Extensions::Hash::ParamBuilder +end + +params do + requires :foo, type: String +end +route_param :foo do + get do + { value: params[:foo] } + end +end +``` + +Request: + +```bash +curl -X POST -H "Content-Type: application/json" localhost:9292/bar -d '{"foo": "baz"}' +``` + +Response prior to v1.8.0: + +```json +{ + "value": "bar" +} +``` + +v1.8.0..v2.0.0: + +```json +{ + "value": "baz" +} +``` + +v2.1.0+: + +```json +{ + "value": "bar" +} +``` + +See [#2378](https://github.com/ruby-grape/grape/pull/2378) for details. + #### Grape::Router::Route.route_xxx methods have been removed - `route_method` is accessible through `request_method` diff --git a/lib/grape/extensions/hash.rb b/lib/grape/extensions/hash.rb index 3ad07b210..cb8bd4b06 100644 --- a/lib/grape/extensions/hash.rb +++ b/lib/grape/extensions/hash.rb @@ -12,8 +12,12 @@ module ParamBuilder def build_params rack_params.deep_dup.tap do |params| - params.deep_merge!(grape_routing_args) if env.key?(Grape::Env::GRAPE_ROUTING_ARGS) params.deep_symbolize_keys! + + if env.key?(Grape::Env::GRAPE_ROUTING_ARGS) + grape_routing_args.deep_symbolize_keys! + params.deep_merge!(grape_routing_args) + end end end end diff --git a/spec/grape/extensions/param_builders/hash_spec.rb b/spec/grape/extensions/param_builders/hash_spec.rb index e35096bf7..872330bb4 100644 --- a/spec/grape/extensions/param_builders/hash_spec.rb +++ b/spec/grape/extensions/param_builders/hash_spec.rb @@ -79,5 +79,34 @@ def app expect(last_response.status).to eq(200) expect(last_response.body).to eq('["bar", nil]') end + + it 'does not overwrite route_param with a regular param if they have same name' do + subject.namespace :route_param do + route_param :foo do + get { params.to_json } + end + end + + get '/route_param/bar', foo: 'baz' + expect(last_response.status).to eq(200) + expect(last_response.body).to eq('{"foo":"bar"}') + end + + it 'does not overwrite route_param with a defined regular param if they have same name' do + subject.namespace :route_param do + params do + requires :foo, type: String + end + route_param :foo do + get do + params[:foo] + end + end + end + + get '/route_param/bar', foo: 'baz' + expect(last_response.status).to eq(200) + expect(last_response.body).to eq('bar') + end end end 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 992a07789..0f741193b 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 @@ -101,5 +101,34 @@ def app expect(last_response.body).to eq('["bar", "bar"]') end end + + it 'does not overwrite route_param with a regular param if they have same name' do + subject.namespace :route_param do + route_param :foo do + get { params.to_json } + end + end + + get '/route_param/bar', foo: 'baz' + expect(last_response.status).to eq(200) + expect(last_response.body).to eq('{"foo":"bar"}') + end + + it 'does not overwrite route_param with a defined regular param if they have same name' do + subject.namespace :route_param do + params do + requires :foo, type: String + end + route_param :foo do + get do + [params[:foo], params['foo']] + end + end + end + + get '/route_param/bar', foo: 'baz' + expect(last_response.status).to eq(200) + expect(last_response.body).to eq('["bar", "bar"]') + end end end diff --git a/spec/grape/extensions/param_builders/hashie/mash_spec.rb b/spec/grape/extensions/param_builders/hashie/mash_spec.rb index 54a187982..7b58c1d3f 100644 --- a/spec/grape/extensions/param_builders/hashie/mash_spec.rb +++ b/spec/grape/extensions/param_builders/hashie/mash_spec.rb @@ -75,5 +75,35 @@ def app expect(last_response.status).to eq(200) expect(last_response.body).to eq('["bar", "bar"]') end + + it 'does not overwrite route_param with a regular param if they have same name' do + subject.namespace :route_param do + route_param :foo do + get { params.to_json } + end + end + + get '/route_param/bar', foo: 'baz' + expect(last_response.status).to eq(200) + expect(last_response.body).to eq('{"foo":"bar"}') + end + + it 'does not overwrite route_param with a defined regular param if they have same name' do + subject.namespace :route_param do + params do + build_with Grape::Extensions::Hashie::Mash::ParamBuilder # rubocop:disable RSpec/DescribedClass + requires :foo, type: String + end + route_param :foo do + get do + [params[:foo], params['foo']] + end + end + end + + get '/route_param/bar', foo: 'baz' + expect(last_response.status).to eq(200) + expect(last_response.body).to eq('["bar", "bar"]') + end end end From 4f0c8339f1ea029d2b2fccd25ae530506ea20ec8 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Mon, 8 Apr 2024 09:50:06 +0200 Subject: [PATCH 214/304] Drop support for rack 1.X series (#2426) --- .github/workflows/test.yml | 2 -- CHANGELOG.md | 1 + gemfiles/rack_1_0.gemfile | 43 -------------------------------------- grape.gemspec | 2 +- 4 files changed, 2 insertions(+), 46 deletions(-) delete mode 100644 gemfiles/rack_1_0.gemfile diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 24fee4887..251e7ac7b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,8 +27,6 @@ jobs: gemfile: [rack_2_0, rack_3_0, rails_6_0, rails_6_1, rails_7_0, rails_7_1] integration: [false] include: - - ruby: '2.7' - gemfile: rack_1_0 - ruby: '2.7' integration: multi_json - ruby: '2.7' diff --git a/CHANGELOG.md b/CHANGELOG.md index 01e159abf..83c8c93df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ * [#2408](https://github.com/ruby-grape/grape/pull/2408): Fix params method redefined warnings - [@ericproulx](https://github.com/ericproulx). * [#2410](https://github.com/ruby-grape/grape/pull/2410): Gem deprecations will raise a DeprecationWarning in specs - [@ericproulx](https://github.com/ericproulx). * [#2389](https://github.com/ruby-grape/grape/pull/2389): Remove rack-accept dependency - [@ericproulx](https://github.com/ericproulx). +* [#2426](https://github.com/ruby-grape/grape/pull/2426): Drop support for rack 1.x series - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/gemfiles/rack_1_0.gemfile b/gemfiles/rack_1_0.gemfile deleted file mode 100644 index c2ace2218..000000000 --- a/gemfiles/rack_1_0.gemfile +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -# This file was generated by Appraisal - -source 'https://rubygems.org' - -gem 'rack', '~> 1.0' - -group :development, :test do - gem 'bundler' - gem 'dry-validation' - gem 'hashie' - gem 'rake' - gem 'rubocop', '1.59.0', require: false - gem 'rubocop-performance', '1.20.1', require: false - gem 'rubocop-rspec', '2.25.0', 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 'grape-entity', '~> 0.6', require: false - gem 'rack-jsonp', require: 'rack/jsonp' - gem 'rack-test', '< 2.1' - gem 'rspec', '< 4' - gem 'ruby-grape-danger', '~> 0.2.0', require: false - gem 'simplecov', '~> 0.21.2' - gem 'simplecov-lcov', '~> 0.8.0' - gem 'test-prof', require: false -end - -platforms :jruby do - gem 'racc' -end - -gemspec path: '../' diff --git a/grape.gemspec b/grape.gemspec index a08547175..1ad463c28 100644 --- a/grape.gemspec +++ b/grape.gemspec @@ -24,7 +24,7 @@ Gem::Specification.new do |s| s.add_runtime_dependency 'builder' s.add_runtime_dependency 'dry-types', '>= 1.1' s.add_runtime_dependency 'mustermann-grape', '~> 1.1.0' - s.add_runtime_dependency 'rack', '>= 1.3.0' + s.add_runtime_dependency 'rack', '>= 2' s.files = Dir['lib/**/*', 'CHANGELOG.md', 'CONTRIBUTING.md', 'README.md', 'grape.png', 'UPGRADING.md', 'LICENSE', 'grape.gemspec'] s.require_paths = ['lib'] From 71330b356504e6c02736bfd6b52e5fd8bde3d4ab Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Wed, 10 Apr 2024 09:14:20 +0200 Subject: [PATCH 215/304] Use `rack-contrib` JSONP instead of rack-jsonp (#2427) --- CHANGELOG.md | 1 + Gemfile | 2 +- gemfiles/multi_json.gemfile | 2 +- gemfiles/multi_xml.gemfile | 2 +- gemfiles/no_dry_validation.gemfile | 2 +- gemfiles/rack_2_0.gemfile | 2 +- gemfiles/rack_3_0.gemfile | 2 +- gemfiles/rack_edge.gemfile | 2 +- gemfiles/rails_6_0.gemfile | 2 +- gemfiles/rails_6_1.gemfile | 2 +- gemfiles/rails_7_0.gemfile | 2 +- gemfiles/rails_7_1.gemfile | 2 +- gemfiles/rails_edge.gemfile | 2 +- spec/grape/entity_spec.rb | 1 + 14 files changed, 14 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83c8c93df..45b5a8fe0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ * [#2410](https://github.com/ruby-grape/grape/pull/2410): Gem deprecations will raise a DeprecationWarning in specs - [@ericproulx](https://github.com/ericproulx). * [#2389](https://github.com/ruby-grape/grape/pull/2389): Remove rack-accept dependency - [@ericproulx](https://github.com/ericproulx). * [#2426](https://github.com/ruby-grape/grape/pull/2426): Drop support for rack 1.x series - [@ericproulx](https://github.com/ericproulx). +* [#2427](https://github.com/ruby-grape/grape/pull/2427): Use `rack-contrib` jsonp instead of rack-jsonp - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/Gemfile b/Gemfile index 544c59d3d..2ac129f2f 100644 --- a/Gemfile +++ b/Gemfile @@ -27,7 +27,7 @@ end group :test do gem 'grape-entity', '~> 0.6', require: false - gem 'rack-jsonp', require: 'rack/jsonp' + gem 'rack-contrib', require: false gem 'rack-test', '< 2.1' gem 'rspec', '< 4' gem 'ruby-grape-danger', '~> 0.2.0', require: false diff --git a/gemfiles/multi_json.gemfile b/gemfiles/multi_json.gemfile index b2b48aa27..894d95e0a 100644 --- a/gemfiles/multi_json.gemfile +++ b/gemfiles/multi_json.gemfile @@ -27,7 +27,7 @@ end group :test do gem 'grape-entity', '~> 0.6', require: false - gem 'rack-jsonp', require: 'rack/jsonp' + gem 'rack-contrib', require: false gem 'rack-test', '< 2.1' gem 'rspec', '< 4' gem 'ruby-grape-danger', '~> 0.2.0', require: false diff --git a/gemfiles/multi_xml.gemfile b/gemfiles/multi_xml.gemfile index 26cd081fd..38de241d8 100644 --- a/gemfiles/multi_xml.gemfile +++ b/gemfiles/multi_xml.gemfile @@ -27,7 +27,7 @@ end group :test do gem 'grape-entity', '~> 0.6', require: false - gem 'rack-jsonp', require: 'rack/jsonp' + gem 'rack-contrib', require: false gem 'rack-test', '< 2.1' gem 'rspec', '< 4' gem 'ruby-grape-danger', '~> 0.2.0', require: false diff --git a/gemfiles/no_dry_validation.gemfile b/gemfiles/no_dry_validation.gemfile index 46f2c2c0b..08c74247a 100644 --- a/gemfiles/no_dry_validation.gemfile +++ b/gemfiles/no_dry_validation.gemfile @@ -24,7 +24,7 @@ end group :test do gem 'grape-entity', '~> 0.6', require: false - gem 'rack-jsonp', require: 'rack/jsonp' + gem 'rack-contrib', require: false gem 'rack-test', '< 2.1' gem 'rspec', '< 4' gem 'ruby-grape-danger', '~> 0.2.0', require: false diff --git a/gemfiles/rack_2_0.gemfile b/gemfiles/rack_2_0.gemfile index 04f3fe947..6f3f9af22 100644 --- a/gemfiles/rack_2_0.gemfile +++ b/gemfiles/rack_2_0.gemfile @@ -27,7 +27,7 @@ end group :test do gem 'grape-entity', '~> 0.6', require: false - gem 'rack-jsonp', require: 'rack/jsonp' + gem 'rack-contrib', require: false gem 'rack-test', '< 2.1' gem 'rspec', '< 4' gem 'ruby-grape-danger', '~> 0.2.0', require: false diff --git a/gemfiles/rack_3_0.gemfile b/gemfiles/rack_3_0.gemfile index 6b4712c22..83df47ca6 100644 --- a/gemfiles/rack_3_0.gemfile +++ b/gemfiles/rack_3_0.gemfile @@ -27,7 +27,7 @@ end group :test do gem 'grape-entity', '~> 0.6', require: false - gem 'rack-jsonp', require: 'rack/jsonp' + gem 'rack-contrib', require: false gem 'rack-test', '< 2.1' gem 'rspec', '< 4' gem 'ruby-grape-danger', '~> 0.2.0', require: false diff --git a/gemfiles/rack_edge.gemfile b/gemfiles/rack_edge.gemfile index a58b7238f..c64d8488d 100644 --- a/gemfiles/rack_edge.gemfile +++ b/gemfiles/rack_edge.gemfile @@ -27,7 +27,7 @@ end group :test do gem 'grape-entity', '~> 0.6', require: false - gem 'rack-jsonp', require: 'rack/jsonp' + gem 'rack-contrib', require: false gem 'rack-test', '< 2.1' gem 'rspec', '< 4' gem 'ruby-grape-danger', '~> 0.2.0', require: false diff --git a/gemfiles/rails_6_0.gemfile b/gemfiles/rails_6_0.gemfile index 0a9d4ffdc..15fd21e32 100644 --- a/gemfiles/rails_6_0.gemfile +++ b/gemfiles/rails_6_0.gemfile @@ -27,7 +27,7 @@ end group :test do gem 'grape-entity', '~> 0.6', require: false - gem 'rack-jsonp', require: 'rack/jsonp' + gem 'rack-contrib', require: false gem 'rack-test', '< 2.1' gem 'rspec', '< 4' gem 'ruby-grape-danger', '~> 0.2.0', require: false diff --git a/gemfiles/rails_6_1.gemfile b/gemfiles/rails_6_1.gemfile index c1121bf80..de78e0dab 100644 --- a/gemfiles/rails_6_1.gemfile +++ b/gemfiles/rails_6_1.gemfile @@ -27,7 +27,7 @@ end group :test do gem 'grape-entity', '~> 0.6', require: false - gem 'rack-jsonp', require: 'rack/jsonp' + gem 'rack-contrib', require: false gem 'rack-test', '< 2.1' gem 'rspec', '< 4' gem 'ruby-grape-danger', '~> 0.2.0', require: false diff --git a/gemfiles/rails_7_0.gemfile b/gemfiles/rails_7_0.gemfile index 17b375975..19d4d581f 100644 --- a/gemfiles/rails_7_0.gemfile +++ b/gemfiles/rails_7_0.gemfile @@ -27,7 +27,7 @@ end group :test do gem 'grape-entity', '~> 0.6', require: false - gem 'rack-jsonp', require: 'rack/jsonp' + gem 'rack-contrib', require: false gem 'rack-test', '< 2.1' gem 'rspec', '< 4' gem 'ruby-grape-danger', '~> 0.2.0', require: false diff --git a/gemfiles/rails_7_1.gemfile b/gemfiles/rails_7_1.gemfile index 5f3452444..637efd85f 100644 --- a/gemfiles/rails_7_1.gemfile +++ b/gemfiles/rails_7_1.gemfile @@ -27,7 +27,7 @@ end group :test do gem 'grape-entity', '~> 0.6', require: false - gem 'rack-jsonp', require: 'rack/jsonp' + gem 'rack-contrib', require: false gem 'rack-test', '< 2.1' gem 'rspec', '< 4' gem 'ruby-grape-danger', '~> 0.2.0', require: false diff --git a/gemfiles/rails_edge.gemfile b/gemfiles/rails_edge.gemfile index 4462cd734..e2dc809b8 100644 --- a/gemfiles/rails_edge.gemfile +++ b/gemfiles/rails_edge.gemfile @@ -27,7 +27,7 @@ end group :test do gem 'grape-entity', '~> 0.6', require: false - gem 'rack-jsonp', require: 'rack/jsonp' + gem 'rack-contrib', require: false gem 'rack-test', '< 2.1' gem 'rspec', '< 4' gem 'ruby-grape-danger', '~> 0.2.0', require: false diff --git a/spec/grape/entity_spec.rb b/spec/grape/entity_spec.rb index d5eaf367f..bb422414d 100644 --- a/spec/grape/entity_spec.rb +++ b/spec/grape/entity_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'grape_entity' +require 'rack/contrib/jsonp' describe Grape::Entity do subject { Class.new(Grape::API) } From 752b6ddfcc631b7b808ac80b8653927fc7b88f41 Mon Sep 17 00:00:00 2001 From: "Daniel (dB.) Doubrovkine" Date: Tue, 26 Mar 2024 15:45:06 -0400 Subject: [PATCH 216/304] DRY-up test workflow. --- .github/workflows/test.yml | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 251e7ac7b..273537e6f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,22 +24,27 @@ jobs: fail-fast: false matrix: ruby: ['2.7', '3.0', '3.1', '3.2', '3.3'] - gemfile: [rack_2_0, rack_3_0, rails_6_0, rails_6_1, rails_7_0, rails_7_1] - integration: [false] + gemfile: [Gemfile, gemfiles/rack_2_0.gemfile, gemfiles/rack_3_0.gemfile, gemfiles/rails_6_0.gemfile, gemfiles/rails_6_1.gemfile, gemfiles/rails_7_0.gemfile, gemfiles/rails_7_1.gemfile] + specs: ['spec --exclude-pattern=spec/integration/**/*_spec.rb'] include: - ruby: '2.7' - integration: multi_json + gemfile: gemfiles/multi_json.gemfile + specs: 'spec/integration/multi_json' - ruby: '2.7' - integration: multi_xml + gemfile: gemfiles/multi_xml.gemfile + specs: 'spec/integration/multi_xml' - ruby: '2.7' - integration: rack_2_0 + gemfile: gemfiles/rack_2_0.gemfile + specs: 'spec/integration/rack_2_0' - ruby: '2.7' - integration: rack_3_0 + gemfile: gemfiles/rack_3_0.gemfile + specs: 'spec/integration/rack_3_0' - ruby: '3.3' - integration: no_dry_validation + gemfile: gemfiles/no_dry_validation.gemfile + specs: 'spec/integration/no_dry_validation' runs-on: ubuntu-latest env: - BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.integration || matrix.gemfile }}.gemfile + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }} steps: - uses: actions/checkout@v4 @@ -49,13 +54,8 @@ jobs: ruby-version: ${{ matrix.ruby }} bundler-cache: true - - name: Run tests - if: ${{ !matrix.integration }} - run: bundle exec rake spec - - - name: Run integration tests (spec/integration/${{ matrix.integration }}) - if: ${{ matrix.integration }} - run: bundle exec rspec spec/integration/${{ matrix.integration }} + - name: Run Tests (${{ matrix.specs }}) + run: bundle exec rspec ${{ matrix.specs }} - name: Coveralls uses: coverallsapp/github-action@master From 2c79b2f95879d3e2c31f92cf69fdfb9717ecd44a Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Thu, 11 Apr 2024 14:43:36 +0200 Subject: [PATCH 217/304] Introducing zeitwerk (#2363) --- .rubocop_todo.yml | 18 +- CHANGELOG.md | 1 + UPGRADING.md | 7 + benchmark/compile_many_routes.rb | 3 - grape.gemspec | 1 + lib/grape.rb | 302 ++---------------- lib/grape/api.rb | 4 - lib/grape/api/instance.rb | 4 +- lib/grape/content_types.rb | 2 - lib/grape/dry_types.rb | 2 - lib/grape/dsl/inside_route.rb | 2 - lib/grape/eager_load.rb | 20 -- lib/grape/{util => }/env.rb | 0 lib/grape/exceptions/validation.rb | 2 - lib/grape/exceptions/validation_errors.rb | 2 - lib/grape/http/headers.rb | 12 +- lib/grape/{util => }/json.rb | 4 +- lib/grape/middleware/auth/base.rb | 2 - lib/grape/middleware/auth/dsl.rb | 2 - lib/grape/middleware/base.rb | 2 - lib/grape/middleware/error.rb | 2 - lib/grape/middleware/formatter.rb | 2 - lib/grape/middleware/globals.rb | 2 - .../versioner/accept_version_header.rb | 2 - lib/grape/middleware/versioner/header.rb | 16 +- lib/grape/middleware/versioner/param.rb | 2 - lib/grape/middleware/versioner/path.rb | 2 - lib/grape/namespace.rb | 2 - lib/grape/path.rb | 2 - lib/grape/request.rb | 6 +- lib/grape/router.rb | 4 - lib/grape/router/greedy_route.rb | 3 - lib/grape/router/pattern.rb | 4 - lib/grape/router/route.rb | 4 - lib/grape/util/accept/header.rb | 19 ++ lib/grape/util/accept_header_handler.rb | 2 - lib/grape/util/cache.rb | 3 - lib/grape/util/endpoint_configuration.rb | 2 +- lib/grape/util/inheritable_values.rb | 2 - lib/grape/util/lazy/block.rb | 29 ++ lib/grape/util/lazy/object.rb | 45 +++ lib/grape/util/lazy/value.rb | 38 +++ lib/grape/util/lazy/value_array.rb | 21 ++ lib/grape/util/lazy/value_enumerable.rb | 34 ++ lib/grape/util/lazy/value_hash.rb | 21 ++ lib/grape/util/lazy_block.rb | 27 -- lib/grape/util/lazy_object.rb | 43 --- lib/grape/util/lazy_value.rb | 91 ------ lib/grape/util/reverse_stackable_values.rb | 2 - lib/grape/util/stackable_values.rb | 2 - lib/grape/validations.rb | 10 +- lib/grape/validations/attributes_doc.rb | 73 +++-- lib/grape/validations/params_scope.rb | 2 - lib/grape/validations/types.rb | 3 - lib/grape/validations/types/array_coercer.rb | 2 - lib/grape/validations/types/build_coercer.rb | 140 ++++---- .../validations/types/dry_type_coercer.rb | 12 +- lib/grape/validations/types/json.rb | 2 - .../validations/types/primitive_coercer.rb | 2 - lib/grape/validations/types/set_coercer.rb | 3 - lib/grape/validations/validators/base.rb | 1 + lib/grape/{util => }/xml.rb | 2 +- spec/grape/api/custom_validations_spec.rb | 70 ++-- spec/grape/api_spec.rb | 5 - spec/grape/endpoint_spec.rb | 4 +- spec/grape/request_spec.rb | 4 +- spec/grape/util/accept_header_handler_spec.rb | 2 - spec/grape/validations/attributes_doc_spec.rb | 2 +- .../validations/instance_behaivour_spec.rb | 43 --- .../grape/validations/validators/base_spec.rb | 38 --- spec/grape/validations_spec.rb | 30 +- .../integration/eager_load/eager_load_spec.rb | 15 - spec/integration/multi_json/json_spec.rb | 2 +- 73 files changed, 432 insertions(+), 861 deletions(-) delete mode 100644 lib/grape/eager_load.rb rename lib/grape/{util => }/env.rb (100%) rename lib/grape/{util => }/json.rb (72%) create mode 100644 lib/grape/util/accept/header.rb create mode 100644 lib/grape/util/lazy/block.rb create mode 100644 lib/grape/util/lazy/object.rb create mode 100644 lib/grape/util/lazy/value.rb create mode 100644 lib/grape/util/lazy/value_array.rb create mode 100644 lib/grape/util/lazy/value_enumerable.rb create mode 100644 lib/grape/util/lazy/value_hash.rb delete mode 100644 lib/grape/util/lazy_block.rb delete mode 100644 lib/grape/util/lazy_object.rb delete mode 100644 lib/grape/util/lazy_value.rb rename lib/grape/{util => }/xml.rb (80%) delete mode 100644 spec/grape/validations/instance_behaivour_spec.rb delete mode 100644 spec/grape/validations/validators/base_spec.rb delete mode 100644 spec/integration/eager_load/eager_load_spec.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 3a11f910b..e221cfd92 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 5000` -# on 2024-03-26 03:54:44 UTC using RuboCop version 1.59.0. +# on 2024-04-01 12:18:08 UTC using RuboCop version 1.59.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 @@ -40,7 +40,7 @@ Lint/EmptyClass: Exclude: - 'lib/grape/dsl/parameters.rb' -# Offense count: 6 +# Offense count: 5 # Configuration parameters: AllowedParentClasses. Lint/MissingSuper: Exclude: @@ -49,7 +49,6 @@ Lint/MissingSuper: - 'lib/grape/namespace.rb' - 'lib/grape/path.rb' - 'lib/grape/router/pattern.rb' - - 'lib/grape/validations/validators/base.rb' # Offense count: 1 # Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns. @@ -92,7 +91,7 @@ RSpec/AnyInstance: - 'spec/grape/api_spec.rb' - 'spec/grape/middleware/base_spec.rb' -# Offense count: 2 +# Offense count: 1 # Configuration parameters: IgnoredMetadata. RSpec/DescribeClass: Exclude: @@ -102,7 +101,6 @@ RSpec/DescribeClass: - '**/spec/system/**/*' - '**/spec/views/**/*' - 'spec/grape/named_api_spec.rb' - - 'spec/grape/validations/instance_behaivour_spec.rb' # Offense count: 3 # This cop supports safe autocorrection (--autocorrect). @@ -156,7 +154,7 @@ RSpec/MessageChain: Exclude: - 'spec/grape/middleware/formatter_spec.rb' -# Offense count: 148 +# Offense count: 144 # Configuration parameters: . # SupportedStyles: have_received, receive RSpec/MessageSpies: @@ -206,17 +204,15 @@ RSpec/RepeatedExampleGroupDescription: - 'spec/grape/util/inheritable_setting_spec.rb' - 'spec/grape/validations/validators/values_spec.rb' -# Offense count: 6 +# Offense count: 2 # This cop supports safe autocorrection (--autocorrect). RSpec/ScatteredSetup: Exclude: - 'spec/grape/util/inheritable_setting_spec.rb' - - 'spec/grape/validations_spec.rb' -# Offense count: 9 +# Offense count: 8 RSpec/StubbedMock: Exclude: - - 'spec/grape/api_spec.rb' - 'spec/grape/dsl/inside_route_spec.rb' - 'spec/grape/dsl/routing_spec.rb' - 'spec/grape/middleware/formatter_spec.rb' @@ -295,7 +291,7 @@ Style/OptionalBooleanParameter: - 'lib/grape/validations/types/primitive_coercer.rb' - 'lib/grape/validations/types/set_coercer.rb' -# Offense count: 28 +# Offense count: 29 # This cop supports safe autocorrection (--autocorrect). Style/RedundantConstantBase: Exclude: diff --git a/CHANGELOG.md b/CHANGELOG.md index 45b5a8fe0..96c236ec3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ * [#2389](https://github.com/ruby-grape/grape/pull/2389): Remove rack-accept dependency - [@ericproulx](https://github.com/ericproulx). * [#2426](https://github.com/ruby-grape/grape/pull/2426): Drop support for rack 1.x series - [@ericproulx](https://github.com/ericproulx). * [#2427](https://github.com/ruby-grape/grape/pull/2427): Use `rack-contrib` jsonp instead of rack-jsonp - [@ericproulx](https://github.com/ericproulx). +* [#2363](https://github.com/ruby-grape/grape/pull/2363): Replace autoload by zeitwerk - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/UPGRADING.md b/UPGRADING.md index d7ef9248b..3bc1fcf31 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -3,6 +3,13 @@ Upgrading Grape ### Upgrading to >= 2.1.0 +#### Zeitwerk + +Grape's autoloader has been updated and it's now based on [Zeitwerk](https://github.com/fxn/zeitwerk). +If you MP (Monkey Patch) some files and you're not following the [file structure](https://github.com/fxn/zeitwerk?tab=readme-ov-file#file-structure), you might end up with a Zeitwerk error. + +See [#2363](https://github.com/ruby-grape/grape/pull/2363) for more information. + #### Changes in rescue_from The `rack_response` method has been deprecated and the `error_response` method has been removed. Use `error!` instead. diff --git a/benchmark/compile_many_routes.rb b/benchmark/compile_many_routes.rb index 9fa858cf3..1b273302f 100644 --- a/benchmark/compile_many_routes.rb +++ b/benchmark/compile_many_routes.rb @@ -3,9 +3,6 @@ $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 diff --git a/grape.gemspec b/grape.gemspec index 1ad463c28..92c2d7fd0 100644 --- a/grape.gemspec +++ b/grape.gemspec @@ -25,6 +25,7 @@ Gem::Specification.new do |s| s.add_runtime_dependency 'dry-types', '>= 1.1' s.add_runtime_dependency 'mustermann-grape', '~> 1.1.0' s.add_runtime_dependency 'rack', '>= 2' + s.add_runtime_dependency 'zeitwerk' s.files = Dir['lib/**/*', 'CHANGELOG.md', 'CONTRIBUTING.md', 'README.md', 'grape.png', 'UPGRADING.md', 'LICENSE', 'grape.gemspec'] s.require_paths = ['lib'] diff --git a/lib/grape.rb b/lib/grape.rb index 4bfecdcb0..2c99d377a 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -1,12 +1,5 @@ # frozen_string_literal: true -require 'logger' -require 'rack' -require 'rack/builder' -require 'rack/auth/basic' -require 'set' -require 'bigdecimal' -require 'date' require 'active_support' require 'active_support/concern' require 'active_support/configurable' @@ -26,293 +19,54 @@ require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/deep_dup' require 'active_support/core_ext/object/duplicable' +require 'active_support/core_ext/string/output_safety' require 'active_support/core_ext/string/exclude' -require 'active_support/dependencies/autoload' require 'active_support/deprecation' require 'active_support/inflector' require 'active_support/notifications' -require 'i18n' + +require 'English' +require 'bigdecimal' +require 'date' +require 'dry-types' +require 'forwardable' +require 'json' +require 'logger' +require 'mustermann/grape' +require 'pathname' +require 'rack' +require 'rack/auth/basic' +require 'rack/builder' +require 'rack/head' +require 'set' +require 'singleton' +require 'zeitwerk' + +loader = Zeitwerk::Loader.for_gem +loader.inflector.inflect( + 'api' => 'API', + 'dsl' => 'DSL' +) +railtie = "#{__dir__}/grape/railtie.rb" +loader.do_not_eager_load(railtie) +loader.setup I18n.load_path << File.expand_path('grape/locale/en.yml', __dir__) module Grape include ActiveSupport::Configurable - extend ::ActiveSupport::Autoload def self.deprecator @deprecator ||= ActiveSupport::Deprecation.new('2.0', 'Grape') end - def self.lowercase_headers? - Rack::CONTENT_TYPE == 'content-type' - end - - eager_autoload do - autoload :API - autoload :Endpoint - - autoload :Namespace - - autoload :Path - autoload :Cookies - autoload :Validations - autoload :ErrorFormatter - autoload :Formatter - autoload :Parser - autoload :Request - autoload :Env, 'grape/util/env' - autoload :Json, 'grape/util/json' - autoload :Xml, 'grape/util/xml' - autoload :DryTypes - end - - module Http - extend ::ActiveSupport::Autoload - eager_autoload do - autoload :Headers - end - end - - module Exceptions - extend ::ActiveSupport::Autoload - 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 :MissingGroupType - autoload :UnsupportedGroupType - autoload :InvalidMessageBody - autoload :InvalidAcceptHeader - autoload :InvalidVersionHeader - autoload :MethodNotAllowed - autoload :InvalidResponse - autoload :EmptyMessageBody - autoload :TooManyMultipartFiles - autoload :MissingGroupTypeError, 'grape/exceptions/missing_group_type' - autoload :UnsupportedGroupTypeError, 'grape/exceptions/unsupported_group_type' - end - end - - module Extensions - extend ::ActiveSupport::Autoload - eager_autoload do - autoload :Hash - end - module ActiveSupport - extend ::ActiveSupport::Autoload - eager_autoload do - autoload :HashWithIndifferentAccess - end - end - - module Hashie - extend ::ActiveSupport::Autoload - eager_autoload do - autoload :Mash - end - end - end - - module Middleware - extend ::ActiveSupport::Autoload - eager_autoload do - autoload :Base - autoload :Versioner - autoload :Formatter - autoload :Error - autoload :Globals - autoload :Stack - autoload :Helpers - end - - module Auth - extend ::ActiveSupport::Autoload - eager_autoload do - autoload :Base - autoload :DSL - autoload :StrategyInfo - autoload :Strategies - end - end - - module Versioner - extend ::ActiveSupport::Autoload - eager_autoload do - autoload :Path - autoload :Header - autoload :Param - autoload :AcceptVersionHeader - end - end - end - - module Util - extend ::ActiveSupport::Autoload - eager_autoload do - autoload :InheritableValues - autoload :StackableValues - autoload :ReverseStackableValues - autoload :InheritableSetting - autoload :StrictHashConfiguration - autoload :Registrable - end - end - - module ErrorFormatter - extend ::ActiveSupport::Autoload - eager_autoload do - autoload :Base - autoload :Json - autoload :Txt - autoload :Xml - end - end - - module Formatter - extend ::ActiveSupport::Autoload - eager_autoload do - autoload :Json - autoload :SerializableHash - autoload :Txt - autoload :Xml - end - end - - module Parser - extend ::ActiveSupport::Autoload - eager_autoload do - autoload :Json - autoload :Xml - end - end - - module DSL - extend ::ActiveSupport::Autoload - eager_autoload do - autoload :API - autoload :Callbacks - autoload :Settings - autoload :Configuration - autoload :InsideRoute - autoload :Helpers - autoload :Middleware - autoload :Parameters - autoload :RequestResponse - autoload :Routing - autoload :Validations - autoload :Logger - autoload :Desc - end - end - - class API - extend ::ActiveSupport::Autoload - eager_autoload do - autoload :Helpers - end - end - - module Presenters - extend ::ActiveSupport::Autoload - eager_autoload do - autoload :Presenter - end - end - - module ServeStream - extend ::ActiveSupport::Autoload - eager_autoload do - autoload :FileBody - autoload :SendfileResponse - autoload :StreamResponse - end - end - - module Validations - extend ::ActiveSupport::Autoload - - eager_autoload do - autoload :AttributesIterator - autoload :MultipleAttributesIterator - autoload :SingleAttributeIterator - autoload :Types - autoload :ParamsScope - autoload :ContractScope - autoload :ValidatorFactory - autoload :Base, 'grape/validations/validators/base' - end - - module Types - extend ::ActiveSupport::Autoload - - eager_autoload do - autoload :InvalidValue - autoload :DryTypeCoercer - autoload :ArrayCoercer - autoload :SetCoercer - autoload :PrimitiveCoercer - autoload :CustomTypeCoercer - autoload :CustomTypeCollectionCoercer - autoload :MultipleTypeCoercer - autoload :VariantCollectionCoercer - end - end - - module Validators - extend ::ActiveSupport::Autoload - - eager_autoload do - autoload :Base - autoload :MultipleParamsBase - autoload :AllOrNoneOfValidator - autoload :AllowBlankValidator - autoload :AsValidator - autoload :AtLeastOneOfValidator - autoload :CoerceValidator - autoload :DefaultValidator - autoload :ExactlyOneOfValidator - autoload :ExceptValuesValidator - autoload :MutualExclusionValidator - autoload :PresenceValidator - autoload :RegexpValidator - autoload :SameAsValidator - autoload :ValuesValidator - end - end - end - - module Types - extend ::ActiveSupport::Autoload - - eager_autoload do - autoload :InvalidValue - end - end - configure do |config| config.param_builder = Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder config.compile_methods! end end -require 'grape/content_types' - -require 'grape/util/lazy_value' -require 'grape/util/lazy_block' -require 'grape/util/endpoint_configuration' -require 'grape/version' - # https://api.rubyonrails.org/classes/ActiveSupport/Deprecation.html # adding Grape.deprecator to Rails App if any require 'grape/railtie' if defined?(Rails::Railtie) && ActiveSupport.gem_version >= Gem::Version.new('7.1') +loader.eager_load diff --git a/lib/grape/api.rb b/lib/grape/api.rb index 536d997e9..38f17dcc5 100644 --- a/lib/grape/api.rb +++ b/lib/grape/api.rb @@ -1,8 +1,5 @@ # 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. @@ -128,7 +125,6 @@ def method_missing(method, *args, &block) end def compile! - require 'grape/eager_load' instance_for_rack.compile! # See API::Instance.compile! end diff --git a/lib/grape/api/instance.rb b/lib/grape/api/instance.rb index a326f44c6..c0c6ba2fd 100644 --- a/lib/grape/api/instance.rb +++ b/lib/grape/api/instance.rb @@ -1,7 +1,5 @@ # 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 @@ -112,7 +110,7 @@ def nest(*blocks, &block) end def evaluate_as_instance_with_configuration(block, lazy: false) - lazy_block = Grape::Util::LazyBlock.new do |configuration| + lazy_block = Grape::Util::Lazy::Block.new do |configuration| value_for_configuration = configuration self.configuration = value_for_configuration.evaluate if value_for_configuration.respond_to?(:lazy?) && value_for_configuration.lazy? response = instance_eval(&block) diff --git a/lib/grape/content_types.rb b/lib/grape/content_types.rb index c6f295154..cc0cc2cab 100644 --- a/lib/grape/content_types.rb +++ b/lib/grape/content_types.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'grape/util/registrable' - module Grape module ContentTypes extend Util::Registrable diff --git a/lib/grape/dry_types.rb b/lib/grape/dry_types.rb index f0676c376..5f1bc3cde 100644 --- a/lib/grape/dry_types.rb +++ b/lib/grape/dry_types.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'dry-types' - module Grape module DryTypes # Call +Dry.Types()+ to add all registered types to +DryTypes+ which is diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index ef3bdec08..d93546061 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'grape/dsl/headers' - module Grape module DSL module InsideRoute diff --git a/lib/grape/eager_load.rb b/lib/grape/eager_load.rb deleted file mode 100644 index ef7bc3ec7..000000000 --- a/lib/grape/eager_load.rb +++ /dev/null @@ -1,20 +0,0 @@ -# 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/util/env.rb b/lib/grape/env.rb similarity index 100% rename from lib/grape/util/env.rb rename to lib/grape/env.rb diff --git a/lib/grape/exceptions/validation.rb b/lib/grape/exceptions/validation.rb index b66fc46c9..8d368d277 100644 --- a/lib/grape/exceptions/validation.rb +++ b/lib/grape/exceptions/validation.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'grape/exceptions/base' - module Grape module Exceptions class Validation < Grape::Exceptions::Base diff --git a/lib/grape/exceptions/validation_errors.rb b/lib/grape/exceptions/validation_errors.rb index 4b3d5b9e0..09e4a37b0 100644 --- a/lib/grape/exceptions/validation_errors.rb +++ b/lib/grape/exceptions/validation_errors.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'grape/exceptions/base' - module Grape module Exceptions class ValidationErrors < Grape::Exceptions::Base diff --git a/lib/grape/http/headers.rb b/lib/grape/http/headers.rb index a7e5984f7..ae0989a3b 100644 --- a/lib/grape/http/headers.rb +++ b/lib/grape/http/headers.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'grape/util/lazy_object' - module Grape module Http module Headers @@ -11,7 +9,11 @@ module Headers REQUEST_METHOD = 'REQUEST_METHOD' QUERY_STRING = 'QUERY_STRING' - if Grape.lowercase_headers? + def self.lowercase? + Rack::CONTENT_TYPE == 'content-type' + end + + if lowercase? ALLOW = 'allow' LOCATION = 'location' TRANSFER_ENCODING = 'transfer-encoding' @@ -32,7 +34,7 @@ module Headers 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 } + SUPPORTED_METHODS_WITHOUT_OPTIONS = Grape::Util::Lazy::Object.new { [GET, POST, PUT, PATCH, DELETE, HEAD].freeze } HTTP_ACCEPT_VERSION = 'HTTP_ACCEPT_VERSION' HTTP_TRANSFER_ENCODING = 'HTTP_TRANSFER_ENCODING' @@ -40,7 +42,7 @@ module Headers FORMAT = 'format' - HTTP_HEADERS = Grape::Util::LazyObject.new do + HTTP_HEADERS = Grape::Util::Lazy::Object.new do common_http_headers = %w[ Version Host diff --git a/lib/grape/util/json.rb b/lib/grape/json.rb similarity index 72% rename from lib/grape/util/json.rb rename to lib/grape/json.rb index 26695e92a..a30014538 100644 --- a/lib/grape/util/json.rb +++ b/lib/grape/json.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true -require 'json' - module Grape - if Object.const_defined? :MultiJson + if defined?(::MultiJson) Json = ::MultiJson else Json = ::JSON diff --git a/lib/grape/middleware/auth/base.rb b/lib/grape/middleware/auth/base.rb index 67a1a53bb..d7703cd78 100644 --- a/lib/grape/middleware/auth/base.rb +++ b/lib/grape/middleware/auth/base.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'rack/auth/basic' - module Grape module Middleware module Auth diff --git a/lib/grape/middleware/auth/dsl.rb b/lib/grape/middleware/auth/dsl.rb index eb27588b7..598358d9d 100644 --- a/lib/grape/middleware/auth/dsl.rb +++ b/lib/grape/middleware/auth/dsl.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'rack/auth/basic' - module Grape module Middleware module Auth diff --git a/lib/grape/middleware/base.rb b/lib/grape/middleware/base.rb index 5eb998fc0..87f2429ff 100644 --- a/lib/grape/middleware/base.rb +++ b/lib/grape/middleware/base.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'grape/dsl/headers' - module Grape module Middleware class Base diff --git a/lib/grape/middleware/error.rb b/lib/grape/middleware/error.rb index 545519a1a..dedb4cd58 100644 --- a/lib/grape/middleware/error.rb +++ b/lib/grape/middleware/error.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'grape/middleware/base' - module Grape module Middleware class Error < Base diff --git a/lib/grape/middleware/formatter.rb b/lib/grape/middleware/formatter.rb index 0e87373d0..2f0f7f0df 100644 --- a/lib/grape/middleware/formatter.rb +++ b/lib/grape/middleware/formatter.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'grape/middleware/base' - module Grape module Middleware class Formatter < Base diff --git a/lib/grape/middleware/globals.rb b/lib/grape/middleware/globals.rb index 850f24196..10d5029dc 100644 --- a/lib/grape/middleware/globals.rb +++ b/lib/grape/middleware/globals.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'grape/middleware/base' - module Grape module Middleware class Globals < Base diff --git a/lib/grape/middleware/versioner/accept_version_header.rb b/lib/grape/middleware/versioner/accept_version_header.rb index 6198ae0cf..98237e947 100644 --- a/lib/grape/middleware/versioner/accept_version_header.rb +++ b/lib/grape/middleware/versioner/accept_version_header.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'grape/middleware/base' - module Grape module Middleware module Versioner diff --git a/lib/grape/middleware/versioner/header.rb b/lib/grape/middleware/versioner/header.rb index f8c9f2823..33ea25660 100644 --- a/lib/grape/middleware/versioner/header.rb +++ b/lib/grape/middleware/versioner/header.rb @@ -1,9 +1,5 @@ # frozen_string_literal: true -require 'grape/middleware/base' -require 'grape/util/media_type' -require 'grape/util/accept_header_handler' - module Grape module Middleware module Versioner @@ -37,11 +33,13 @@ def before content_types: content_types, allowed_methods: env[Grape::Env::GRAPE_ALLOWED_METHODS] ) do |media_type| - env[Grape::Env::API_TYPE] = media_type.type - env[Grape::Env::API_SUBTYPE] = media_type.subtype - env[Grape::Env::API_VENDOR] = media_type.vendor - env[Grape::Env::API_VERSION] = media_type.version - env[Grape::Env::API_FORMAT] = media_type.format + env.update( + Grape::Env::API_TYPE => media_type.type, + Grape::Env::API_SUBTYPE => media_type.subtype, + Grape::Env::API_VENDOR => media_type.vendor, + Grape::Env::API_VERSION => media_type.version, + Grape::Env::API_FORMAT => media_type.format + ) end end end diff --git a/lib/grape/middleware/versioner/param.rb b/lib/grape/middleware/versioner/param.rb index 8e9fe36c0..69b1d0f48 100644 --- a/lib/grape/middleware/versioner/param.rb +++ b/lib/grape/middleware/versioner/param.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'grape/middleware/base' - module Grape module Middleware module Versioner diff --git a/lib/grape/middleware/versioner/path.rb b/lib/grape/middleware/versioner/path.rb index ec15a6d7c..d6c3e90dd 100644 --- a/lib/grape/middleware/versioner/path.rb +++ b/lib/grape/middleware/versioner/path.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'grape/middleware/base' - module Grape module Middleware module Versioner diff --git a/lib/grape/namespace.rb b/lib/grape/namespace.rb index 3473d3efb..8375e3a56 100644 --- a/lib/grape/namespace.rb +++ b/lib/grape/namespace.rb @@ -1,7 +1,5 @@ # 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. diff --git a/lib/grape/path.rb b/lib/grape/path.rb index e08725e52..e6fa540e7 100644 --- a/lib/grape/path.rb +++ b/lib/grape/path.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'grape/util/cache' - module Grape # Represents a path to an endpoint. class Path diff --git a/lib/grape/request.rb b/lib/grape/request.rb index c907f0b4a..8b75da98e 100644 --- a/lib/grape/request.rb +++ b/lib/grape/request.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'grape/util/lazy_object' - module Grape class Request < Rack::Request HTTP_PREFIX = 'HTTP_' @@ -36,7 +34,7 @@ def grape_routing_args end def build_headers - Grape::Util::LazyObject.new do + Grape::Util::Lazy::Object.new do env.each_pair.with_object({}) do |(k, v), headers| next unless k.to_s.start_with? HTTP_PREFIX @@ -46,7 +44,7 @@ def build_headers end end - if Grape.lowercase_headers? + if Grape::Http::Headers.lowercase? def transform_header(header) -header[5..].tr('_', '-').downcase end diff --git a/lib/grape/router.rb b/lib/grape/router.rb index 51107efba..7d9dd8564 100644 --- a/lib/grape/router.rb +++ b/lib/grape/router.rb @@ -1,9 +1,5 @@ # frozen_string_literal: true -require 'grape/router/route' -require 'grape/router/greedy_route' -require 'grape/util/cache' - module Grape class Router attr_reader :map, :compiled diff --git a/lib/grape/router/greedy_route.rb b/lib/grape/router/greedy_route.rb index 765aa83de..787c1265b 100644 --- a/lib/grape/router/greedy_route.rb +++ b/lib/grape/router/greedy_route.rb @@ -1,8 +1,5 @@ # frozen_string_literal: true -require 'grape/router/attribute_translator' -require 'forwardable' - # Act like a Grape::Router::Route but for greedy_match # see @neutral_map diff --git a/lib/grape/router/pattern.rb b/lib/grape/router/pattern.rb index a1fce07a0..0f646bea9 100644 --- a/lib/grape/router/pattern.rb +++ b/lib/grape/router/pattern.rb @@ -1,9 +1,5 @@ # frozen_string_literal: true -require 'forwardable' -require 'mustermann/grape' -require 'grape/util/cache' - module Grape class Router class Pattern diff --git a/lib/grape/router/route.rb b/lib/grape/router/route.rb index 7d4a912a3..2ee3bb89f 100644 --- a/lib/grape/router/route.rb +++ b/lib/grape/router/route.rb @@ -1,9 +1,5 @@ # frozen_string_literal: true -require 'grape/router/pattern' -require 'grape/router/attribute_translator' -require 'forwardable' - module Grape class Router class Route diff --git a/lib/grape/util/accept/header.rb b/lib/grape/util/accept/header.rb new file mode 100644 index 000000000..3f6b3a67a --- /dev/null +++ b/lib/grape/util/accept/header.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Grape + module Util + module Accept + module Header + ALLOWED_CHARACTERS = %r{^([a-z*]+)/([a-z0-9*&\^\-_#{$ERROR_INFO}.+]+)(?:;([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&.match(ALLOWED_CHARACTERS) + m ? [m[1], m[2], m[3] || ''] : [] + end + end + end + end + end +end diff --git a/lib/grape/util/accept_header_handler.rb b/lib/grape/util/accept_header_handler.rb index 25d703631..c7fc7bee9 100644 --- a/lib/grape/util/accept_header_handler.rb +++ b/lib/grape/util/accept_header_handler.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'grape/util/media_type' - module Grape module Util class AcceptHeaderHandler diff --git a/lib/grape/util/cache.rb b/lib/grape/util/cache.rb index b58f43240..7514296c2 100644 --- a/lib/grape/util/cache.rb +++ b/lib/grape/util/cache.rb @@ -1,8 +1,5 @@ # frozen_string_literal: true -require 'singleton' -require 'forwardable' - module Grape module Util class Cache diff --git a/lib/grape/util/endpoint_configuration.rb b/lib/grape/util/endpoint_configuration.rb index 90ad256f8..498556255 100644 --- a/lib/grape/util/endpoint_configuration.rb +++ b/lib/grape/util/endpoint_configuration.rb @@ -2,7 +2,7 @@ module Grape module Util - class EndpointConfiguration < LazyValueHash + class EndpointConfiguration < Lazy::ValueHash end end end diff --git a/lib/grape/util/inheritable_values.rb b/lib/grape/util/inheritable_values.rb index 6b43f8ca2..48cebb23a 100644 --- a/lib/grape/util/inheritable_values.rb +++ b/lib/grape/util/inheritable_values.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_relative 'base_inheritable' - module Grape module Util class InheritableValues < BaseInheritable diff --git a/lib/grape/util/lazy/block.rb b/lib/grape/util/lazy/block.rb new file mode 100644 index 000000000..a47d44b09 --- /dev/null +++ b/lib/grape/util/lazy/block.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Grape + module Util + module Lazy + class Block + def initialize(&new_block) + @block = new_block + end + + def evaluate_from(configuration) + @block.call(configuration) + end + + def evaluate + @block.call({}) + end + + def lazy? + true + end + + def to_s + evaluate.to_s + end + end + end + end +end diff --git a/lib/grape/util/lazy/object.rb b/lib/grape/util/lazy/object.rb new file mode 100644 index 000000000..6c10dadfc --- /dev/null +++ b/lib/grape/util/lazy/object.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +# Based on https://github.com/HornsAndHooves/lazy_object + +module Grape + module Util + module Lazy + class Object < 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 +end diff --git a/lib/grape/util/lazy/value.rb b/lib/grape/util/lazy/value.rb new file mode 100644 index 000000000..59b799947 --- /dev/null +++ b/lib/grape/util/lazy/value.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Grape + module Util + module Lazy + class Value + attr_reader :access_keys + + def initialize(value, access_keys = []) + @value = value + @access_keys = access_keys + end + + def evaluate_from(configuration) + matching_lazy_value = configuration.fetch(@access_keys) + matching_lazy_value.evaluate + end + + def evaluate + @value + end + + def lazy? + true + end + + def reached_by(parent_access_keys, access_key) + @access_keys = parent_access_keys + [access_key] + self + end + + def to_s + evaluate.to_s + end + end + end + end +end diff --git a/lib/grape/util/lazy/value_array.rb b/lib/grape/util/lazy/value_array.rb new file mode 100644 index 000000000..b4c6a88ab --- /dev/null +++ b/lib/grape/util/lazy/value_array.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Grape + module Util + module Lazy + class ValueArray < ValueEnumerable + def initialize(array) + super + @value_hash = [] + array.each_with_index do |value, index| + self[index] = value + end + end + + def evaluate + @value_hash.map(&:evaluate) + end + end + end + end +end diff --git a/lib/grape/util/lazy/value_enumerable.rb b/lib/grape/util/lazy/value_enumerable.rb new file mode 100644 index 000000000..ce15693aa --- /dev/null +++ b/lib/grape/util/lazy/value_enumerable.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Grape + module Util + module Lazy + class ValueEnumerable < Value + def [](key) + if @value_hash[key].nil? + Value.new(nil).reached_by(access_keys, key) + else + @value_hash[key].reached_by(access_keys, key) + end + end + + def fetch(access_keys) + fetched_keys = access_keys.dup + value = self[fetched_keys.shift] + fetched_keys.any? ? value.fetch(fetched_keys) : value + end + + def []=(key, value) + @value_hash[key] = case value + when Hash + ValueHash.new(value) + when Array + ValueArray.new(value) + else + Value.new(value) + end + end + end + end + end +end diff --git a/lib/grape/util/lazy/value_hash.rb b/lib/grape/util/lazy/value_hash.rb new file mode 100644 index 000000000..c3447a0dd --- /dev/null +++ b/lib/grape/util/lazy/value_hash.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Grape + module Util + module Lazy + class ValueHash < ValueEnumerable + def initialize(hash) + super + @value_hash = ActiveSupport::HashWithIndifferentAccess.new + hash.each do |key, value| + self[key] = value + end + end + + def evaluate + @value_hash.transform_values(&:evaluate) + end + end + end + end +end diff --git a/lib/grape/util/lazy_block.rb b/lib/grape/util/lazy_block.rb deleted file mode 100644 index 6e7d18b8a..000000000 --- a/lib/grape/util/lazy_block.rb +++ /dev/null @@ -1,27 +0,0 @@ -# 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 deleted file mode 100644 index 22ec7c440..000000000 --- a/lib/grape/util/lazy_object.rb +++ /dev/null @@ -1,43 +0,0 @@ -# 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 deleted file mode 100644 index cc732562f..000000000 --- a/lib/grape/util/lazy_value.rb +++ /dev/null @@ -1,91 +0,0 @@ -# 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] = case value - when Hash - LazyValueHash.new(value) - when 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 - @value_hash.map(&:evaluate) - end - end - - class LazyValueHash < LazyValueEnumerable - def initialize(hash) - super - @value_hash = ActiveSupport::HashWithIndifferentAccess.new - hash.each do |key, value| - self[key] = value - end - end - - def evaluate - @value_hash.transform_values(&:evaluate) - end - end - end -end diff --git a/lib/grape/util/reverse_stackable_values.rb b/lib/grape/util/reverse_stackable_values.rb index 171f390f7..3b008a926 100644 --- a/lib/grape/util/reverse_stackable_values.rb +++ b/lib/grape/util/reverse_stackable_values.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_relative 'stackable_values' - module Grape module Util class ReverseStackableValues < StackableValues diff --git a/lib/grape/util/stackable_values.rb b/lib/grape/util/stackable_values.rb index 01a568196..c19e2f582 100644 --- a/lib/grape/util/stackable_values.rb +++ b/lib/grape/util/stackable_values.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_relative 'base_inheritable' - module Grape module Util class StackableValues < BaseInheritable diff --git a/lib/grape/validations.rb b/lib/grape/validations.rb index 74d534f37..9ae22ae6a 100644 --- a/lib/grape/validations.rb +++ b/lib/grape/validations.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Grape - # Registry to store and locate known Validators. module Validations module_function @@ -12,7 +11,7 @@ def validators # Register a new validator, so it can be used to validate parameters. # @param short_name [String] all lower-case, no spaces # @param klass [Class] the validator class. Should inherit from - # Validations::Base. + # Grape::Validations::Validators::Base. def register_validator(short_name, klass) validators[short_name] = klass end @@ -21,14 +20,11 @@ def deregister_validator(short_name) validators.delete(short_name) end - # Find a validator and if not found will try to load it def require_validator(short_name) str_name = short_name.to_s - validators.fetch(str_name) do - Grape::Validations::Validators.const_get(:"#{str_name.camelize}Validator") - end + validators.fetch(str_name) { Grape::Validations::Validators.const_get(:"#{str_name.camelize}Validator") } rescue NameError - raise Grape::Exceptions::UnknownValidator.new(short_name) + raise Grape::Exceptions::UnknownValidator, short_name end end end diff --git a/lib/grape/validations/attributes_doc.rb b/lib/grape/validations/attributes_doc.rb index a04670703..f9e15c148 100644 --- a/lib/grape/validations/attributes_doc.rb +++ b/lib/grape/validations/attributes_doc.rb @@ -2,56 +2,55 @@ module Grape module Validations - class ParamsScope - # Documents parameters of an endpoint. If documentation isn't needed (for instance, it is an - # internal API), the class only cleans up attributes to avoid junk in RAM. - class AttributesDoc - attr_accessor :type, :values - - # @param api [Grape::API::Instance] - # @param scope [Validations::ParamsScope] - def initialize(api, scope) - @api = api - @scope = scope - @type = type - end + # Documents parameters of an endpoint. If documentation isn't needed (for instance, it is an + # internal API), the class only cleans up attributes to avoid junk in RAM. + + class AttributesDoc + attr_accessor :type, :values + + # @param api [Grape::API::Instance] + # @param scope [Validations::ParamsScope] + def initialize(api, scope) + @api = api + @scope = scope + @type = type + end - def extract_details(validations) - details[:required] = validations.key?(:presence) + def extract_details(validations) + details[:required] = validations.key?(:presence) - desc = validations.delete(:desc) || validations.delete(:description) + desc = validations.delete(:desc) || validations.delete(:description) - details[:desc] = desc if desc + details[:desc] = desc if desc - documentation = validations.delete(:documentation) + documentation = validations.delete(:documentation) - details[:documentation] = documentation if documentation + details[:documentation] = documentation if documentation - details[:default] = validations[:default] if validations.key?(:default) - end - - def document(attrs) - return if @api.namespace_inheritable(:do_not_document) + details[:default] = validations[:default] if validations.key?(:default) + end - details[:type] = type.to_s if type - details[:values] = values if values + def document(attrs) + return if @api.namespace_inheritable(:do_not_document) - documented_attrs = attrs.each_with_object({}) do |name, memo| - memo[@scope.full_name(name)] = details - end + details[:type] = type.to_s if type + details[:values] = values if values - @api.namespace_stackable(:params, documented_attrs) + documented_attrs = attrs.each_with_object({}) do |name, memo| + memo[@scope.full_name(name)] = details end - def required - details[:required] - end + @api.namespace_stackable(:params, documented_attrs) + end - protected + def required + details[:required] + end - def details - @details ||= {} - end + protected + + def details + @details ||= {} end end end diff --git a/lib/grape/validations/params_scope.rb b/lib/grape/validations/params_scope.rb index 6eaf7abaa..026e7df9c 100644 --- a/lib/grape/validations/params_scope.rb +++ b/lib/grape/validations/params_scope.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_relative 'attributes_doc' - module Grape module Validations class ParamsScope diff --git a/lib/grape/validations/types.rb b/lib/grape/validations/types.rb index 1e45c4f7f..86f9c9b60 100644 --- a/lib/grape/validations/types.rb +++ b/lib/grape/validations/types.rb @@ -1,8 +1,5 @@ # frozen_string_literal: true -require 'grape/validations/types/json' -require 'grape/validations/types/file' - module Grape module Validations # Module for code related to grape's system for diff --git a/lib/grape/validations/types/array_coercer.rb b/lib/grape/validations/types/array_coercer.rb index c6d6b106e..ec4ca41de 100644 --- a/lib/grape/validations/types/array_coercer.rb +++ b/lib/grape/validations/types/array_coercer.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_relative 'dry_type_coercer' - module Grape module Validations module Types diff --git a/lib/grape/validations/types/build_coercer.rb b/lib/grape/validations/types/build_coercer.rb index c55e048db..5f15a1a0c 100644 --- a/lib/grape/validations/types/build_coercer.rb +++ b/lib/grape/validations/types/build_coercer.rb @@ -1,94 +1,92 @@ # frozen_string_literal: true -require_relative 'array_coercer' -require_relative 'set_coercer' -require_relative 'primitive_coercer' - module Grape module Validations module Types - # Chooses the best coercer for the given type. For example, if the type - # is Integer, it will return a coercer which will be able to coerce a value - # to the integer. - # - # There are a few very special coercers which might be returned. - # - # +Grape::Types::MultipleTypeCoercer+ is a coercer which is returned when - # the given type implies values in an array with different types. - # For example, +[Integer, String]+ allows integer and string values in - # an array. - # - # +Grape::Types::CustomTypeCoercer+ is a coercer which is returned when - # a method is specified by a user with +coerce_with+ option or the user - # specifies a custom type which implements requirments of - # +Grape::Types::CustomTypeCoercer+. - # - # +Grape::Types::CustomTypeCollectionCoercer+ is a very similar to the - # previous one, but it expects an array or set of values having a custom - # type implemented by the user. - # - # There is also a group of custom types implemented by Grape, check - # +Grape::Validations::Types::SPECIAL+ to get the full list. - # - # @param type [Class] the type to which input strings - # should be coerced - # @param method [Class,#call] the coercion method to use - # @return [Object] object to be used - # for coercion and type validation - def self.build_coercer(type, method: nil, strict: false) - cache_instance(type, method, strict) do - create_coercer_instance(type, method, strict) + module BuildCoercer + # Chooses the best coercer for the given type. For example, if the type + # is Integer, it will return a coercer which will be able to coerce a value + # to the integer. + # + # There are a few very special coercers which might be returned. + # + # +Grape::Types::MultipleTypeCoercer+ is a coercer which is returned when + # the given type implies values in an array with different types. + # For example, +[Integer, String]+ allows integer and string values in + # an array. + # + # +Grape::Types::CustomTypeCoercer+ is a coercer which is returned when + # a method is specified by a user with +coerce_with+ option or the user + # specifies a custom type which implements requirments of + # +Grape::Types::CustomTypeCoercer+. + # + # +Grape::Types::CustomTypeCollectionCoercer+ is a very similar to the + # previous one, but it expects an array or set of values having a custom + # type implemented by the user. + # + # There is also a group of custom types implemented by Grape, check + # +Grape::Validations::Types::SPECIAL+ to get the full list. + # + # @param type [Class] the type to which input strings + # should be coerced + # @param method [Class,#call] the coercion method to use + # @return [Object] object to be used + # for coercion and type validation + def self.build_coercer(type, method: nil, strict: false) + cache_instance(type, method, strict) do + create_coercer_instance(type, method, strict) + end end - 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) + 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) - MultipleTypeCoercer.new(type, method) + # Use a special coercer for multiply-typed parameters. + if Types.multiple?(type) + MultipleTypeCoercer.new(type, method) - # Use a special coercer for custom types and coercion methods. - elsif method || Types.custom?(type) - CustomTypeCoercer.new(type, method) + # Use a special coercer for custom types and coercion methods. + elsif method || Types.custom?(type) + CustomTypeCoercer.new(type, method) - # Special coercer for collections of types that implement a parse method. - # CustomTypeCoercer (above) already handles such types when an explicit coercion - # method is supplied. - elsif Types.collection_of_custom?(type) - Types::CustomTypeCollectionCoercer.new( - Types.map_special(type.first), type.is_a?(Set) - ) - else - DryTypeCoercer.coercer_instance_for(type, strict) + # Special coercer for collections of types that implement a parse method. + # CustomTypeCoercer (above) already handles such types when an explicit coercion + # method is supplied. + elsif Types.collection_of_custom?(type) + Types::CustomTypeCollectionCoercer.new( + Types.map_special(type.first), type.is_a?(Set) + ) + else + DryTypeCoercer.coercer_instance_for(type, strict) + end end - end - def self.cache_instance(type, method, strict, &_block) - key = cache_key(type, method, strict) + def self.cache_instance(type, method, strict, &_block) + key = cache_key(type, method, strict) - return @__cache[key] if @__cache.key?(key) + return @__cache[key] if @__cache.key?(key) - instance = yield + instance = yield - @__cache_write_lock.synchronize do - @__cache[key] = instance - end + @__cache_write_lock.synchronize do + @__cache[key] = instance + end - instance - end + instance + end - def self.cache_key(type, method, strict) - [type, method, strict].each_with_object(+'_') do |val, memo| - next if val.nil? + def self.cache_key(type, method, strict) + [type, method, strict].each_with_object(+'_') do |val, memo| + next if val.nil? - memo << '_' << val.to_s + memo << '_' << val.to_s + end end - end - instance_variable_set(:@__cache, {}) - instance_variable_set(:@__cache_write_lock, Mutex.new) + instance_variable_set(:@__cache, {}) + instance_variable_set(:@__cache_write_lock, Mutex.new) + end end end end diff --git a/lib/grape/validations/types/dry_type_coercer.rb b/lib/grape/validations/types/dry_type_coercer.rb index cc529c07c..1067eaf3a 100644 --- a/lib/grape/validations/types/dry_type_coercer.rb +++ b/lib/grape/validations/types/dry_type_coercer.rb @@ -1,7 +1,5 @@ # 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 @@ -24,9 +22,7 @@ class << self # collection_coercer_for(Array) # #=> Grape::Validations::Types::ArrayCoercer def collection_coercer_for(type) - collection_coercers.fetch(type) do - DryTypeCoercer.collection_coercers[type] = Grape::Validations::Types.const_get(:"#{type.name.camelize}Coercer") - end + Grape::Validations::Types.const_get(:"#{type.name.camelize}Coercer") end # Returns an instance of a coercer for a given type @@ -37,12 +33,6 @@ def coercer_instance_for(type, strict = false) # 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) diff --git a/lib/grape/validations/types/json.rb b/lib/grape/validations/types/json.rb index 3240de27b..61b01131c 100644 --- a/lib/grape/validations/types/json.rb +++ b/lib/grape/validations/types/json.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'json' - module Grape module Validations module Types diff --git a/lib/grape/validations/types/primitive_coercer.rb b/lib/grape/validations/types/primitive_coercer.rb index e2e3f9df5..e59b5c6eb 100644 --- a/lib/grape/validations/types/primitive_coercer.rb +++ b/lib/grape/validations/types/primitive_coercer.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_relative 'dry_type_coercer' - module Grape module Validations module Types diff --git a/lib/grape/validations/types/set_coercer.rb b/lib/grape/validations/types/set_coercer.rb index 2d385c935..9b1b311f8 100644 --- a/lib/grape/validations/types/set_coercer.rb +++ b/lib/grape/validations/types/set_coercer.rb @@ -1,8 +1,5 @@ # frozen_string_literal: true -require 'set' -require_relative 'array_coercer' - module Grape module Validations module Types diff --git a/lib/grape/validations/validators/base.rb b/lib/grape/validations/validators/base.rb index c76eb4b39..3dd49fd79 100644 --- a/lib/grape/validations/validators/base.rb +++ b/lib/grape/validations/validators/base.rb @@ -59,6 +59,7 @@ def validate!(params) end def self.inherited(klass) + super return if klass.name.blank? short_validator_name = klass.name.demodulize.underscore.delete_suffix('_validator') diff --git a/lib/grape/util/xml.rb b/lib/grape/xml.rb similarity index 80% rename from lib/grape/util/xml.rb rename to lib/grape/xml.rb index d948f8012..85287814a 100644 --- a/lib/grape/util/xml.rb +++ b/lib/grape/xml.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Grape - if Object.const_defined? :MultiXml + if defined?(::MultiXml) Xml = ::MultiXml else Xml = ::ActiveSupport::XmlMini diff --git a/spec/grape/api/custom_validations_spec.rb b/spec/grape/api/custom_validations_spec.rb index a6b7a629c..49571a803 100644 --- a/spec/grape/api/custom_validations_spec.rb +++ b/spec/grape/api/custom_validations_spec.rb @@ -35,13 +35,7 @@ def validate_param!(attr_name, params) end let(:app) { Rack::Builder.new(subject) } - before do - described_class.register_validator('default_length', default_length_validator) - end - - after do - described_class.deregister_validator('default_length') - end + before { stub_const('Grape::Validations::Validators::DefaultLengthValidator', default_length_validator) } it 'under 140 characters' do get '/', text: 'abc' @@ -83,13 +77,7 @@ def validate(request) end let(:app) { Rack::Builder.new(subject) } - before do - described_class.register_validator('in_body', in_body_validator) - end - - after do - described_class.deregister_validator('in_body') - end + before { stub_const('Grape::Validations::Validators::InBodyValidator', in_body_validator) } it 'allows field in body' do get '/', text: 'abc' @@ -125,13 +113,7 @@ def validate_param!(attr_name, _params) end let(:app) { Rack::Builder.new(subject) } - before do - described_class.register_validator('with_message_key', message_key_validator) - end - - after do - described_class.deregister_validator('with_message_key') - end + before { stub_const('Grape::Validations::Validators::WithMessageKeyValidator', message_key_validator) } it 'fails with message' do get '/', text: 'foobar' @@ -169,20 +151,15 @@ def validate(request) end def access_header - Grape.lowercase_headers? ? 'x-access-token' : 'X-Access-Token' + Grape::Http::Headers.lowercase? ? 'x-access-token' : 'X-Access-Token' end end end + let(:app) { Rack::Builder.new(subject) } - let(:x_access_token_header) { Grape.lowercase_headers? ? 'x-access-token' : 'X-Access-Token' } + let(:x_access_token_header) { Grape::Http::Headers.lowercase? ? 'x-access-token' : 'X-Access-Token' } - before do - described_class.register_validator('admin', admin_validator) - end - - after do - described_class.deregister_validator('admin') - end + before { stub_const('Grape::Validations::Validators::AdminValidator', admin_validator) } it 'fail when non-admin user sets an admin field' do get '/', admin_field: 'tester', non_admin_field: 'toaster' @@ -216,4 +193,37 @@ def access_header expect(last_response.body).to include 'Can not set Admin only field.' end end + + describe 'using a custom validator with instance variable' do + let(:validator_type) do + Class.new(Grape::Validations::Validators::Base) do + def validate_param!(_attr_name, _params) + if instance_variable_defined?(:@instance_variable) && @instance_variable + raise Grape::Exceptions::Validation.new(params: ['params'], + message: 'This should never happen') + end + @instance_variable = true + end + end + end + let(:app) do + Class.new(Grape::API) do + params do + optional :param_to_validate, instance_validator: true + optional :another_param_to_validate, instance_validator: true + end + get do + 'noop' + end + end + end + + before { stub_const('Grape::Validations::Validators::InstanceValidatorValidator', validator_type) } + + it 'passes validation every time' do + expect(validator_type).to receive(:new).twice.and_call_original + get '/', param_to_validate: 'value', another_param_to_validate: 'value' + expect(last_response.status).to eq 200 + end + end end diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index f8bb785ff..75f626f1a 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -998,11 +998,6 @@ def to_txt end describe '.compile!' do - it 'requires the grape/eager_load file' do - expect(app).to receive(:require).with('grape/eager_load').and_return(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 } diff --git a/spec/grape/endpoint_spec.rb b/spec/grape/endpoint_spec.rb index d8e88aeb1..c2443cbe7 100644 --- a/spec/grape/endpoint_spec.rb +++ b/spec/grape/endpoint_spec.rb @@ -146,7 +146,7 @@ def app it 'includes additional request headers' do get '/headers', nil, 'HTTP_X_GRAPE_CLIENT' => '1' - x_grape_client_header = Grape.lowercase_headers? ? 'x-grape-client' : 'X-Grape-Client' + x_grape_client_header = Grape::Http::Headers.lowercase? ? 'x-grape-client' : 'X-Grape-Client' expect(JSON.parse(last_response.body)[x_grape_client_header]).to eq('1') end @@ -154,7 +154,7 @@ def app env = Rack::MockRequest.env_for('/headers') env[:HTTP_SYMBOL_HEADER] = 'Goliath passes symbols' body = read_chunks(subject.call(env)[2]).join - symbol_header = Grape.lowercase_headers? ? 'symbol-header' : 'Symbol-Header' + symbol_header = Grape::Http::Headers.lowercase? ? 'symbol-header' : 'Symbol-Header' expect(JSON.parse(body)[symbol_header]).to eq('Goliath passes symbols') end end diff --git a/spec/grape/request_spec.rb b/spec/grape/request_spec.rb index b84b6dffd..94d9f505a 100644 --- a/spec/grape/request_spec.rb +++ b/spec/grape/request_spec.rb @@ -90,7 +90,7 @@ module Grape } end let(:x_grape_is_cool_header) do - Grape.lowercase_headers? ? 'x-grape-is-cool' : 'X-Grape-Is-Cool' + Grape::Http::Headers.lowercase? ? 'x-grape-is-cool' : 'X-Grape-Is-Cool' end it 'cuts HTTP_ prefix and capitalizes header name words' do @@ -120,7 +120,7 @@ module Grape default_env.merge(request_headers) end let(:grape_likes_symbolic_header) do - Grape.lowercase_headers? ? 'grape-likes-symbolic' : 'Grape-Likes-Symbolic' + Grape::Http::Headers.lowercase? ? 'grape-likes-symbolic' : 'Grape-Likes-Symbolic' end it 'converts them to string' do diff --git a/spec/grape/util/accept_header_handler_spec.rb b/spec/grape/util/accept_header_handler_spec.rb index ac4ff3f46..de85ed26b 100644 --- a/spec/grape/util/accept_header_handler_spec.rb +++ b/spec/grape/util/accept_header_handler_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'grape/util/accept_header_handler' - RSpec.describe Grape::Util::AcceptHeaderHandler do subject(:match_best_quality_media_type!) { instance.match_best_quality_media_type! } diff --git a/spec/grape/validations/attributes_doc_spec.rb b/spec/grape/validations/attributes_doc_spec.rb index a21ce3591..f1ae0c93e 100644 --- a/spec/grape/validations/attributes_doc_spec.rb +++ b/spec/grape/validations/attributes_doc_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe Grape::Validations::ParamsScope::AttributesDoc do +describe Grape::Validations::AttributesDoc do shared_examples 'an optional doc attribute' do |attr| it 'does not mention it' do expected_opts.delete(attr) diff --git a/spec/grape/validations/instance_behaivour_spec.rb b/spec/grape/validations/instance_behaivour_spec.rb deleted file mode 100644 index c8df96756..000000000 --- a/spec/grape/validations/instance_behaivour_spec.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -describe 'Validator with instance variables' do - let(:validator_type) do - Class.new(Grape::Validations::Validators::Base) do - def validate_param!(_attr_name, _params) - 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 - end - end - let(:app) do - Class.new(Grape::API) do - params do - optional :param_to_validate, instance_validator: true - optional :another_param_to_validate, instance_validator: true - end - get do - 'noop' - end - end - end - - before do - Grape::Validations.register_validator('instance_validator', validator_type) - end - - after do - Grape::Validations.deregister_validator('instance_validator') - end - - it 'passes validation every time' do - expect(validator_type).to receive(:new).exactly(4).times.and_call_original - - 2.times do - get '/', param_to_validate: 'value', another_param_to_validate: 'value' - expect(last_response.status).to eq 200 - end - end -end diff --git a/spec/grape/validations/validators/base_spec.rb b/spec/grape/validations/validators/base_spec.rb deleted file mode 100644 index 9f1ff9760..000000000 --- a/spec/grape/validations/validators/base_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Grape::Validations::Validators::Base do - describe '#inherited' do - context 'when validator is anonymous' do - subject(:custom_validator) { Class.new(described_class) } - - it 'does not register the validator' do - expect(Grape::Validations).not_to receive(:register_validator) - custom_validator - end - end - - # Anonymous class does not have a name and class A < B would leak. - # Simulates inherited callback - context "when validator's underscored name does not end with _validator" do - subject(:custom_validator) { described_class.inherited(TestModule::CustomValidatorABC) } - - before { stub_const('TestModule::CustomValidatorABC', Class.new) } - - it 'registers the custom validator with a short name' do - expect(Grape::Validations).to receive(:register_validator).with('custom_validator_abc', TestModule::CustomValidatorABC) - custom_validator - end - end - - context "when validator's underscored name ends with _validator" do - subject(:custom_validator) { described_class.inherited(TestModule::CustomValidator) } - - before { stub_const('TestModule::CustomValidator', Class.new) } - - it 'registers the custom validator with short name not ending with validator' do - expect(Grape::Validations).to receive(:register_validator).with('custom', TestModule::CustomValidator) - custom_validator - end - end - end -end diff --git a/spec/grape/validations_spec.rb b/spec/grape/validations_spec.rb index 58ab8a579..12acb33d3 100644 --- a/spec/grape/validations_spec.rb +++ b/spec/grape/validations_spec.rb @@ -3,9 +3,8 @@ describe Grape::Validations do subject { Class.new(Grape::API) } - def app - subject - end + let(:app) { subject } + let(:declard_params) {} def declared_params subject.namespace_stackable(:declared_params).flatten @@ -499,14 +498,7 @@ def validate_param!(attr_name, params) end before do - described_class.register_validator('date_range', date_range_validator) - end - - after do - described_class.deregister_validator('date_range') - end - - before do + stub_const('Grape::Validations::Validators::DateRangeValidator', date_range_validator) subject.params do optional :date_range, date_range: true, type: Hash do requires :from, type: Integer @@ -1198,13 +1190,7 @@ def validate_param!(attr_name, params) end end - before do - described_class.register_validator('customvalidator', custom_validator) - end - - after do - described_class.deregister_validator('customvalidator') - end + before { stub_const('Grape::Validations::Validators::CustomvalidatorValidator', custom_validator) } context 'when using optional with a custom validator' do before do @@ -1356,14 +1342,8 @@ def validate_param!(attr_name, params) end before do - described_class.register_validator('customvalidator_with_options', custom_validator_with_options) - end + stub_const('Grape::Validations::Validators::CustomvalidatorWithOptionsValidator', custom_validator_with_options) - after do - described_class.deregister_validator('customvalidator_with_options') - end - - before do subject.params do optional :custom, customvalidator_with_options: { text: 'im custom with options', message: 'is not custom with options!' } end diff --git a/spec/integration/eager_load/eager_load_spec.rb b/spec/integration/eager_load/eager_load_spec.rb deleted file mode 100644 index 174ba9944..000000000 --- a/spec/integration/eager_load/eager_load_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -# 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 { described_class.eager_load! }.not_to raise_error - end - - it 'compile!' do - expect { Class.new(Grape::API).compile! }.not_to raise_error - end -end diff --git a/spec/integration/multi_json/json_spec.rb b/spec/integration/multi_json/json_spec.rb index 18ac22b72..a4227cdde 100644 --- a/spec/integration/multi_json/json_spec.rb +++ b/spec/integration/multi_json/json_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe Grape::Json do +describe Grape::Json, if: defined?(::MultiJson) do it 'uses multi_json' do expect(described_class).to eq(::MultiJson) end From e2b23dee8ae7ee9ce946308916f82c974d954a0c Mon Sep 17 00:00:00 2001 From: Dhruv Paranjape Date: Sat, 6 Apr 2024 20:30:32 +0200 Subject: [PATCH 218/304] Replace Hash with Rack::Header/Rack::Utils::HeaderHash --- CHANGELOG.md | 1 + lib/grape/dsl/headers.rb | 2 +- lib/grape/endpoint.rb | 2 +- lib/grape/request.rb | 12 +++--------- lib/grape/util/header.rb | 13 +++++++++++++ spec/grape/api/custom_validations_spec.rb | 4 ++-- spec/grape/dsl/headers_spec.rb | 20 ++++++++++---------- spec/grape/endpoint_spec.rb | 11 +++++++---- spec/grape/request_spec.rb | 4 ++-- 9 files changed, 40 insertions(+), 29 deletions(-) create mode 100644 lib/grape/util/header.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 96c236ec3..06581d597 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ * [#2426](https://github.com/ruby-grape/grape/pull/2426): Drop support for rack 1.x series - [@ericproulx](https://github.com/ericproulx). * [#2427](https://github.com/ruby-grape/grape/pull/2427): Use `rack-contrib` jsonp instead of rack-jsonp - [@ericproulx](https://github.com/ericproulx). * [#2363](https://github.com/ruby-grape/grape/pull/2363): Replace autoload by zeitwerk - [@ericproulx](https://github.com/ericproulx). +* [#2425](https://github.com/ruby-grape/grape/pull/2425): Replace `{}` with `Rack::Header` or `Rack::Utils::HeaderHash` - [@dhruvCW](https://github.com/dhruvCW). * Your contribution here. #### Fixes diff --git a/lib/grape/dsl/headers.rb b/lib/grape/dsl/headers.rb index b84c4efe4..a02bdd588 100644 --- a/lib/grape/dsl/headers.rb +++ b/lib/grape/dsl/headers.rb @@ -12,7 +12,7 @@ def header(key = nil, val = nil) if key val ? header[key.to_s] = val : header.delete(key.to_s) else - @header ||= {} + @header ||= Grape::Util::Header.new end end alias headers header diff --git a/lib/grape/endpoint.rb b/lib/grape/endpoint.rb index 121b1c4d6..a729216bb 100644 --- a/lib/grape/endpoint.rb +++ b/lib/grape/endpoint.rb @@ -238,7 +238,7 @@ def equals?(e) def run ActiveSupport::Notifications.instrument('endpoint_run.grape', endpoint: self, env: env) do - @header = {} + @header = Grape::Util::Header.new @request = Grape::Request.new(env, build_params_with: namespace_inheritable(:build_params_with)) @params = @request.params @headers = @request.headers diff --git a/lib/grape/request.rb b/lib/grape/request.rb index 8b75da98e..7093ab95d 100644 --- a/lib/grape/request.rb +++ b/lib/grape/request.rb @@ -35,7 +35,7 @@ def grape_routing_args def build_headers Grape::Util::Lazy::Object.new do - env.each_pair.with_object({}) do |(k, v), headers| + env.each_pair.with_object(Grape::Util::Header.new) do |(k, v), headers| next unless k.to_s.start_with? HTTP_PREFIX transformed_header = Grape::Http::Headers::HTTP_HEADERS[k] || transform_header(k) @@ -44,14 +44,8 @@ def build_headers end end - if Grape::Http::Headers.lowercase? - def transform_header(header) - -header[5..].tr('_', '-').downcase - end - else - def transform_header(header) - -header[5..].split('_').map(&:capitalize).join('-') - end + def transform_header(header) + -header[5..].tr('_', '-').downcase end end end diff --git a/lib/grape/util/header.rb b/lib/grape/util/header.rb new file mode 100644 index 000000000..632480798 --- /dev/null +++ b/lib/grape/util/header.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Grape + module Util + if Gem::Version.new(Rack.release) >= Gem::Version.new('3') + require 'rack/headers' + Header = Rack::Headers + else + require 'rack/utils' + Header = Rack::Utils::HeaderHash + end + end +end diff --git a/spec/grape/api/custom_validations_spec.rb b/spec/grape/api/custom_validations_spec.rb index 49571a803..558da7576 100644 --- a/spec/grape/api/custom_validations_spec.rb +++ b/spec/grape/api/custom_validations_spec.rb @@ -151,13 +151,13 @@ def validate(request) end def access_header - Grape::Http::Headers.lowercase? ? 'x-access-token' : 'X-Access-Token' + 'x-access-token' end end end let(:app) { Rack::Builder.new(subject) } - let(:x_access_token_header) { Grape::Http::Headers.lowercase? ? 'x-access-token' : 'X-Access-Token' } + let(:x_access_token_header) { 'x-access-token' } before { stub_const('Grape::Validations::Validators::AdminValidator', admin_validator) } diff --git a/spec/grape/dsl/headers_spec.rb b/spec/grape/dsl/headers_spec.rb index 9ced6304f..154fbf82b 100644 --- a/spec/grape/dsl/headers_spec.rb +++ b/spec/grape/dsl/headers_spec.rb @@ -11,8 +11,8 @@ class Dummy subject { HeadersSpec::Dummy.new } let(:header_data) do - { 'First Key' => 'First Value', - 'Second Key' => 'Second Value' } + { 'first key' => 'First Value', + 'second key' => 'Second Value' } end context 'when headers are set' do @@ -23,8 +23,8 @@ class Dummy describe 'get' do it 'returns a specifc value' do - expect(subject.header['First Key']).to eq 'First Value' - expect(subject.header['Second Key']).to eq 'Second Value' + expect(subject.header['first key']).to eq 'First Value' + expect(subject.header['second key']).to eq 'Second Value' end it 'returns all set headers' do @@ -35,15 +35,15 @@ class Dummy describe 'set' do it 'returns value' do - expect(subject.header('Third Key', 'Third Value')) - expect(subject.header['Third Key']).to eq 'Third Value' + expect(subject.header('third key', 'Third Value')) + expect(subject.header['third key']).to eq 'Third Value' end end describe 'delete' do it 'deletes a header key-value pair' do - expect(subject.header('First Key')).to eq header_data['First Key'] - expect(subject.header).not_to have_key('First Key') + expect(subject.header('first key')).to eq header_data['first key'] + expect(subject.header).not_to have_key('first key') end end end @@ -52,8 +52,8 @@ class Dummy context 'when no headers are set' do describe '#header' do it 'returns nil' do - expect(subject.header['First Key']).to be_nil - expect(subject.header('First Key')).to be_nil + expect(subject.header['first key']).to be_nil + expect(subject.header('first key')).to be_nil end end end diff --git a/spec/grape/endpoint_spec.rb b/spec/grape/endpoint_spec.rb index c2443cbe7..8b217af9e 100644 --- a/spec/grape/endpoint_spec.rb +++ b/spec/grape/endpoint_spec.rb @@ -138,15 +138,18 @@ def app it 'includes request headers' do get '/headers' + cookie_header = Grape::Http::Headers.lowercase? ? 'cookie' : 'Cookie' + host_header = Grape::Http::Headers.lowercase? ? 'host' : 'Host' + expect(JSON.parse(last_response.body)).to include( - 'Host' => 'example.org', - 'Cookie' => '' + host_header => 'example.org', + cookie_header => '' ) end it 'includes additional request headers' do get '/headers', nil, 'HTTP_X_GRAPE_CLIENT' => '1' - x_grape_client_header = Grape::Http::Headers.lowercase? ? 'x-grape-client' : 'X-Grape-Client' + x_grape_client_header = 'x-grape-client' expect(JSON.parse(last_response.body)[x_grape_client_header]).to eq('1') end @@ -154,7 +157,7 @@ def app env = Rack::MockRequest.env_for('/headers') env[:HTTP_SYMBOL_HEADER] = 'Goliath passes symbols' body = read_chunks(subject.call(env)[2]).join - symbol_header = Grape::Http::Headers.lowercase? ? 'symbol-header' : 'Symbol-Header' + symbol_header = 'symbol-header' expect(JSON.parse(body)[symbol_header]).to eq('Goliath passes symbols') end end diff --git a/spec/grape/request_spec.rb b/spec/grape/request_spec.rb index 94d9f505a..eaf9a6ac9 100644 --- a/spec/grape/request_spec.rb +++ b/spec/grape/request_spec.rb @@ -90,7 +90,7 @@ module Grape } end let(:x_grape_is_cool_header) do - Grape::Http::Headers.lowercase? ? 'x-grape-is-cool' : 'X-Grape-Is-Cool' + 'x-grape-is-cool' end it 'cuts HTTP_ prefix and capitalizes header name words' do @@ -120,7 +120,7 @@ module Grape default_env.merge(request_headers) end let(:grape_likes_symbolic_header) do - Grape::Http::Headers.lowercase? ? 'grape-likes-symbolic' : 'Grape-Likes-Symbolic' + 'grape-likes-symbolic' end it 'converts them to string' do From 9e68e460ddc82d13670f2bf493cc9e076d3f37a6 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Mon, 15 Apr 2024 19:17:15 +0200 Subject: [PATCH 219/304] Isolate extensions within specific Gemfile (#2430) * Extensions in separate Gemfile - hashie - grape-entity - dry-validation --- .github/workflows/test.yml | 10 +- .rubocop_todo.yml | 11 +- Appraisals | 2 +- CHANGELOG.md | 1 + Gemfile | 3 - ...idation.gemfile => dry_validation.gemfile} | 4 +- gemfiles/grape_entity.gemfile | 40 +++ gemfiles/hashie.gemfile | 40 +++ gemfiles/multi_json.gemfile | 3 - gemfiles/multi_xml.gemfile | 3 - gemfiles/rack_2_0.gemfile | 3 - gemfiles/rack_3_0.gemfile | 3 - gemfiles/rack_edge.gemfile | 3 - gemfiles/rails_6_0.gemfile | 3 - gemfiles/rails_6_1.gemfile | 3 - gemfiles/rails_7_0.gemfile | 3 - gemfiles/rails_7_1.gemfile | 3 - gemfiles/rails_edge.gemfile | 3 - spec/grape/api/documentation_spec.rb | 2 - spec/grape/api_spec.rb | 283 +++++++--------- spec/grape/dsl/validations_spec.rb | 65 ---- spec/grape/endpoint/declared_spec.rb | 63 ++-- .../param_builders/hashie/mash_spec.rb | 109 ------- spec/grape/middleware/error_spec.rb | 16 +- spec/grape/request_spec.rb | 178 +++++----- spec/grape/validations/contract_scope_spec.rb | 179 ----------- .../validations/validators/coerce_spec.rb | 294 +++++++---------- .../dry_validation/dry_validation_spec.rb | 239 ++++++++++++++ .../grape_entity}/entity_spec.rb | 180 ++++++++--- spec/integration/hashie/hashie_spec.rb | 304 ++++++++++++++++++ spec/integration/multi_json/json_spec.rb | 9 +- spec/integration/multi_xml/xml_spec.rb | 6 +- .../no_dry_validation_spec.rb | 28 -- 33 files changed, 1115 insertions(+), 981 deletions(-) rename gemfiles/{no_dry_validation.gemfile => dry_validation.gemfile} (92%) create mode 100644 gemfiles/grape_entity.gemfile create mode 100644 gemfiles/hashie.gemfile delete mode 100644 spec/grape/dsl/validations_spec.rb delete mode 100644 spec/grape/extensions/param_builders/hashie/mash_spec.rb delete mode 100644 spec/grape/validations/contract_scope_spec.rb create mode 100644 spec/integration/dry_validation/dry_validation_spec.rb rename spec/{grape => integration/grape_entity}/entity_spec.rb (69%) create mode 100644 spec/integration/hashie/hashie_spec.rb delete mode 100644 spec/integration/no_dry_validation/no_dry_validation_spec.rb diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 273537e6f..e6c0a7418 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,8 +40,14 @@ jobs: gemfile: gemfiles/rack_3_0.gemfile specs: 'spec/integration/rack_3_0' - ruby: '3.3' - gemfile: gemfiles/no_dry_validation.gemfile - specs: 'spec/integration/no_dry_validation' + gemfile: gemfiles/grape_entity.gemfile + specs: 'spec/integration/grape_entity' + - ruby: '3.3' + gemfile: gemfiles/hashie.gemfile + specs: 'spec/integration/hashie' + - ruby: '3.3' + gemfile: gemfiles/dry_validation.gemfile + specs: 'spec/integration/dry_validation' runs-on: ubuntu-latest env: BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }} diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index e221cfd92..faac38752 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 5000` -# on 2024-04-01 12:18:08 UTC using RuboCop version 1.59.0. +# on 2024-04-15 16:22:26 UTC using RuboCop version 1.59.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 @@ -165,22 +165,15 @@ RSpec/MissingExampleGroupArgument: Exclude: - 'spec/grape/middleware/exception_spec.rb' -# Offense count: 18 +# Offense count: 17 # Configuration parameters: AllowedPatterns. # AllowedPatterns: ^expect_, ^assert_ RSpec/NoExpectationExample: Exclude: - 'spec/grape/api_remount_spec.rb' - 'spec/grape/api_spec.rb' - - 'spec/grape/entity_spec.rb' - 'spec/grape/validations_spec.rb' -# Offense count: 6 -# This cop supports unsafe autocorrection (--autocorrect-all). -RSpec/Rails/HaveHttpStatus: - Exclude: - - 'spec/grape/api_spec.rb' - # Offense count: 12 RSpec/RepeatedDescription: Exclude: diff --git a/Appraisals b/Appraisals index ad4ce12c0..e9a6f2a42 100644 --- a/Appraisals +++ b/Appraisals @@ -53,7 +53,7 @@ appraise 'rack_3_0' do gem 'rack', '~> 3.0.0' end -appraise 'no_dry_validation' do +appraise 'dry_validation' do group :development, :test do remove_gem 'dry-validation' end diff --git a/CHANGELOG.md b/CHANGELOG.md index 06581d597..6e2c30ae9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ * [#2427](https://github.com/ruby-grape/grape/pull/2427): Use `rack-contrib` jsonp instead of rack-jsonp - [@ericproulx](https://github.com/ericproulx). * [#2363](https://github.com/ruby-grape/grape/pull/2363): Replace autoload by zeitwerk - [@ericproulx](https://github.com/ericproulx). * [#2425](https://github.com/ruby-grape/grape/pull/2425): Replace `{}` with `Rack::Header` or `Rack::Utils::HeaderHash` - [@dhruvCW](https://github.com/dhruvCW). +* [#2430](https://github.com/ruby-grape/grape/pull/2430): Isolate extensions within specific gemfile - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/Gemfile b/Gemfile index 2ac129f2f..fe90b51e8 100644 --- a/Gemfile +++ b/Gemfile @@ -8,8 +8,6 @@ gemspec group :development, :test do gem 'bundler' - gem 'dry-validation' - gem 'hashie' gem 'rake' gem 'rubocop', '1.59.0', require: false gem 'rubocop-performance', '1.20.1', require: false @@ -26,7 +24,6 @@ group :development do end group :test do - gem 'grape-entity', '~> 0.6', require: false gem 'rack-contrib', require: false gem 'rack-test', '< 2.1' gem 'rspec', '< 4' diff --git a/gemfiles/no_dry_validation.gemfile b/gemfiles/dry_validation.gemfile similarity index 92% rename from gemfiles/no_dry_validation.gemfile rename to gemfiles/dry_validation.gemfile index 08c74247a..65231ea80 100644 --- a/gemfiles/no_dry_validation.gemfile +++ b/gemfiles/dry_validation.gemfile @@ -4,9 +4,10 @@ source 'https://rubygems.org' +gem 'dry-validation' + group :development, :test do gem 'bundler' - gem 'hashie' gem 'rake' gem 'rubocop', '1.59.0', require: false gem 'rubocop-performance', '1.20.1', require: false @@ -23,7 +24,6 @@ group :development do end group :test do - gem 'grape-entity', '~> 0.6', require: false gem 'rack-contrib', require: false gem 'rack-test', '< 2.1' gem 'rspec', '< 4' diff --git a/gemfiles/grape_entity.gemfile b/gemfiles/grape_entity.gemfile new file mode 100644 index 000000000..241e0adb1 --- /dev/null +++ b/gemfiles/grape_entity.gemfile @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +# This file was generated by Appraisal + +source 'https://rubygems.org' + +gem 'grape-entity' + +group :development, :test do + gem 'bundler' + gem 'rake' + gem 'rubocop', '1.59.0', require: false + gem 'rubocop-performance', '1.20.1', require: false + gem 'rubocop-rspec', '2.25.0', 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 'rack-contrib', require: false + gem 'rack-test', '< 2.1' + gem 'rspec', '< 4' + gem 'ruby-grape-danger', '~> 0.2.0', require: false + gem 'simplecov', '~> 0.21.2' + gem 'simplecov-lcov', '~> 0.8.0' + gem 'test-prof', require: false +end + +platforms :jruby do + gem 'racc' +end + +gemspec path: '../' diff --git a/gemfiles/hashie.gemfile b/gemfiles/hashie.gemfile new file mode 100644 index 000000000..7bf7b2bbc --- /dev/null +++ b/gemfiles/hashie.gemfile @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +# This file was generated by Appraisal + +source 'https://rubygems.org' + +gem 'hashie' + +group :development, :test do + gem 'bundler' + gem 'rake' + gem 'rubocop', '1.59.0', require: false + gem 'rubocop-performance', '1.20.1', require: false + gem 'rubocop-rspec', '2.25.0', 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 'rack-contrib', require: false + gem 'rack-test', '< 2.1' + gem 'rspec', '< 4' + gem 'ruby-grape-danger', '~> 0.2.0', require: false + gem 'simplecov', '~> 0.21.2' + gem 'simplecov-lcov', '~> 0.8.0' + gem 'test-prof', require: false +end + +platforms :jruby do + gem 'racc' +end + +gemspec path: '../' diff --git a/gemfiles/multi_json.gemfile b/gemfiles/multi_json.gemfile index 894d95e0a..20e5e98cb 100644 --- a/gemfiles/multi_json.gemfile +++ b/gemfiles/multi_json.gemfile @@ -8,8 +8,6 @@ gem 'multi_json', require: 'multi_json' group :development, :test do gem 'bundler' - gem 'dry-validation' - gem 'hashie' gem 'rake' gem 'rubocop', '1.59.0', require: false gem 'rubocop-performance', '1.20.1', require: false @@ -26,7 +24,6 @@ group :development do end group :test do - gem 'grape-entity', '~> 0.6', require: false gem 'rack-contrib', require: false gem 'rack-test', '< 2.1' gem 'rspec', '< 4' diff --git a/gemfiles/multi_xml.gemfile b/gemfiles/multi_xml.gemfile index 38de241d8..c4f147df1 100644 --- a/gemfiles/multi_xml.gemfile +++ b/gemfiles/multi_xml.gemfile @@ -8,8 +8,6 @@ gem 'multi_xml', require: 'multi_xml' group :development, :test do gem 'bundler' - gem 'dry-validation' - gem 'hashie' gem 'rake' gem 'rubocop', '1.59.0', require: false gem 'rubocop-performance', '1.20.1', require: false @@ -26,7 +24,6 @@ group :development do end group :test do - gem 'grape-entity', '~> 0.6', require: false gem 'rack-contrib', require: false gem 'rack-test', '< 2.1' gem 'rspec', '< 4' diff --git a/gemfiles/rack_2_0.gemfile b/gemfiles/rack_2_0.gemfile index 6f3f9af22..323b23ce7 100644 --- a/gemfiles/rack_2_0.gemfile +++ b/gemfiles/rack_2_0.gemfile @@ -8,8 +8,6 @@ gem 'rack', '~> 2.0' group :development, :test do gem 'bundler' - gem 'dry-validation' - gem 'hashie' gem 'rake' gem 'rubocop', '1.59.0', require: false gem 'rubocop-performance', '1.20.1', require: false @@ -26,7 +24,6 @@ group :development do end group :test do - gem 'grape-entity', '~> 0.6', require: false gem 'rack-contrib', require: false gem 'rack-test', '< 2.1' gem 'rspec', '< 4' diff --git a/gemfiles/rack_3_0.gemfile b/gemfiles/rack_3_0.gemfile index 83df47ca6..55188980d 100644 --- a/gemfiles/rack_3_0.gemfile +++ b/gemfiles/rack_3_0.gemfile @@ -8,8 +8,6 @@ gem 'rack', '~> 3.0.0' group :development, :test do gem 'bundler' - gem 'dry-validation' - gem 'hashie' gem 'rake' gem 'rubocop', '1.59.0', require: false gem 'rubocop-performance', '1.20.1', require: false @@ -26,7 +24,6 @@ group :development do end group :test do - gem 'grape-entity', '~> 0.6', require: false gem 'rack-contrib', require: false gem 'rack-test', '< 2.1' gem 'rspec', '< 4' diff --git a/gemfiles/rack_edge.gemfile b/gemfiles/rack_edge.gemfile index c64d8488d..53ea7d831 100644 --- a/gemfiles/rack_edge.gemfile +++ b/gemfiles/rack_edge.gemfile @@ -8,8 +8,6 @@ gem 'rack', github: 'rack/rack' group :development, :test do gem 'bundler' - gem 'dry-validation' - gem 'hashie' gem 'rake' gem 'rubocop', '1.59.0', require: false gem 'rubocop-performance', '1.20.1', require: false @@ -26,7 +24,6 @@ group :development do end group :test do - gem 'grape-entity', '~> 0.6', require: false gem 'rack-contrib', require: false gem 'rack-test', '< 2.1' gem 'rspec', '< 4' diff --git a/gemfiles/rails_6_0.gemfile b/gemfiles/rails_6_0.gemfile index 15fd21e32..ae6ae7c42 100644 --- a/gemfiles/rails_6_0.gemfile +++ b/gemfiles/rails_6_0.gemfile @@ -8,8 +8,6 @@ gem 'rails', '~> 6.0.0' group :development, :test do gem 'bundler' - gem 'dry-validation' - gem 'hashie' gem 'rake' gem 'rubocop', '1.59.0', require: false gem 'rubocop-performance', '1.20.1', require: false @@ -26,7 +24,6 @@ group :development do end group :test do - gem 'grape-entity', '~> 0.6', require: false gem 'rack-contrib', require: false gem 'rack-test', '< 2.1' gem 'rspec', '< 4' diff --git a/gemfiles/rails_6_1.gemfile b/gemfiles/rails_6_1.gemfile index de78e0dab..b7553c752 100644 --- a/gemfiles/rails_6_1.gemfile +++ b/gemfiles/rails_6_1.gemfile @@ -8,8 +8,6 @@ gem 'rails', '~> 6.1' group :development, :test do gem 'bundler' - gem 'dry-validation' - gem 'hashie' gem 'rake' gem 'rubocop', '1.59.0', require: false gem 'rubocop-performance', '1.20.1', require: false @@ -26,7 +24,6 @@ group :development do end group :test do - gem 'grape-entity', '~> 0.6', require: false gem 'rack-contrib', require: false gem 'rack-test', '< 2.1' gem 'rspec', '< 4' diff --git a/gemfiles/rails_7_0.gemfile b/gemfiles/rails_7_0.gemfile index 19d4d581f..118579d80 100644 --- a/gemfiles/rails_7_0.gemfile +++ b/gemfiles/rails_7_0.gemfile @@ -8,8 +8,6 @@ gem 'rails', '~> 7.0.0' group :development, :test do gem 'bundler' - gem 'dry-validation' - gem 'hashie' gem 'rake' gem 'rubocop', '1.59.0', require: false gem 'rubocop-performance', '1.20.1', require: false @@ -26,7 +24,6 @@ group :development do end group :test do - gem 'grape-entity', '~> 0.6', require: false gem 'rack-contrib', require: false gem 'rack-test', '< 2.1' gem 'rspec', '< 4' diff --git a/gemfiles/rails_7_1.gemfile b/gemfiles/rails_7_1.gemfile index 637efd85f..610d0b6cf 100644 --- a/gemfiles/rails_7_1.gemfile +++ b/gemfiles/rails_7_1.gemfile @@ -8,8 +8,6 @@ gem 'rails', '~> 7.1.0' group :development, :test do gem 'bundler' - gem 'dry-validation' - gem 'hashie' gem 'rake' gem 'rubocop', '1.59.0', require: false gem 'rubocop-performance', '1.20.1', require: false @@ -26,7 +24,6 @@ group :development do end group :test do - gem 'grape-entity', '~> 0.6', require: false gem 'rack-contrib', require: false gem 'rack-test', '< 2.1' gem 'rspec', '< 4' diff --git a/gemfiles/rails_edge.gemfile b/gemfiles/rails_edge.gemfile index e2dc809b8..bc18d40f6 100644 --- a/gemfiles/rails_edge.gemfile +++ b/gemfiles/rails_edge.gemfile @@ -8,8 +8,6 @@ gem 'rails', github: 'rails/rails' group :development, :test do gem 'bundler' - gem 'dry-validation' - gem 'hashie' gem 'rake' gem 'rubocop', '1.59.0', require: false gem 'rubocop-performance', '1.20.1', require: false @@ -26,7 +24,6 @@ group :development do end group :test do - gem 'grape-entity', '~> 0.6', require: false gem 'rack-contrib', require: false gem 'rack-test', '< 2.1' gem 'rspec', '< 4' diff --git a/spec/grape/api/documentation_spec.rb b/spec/grape/api/documentation_spec.rb index f7c50fd49..27ec7cc53 100644 --- a/spec/grape/api/documentation_spec.rb +++ b/spec/grape/api/documentation_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe Grape::API do subject { Class.new(described_class) } diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index 75f626f1a..fcf66ab63 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -1,16 +1,11 @@ # frozen_string_literal: true require 'shared/versioning_examples' -require 'grape-entity' describe Grape::API do - subject do - Class.new(described_class) - end + subject { Class.new(described_class) } - def app - subject - end + let(:app) { subject } describe '.prefix' do it 'routes root through with the prefix' do @@ -20,7 +15,7 @@ def app end get 'awesome/sauce/' - expect(last_response.status).to be 200 + expect(last_response).to be_successful expect(last_response.body).to eql 'Hello there.' end @@ -34,7 +29,7 @@ def app expect(last_response.body).to eql 'Hello there.' get '/hello' - expect(last_response.status).to be 404 + expect(last_response).to be_not_found end it 'supports OPTIONS' do @@ -44,7 +39,7 @@ def app end options 'awesome/sauce' - expect(last_response.status).to be 204 + expect(last_response).to be_no_content expect(last_response.body).to be_blank end @@ -53,7 +48,7 @@ def app subject.get post 'awesome/sauce' - expect(last_response.status).to be 405 + expect(last_response).to be_method_not_allowed end end @@ -241,9 +236,9 @@ def app end get '/users/michael' - expect(last_response.status).to eq(404) + expect(last_response).to be_not_found get '/users/23' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful end context 'with param type definitions' do @@ -388,13 +383,13 @@ def to_txt it 'allows .json' do get '/abc.json' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eql 'abc' # json-encoded symbol end it 'allows .txt' do get '/abc.txt' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eql 'def' # raw text end end @@ -567,7 +562,7 @@ def to_txt end post '/example' - expect(last_response.status).to be 201 + expect(last_response).to be_created expect(last_response.body).to eql 'Created' end @@ -577,7 +572,7 @@ def to_txt 'example' end put '/example' - expect(last_response.status).to be 405 + expect(last_response).to be_method_not_allowed expect(last_response.body).to eql '405 Not Allowed' expect(last_response.headers['X-Custom-Header']).to eql 'foo' end @@ -595,7 +590,7 @@ def to_txt end post '/example' - expect(last_response.status).to be 405 + expect(last_response).to be_method_not_allowed expect(last_response.headers['X-Custom-Header']).to eql 'foo' end @@ -613,7 +608,7 @@ def to_txt end post '/example' - expect(last_response.status).to be 405 + expect(last_response).to be_method_not_allowed expect(last_response.headers['X-Custom-Header']).to eql 'foo' end @@ -630,7 +625,7 @@ def to_txt end options '/example' - expect(last_response.status).to be 200 + expect(last_response).to be_successful expect(last_response.body).to eql 'yup' expect(last_response.headers['Allow']).to be_nil expect(last_response.headers['X-Custom-Header-1']).to eql 'foo' @@ -647,7 +642,7 @@ def to_txt end put '/example' - expect(last_response.status).to be 405 + expect(last_response).to be_method_not_allowed expect(last_response.body).to eq <<~XML @@ -667,7 +662,7 @@ def to_txt 'example' end put '/example' - expect(last_response.status).to be 405 + expect(last_response).to be_method_not_allowed expect(last_response.body).to eql '405 Not Allowed' end end @@ -711,7 +706,7 @@ def to_txt end it 'returns a 204' do - expect(last_response.status).to be 204 + expect(last_response).to be_no_content end it 'has an empty body' do @@ -775,7 +770,7 @@ def to_txt describe 'it adds an OPTIONS route for namespaced endpoints that' do it 'returns a 204' do - expect(last_response.status).to be 204 + expect(last_response).to be_no_content end it 'has an empty body' do @@ -802,7 +797,7 @@ def to_txt end it 'returns a 204' do - expect(last_response.status).to be 204 + expect(last_response).to be_no_content end it 'has an empty body' do @@ -840,7 +835,7 @@ def to_txt end it 'returns a 405' do - expect(last_response.status).to be 405 + expect(last_response).to be_method_not_allowed end it 'contains error message in body' do @@ -879,7 +874,7 @@ def to_txt let(:response) { delete('/example') } it 'responds with a 405 status' do - expect(response.status).to be 405 + expect(response).to be_method_not_allowed end end @@ -887,7 +882,7 @@ def to_txt let(:response) { post('/example?secret=incorrect_password') } it 'responds with the defined error in the before hook' do - expect(response.status).to be 401 + expect(response).to be_unauthorized end end @@ -895,7 +890,7 @@ def to_txt let(:response) { post('/example?secret=password&namespace_secret=wrong_namespace_password') } it 'ends up in the endpoint' do - expect(response.status).to be 401 + expect(response).to be_unauthorized end end @@ -903,7 +898,7 @@ def to_txt let(:response) { post('/example?secret=password&namespace_secret=namespace_password') } it 'ends up in the endpoint' do - expect(response.status).to be 201 + expect(response).to be_created end end @@ -911,7 +906,7 @@ def to_txt let(:response) { head('/example?id=504') } it 'responds with 401 because before expectations in before hooks are not met' do - expect(response.status).to be 401 + expect(response).to be_unauthorized end end @@ -919,7 +914,7 @@ def to_txt let(:response) { head('/example?id=504&secret=password') } it 'responds with 200 because before hooks are not called' do - expect(response.status).to be 200 + expect(response).to be_successful end end end @@ -936,7 +931,7 @@ def to_txt end it 'returns a 200' do - expect(last_response.status).to be 200 + expect(last_response).to be_successful end it 'has an empty body' do @@ -952,7 +947,7 @@ def to_txt 'example' end head '/example' - expect(last_response.status).to be 400 + expect(last_response).to be_bad_request end end @@ -966,14 +961,14 @@ def to_txt it 'options does not contain HEAD' do options '/example' - expect(last_response.status).to be 204 + expect(last_response).to be_no_content expect(last_response.body).to eql '' expect(last_response.headers['Allow']).to eql 'OPTIONS, GET' end it 'does not allow HEAD on a GET request' do head '/example' - expect(last_response.status).to be 405 + expect(last_response).to be_method_not_allowed end end @@ -987,12 +982,12 @@ def to_txt it 'does not create an OPTIONS route' do options '/example' - expect(last_response.status).to be 405 + expect(last_response).to be_method_not_allowed end it 'does not include OPTIONS in Allow header' do options '/example' - expect(last_response.status).to be 405 + expect(last_response).to be_method_not_allowed expect(last_response.headers['Allow']).to eql 'GET, HEAD' end end @@ -1118,7 +1113,7 @@ def to_txt expect(d).to receive(:do_something!).once get '/123' - expect(last_response.status).to be 200 + expect(last_response).to be_successful expect(last_response.body).to eql 'got it' end @@ -1149,7 +1144,7 @@ def to_txt expect(d).to receive(:do_something!).exactly(0).times get '/4' - expect(last_response.status).to be 400 + expect(last_response).to be_bad_request expect(last_response.body).to eql 'id does not have a valid value' end @@ -1181,7 +1176,7 @@ def to_txt expect(d).to receive(:here).with(4).once get '/123' - expect(last_response.status).to be 200 + expect(last_response).to be_successful expect(last_response.body).to eql 'got it' end end @@ -1272,7 +1267,7 @@ def to_txt subject.format :json subject.get('/error') { error!('error in json', 500) } get '/error.json' - expect(last_response.status).to be 500 + expect(last_response).to be_server_error expect(last_response.content_type).to eql 'application/json' end @@ -1280,7 +1275,7 @@ def to_txt subject.format :xml subject.get('/error') { error!('error in xml', 500) } get '/error' - expect(last_response.status).to be 500 + expect(last_response).to be_server_error expect(last_response.content_type).to eql 'application/xml' end @@ -1288,7 +1283,7 @@ def to_txt subject.get(':id') { params[:format] } get '/baz.bar' - expect(last_response.status).to eq 200 + expect(last_response).to be_successful expect(last_response.body).to eq 'bar' end @@ -1297,7 +1292,7 @@ def to_txt subject.get(':id') { params } get '/baz.bar' - expect(last_response.status).to eq 404 + expect(last_response).to be_not_found end context 'with a custom content_type' do @@ -1339,7 +1334,7 @@ def to_txt it "uploads and downloads a PNG file via #{url}" do image_filename = 'grape.png' post url, file: Rack::Test::UploadedFile.new(image_filename, content_type, true) - expect(last_response.status).to eq(201) + expect(last_response).to be_created expect(last_response.content_type).to eq(content_type) expect(last_response.headers['Content-Disposition']).to eq("attachment; filename*=UTF-8''grape.png") File.open(image_filename, 'rb') do |io| @@ -1355,7 +1350,7 @@ def to_txt it 'uploads and downloads a Ruby file' do filename = __FILE__ post '/attachment.rb', file: Rack::Test::UploadedFile.new(filename, content_type, true) - expect(last_response.status).to eq(201) + expect(last_response).to be_created expect(last_response.content_type).to eq(content_type) expect(last_response.headers['Content-Disposition']).to eq("attachment; filename*=UTF-8''api_spec.rb") File.open(filename, 'rb') do |io| @@ -1472,7 +1467,7 @@ def before subject.get '/' do end get '/' - expect(last_response.status).to eq(400) + expect(last_response).to be_bad_request expect(last_response.body).to eq('Caught in the Net') end end @@ -1549,9 +1544,9 @@ def call(env) end subject.get(:hello) { 'Hello, world.' } get '/hello' - expect(last_response.status).to be 401 + expect(last_response).to be_unauthorized get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever') - expect(last_response.status).to be 200 + expect(last_response).to be_successful end it 'is scopable' do @@ -1565,9 +1560,9 @@ def call(env) end get '/hello' - expect(last_response.status).to be 200 + expect(last_response).to be_successful get '/admin/hello' - expect(last_response.status).to be 401 + expect(last_response).to be_unauthorized end it 'is callable via .auth as well' do @@ -1577,9 +1572,9 @@ def call(env) subject.get(:hello) { 'Hello, world.' } get '/hello' - expect(last_response.status).to be 401 + expect(last_response).to be_unauthorized get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever') - expect(last_response.status).to be 200 + expect(last_response).to be_successful end it 'has access to the current endpoint' do @@ -1609,9 +1604,9 @@ def authorize(u, p) subject.get(:hello) { 'Hello, world.' } get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever') - expect(last_response.status).to be 200 + expect(last_response).to be_successful get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('disallow', 'whatever') - expect(last_response.status).to be 401 + expect(last_response).to be_unauthorized end it 'can set instance variables accessible to routes' do @@ -1623,7 +1618,7 @@ def authorize(u, p) subject.get(:hello) { @hello } get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever') - expect(last_response.status).to be 200 + expect(last_response).to be_successful expect(last_response.body).to eql 'Hello, world.' end end @@ -1778,13 +1773,13 @@ def three end get '/new/abc' - expect(last_response.status).to be 404 + expect(last_response).to be_not_found get '/legacy/abc' - expect(last_response.status).to be 200 + expect(last_response).to be_successful get '/legacy/def' - expect(last_response.status).to be 404 + expect(last_response).to be_not_found get '/new/def' - expect(last_response.status).to be 200 + expect(last_response).to be_successful end end @@ -2039,7 +2034,7 @@ def foo raise 'rain!' end get '/exception' - expect(last_response.status).to be 500 + expect(last_response).to be_server_error expect(last_response.body).to eq 'rain!' end @@ -2051,7 +2046,7 @@ def foo raise 'rain!' end get '/exception' - expect(last_response.status).to be 500 + expect(last_response).to be_server_error expect(last_response.body).to eq({ error: 'rain!' }.to_json) end @@ -2061,7 +2056,7 @@ def foo subject.get('/unrescued') { raise 'beefcake' } get '/rescued' - expect(last_response.status).to be 500 + expect(last_response).to be_server_error expect { get '/unrescued' }.to raise_error(RuntimeError, 'beefcake') end @@ -2083,7 +2078,7 @@ def foo expect(last_response.status).to be 402 get '/standard_error' - expect(last_response.status).to be 401 + expect(last_response).to be_unauthorized end context 'CustomError subclass of Grape::Exceptions::Base' do @@ -2106,7 +2101,7 @@ def foo end get '/custom_error' - expect(last_response.status).to eq(400) + expect(last_response).to be_bad_request expect(last_response.body).to eq('New Error') end end @@ -2122,7 +2117,7 @@ def foo subject.get('/formatter_exception') { 'Hello world' } get '/formatter_exception' - expect(last_response.status).to be 500 + expect(last_response).to be_server_error expect(last_response.body).to eq('Formatter Error') end @@ -2132,7 +2127,7 @@ def foo expect_any_instance_of(Grape::Middleware::Error).to receive(:default_rescue_handler).and_call_original get '/' - expect(last_response.status).to be 500 + expect(last_response).to be_server_error expect(last_response.body).to eql 'Invalid response' end end @@ -2146,7 +2141,7 @@ def foo raise 'rain!' end get '/exception' - expect(last_response.status).to be 202 + expect(last_response).to be_accepted expect(last_response.body).to eq('rescued from rain!') end @@ -2165,7 +2160,7 @@ def foo raise ConnectionError end get '/exception' - expect(last_response.status).to be 500 + expect(last_response).to be_server_error expect(last_response.body).to eq('rescued from ConnectionError') end @@ -2177,7 +2172,7 @@ def foo raise ConnectionError end get '/exception' - expect(last_response.status).to be 500 + expect(last_response).to be_server_error expect(last_response.body).to eq('rescued from ConnectionError') end @@ -2189,7 +2184,7 @@ def foo raise ConnectionError end get '/exception' - expect(last_response.status).to be 500 + expect(last_response).to be_server_error expect(last_response.body).to eq('rescued from ConnectionError') end @@ -2207,10 +2202,10 @@ def foo raise DatabaseError end get '/connection' - expect(last_response.status).to be 500 + expect(last_response).to be_server_error expect(last_response.body).to eq('rescued from ConnectionError') get '/database' - expect(last_response.status).to be 500 + expect(last_response).to be_server_error expect(last_response.body).to eq('rescued from DatabaseError') end @@ -2234,7 +2229,7 @@ def foo subject.get('/rescue_lambda') { raise ArgumentError } get '/rescue_lambda' - expect(last_response.status).to eq(400) + expect(last_response).to be_bad_request expect(last_response.body).to eq('rescued with a lambda') end @@ -2245,7 +2240,7 @@ def foo subject.get('/rescue_lambda') { raise ArgumentError, 'lambda takes an argument' } get '/rescue_lambda' - expect(last_response.status).to eq(400) + expect(last_response).to be_bad_request expect(last_response.body).to eq('lambda takes an argument') end end @@ -2267,11 +2262,11 @@ def rescue_no_method_error subject.get('/rescue_no_method_error') { raise NoMethodError } get '/rescue_arg_error' - expect(last_response.status).to eq(500) + expect(last_response).to be_server_error expect(last_response.body).to eq('500 ArgumentError') get '/rescue_no_method_error' - expect(last_response.status).to eq(500) + expect(last_response).to be_server_error expect(last_response.body).to eq('500 NoMethodError') end @@ -2299,11 +2294,11 @@ def rescue_all_errors subject.get('/another_error') { raise NoMethodError } get '/argument_error' - expect(last_response.status).to eq(500) + expect(last_response).to be_server_error expect(last_response.body).to eq('500 ArgumentError') get '/another_error' - expect(last_response.status).to eq(500) + expect(last_response).to be_server_error expect(last_response.body).to eq('500 AnotherError') end end @@ -2330,9 +2325,9 @@ def rescue_all_errors end get '/caught_child' - expect(last_response.status).to be 500 + expect(last_response).to be_server_error get '/caught_parent' - expect(last_response.status).to be 500 + expect(last_response).to be_server_error expect { get '/uncaught_parent' }.to raise_error(StandardError) end @@ -2345,7 +2340,7 @@ def rescue_all_errors end get '/caught_child' - expect(last_response.status).to be 500 + expect(last_response).to be_server_error end it 'does not rescue child errors if rescue_subclasses is false' do @@ -2388,7 +2383,7 @@ def rescue_all_errors get '/grape_exception' - expect(last_response.status).to eq(403) + expect(last_response).to be_forbidden expect(last_response.body).to eq('Redefined Error') end end @@ -2629,7 +2624,7 @@ def self.call(object, _env) { x: params[:x] } end post '/data', '{"x":42}', 'CONTENT_TYPE' => 'application/json' - expect(last_response.status).to eq(201) + expect(last_response).to be_created expect(last_response.body).to eq('{"x":42}') end @@ -2646,7 +2641,7 @@ def self.call(object, _env) ['text/custom', 'text/custom; charset=UTF-8'].each do |content_type| it "uses parser for #{content_type}" do put '/simple', 'simple', 'CONTENT_TYPE' => content_type - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eql 'elpmis' end end @@ -2672,7 +2667,7 @@ def self.call(object, _env) it 'uses custom parser' do put '/simple', 'simple', 'CONTENT_TYPE' => 'text/custom' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eql 'elpmis' end end @@ -2684,7 +2679,7 @@ def self.call(object, _env) params[:tag] end put '/yaml', 'a123', 'CONTENT_TYPE' => 'application/xml' - expect(last_response.status).to eq(400) + expect(last_response).to be_bad_request expect(last_response.body).to eql 'Disallowed type attribute: "symbol"' end end @@ -2695,7 +2690,7 @@ def self.call(object, _env) params[:tag] end put '/yaml', 'a123', 'CONTENT_TYPE' => 'application/xml' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eql '{"type"=>"symbol", "__content__"=>"a123"}' end end @@ -2710,7 +2705,7 @@ def self.call(object, _env) it 'does not parse data' do put '/data', 'not valid json', 'CONTENT_TYPE' => 'application/json' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('body: not valid json') end end @@ -2727,7 +2722,7 @@ def self.call(object, _env) { x: 42 } end get '/data' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('{"x":42}') end @@ -2736,7 +2731,7 @@ def self.call(object, _env) { x: params[:x] } end post '/data', '{"x":42}', 'CONTENT_TYPE' => '' - expect(last_response.status).to eq(201) + expect(last_response).to be_created expect(last_response.body).to eq('{"x":42}') end end @@ -2749,7 +2744,7 @@ def self.call(object, _env) raise 'rain!' end get '/exception' - expect(last_response.status).to be 200 + expect(last_response).to be_successful end it 'has a default error status' do @@ -2758,7 +2753,7 @@ def self.call(object, _env) raise 'rain!' end get '/exception' - expect(last_response.status).to be 500 + expect(last_response).to be_server_error end it 'uses the default error status in error!' do @@ -2768,45 +2763,7 @@ def self.call(object, _env) error! 'rain!' end get '/exception' - expect(last_response.status).to be 400 - end - end - - context 'http_codes' do - let(:error_presenter) do - Class.new(Grape::Entity) do - expose :code - expose :static - - def static - 'some static text' - end - end - end - - it 'is used as presenter' do - subject.desc 'some desc', http_codes: [ - [408, 'Unauthorized', error_presenter] - ] - - subject.get '/exception' do - error!({ code: 408 }, 408) - end - - get '/exception' - expect(last_response.status).to be 408 - expect(last_response.body).to eql({ code: 408, static: 'some static text' }.to_json) - end - - it 'presented with' do - error = { code: 408, with: error_presenter }.freeze - subject.get '/exception' do - error! error, 408 - end - - get '/exception' - expect(last_response.status).to be 408 - expect(last_response.body).to eql({ code: 408, static: 'some static text' }.to_json) + expect(last_response).to be_bad_request end end @@ -3359,7 +3316,7 @@ def static end get '/mounted/fail' - expect(last_response.status).to be 202 + expect(last_response).to be_accepted expect(last_response.body).to eq('rescued from doh!') end @@ -3442,7 +3399,7 @@ def static mount app => '/mounted' end get '/mounted/cool/awesome' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('sauce') end @@ -3457,10 +3414,10 @@ def static app3.mount app1 => '/app1' app1.mount app2 => '/app2' get '/app1/app2/nice' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('play') options '/app1/app2/nice' - expect(last_response.status).to eq(204) + expect(last_response).to be_no_content end it 'responds to options' do @@ -3478,15 +3435,15 @@ def static end get '/apples/colour' - expect(last_response.status).to be 200 + expect(last_response).to be_successful expect(last_response.body).to eq('red') options '/apples/colour' - expect(last_response.status).to be 204 + expect(last_response).to be_no_content get '/apples/pears/colour' - expect(last_response.status).to be 200 + expect(last_response).to be_successful expect(last_response.body).to eq('green') options '/apples/pears/colour' - expect(last_response.status).to be 204 + expect(last_response).to be_no_content end it 'responds to options with path versioning' do @@ -3500,10 +3457,10 @@ def static end get '/v1/apples/colour' - expect(last_response.status).to be 200 + expect(last_response).to be_successful expect(last_response.body).to eq('red') options '/v1/apples/colour' - expect(last_response.status).to be 204 + expect(last_response).to be_no_content end it 'mounts a versioned API with nested resources' do @@ -3566,7 +3523,7 @@ def static subject.mount api get '/users' - expect(last_response.status).to eq(401) + expect(last_response).to be_unauthorized get '/users', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('username', 'password') expect(last_response.body).to eq({ users: true }.to_json) @@ -3621,10 +3578,10 @@ def static subject.mount b => '/two' get '/one/v1/hello' - expect(last_response.status).to eq 200 + expect(last_response).to be_successful get '/two/v1/world' - expect(last_response.status).to eq 200 + expect(last_response).to be_successful end context 'when mounting class extends a subclass of Grape::API' do @@ -3815,7 +3772,7 @@ def my_method it 'does not accept extensions other than specified' do get '/meaning_of_life.json' - expect(last_response.status).to eq(404) + expect(last_response).to be_not_found end it 'forces txt from a non-accepting header' do @@ -3921,7 +3878,7 @@ def serializable_hash 'example' end get '/example' - expect(last_response.status).to eq(500) + expect(last_response).to be_server_error expect(last_response.body).to eq <<~XML @@ -3938,7 +3895,7 @@ def serializable_hash } end get '/example' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq <<~XML @@ -3953,7 +3910,7 @@ def serializable_hash %w[example1 example2] end get '/example' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq <<~XML @@ -4044,19 +4001,19 @@ def before error!("Unrecognized request path: #{params[:path]} - #{env['PATH_INFO']}#{env['SCRIPT_NAME']}", 404) end get '/v1/hello' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('v1') get '/v2/hello' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('v2') options '/v2/hello' - expect(last_response.status).to eq(204) + expect(last_response).to be_no_content expect(last_response.body).to be_blank head '/v2/hello' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to be_blank get '/foobar' - expect(last_response.status).to eq(404) + expect(last_response).to be_not_found expect(last_response.body).to eq('Unrecognized request path: foobar - /foobar') end end @@ -4067,14 +4024,14 @@ def before it 'cascades' do subject.version 'v1', using: :path, cascade: true get '/v1/hello' - expect(last_response.status).to eq(404) + expect(last_response).to be_not_found expect(last_response.headers[Grape::Http::Headers::X_CASCADE]).to eq('pass') end it 'does not cascade' do subject.version 'v2', using: :path, cascade: false get '/v2/hello' - expect(last_response.status).to eq(404) + expect(last_response).to be_not_found expect(last_response.headers.keys).not_to include Grape::Http::Headers::X_CASCADE end end @@ -4083,14 +4040,14 @@ def before it 'cascades' do subject.cascade true get '/hello' - expect(last_response.status).to eq(404) + expect(last_response).to be_not_found expect(last_response.headers[Grape::Http::Headers::X_CASCADE]).to eq('pass') end it 'does not cascade' do subject.cascade false get '/hello' - expect(last_response.status).to eq(404) + expect(last_response).to be_not_found expect(last_response.headers.keys).not_to include Grape::Http::Headers::X_CASCADE end end @@ -4145,7 +4102,7 @@ def before it 'returns blank body' do get '/blank' - expect(last_response.status).to eq(204) + expect(last_response).to be_no_content expect(last_response.body).to be_blank end end @@ -4161,7 +4118,7 @@ def before it 'returns blank body' do get '/text' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq 'Hello World' end end @@ -4370,7 +4327,7 @@ def uniqe_id_route end get '/' - expect(last_response.status).to be 400 + expect(last_response).to be_bad_request expect(last_response.body).to eq({ my_var: expected_instance_variable_value }.to_json) end @@ -4405,7 +4362,7 @@ def uniqe_id_route end get '/books/other' - expect(last_response.status).to be 404 + expect(last_response).to be_not_found end end end diff --git a/spec/grape/dsl/validations_spec.rb b/spec/grape/dsl/validations_spec.rb deleted file mode 100644 index 3bb63cc97..000000000 --- a/spec/grape/dsl/validations_spec.rb +++ /dev/null @@ -1,65 +0,0 @@ -# frozen_string_literal: true - -module Grape - module DSL - module ValidationsSpec - class Dummy - include Grape::DSL::Validations - end - end - - describe Validations do - subject { ValidationsSpec::Dummy } - - describe '.reset_validations!' do - before do - subject.namespace_stackable :declared_params, ['dummy'] - subject.namespace_stackable :validations, ['dummy'] - subject.namespace_stackable :params, ['dummy'] - subject.route_setting :description, description: 'lol', params: ['dummy'] - subject.reset_validations! - end - - after do - subject.unset_route_setting :description - end - - it 'resets declared params' do - expect(subject.namespace_stackable(:declared_params)).to eq [] - end - - it 'resets validations' do - expect(subject.namespace_stackable(:validations)).to eq [] - end - - it 'resets params' do - expect(subject.namespace_stackable(:params)).to eq [] - end - - it 'does not reset documentation description' do - expect(subject.route_setting(:description)[:description]).to eq 'lol' - end - end - - describe '.params' do - it 'returns a ParamsScope' do - expect(subject.params).to be_a Grape::Validations::ParamsScope - end - - it 'evaluates block' do - expect { subject.params { raise 'foo' } }.to raise_error RuntimeError, 'foo' - end - end - - describe '.contract' do - it 'saves the schema instance' do - expect(subject.contract(Dry::Schema.Params)).to be_a Grape::Validations::ContractScope - end - - it 'errors without params or block' do - expect { subject.contract }.to raise_error(ArgumentError) - end - end - end - end -end diff --git a/spec/grape/endpoint/declared_spec.rb b/spec/grape/endpoint/declared_spec.rb index b73538fdb..dcf2fe9d6 100644 --- a/spec/grape/endpoint/declared_spec.rb +++ b/spec/grape/endpoint/declared_spec.rb @@ -3,9 +3,7 @@ describe Grape::Endpoint do subject { Class.new(Grape::API) } - def app - subject - end + let(:app) { subject } describe '#declared' do before do @@ -59,19 +57,6 @@ def app 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 @@ -92,7 +77,7 @@ def app end get '/declared?first=present' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(JSON.parse(last_response.body)['nested']['fourth']).to be_nil end @@ -102,7 +87,7 @@ def app end get '/declared?first=present' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(JSON.parse(last_response.body)['multiple_types']).to be_nil end @@ -122,7 +107,7 @@ def app declared(params) end get '/declared?first=present' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(JSON.parse(last_response.body).keys.size).to eq(12) end @@ -131,7 +116,7 @@ def app declared(params) end get '/declared?first=one' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(JSON.parse(last_response.body)['third']).to eql('third-default') end @@ -141,7 +126,7 @@ def app end get '/declared?first=present&nested[fourth]=1' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(JSON.parse(last_response.body)['nested'].keys.size).to eq 9 end @@ -153,7 +138,7 @@ def app subject.post('/declared') { declared(params) } post '/declared', first: 'present', second: ['present'] - expect(last_response.status).to eq(201) + expect(last_response).to be_created body = JSON.parse(last_response.body) expect(body['second']).to eq(['present']) @@ -175,7 +160,7 @@ def app end get '/declared?first=present&nested[][fourth]=1&nested[][fourth]=2' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(JSON.parse(last_response.body)['nested'].size).to eq 2 end @@ -186,7 +171,7 @@ def app it 'sets nested objects to be nil' do get '/declared?first=present' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(JSON.parse(last_response.body)['nested']).to be_nil end end @@ -198,7 +183,7 @@ def app it 'sets objects with type=Hash to be a hash' do get '/declared?first=present' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful body = JSON.parse(last_response.body) expect(body['empty_hash']).to eq({}) @@ -210,7 +195,7 @@ def app it 'sets objects with type=Set to be a set' do get '/declared?first=present' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful body = JSON.parse(last_response.body) expect(['#', []]).to include(body['empty_set']) @@ -221,7 +206,7 @@ def app it 'sets objects with type=Array to be an array' do get '/declared?first=present' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful body = JSON.parse(last_response.body) expect(body['empty_arr']).to eq([]) @@ -234,7 +219,7 @@ def app it 'includes all declared children when type=Hash' do get '/declared?first=present' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful body = JSON.parse(last_response.body) expect(body['nested'].keys).to eq(%w[fourth fifth nested_two nested_arr empty_arr empty_typed_arr empty_hash empty_set empty_typed_set]) @@ -248,7 +233,7 @@ def app declared(params) end get '/declared?first=one&other=two' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(JSON.parse(last_response.body).key?(:other)).to be false end @@ -258,7 +243,7 @@ def app end get '/declared?first=one&other=two' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(JSON.parse(last_response.body)['first']).to eq 'one' end @@ -269,7 +254,7 @@ def app end get '/declared?first=one&other=two' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful end it 'does not include renamed missing attributes if that option is passed' do @@ -282,7 +267,7 @@ def app end get '/declared?first=one&other=two' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful end it 'includes attributes with value that evaluates to false' do @@ -297,7 +282,7 @@ def app end post '/declared', ::Grape::Json.dump(first: 'one', boolean: false), 'CONTENT_TYPE' => 'application/json' - expect(last_response.status).to eq(201) + expect(last_response).to be_created end it 'includes attributes with value that evaluates to nil' do @@ -312,7 +297,7 @@ def app end post '/declared', ::Grape::Json.dump(first: 'one', second: nil), 'CONTENT_TYPE' => 'application/json' - expect(last_response.status).to eq(201) + expect(last_response).to be_created end it 'includes missing attributes with defaults when there are nested hashes' do @@ -339,7 +324,7 @@ def app 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(last_response).to be_successful expect(json['first']).to eq 'present' expect(json['nested'].keys).to eq %w[fourth fifth nested_nested] expect(json['nested']['fourth']).to eq '' @@ -367,7 +352,7 @@ def app get '/declared?first=present&nested[fourth]=4' json = JSON.parse(last_response.body) - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(json['first']).to eq 'present' expect(json['nested'].keys).to eq %w[fourth] expect(json['nested']['fourth']).to eq '4' @@ -409,7 +394,7 @@ def app let(:parsed_response) { JSON.parse(last_response.body, symbolize_names: true) } - it { expect(last_response.status).to eq 200 } + it { expect(last_response).to be_successful } context 'with include_parent_namespaces: false' do it 'returns declared parameters only from current namespace' do @@ -483,7 +468,7 @@ def app it 'can access parent attributes' do get '/something/123/another/456/more/789' - expect(last_response.status).to eq 200 + expect(last_response).to be_successful json = JSON.parse(last_response.body, symbolize_names: true) # test all three levels of params @@ -518,7 +503,7 @@ def app it 'can access parent route_param' do get '/users/123/foo', bar: 'bar' - expect(last_response.status).to eq 200 + expect(last_response).to be_successful json = JSON.parse(last_response.body, symbolize_names: true) expect(json[:declared_params][:id]).to eq 123 diff --git a/spec/grape/extensions/param_builders/hashie/mash_spec.rb b/spec/grape/extensions/param_builders/hashie/mash_spec.rb deleted file mode 100644 index 7b58c1d3f..000000000 --- a/spec/grape/extensions/param_builders/hashie/mash_spec.rb +++ /dev/null @@ -1,109 +0,0 @@ -# frozen_string_literal: true - -describe Grape::Extensions::Hashie::Mash::ParamBuilder do - subject { Class.new(Grape::API) } - - def app - subject - end - - describe 'in an endpoint' do - describe '#params' do - before do - subject.params do - build_with Grape::Extensions::Hashie::Mash::ParamBuilder # rubocop:disable RSpec/DescribedClass - end - - subject.get do - params.class - end - end - - it 'is of type Hashie::Mash' do - get '/' - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('Hashie::Mash') - end - end - end - - describe 'in an api' do - before do - subject.send(:include, Grape::Extensions::Hashie::Mash::ParamBuilder) # rubocop:disable RSpec/DescribedClass - end - - describe '#params' do - before do - subject.get do - params.class - end - end - - it 'is Hashie::Mash' do - get '/' - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('Hashie::Mash') - end - end - - context 'in a nested namespace api' do - before do - subject.namespace :foo do - get do - params.class - end - end - end - - it 'is Hashie::Mash' do - get '/foo' - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('Hashie::Mash') - end - end - - it 'is indifferent to key or symbol access' do - subject.params do - build_with Grape::Extensions::Hashie::Mash::ParamBuilder # rubocop:disable RSpec/DescribedClass - requires :a, type: String - end - subject.get '/foo' do - [params[:a], params['a']] - end - - get '/foo', a: 'bar' - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('["bar", "bar"]') - end - - it 'does not overwrite route_param with a regular param if they have same name' do - subject.namespace :route_param do - route_param :foo do - get { params.to_json } - end - end - - get '/route_param/bar', foo: 'baz' - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('{"foo":"bar"}') - end - - it 'does not overwrite route_param with a defined regular param if they have same name' do - subject.namespace :route_param do - params do - build_with Grape::Extensions::Hashie::Mash::ParamBuilder # rubocop:disable RSpec/DescribedClass - requires :foo, type: String - end - route_param :foo do - get do - [params[:foo], params['foo']] - end - end - end - - get '/route_param/bar', foo: 'baz' - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('["bar", "bar"]') - end - end -end diff --git a/spec/grape/middleware/error_spec.rb b/spec/grape/middleware/error_spec.rb index 08ae7d460..88eb88725 100644 --- a/spec/grape/middleware/error_spec.rb +++ b/spec/grape/middleware/error_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'grape-entity' - describe Grape::Middleware::Error do let(:error_entity) do Class.new(Grape::Entity) do @@ -26,12 +24,12 @@ def call(_env) end let(:options) { { default_message: 'Aww, hamburgers.' } } - def app + let(:app) do opts = options context = self Rack::Builder.app do use Spec::Support::EndpointFaker - use Grape::Middleware::Error, **opts + use Grape::Middleware::Error, **opts # rubocop:disable RSpec/DescribedClass run context.err_app end end @@ -57,7 +55,7 @@ def app it 'defaults to a 500 status' do err_app.error = {} get '/' - expect(last_response.status).to eq(500) + expect(last_response).to be_server_error end it 'has a default message' do @@ -75,13 +73,5 @@ def app expect(last_response.body).to eq({ code: 200 }.to_json) end - - it 'presents an error message' do - entity = error_entity - err_app.error = { message: { code: 200, with: entity } } - get '/' - - expect(last_response.body).to eq({ code: 200, static: 'static text' }.to_json) - end end end diff --git a/spec/grape/request_spec.rb b/spec/grape/request_spec.rb index eaf9a6ac9..4c235ebf2 100644 --- a/spec/grape/request_spec.rb +++ b/spec/grape/request_spec.rb @@ -1,131 +1,113 @@ # frozen_string_literal: true -module Grape - describe Request do - let(:default_method) { 'GET' } - let(:default_params) { {} } - let(:default_options) do +describe Grape::Request do + let(:default_method) { 'GET' } + let(:default_params) { {} } + let(:default_options) do + { + method: method, + params: params + } + end + let(:default_env) do + Rack::MockRequest.env_for('/', options) + end + let(:method) { default_method } + let(:params) { default_params } + let(:options) { default_options } + let(:env) { default_env } + + let(:request) do + described_class.new(env) + end + + describe '#params' do + let(:params) do { - method: method, - params: params + a: '123', + b: 'xyz' } end - let(:default_env) do - Rack::MockRequest.env_for('/', options) + + it 'by default returns stringified parameter keys' do + expect(request.params).to eq(ActiveSupport::HashWithIndifferentAccess.new('a' => '123', 'b' => 'xyz')) end - let(:method) { default_method } - let(:params) { default_params } - let(:options) { default_options } - let(:env) { default_env } - let(:request) do - described_class.new(env) + context 'when build_params_with: Grape::Extensions::Hash::ParamBuilder is specified' do + let(:request) do + described_class.new(env, build_params_with: Grape::Extensions::Hash::ParamBuilder) + end + + it 'returns symbolized params' do + expect(request.params).to eq(a: '123', b: 'xyz') + end end - describe '#params' do - let(:params) do + describe 'with grape.routing_args' do + let(:options) do + default_options.merge('grape.routing_args' => routing_args) + end + let(:routing_args) do { - a: '123', - b: 'xyz' + version: '123', + route_info: '456', + c: 'ccc' } end - it 'by default returns stringified parameter keys' do - expect(request.params).to eq(ActiveSupport::HashWithIndifferentAccess.new('a' => '123', 'b' => 'xyz')) + it 'cuts version and route_info' do + expect(request.params).to eq(ActiveSupport::HashWithIndifferentAccess.new(a: '123', b: 'xyz', c: 'ccc')) end + end + end - context 'when build_params_with: Grape::Extensions::Hash::ParamBuilder is specified' do - let(:request) do - described_class.new(env, build_params_with: Grape::Extensions::Hash::ParamBuilder) - end + describe '#headers' do + let(:options) do + default_options.merge(request_headers) + end - it 'returns symbolized params' do - expect(request.params).to eq(a: '123', b: 'xyz') - end + describe 'with http headers in env' do + let(:request_headers) do + { + 'HTTP_X_GRAPE_IS_COOL' => 'yeah' + } + end + let(:x_grape_is_cool_header) do + 'x-grape-is-cool' end - describe 'with grape.routing_args' do - let(:options) do - default_options.merge('grape.routing_args' => routing_args) - end - let(:routing_args) do - { - version: '123', - route_info: '456', - c: 'ccc' - } - end - - it 'cuts version and route_info' do - expect(request.params).to eq(ActiveSupport::HashWithIndifferentAccess.new(a: '123', b: 'xyz', c: 'ccc')) - end + it 'cuts HTTP_ prefix and capitalizes header name words' do + expect(request.headers).to eq(x_grape_is_cool_header => 'yeah') end end - describe 'when the build_params_with is set to Hashie' do - subject(:request_params) { described_class.new(env, **opts).params } - - context 'when the API does not include a specific param builder' do - let(:opts) { {} } - - it { is_expected.to be_a(Hash) } + describe 'with non-HTTP_* stuff in env' do + let(:request_headers) do + { + 'HTP_X_GRAPE_ENTITY_TOO' => 'but now we are testing Grape' + } end - context 'when the API includes a specific param builder' do - let(:opts) { { build_params_with: Grape::Extensions::Hashie::Mash::ParamBuilder } } - - it { is_expected.to be_a(Hashie::Mash) } + it 'does not include them' do + expect(request.headers).to eq({}) end end - describe '#headers' do - let(:options) do - default_options.merge(request_headers) + describe 'with symbolic header names' do + let(:request_headers) do + { + HTTP_GRAPE_LIKES_SYMBOLIC: 'it is true' + } end - - describe 'with http headers in env' do - let(:request_headers) do - { - 'HTTP_X_GRAPE_IS_COOL' => 'yeah' - } - end - let(:x_grape_is_cool_header) do - 'x-grape-is-cool' - end - - it 'cuts HTTP_ prefix and capitalizes header name words' do - expect(request.headers).to eq(x_grape_is_cool_header => 'yeah') - end + let(:env) do + default_env.merge(request_headers) end - - describe 'with non-HTTP_* stuff in env' do - let(:request_headers) do - { - 'HTP_X_GRAPE_ENTITY_TOO' => 'but now we are testing Grape' - } - end - - it 'does not include them' do - expect(request.headers).to eq({}) - end + let(:grape_likes_symbolic_header) do + 'grape-likes-symbolic' end - describe 'with symbolic header names' do - let(:request_headers) do - { - HTTP_GRAPE_LIKES_SYMBOLIC: 'it is true' - } - end - let(:env) do - default_env.merge(request_headers) - end - let(:grape_likes_symbolic_header) do - 'grape-likes-symbolic' - end - - it 'converts them to string' do - expect(request.headers).to eq(grape_likes_symbolic_header => 'it is true') - end + it 'converts them to string' do + expect(request.headers).to eq(grape_likes_symbolic_header => 'it is true') end end end diff --git a/spec/grape/validations/contract_scope_spec.rb b/spec/grape/validations/contract_scope_spec.rb deleted file mode 100644 index c17378bc9..000000000 --- a/spec/grape/validations/contract_scope_spec.rb +++ /dev/null @@ -1,179 +0,0 @@ -# frozen_string_literal: true - -require 'pry' - -describe Grape::Validations::ContractScope do - let(:validated_params) { {} } - let(:app) do - vp = validated_params - - Class.new(Grape::API) do - after_validation do - vp.replace(params) - end - end - end - - context 'with simple schema, pre-defined' do - let(:contract) do - Dry::Schema.Params do - required(:number).filled(:integer) - end - end - - before do - app.contract(contract) - app.post('/required') - end - - it 'coerces the parameter value one level deep' do - post '/required', number: '1' - expect(last_response.status).to eq(201) - expect(validated_params).to eq('number' => 1) - end - - it 'shows expected validation error' do - post '/required' - expect(last_response.status).to eq(400) - expect(last_response.body).to eq('number is missing') - end - end - - context 'with contract class' do - let(:contract) do - Class.new(Dry::Validation::Contract) do - params do - required(:number).filled(:integer) - required(:name).filled(:string) - end - - rule(:number) do - key.failure('is too high') if value > 5 - end - end - end - - before do - app.contract(contract) - app.post('/required') - end - - it 'coerces the parameter' do - post '/required', number: '1', name: '2' - expect(last_response.status).to eq(201) - expect(validated_params).to eq('number' => 1, 'name' => '2') - end - - it 'shows expected validation error' do - post '/required', number: '6' - expect(last_response.status).to eq(400) - expect(last_response.body).to eq('name is missing, number is too high') - end - end - - context 'with nested schema' do - before do - app.contract do - required(:home).hash do - required(:address).hash do - required(:number).filled(:integer) - end - end - required(:turns).array(:integer) - end - - app.post('/required') - end - - it 'keeps unknown parameters' do - post '/required', home: { address: { number: '1', street: 'Baker' } }, turns: %w[2 3] - expect(last_response.status).to eq(201) - expected = { 'home' => { 'address' => { 'number' => 1, 'street' => 'Baker' } }, 'turns' => [2, 3] } - expect(validated_params).to eq(expected) - end - - it 'shows expected validation error' do - post '/required', home: { address: { something: 'else' } } - expect(last_response.status).to eq(400) - expect(last_response.body).to eq('home[address][number] is missing, turns is missing') - end - end - - context 'with mixed validation sources' do - before do - app.resource :foos do - route_param :foo_id, type: Integer do - contract do - required(:number).filled(:integer) - end - post('/required') - end - end - end - - it 'combines the coercions' do - post '/foos/123/required', number: '1' - expect(last_response.status).to eq(201) - expected = { 'foo_id' => 123, 'number' => 1 } - expect(validated_params).to eq(expected) - end - - it 'shows validation error for missing' do - post '/foos/123/required' - expect(last_response.status).to eq(400) - expect(last_response.body).to eq('number is missing') - end - - it 'includes keys from all sources into declared' do - declared_params = nil - - app.after_validation do - declared_params = declared(params) - end - - post '/foos/123/required', number: '1', string: '2' - expect(last_response.status).to eq(201) - expected = { 'foo_id' => 123, 'number' => 1 } - expect(validated_params).to eq(expected.merge('string' => '2')) - expect(declared_params).to eq(expected) - end - end - - context 'with schema config validate_keys=true' do - it 'validates the whole params hash' do - app.resource :foos do - route_param :foo_id do - contract do - config.validate_keys = true - - required(:number).filled(:integer) - required(:foo_id).filled(:integer) - end - post('/required') - end - end - - post '/foos/123/required', number: '1' - expect(last_response.status).to eq(201) - expected = { 'foo_id' => 123, 'number' => 1 } - expect(validated_params).to eq(expected) - end - - it 'fails validation for any parameters not in schema' do - app.resource :foos do - route_param :foo_id, type: Integer do - contract do - config.validate_keys = true - - required(:number).filled(:integer) - end - post('/required') - end - end - - post '/foos/123/required', number: '1' - expect(last_response.status).to eq(400) - expect(last_response.body).to eq('foo_id is not allowed') - end - end -end diff --git a/spec/grape/validations/validators/coerce_spec.rb b/spec/grape/validations/validators/coerce_spec.rb index 0afe720e5..6b8f50068 100644 --- a/spec/grape/validations/validators/coerce_spec.rb +++ b/spec/grape/validations/validators/coerce_spec.rb @@ -1,13 +1,9 @@ # frozen_string_literal: true describe Grape::Validations::Validators::CoerceValidator do - subject do - Class.new(Grape::API) - end + subject { Class.new(Grape::API) } - def app - subject - end + let(:app) { subject } describe 'coerce' do let(:secure_uri_only) do @@ -42,7 +38,7 @@ def self.parsed?(value) end get '/single', age: '43a' - expect(last_response.status).to eq(400) + expect(last_response).to be_bad_request expect(last_response.body).to eq('年龄格式不正确') end @@ -57,7 +53,7 @@ def self.parsed?(value) end get '/single', age: '43a' - expect(last_response.status).to eq(400) + expect(last_response).to be_bad_request expect(last_response.body).to eq('age is invalid') end end @@ -72,11 +68,11 @@ def self.parsed?(value) end get '/single', int: '43a' - expect(last_response.status).to eq(400) + expect(last_response).to be_bad_request expect(last_response.body).to eq('int type cast is invalid') get '/single', int: '43' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('int works') end @@ -102,19 +98,19 @@ def self.parsed?(value) it 'respects :coerce_with' do get '/', a: 'yup' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('TrueClass') end it 'still validates type' do get '/', a: 'false' - expect(last_response.status).to eq(400) + expect(last_response).to be_bad_request expect(last_response.body).to eq('a type cast is invalid') end it 'performs no additional coercion' do get '/', a: 'true' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('String') end end @@ -129,11 +125,11 @@ def self.parsed?(value) end get '/single', int: '43a' - expect(last_response.status).to eq(400) + expect(last_response).to be_bad_request expect(last_response.body).to eq('int is invalid') get '/single', int: '43' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('int works') end @@ -146,11 +142,11 @@ def self.parsed?(value) end get 'array', ids: %w[1 2 az] - expect(last_response.status).to eq(400) + expect(last_response).to be_bad_request expect(last_response.body).to eq('ids is invalid') get 'array', ids: %w[1 2 890] - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('array int works') end @@ -167,7 +163,7 @@ def self.parsed?(value) end post '/bigdecimal', { bigdecimal: 45.1 }.to_json, headers - expect(last_response.status).to eq(201) + expect(last_response).to be_created expect(last_response.body).to eq('BigDecimal 45.1') end @@ -180,7 +176,7 @@ def self.parsed?(value) end post '/boolean', { boolean: 'true' }.to_json, headers - expect(last_response.status).to eq(201) + expect(last_response).to be_created expect(last_response.body).to eq('true') end end @@ -194,7 +190,7 @@ def self.parsed?(value) end get '/bigdecimal', bigdecimal: '45' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('BigDecimal') end @@ -207,7 +203,7 @@ def self.parsed?(value) end get '/int', int: '45' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq(integer_class_name) end @@ -220,11 +216,11 @@ def self.parsed?(value) end get '/string', string: 45 - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('String') get '/string', string: nil - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('NilClass') end @@ -240,12 +236,12 @@ def self.parsed?(value) get 'secure_uri', uri: 'https://www.example.com' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('URI::HTTPS') get 'secure_uri', uri: 'http://www.example.com' - expect(last_response.status).to eq(400) + expect(last_response).to be_bad_request expect(last_response.body).to eq('uri is invalid') end @@ -270,7 +266,7 @@ def self.parse(_val) get 'whatever', name: 'Bob' - expect(last_response.status).to eq(400) + expect(last_response).to be_bad_request expect(last_response.body).to eq('name must be unique') end end @@ -286,7 +282,7 @@ def self.parse(_val) end get '/array', arry: %w[1 2 3] - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq(integer_class_name) end @@ -299,7 +295,7 @@ def self.parse(_val) end get 'array', arry: [1, 0] - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('TrueClass') end @@ -311,7 +307,7 @@ def self.parse(_val) params[:uri][0].class end get 'uri_array', uri: ['http://www.google.com'] - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('URI::HTTP') end @@ -323,7 +319,7 @@ def self.parse(_val) "#{params[:uri].class},#{params[:uri].first.class},#{params[:uri].size}" end get 'uri_array', uri: Array.new(2) { 'http://www.example.com' } - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('Set,URI::HTTP,1') end @@ -336,10 +332,10 @@ def self.parse(_val) params[:uri].first.class end get 'secure_uris', uri: ['https://www.example.com'] - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('URI::HTTPS') get 'secure_uris', uri: ['https://www.example.com', 'http://www.example.com'] - expect(last_response.status).to eq(400) + expect(last_response).to be_bad_request expect(last_response.body).to eq('uri is invalid') end end @@ -354,7 +350,7 @@ def self.parse(_val) end get '/set', set: Set.new([1, 2, 3, 4]).to_a - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq(integer_class_name) end @@ -367,7 +363,7 @@ def self.parse(_val) end get '/set', set: Set.new([1, 0]).to_a - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('TrueClass') end end @@ -381,7 +377,7 @@ def self.parse(_val) end get '/boolean', boolean: 1 - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('TrueClass') end @@ -398,11 +394,11 @@ def self.parse(_val) end post '/upload', file: file - expect(last_response.status).to eq(201) + expect(last_response).to be_created expect(last_response.body).to eq(filename) post '/upload', file: 'not a file' - expect(last_response.status).to eq(400) + expect(last_response).to be_bad_request expect(last_response.body).to eq('file is invalid') end @@ -415,15 +411,15 @@ def self.parse(_val) end post '/upload', file: file - expect(last_response.status).to eq(201) + expect(last_response).to be_created expect(last_response.body).to eq(filename) post '/upload', file: 'not a file' - expect(last_response.status).to eq(400) + expect(last_response).to be_bad_request expect(last_response.body).to eq('file is invalid') post '/upload', file: { filename: 'fake file', tempfile: '/etc/passwd' } - expect(last_response.status).to eq(400) + expect(last_response).to be_bad_request expect(last_response.body).to eq('file is invalid') end @@ -436,7 +432,7 @@ def self.parse(_val) end post '/upload', files: [file] - expect(last_response.status).to eq(201) + expect(last_response).to be_created expect(last_response.body).to eq(filename) end end @@ -452,7 +448,7 @@ def self.parse(_val) end get '/int', integers: { int: '45' } - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq(integer_class_name) end @@ -468,7 +464,7 @@ def self.parse(_val) end get '/nil_value', param: nil - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('NilClass') end end @@ -485,7 +481,7 @@ def self.parse(_val) end get '/nil_value', param: nil - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('NilClass') end end @@ -502,7 +498,7 @@ def self.parse(_val) end get '/nil_value', param: nil - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('NilClass') end end @@ -521,7 +517,7 @@ def self.parse(_val) end get '/nil_value', param: nil - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('NilClass') end end @@ -541,7 +537,7 @@ def self.parse(_val) end get '/empty_string', param: '' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('NilClass') end end @@ -555,7 +551,7 @@ def self.parse(_val) end get '/empty_string', param: '' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('String') end end @@ -571,7 +567,7 @@ def self.parse(_val) end get '/empty_string', param: '' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('NilClass') end end @@ -588,7 +584,7 @@ def self.parse(_val) end get '/empty_string', param: '' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('NilClass') end end @@ -607,7 +603,7 @@ def self.parse(_val) end get '/empty_string', param: '' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('NilClass') end end @@ -626,11 +622,11 @@ def self.parse(_val) end get '/ints', values: '1 2 3 4' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(JSON.parse(last_response.body)).to eq([1, 2, 3, 4]) get '/ints', values: 'a b c d' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(JSON.parse(last_response.body)).to eq([0, 0, 0, 0]) end @@ -643,11 +639,11 @@ def self.parse(_val) end get '/strings', values: '1 2 3 4' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(JSON.parse(last_response.body)).to eq(%w[1 2 3 4]) get '/strings', values: 'a b c d' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(JSON.parse(last_response.body)).to eq(%w[a b c d]) end @@ -660,19 +656,19 @@ def self.parse(_val) 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(last_response).to be_created expect(JSON.parse(last_response.body)).to eq([%w[a b c d]]) post '/coerce_nested_strings', ::Grape::Json.dump(values: [%w[a c], %w[b]]), 'CONTENT_TYPE' => 'application/json' - expect(last_response.status).to eq(201) + expect(last_response).to be_created expect(JSON.parse(last_response.body)).to eq([%w[a c], %w[b]]) post '/coerce_nested_strings', ::Grape::Json.dump(values: [[]]), 'CONTENT_TYPE' => 'application/json' - expect(last_response.status).to eq(201) + expect(last_response).to be_created expect(JSON.parse(last_response.body)).to eq([[]]) post '/coerce_nested_strings', ::Grape::Json.dump(values: [['a', { bar: 0 }], ['b']]), 'CONTENT_TYPE' => 'application/json' - expect(last_response.status).to eq(400) + expect(last_response).to be_bad_request end it 'parses parameters with Array[Integer] type' do @@ -684,11 +680,11 @@ def self.parse(_val) end get '/ints', values: '1 2 3 4' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(JSON.parse(last_response.body)).to eq([1, 2, 3, 4]) get '/ints', values: 'a b c d' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(JSON.parse(last_response.body)).to eq([0, 0, 0, 0]) end @@ -701,11 +697,11 @@ def self.parse(_val) end get '/ints', values: [1, 2, 3, 4] - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(JSON.parse(last_response.body)).to eq([2, 3, 4, 5]) get '/ints', values: %w[a b c d] - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(JSON.parse(last_response.body)).to eq([1, 1, 1, 1]) end @@ -728,21 +724,21 @@ def self.parse(_val) it 'coerce nil value to array' do get '/', arr: nil - expect(last_response.status).to eq(200) + expect(last_response).to be_successful 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).to be_successful 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).to be_successful expect(last_response.body).to eq('Array') end end @@ -760,15 +756,15 @@ def self.parse(_val) end get '/ints', ints: [{ i: 1, j: '2' }] - expect(last_response.status).to eq(400) + expect(last_response).to be_bad_request expect(last_response.body).to eq('ints is invalid') get '/ints', ints: '{"i":1,"j":"2"}' - expect(last_response.status).to eq(400) + expect(last_response).to be_bad_request expect(last_response.body).to eq('ints is invalid') get '/ints', ints: '[{"i":"1","j":"2"}]' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('coercion works') end @@ -783,15 +779,15 @@ def self.parse(_val) end get '/ints', ints: '{"int":"3"}' - expect(last_response.status).to eq(400) + expect(last_response).to be_bad_request expect(last_response.body).to eq('ints[int] is invalid') get '/ints', ints: '{"int":"three"}' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('3') get '/ints', ints: '{"int":3}' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('3') end @@ -814,21 +810,21 @@ def self.parse(_val) it 'coerce nil value to integer' do get '/', int: nil - expect(last_response.status).to eq(200) + expect(last_response).to be_successful 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).to be_successful 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).to be_successful expect(last_response.body).to eq('Integer') end end @@ -855,21 +851,21 @@ def self.parse(_val) it 'accepts value that coerces to nil' do get '/', int: '0' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('NilClass') end it 'coerces to Integer' do get '/', int: '1' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('Integer') end it 'returns invalid value if coercion returns a wrong type' do get '/', int: 'lol' - expect(last_response.status).to eq(400) + expect(last_response).to be_bad_request expect(last_response.body).to eq('int is invalid') end end @@ -903,35 +899,35 @@ def self.parse(_val) end get '/', splines: '{"x":1,"ints":[1,2,3],"obj":{"y":"woof"}}' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('woof') get '/', splines: { x: 1, ints: [1, 2, 3], obj: { y: 'woof' } } - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('woof') get '/', splines: '[{"x":2,"ints":[]},{"x":3,"ints":[4],"obj":{"y":"quack"}}]' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('arrays work') get '/', splines: [{ x: 2, ints: [5] }, { x: 3, ints: [4], obj: { y: 'quack' } }] - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('arrays work') get '/', splines: '{"x":4,"ints":[2]}' - expect(last_response.status).to eq(400) + expect(last_response).to be_bad_request expect(last_response.body).to eq('splines[x] does not have a valid value') get '/', splines: { x: 4, ints: [2] } - expect(last_response.status).to eq(400) + expect(last_response).to be_bad_request expect(last_response.body).to eq('splines[x] does not have a valid value') get '/', splines: '[{"x":1,"ints":[]},{"x":4,"ints":[]}]' - expect(last_response.status).to eq(400) + expect(last_response).to be_bad_request expect(last_response.body).to eq('splines[x] does not have a valid value') get '/', splines: [{ x: 1, ints: [5] }, { x: 4, ints: [6] }] - expect(last_response.status).to eq(400) + expect(last_response).to be_bad_request expect(last_response.body).to eq('splines[x] does not have a valid value') end @@ -954,19 +950,19 @@ def self.parse(_val) end get '/', splines: '{"x":1,"ints":[1,2,3],"obj":{"y":"woof"}}' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('woof') get '/', splines: '[{"x":2,"ints":[]},{"x":3,"ints":[4],"obj":{"y":"quack"}}]' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('arrays work') get '/', splines: '{"x":4,"ints":[2]}' - expect(last_response.status).to eq(400) + expect(last_response).to be_bad_request expect(last_response.body).to eq('splines[x] does not have a valid value') get '/', splines: '[{"x":1,"ints":[]},{"x":4,"ints":[]}]' - expect(last_response.status).to eq(400) + expect(last_response).to be_bad_request expect(last_response.body).to eq('splines[x] does not have a valid value') end @@ -984,19 +980,19 @@ def self.parse(_val) end get '/', splines: '{"x":"1","y":"woof"}' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq("#{integer_class_name}.String") get '/', splines: '[{"x":1,"y":2},{"x":1,"y":"quack"}]' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq("#{integer_class_name}.#{integer_class_name}") get '/', splines: '{"x":"4","y":"woof"}' - expect(last_response.status).to eq(400) + expect(last_response).to be_bad_request expect(last_response.body).to eq('splines[x] does not have a valid value') get '/', splines: '[{"x":"4","y":"woof"}]' - expect(last_response.status).to eq(400) + expect(last_response).to be_bad_request expect(last_response.body).to eq('splines[x] does not have a valid value') end @@ -1029,15 +1025,15 @@ def self.parse(_val) end get '/', a: 'true' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('TrueClass') get '/', a: '5' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq(integer_class_name) get '/', a: 'anything else' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('String') end @@ -1050,11 +1046,11 @@ def self.parse(_val) end get '/', a: true - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('TrueClass') get '/', a: 'not good' - expect(last_response.status).to eq(400) + expect(last_response).to be_bad_request expect(last_response.body).to eq('a is invalid') end @@ -1078,127 +1074,55 @@ def self.parse(_val) it 'allows singular form declaration' do get '/', a: 'one way' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('"one way"') get '/', a: %w[the other] - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('["the", "other"]') get '/', a: { a: 1, b: 2 } - expect(last_response.status).to eq(400) + expect(last_response).to be_bad_request expect(last_response.body).to eq('a is invalid') get '/', a: [1, 2, 3] - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('["1", "2", "3"]') end it 'allows multiple collection types' do get '/', b: [1, 2, 3] - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('[1, 2, 3]') get '/', b: %w[1 2 3] - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('[1, 2, 3]') get '/', b: [1, true, 'three'] - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('["1", "true", "three"]') end it 'allows collections with multiple types' do get '/', c: [1, '2', true, 'three'] - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('[1, 2, "true", "three"]') get '/', d: '1' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('1') get '/', d: 'one' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('"one"') get '/', d: %w[1 two] - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('#') end end - context 'when params is Hashie::Mash' do - context 'for primitive collections' do - before do - subject.params do - build_with Grape::Extensions::Hashie::Mash::ParamBuilder - optional :a, types: [String, Array[String]] - optional :b, types: [Array[Integer], Array[String]] - optional :c, type: Array[Integer, String] - optional :d, types: [Integer, String, Set[Integer, String]] - end - subject.get '/' do - ( - params.a || - params.b || - params.c || - params.d - ).inspect - end - end - - it 'allows singular form declaration' do - get '/', a: 'one way' - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('"one way"') - - get '/', a: %w[the other] - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('#') - - get '/', a: { a: 1, b: 2 } - expect(last_response.status).to eq(400) - expect(last_response.body).to eq('a is invalid') - - get '/', a: [1, 2, 3] - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('#') - end - - it 'allows multiple collection types' do - get '/', b: [1, 2, 3] - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('#') - - get '/', b: %w[1 2 3] - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('#') - - get '/', b: [1, true, 'three'] - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('#') - end - - it 'allows collections with multiple types' do - get '/', c: [1, '2', true, 'three'] - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('#') - - get '/', d: '1' - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('1') - - get '/', d: 'one' - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('"one"') - - get '/', d: %w[1 two] - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('#') - end - end - end - context 'custom coercion rules' do before do subject.params do @@ -1220,19 +1144,19 @@ def self.parse(_val) it 'respects :coerce_with' do get '/', a: 'yup' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('TrueClass') end it 'still validates type' do get '/', a: 'false' - expect(last_response.status).to eq(400) + expect(last_response).to be_bad_request expect(last_response.body).to eq('a is invalid') end it 'performs no additional coercion' do get '/', a: 'true' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('String') end end diff --git a/spec/integration/dry_validation/dry_validation_spec.rb b/spec/integration/dry_validation/dry_validation_spec.rb new file mode 100644 index 000000000..d7b2f8efa --- /dev/null +++ b/spec/integration/dry_validation/dry_validation_spec.rb @@ -0,0 +1,239 @@ +# frozen_string_literal: true + +describe 'Dry::Schema', if: defined?(Dry::Schema) do + describe 'Grape::DSL::Validations' do + subject { app } + + let(:app) do + Class.new do + include Grape::DSL::Validations + end + end + + describe '.reset_validations!' do + before do + subject.namespace_stackable :declared_params, ['dummy'] + subject.namespace_stackable :validations, ['dummy'] + subject.namespace_stackable :params, ['dummy'] + subject.route_setting :description, description: 'lol', params: ['dummy'] + subject.reset_validations! + end + + after do + subject.unset_route_setting :description + end + + it 'resets declared params' do + expect(subject.namespace_stackable(:declared_params)).to be_empty + end + + it 'resets validations' do + expect(subject.namespace_stackable(:validations)).to be_empty + end + + it 'resets params' do + expect(subject.namespace_stackable(:params)).to be_empty + end + + it 'does not reset documentation description' do + expect(subject.route_setting(:description)[:description]).to eq 'lol' + end + end + + describe '.params' do + it 'returns a ParamsScope' do + expect(subject.params).to be_a Grape::Validations::ParamsScope + end + + it 'evaluates block' do + expect { subject.params { raise 'foo' } }.to raise_error RuntimeError, 'foo' + end + end + + describe '.contract' do + it 'saves the schema instance' do + expect(subject.contract(Dry::Schema.Params)).to be_a Grape::Validations::ContractScope + end + + it 'errors without params or block' do + expect { subject.contract }.to raise_error(ArgumentError) + end + end + end + + describe 'Grape::Validations::ContractScope' do + let(:validated_params) { {} } + let(:app) do + vp = validated_params + + Class.new(Grape::API) do + after_validation do + vp.replace(params) + end + end + end + + context 'with simple schema, pre-defined' do + let(:contract) do + Dry::Schema.Params do + required(:number).filled(:integer) + end + end + + before do + app.contract(contract) + app.post('/required') + end + + it 'coerces the parameter value one level deep' do + post '/required', number: '1' + expect(last_response).to be_created + expect(validated_params).to eq('number' => 1) + end + + it 'shows expected validation error' do + post '/required' + expect(last_response).to be_bad_request + expect(last_response.body).to eq('number is missing') + end + end + + context 'with contract class' do + let(:contract) do + Class.new(Dry::Validation::Contract) do + params do + required(:number).filled(:integer) + required(:name).filled(:string) + end + + rule(:number) do + key.failure('is too high') if value > 5 + end + end + end + + before do + app.contract(contract) + app.post('/required') + end + + it 'coerces the parameter' do + post '/required', number: '1', name: '2' + expect(last_response).to be_created + expect(validated_params).to eq('number' => 1, 'name' => '2') + end + + it 'shows expected validation error' do + post '/required', number: '6' + expect(last_response).to be_bad_request + expect(last_response.body).to eq('name is missing, number is too high') + end + end + + context 'with nested schema' do + before do + app.contract do + required(:home).hash do + required(:address).hash do + required(:number).filled(:integer) + end + end + required(:turns).array(:integer) + end + + app.post('/required') + end + + it 'keeps unknown parameters' do + post '/required', home: { address: { number: '1', street: 'Baker' } }, turns: %w[2 3] + expect(last_response).to be_created + expected = { 'home' => { 'address' => { 'number' => 1, 'street' => 'Baker' } }, 'turns' => [2, 3] } + expect(validated_params).to eq(expected) + end + + it 'shows expected validation error' do + post '/required', home: { address: { something: 'else' } } + expect(last_response).to be_bad_request + expect(last_response.body).to eq('home[address][number] is missing, turns is missing') + end + end + + context 'with mixed validation sources' do + before do + app.resource :foos do + route_param :foo_id, type: Integer do + contract do + required(:number).filled(:integer) + end + post('/required') + end + end + end + + it 'combines the coercions' do + post '/foos/123/required', number: '1' + expect(last_response).to be_created + expected = { 'foo_id' => 123, 'number' => 1 } + expect(validated_params).to eq(expected) + end + + it 'shows validation error for missing' do + post '/foos/123/required' + expect(last_response).to be_bad_request + expect(last_response.body).to eq('number is missing') + end + + it 'includes keys from all sources into declared' do + declared_params = nil + + app.after_validation do + declared_params = declared(params) + end + + post '/foos/123/required', number: '1', string: '2' + expect(last_response).to be_created + expected = { 'foo_id' => 123, 'number' => 1 } + expect(validated_params).to eq(expected.merge('string' => '2')) + expect(declared_params).to eq(expected) + end + end + + context 'with schema config validate_keys=true' do + it 'validates the whole params hash' do + app.resource :foos do + route_param :foo_id do + contract do + config.validate_keys = true + + required(:number).filled(:integer) + required(:foo_id).filled(:integer) + end + post('/required') + end + end + + post '/foos/123/required', number: '1' + expect(last_response).to be_created + expected = { 'foo_id' => 123, 'number' => 1 } + expect(validated_params).to eq(expected) + end + + it 'fails validation for any parameters not in schema' do + app.resource :foos do + route_param :foo_id, type: Integer do + contract do + config.validate_keys = true + + required(:number).filled(:integer) + end + post('/required') + end + end + + post '/foos/123/required', number: '1' + expect(last_response).to be_bad_request + expect(last_response.body).to eq('foo_id is not allowed') + end + end + end +end diff --git a/spec/grape/entity_spec.rb b/spec/integration/grape_entity/entity_spec.rb similarity index 69% rename from spec/grape/entity_spec.rb rename to spec/integration/grape_entity/entity_spec.rb index bb422414d..cdac039a6 100644 --- a/spec/grape/entity_spec.rb +++ b/spec/integration/grape_entity/entity_spec.rb @@ -1,16 +1,22 @@ # frozen_string_literal: true -require 'grape_entity' require 'rack/contrib/jsonp' -describe Grape::Entity do - subject { Class.new(Grape::API) } +describe 'Grape::Entity', if: defined?(Grape::Entity) do + describe '#present' do + subject { Class.new(Grape::API) } - def app - subject - end + let(:app) { subject } + + before do + stub_const('TestObject', Class.new) + stub_const('FakeCollection', Class.new do + def first + TestObject.new + end + end) + end - describe '#present' do it 'sets the object as the body if no options are provided' do inner_body = nil subject.get '/example' do @@ -21,18 +27,8 @@ def app expect(inner_body).to eql(abc: 'def') end - it 'calls through to the provided entity class if one is given' do - entity_mock = Object.new - allow(entity_mock).to receive(:represent) - - subject.get '/example' do - present Object.new, with: entity_mock - end - get '/example' - end - it 'pulls a representation from the class options if it exists' do - entity = Class.new(described_class) + entity = Class.new(Grape::Entity) allow(entity).to receive(:represent).and_return('Hiya') subject.represent Object, with: entity @@ -44,23 +40,16 @@ def app end it 'pulls a representation from the class options if the presented object is a collection of objects' do - entity = Class.new(described_class) + entity = Class.new(Grape::Entity) allow(entity).to receive(:represent).and_return('Hiya') - test_object_class = Class.new - fake_collection_class = Class.new do - define_method(:first) do - test_object_class.new - end - end - - subject.represent test_object_class, with: entity + subject.represent TestObject, with: entity subject.get '/example' do - present [test_object_class.new] + present [TestObject.new] end subject.get '/example2' do - present fake_collection_class.new + present FakeCollection.new end get '/example' @@ -71,7 +60,7 @@ def app end it 'pulls a representation from the class ancestor if it exists' do - entity = Class.new(described_class) + entity = Class.new(Grape::Entity) allow(entity).to receive(:represent).and_return('Hiya') subclass = Class.new(Object) @@ -86,7 +75,7 @@ def app it 'automatically uses Klass::Entity if that exists' do some_model = Class.new - entity = Class.new(described_class) + entity = Class.new(Grape::Entity) allow(entity).to receive(:represent).and_return('Auto-detect!') some_model.const_set :Entity, entity @@ -100,7 +89,7 @@ def app it 'automatically uses Klass::Entity based on the first object in the collection being presented' do some_model = Class.new - entity = Class.new(described_class) + entity = Class.new(Grape::Entity) allow(entity).to receive(:represent).and_return('Auto-detect!') some_model.const_set :Entity, entity @@ -113,7 +102,7 @@ def app end it 'does not run autodetection for Entity when explicitly provided' do - entity = Class.new(described_class) + entity = Class.new(Grape::Entity) some_array = [] subject.get '/example' do @@ -125,7 +114,7 @@ def app end it 'does not use #first method on ActiveRecord::Relation to prevent needless sql query' do - entity = Class.new(described_class) + entity = Class.new(Grape::Entity) some_relation = Class.new some_model = Class.new @@ -169,7 +158,7 @@ def app %i[json serializable_hash].each do |format| it "presents with #{format}" do - entity = Class.new(described_class) + entity = Class.new(Grape::Entity) entity.root 'examples', 'example' entity.expose :id @@ -186,12 +175,12 @@ def initialize(id) end get '/example' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('{"example":{"id":1}}') end it "presents with #{format} collection" do - entity = Class.new(described_class) + entity = Class.new(Grape::Entity) entity.root 'examples', 'example' entity.expose :id @@ -209,13 +198,13 @@ def initialize(id) end get '/examples' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('{"examples":[{"id":1},{"id":2}]}') end end it 'presents with xml' do - entity = Class.new(described_class) + entity = Class.new(Grape::Entity) entity.root 'examples', 'example' entity.expose :name @@ -232,7 +221,7 @@ def initialize(args) present c.new(name: 'johnnyiller'), with: entity end get '/example' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.content_type).to eq('application/xml') expect(last_response.body).to eq <<~XML @@ -245,7 +234,7 @@ def initialize(args) end it 'presents with json' do - entity = Class.new(described_class) + entity = Class.new(Grape::Entity) entity.root 'examples', 'example' entity.expose :name @@ -262,16 +251,15 @@ def initialize(args) present c.new(name: 'johnnyiller'), with: entity end get '/example' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.content_type).to eq('application/json') expect(last_response.body).to eq('{"example":{"name":"johnnyiller"}}') end it 'presents with jsonp utilising Rack::JSONP' do - # Include JSONP middleware subject.use Rack::JSONP - entity = Class.new(described_class) + entity = Class.new(Grape::Entity) entity.root 'examples', 'example' entity.expose :name @@ -294,7 +282,7 @@ def initialize(args) end get '/example?callback=abcDef' - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.content_type).to eq('application/javascript') expect(last_response.body).to include 'abcDef({"example":{"name":"johnnyiller"}})' end @@ -311,7 +299,7 @@ def initialize(args) user1 = user.new(name: 'user1') user2 = user.new(name: 'user2') - entity = Class.new(described_class) + entity = Class.new(Grape::Entity) entity.expose :name subject.format :json @@ -330,4 +318,104 @@ def initialize(args) end end end + + describe 'Grape::Middleware::Error' do + let(:error_entity) do + Class.new(Grape::Entity) do + expose :code + expose :static + + def static + 'static text' + end + end + end + let(:options) { { default_message: 'Aww, hamburgers.' } } + + let(:error_app) do + Class.new do + class << self + attr_accessor :error, :format + + def call(_env) + throw :error, error + end + end + end + end + + let(:app) do + opts = options + Rack::Builder.app do + use Spec::Support::EndpointFaker + use Grape::Middleware::Error, **opts + run ErrApp + end + end + + before do + stub_const('ErrApp', error_app) + stub_const('ErrorEntity', error_entity) + end + + context 'with http code' do + it 'presents an error message' do + ErrApp.error = { message: { code: 200, with: ErrorEntity } } + get '/' + + expect(last_response.body).to eq({ code: 200, static: 'static text' }.to_json) + end + end + end + + describe 'error_presenter' do + subject { last_response } + + let(:error_presenter) do + Class.new(Grape::Entity) do + expose :code + expose :static + + def static + 'some static text' + end + end + end + + before do + stub_const('ErrorPresenter', error_presenter) + get '/exception' + end + + context 'when using http_codes' do + let(:app) do + Class.new(Grape::API) do + desc 'some desc', http_codes: [[408, 'Unauthorized', ErrorPresenter]] + get '/exception' do + error!({ code: 408 }, 408) + end + end + end + + it 'is used as presenter' do + expect(subject).to be_request_timeout + expect(subject.body).to eql({ code: 408, static: 'some static text' }.to_json) + end + end + + context 'when using with' do + let(:app) do + Class.new(Grape::API) do + get '/exception' do + error!({ code: 408, with: ErrorPresenter }, 408) + end + end + end + + it 'presented with' do + expect(subject).to be_request_timeout + expect(subject.body).to eql({ code: 408, static: 'some static text' }.to_json) + end + end + end end diff --git a/spec/integration/hashie/hashie_spec.rb b/spec/integration/hashie/hashie_spec.rb new file mode 100644 index 000000000..094c686db --- /dev/null +++ b/spec/integration/hashie/hashie_spec.rb @@ -0,0 +1,304 @@ +# frozen_string_literal: true + +describe 'Hashie', if: defined?(Hashie) do + subject { Class.new(Grape::API) } + + let(:app) { subject } + + describe 'Grape::Extensions::Hashie::Mash::ParamBuilder' do + describe 'in an endpoint' do + describe '#params' do + before do + subject.params do + build_with Grape::Extensions::Hashie::Mash::ParamBuilder + end + + subject.get do + params.class + end + end + + it 'is of type Hashie::Mash' do + get '/' + expect(last_response).to be_successful + expect(last_response.body).to eq('Hashie::Mash') + end + end + end + + describe 'in an api' do + before do + subject.send(:include, Grape::Extensions::Hashie::Mash::ParamBuilder) + end + + describe '#params' do + before do + subject.get do + params.class + end + end + + it 'is Hashie::Mash' do + get '/' + expect(last_response).to be_successful + expect(last_response.body).to eq('Hashie::Mash') + end + end + + context 'in a nested namespace api' do + before do + subject.namespace :foo do + get do + params.class + end + end + end + + it 'is Hashie::Mash' do + get '/foo' + expect(last_response).to be_successful + expect(last_response.body).to eq('Hashie::Mash') + end + end + + it 'is indifferent to key or symbol access' do + subject.params do + build_with Grape::Extensions::Hashie::Mash::ParamBuilder + requires :a, type: String + end + subject.get '/foo' do + [params[:a], params['a']] + end + + get '/foo', a: 'bar' + expect(last_response).to be_successful + expect(last_response.body).to eq('["bar", "bar"]') + end + + it 'does not overwrite route_param with a regular param if they have same name' do + subject.namespace :route_param do + route_param :foo do + get { params.to_json } + end + end + + get '/route_param/bar', foo: 'baz' + expect(last_response).to be_successful + expect(last_response.body).to eq('{"foo":"bar"}') + end + + it 'does not overwrite route_param with a defined regular param if they have same name' do + subject.namespace :route_param do + params do + build_with Grape::Extensions::Hashie::Mash::ParamBuilder + requires :foo, type: String + end + route_param :foo do + get do + [params[:foo], params['foo']] + end + end + end + + get '/route_param/bar', foo: 'baz' + expect(last_response).to be_successful + expect(last_response.body).to eq('["bar", "bar"]') + end + end + end + + describe 'Grape::Request' do + let(:default_method) { 'GET' } + let(:default_params) { {} } + let(:default_options) do + { + method: method, + params: params + } + end + let(:default_env) do + Rack::MockRequest.env_for('/', options) + end + let(:method) { default_method } + let(:params) { default_params } + let(:options) { default_options } + let(:env) { default_env } + let(:request) { Grape::Request.new(env) } + + describe '#params' do + let(:params) do + { + a: '123', + b: 'xyz' + } + end + + it 'by default returns stringified parameter keys' do + expect(request.params).to eq(ActiveSupport::HashWithIndifferentAccess.new('a' => '123', 'b' => 'xyz')) + end + + context 'when build_params_with: Grape::Extensions::Hash::ParamBuilder is specified' do + let(:request) { Grape::Request.new(env, build_params_with: Grape::Extensions::Hash::ParamBuilder) } + + it 'returns symbolized params' do + expect(request.params).to eq(a: '123', b: 'xyz') + end + end + + describe 'with grape.routing_args' do + let(:options) do + default_options.merge('grape.routing_args' => routing_args) + end + let(:routing_args) do + { + version: '123', + route_info: '456', + c: 'ccc' + } + end + + it 'cuts version and route_info' do + expect(request.params).to eq(ActiveSupport::HashWithIndifferentAccess.new(a: '123', b: 'xyz', c: 'ccc')) + end + end + end + + describe 'when the build_params_with is set to Hashie' do + subject(:request_params) { Grape::Request.new(env, **opts).params } + + context 'when the API includes a specific param builder' do + let(:opts) { { build_params_with: Grape::Extensions::Hashie::Mash::ParamBuilder } } + + it { is_expected.to be_a(Hashie::Mash) } + end + end + end + + describe 'Grape::Validations::Validators::CoerceValidator' do + context 'when params is Hashie::Mash' do + context 'for primitive collections' do + before do + subject.params do + build_with Grape::Extensions::Hashie::Mash::ParamBuilder + optional :a, types: [String, Array[String]] + optional :b, types: [Array[Integer], Array[String]] + optional :c, type: Array[Integer, String] + optional :d, types: [Integer, String, Set[Integer, String]] + end + subject.get '/' do + ( + params.a || + params.b || + params.c || + params.d + ).inspect + end + end + + it 'allows singular form declaration' do + get '/', a: 'one way' + expect(last_response).to be_successful + expect(last_response.body).to eq('"one way"') + + get '/', a: %w[the other] + expect(last_response).to be_successful + expect(last_response.body).to eq('#') + + get '/', a: { a: 1, b: 2 } + expect(last_response).to be_bad_request + expect(last_response.body).to eq('a is invalid') + + get '/', a: [1, 2, 3] + expect(last_response).to be_successful + expect(last_response.body).to eq('#') + end + + it 'allows multiple collection types' do + get '/', b: [1, 2, 3] + expect(last_response).to be_successful + expect(last_response.body).to eq('#') + + get '/', b: %w[1 2 3] + expect(last_response).to be_successful + expect(last_response.body).to eq('#') + + get '/', b: [1, true, 'three'] + expect(last_response).to be_successful + expect(last_response.body).to eq('#') + end + + it 'allows collections with multiple types' do + get '/', c: [1, '2', true, 'three'] + expect(last_response).to be_successful + expect(last_response.body).to eq('#') + + get '/', d: '1' + expect(last_response).to be_successful + expect(last_response.body).to eq('1') + + get '/', d: 'one' + expect(last_response).to be_successful + expect(last_response.body).to eq('"one"') + + get '/', d: %w[1 two] + expect(last_response).to be_successful + expect(last_response.body).to eq('#') + end + end + end + end + + describe 'Grape::Endpoint' do + before do + subject.format :json + subject.params do + requires :first + optional :second + optional :third, default: 'third-default' + optional :multiple_types, types: [Integer, String] + optional :nested, type: Hash do + optional :fourth + optional :fifth + optional :nested_two, type: Hash do + optional :sixth + optional :nested_three, type: Hash do + optional :seventh + end + end + optional :nested_arr, type: Array do + optional :eighth + end + optional :empty_arr, type: Array + optional :empty_typed_arr, type: Array[String] + optional :empty_hash, type: Hash + optional :empty_set, type: Set + optional :empty_typed_set, type: Set[String] + end + optional :arr, type: Array do + optional :nineth + end + optional :empty_arr, type: Array + optional :empty_typed_arr, type: Array[String] + optional :empty_hash, type: Hash + optional :empty_hash_two, type: Hash + optional :empty_set, type: Set + optional :empty_typed_set, type: Set[String] + end + end + + context 'when params are not built with default class' do + it 'returns an object that corresponds with the params class - hashie mash' do + subject.params do + build_with 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 + end + end +end diff --git a/spec/integration/multi_json/json_spec.rb b/spec/integration/multi_json/json_spec.rb index a4227cdde..994a0aeb2 100644 --- a/spec/integration/multi_json/json_spec.rb +++ b/spec/integration/multi_json/json_spec.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true -describe Grape::Json, if: defined?(::MultiJson) do - it 'uses multi_json' do - expect(described_class).to eq(::MultiJson) - end +# grape_entity depends on multi-json and it breaks the test. +describe Grape::Json, if: defined?(::MultiJson) && !defined?(Grape::Entity) do + subject { described_class } + + it { is_expected.to eq(::MultiJson) } end diff --git a/spec/integration/multi_xml/xml_spec.rb b/spec/integration/multi_xml/xml_spec.rb index 9dc4b5094..cda102b1d 100644 --- a/spec/integration/multi_xml/xml_spec.rb +++ b/spec/integration/multi_xml/xml_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true describe Grape::Xml, if: defined?(MultiXml) do - it 'uses multi_xml' do - expect(described_class).to eq(::MultiXml) - end + subject { described_class } + + it { is_expected.to eq(::MultiXml) } end diff --git a/spec/integration/no_dry_validation/no_dry_validation_spec.rb b/spec/integration/no_dry_validation/no_dry_validation_spec.rb deleted file mode 100644 index 3954ad07a..000000000 --- a/spec/integration/no_dry_validation/no_dry_validation_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', '..', 'lib')) -require 'grape' - -describe Grape do - let(:app) do - Class.new(Grape::API) do - resource :foos do - params do - requires :type, type: String - optional :limit, type: Integer - end - get do - declared(params).to_json - end - end - end - end - - it 'executes request normally' do - get '/foos', type: 'bar', limit: 4, qux: 'tee' - - expect(last_response.status).to eq(200) - result = JSON.parse(last_response.body) - expect(result).to eq({ 'type' => 'bar', 'limit' => 4 }) - end -end From 79b2784df17346941b79f588c3add4260926689e Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 16 Apr 2024 12:09:08 +0200 Subject: [PATCH 220/304] Drop Appraisals replace by eval_gemfile Update test gems including rubocop Regenerate Rubocop's todo --- .rubocop_todo.yml | 56 ++++++++++++++--- Appraisals | 60 ------------------- Gemfile | 17 +++--- gemfiles/dry_validation.gemfile | 37 +----------- gemfiles/grape_entity.gemfile | 37 +----------- gemfiles/hashie.gemfile | 37 +----------- gemfiles/multi_json.gemfile | 37 +----------- gemfiles/multi_xml.gemfile | 37 +----------- gemfiles/rack_2_0.gemfile | 37 +----------- gemfiles/rack_3_0.gemfile | 37 +----------- gemfiles/rack_edge.gemfile | 37 +----------- gemfiles/rails_6_0.gemfile | 37 +----------- gemfiles/rails_6_1.gemfile | 37 +----------- gemfiles/rails_7_0.gemfile | 37 +----------- gemfiles/rails_7_1.gemfile | 37 +----------- gemfiles/rails_edge.gemfile | 37 +----------- spec/grape/api_remount_spec.rb | 30 +++++----- spec/grape/api_spec.rb | 4 +- .../exceptions/validation_errors_spec.rb | 4 +- spec/grape/middleware/auth/base_spec.rb | 10 +--- spec/grape/middleware/auth/strategies_spec.rb | 13 ++-- spec/support/basic_auth_encode_helpers.rb | 2 - spec/support/chunks.rb | 2 +- 23 files changed, 94 insertions(+), 585 deletions(-) delete mode 100644 Appraisals diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index faac38752..6569f408c 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 5000` -# on 2024-04-15 16:22:26 UTC using RuboCop version 1.59.0. +# on 2024-04-16 09:59:31 UTC using RuboCop version 1.63.2. # 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 @@ -246,6 +246,52 @@ RSpec/VoidExpect: - 'spec/grape/api_spec.rb' - 'spec/grape/dsl/headers_spec.rb' +# Offense count: 598 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: ResponseMethods. +# ResponseMethods: response, last_response +RSpecRails/HaveHttpStatus: + Exclude: + - 'spec/grape/api/custom_validations_spec.rb' + - 'spec/grape/api/deeply_included_options_spec.rb' + - 'spec/grape/api/defines_boolean_in_params_spec.rb' + - 'spec/grape/api/invalid_format_spec.rb' + - 'spec/grape/api/mount_and_helpers_order_spec.rb' + - 'spec/grape/api/mount_and_rescue_from_spec.rb' + - 'spec/grape/api/namespace_parameters_in_route_spec.rb' + - 'spec/grape/api/optional_parameters_in_route_spec.rb' + - 'spec/grape/api/parameters_modification_spec.rb' + - 'spec/grape/api/patch_method_helpers_spec.rb' + - 'spec/grape/api/required_parameters_in_route_spec.rb' + - 'spec/grape/api/required_parameters_with_invalid_method_spec.rb' + - 'spec/grape/api/routes_with_requirements_spec.rb' + - 'spec/grape/api/shared_helpers_exactly_one_of_spec.rb' + - 'spec/grape/api/shared_helpers_spec.rb' + - 'spec/grape/api_spec.rb' + - 'spec/grape/endpoint_spec.rb' + - 'spec/grape/exceptions/body_parse_errors_spec.rb' + - 'spec/grape/exceptions/invalid_accept_header_spec.rb' + - 'spec/grape/exceptions/validation_errors_spec.rb' + - 'spec/grape/extensions/param_builders/hash_spec.rb' + - 'spec/grape/extensions/param_builders/hash_with_indifferent_access_spec.rb' + - 'spec/grape/integration/global_namespace_function_spec.rb' + - 'spec/grape/integration/rack_spec.rb' + - 'spec/grape/middleware/error_spec.rb' + - 'spec/grape/middleware/exception_spec.rb' + - 'spec/grape/validations/params_scope_spec.rb' + - 'spec/grape/validations/validators/all_or_none_spec.rb' + - 'spec/grape/validations/validators/allow_blank_spec.rb' + - 'spec/grape/validations/validators/at_least_one_of_spec.rb' + - 'spec/grape/validations/validators/default_spec.rb' + - 'spec/grape/validations/validators/exactly_one_of_spec.rb' + - 'spec/grape/validations/validators/mutual_exclusion_spec.rb' + - 'spec/grape/validations/validators/presence_spec.rb' + - 'spec/grape/validations/validators/regexp_spec.rb' + - 'spec/grape/validations/validators/same_as_spec.rb' + - 'spec/grape/validations/validators/values_spec.rb' + - 'spec/grape/validations_spec.rb' + - 'spec/shared/versioning_examples.rb' + # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). Style/CombinableLoops: @@ -259,14 +305,6 @@ Style/CombinableLoops: Style/FormatStringToken: EnforcedStyle: template -# Offense count: 1 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AllowedReceivers. -# AllowedReceivers: Thread.current -Style/HashEachMethods: - Exclude: - - 'lib/grape/middleware/stack.rb' - # Offense count: 12 # Configuration parameters: AllowedMethods. # AllowedMethods: respond_to_missing? diff --git a/Appraisals b/Appraisals deleted file mode 100644 index e9a6f2a42..000000000 --- a/Appraisals +++ /dev/null @@ -1,60 +0,0 @@ -# frozen_string_literal: true - -customize_gemfiles do - { - single_quotes: true, - heading: "frozen_string_literal: true - -This file was generated by Appraisal" - } -end - -appraise 'rails-6-0' do - gem 'rails', '~> 6.0.0' -end - -appraise 'rails-6-1' do - gem 'rails', '~> 6.1' -end - -appraise 'rails-7-0' do - gem 'rails', '~> 7.0.0' -end - -appraise 'rails-7-1' do - gem 'rails', '~> 7.1.0' -end - -appraise 'rails-edge' do - gem 'rails', github: 'rails/rails' -end - -appraise 'rack-edge' do - gem 'rack', github: 'rack/rack' -end - -appraise 'multi_json' do - gem 'multi_json', require: 'multi_json' -end - -appraise 'multi_xml' do - gem 'multi_xml', require: 'multi_xml' -end - -appraise 'rack_1_0' do - gem 'rack', '~> 1.0' -end - -appraise 'rack_2_0' do - gem 'rack', '~> 2.0' -end - -appraise 'rack_3_0' do - gem 'rack', '~> 3.0.0' -end - -appraise 'dry_validation' do - group :development, :test do - remove_gem 'dry-validation' - end -end diff --git a/Gemfile b/Gemfile index fe90b51e8..cf63280c5 100644 --- a/Gemfile +++ b/Gemfile @@ -9,13 +9,12 @@ gemspec group :development, :test do gem 'bundler' gem 'rake' - gem 'rubocop', '1.59.0', require: false - gem 'rubocop-performance', '1.20.1', require: false - gem 'rubocop-rspec', '2.25.0', require: false + gem 'rubocop', '1.63.2', require: false + gem 'rubocop-performance', '1.21.0', require: false + gem 'rubocop-rspec', '2.29.1', require: false end group :development do - gem 'appraisal' gem 'benchmark-ips' gem 'benchmark-memory' gem 'guard' @@ -25,11 +24,11 @@ end group :test do gem 'rack-contrib', require: false - gem 'rack-test', '< 2.1' - gem 'rspec', '< 4' - gem 'ruby-grape-danger', '~> 0.2.0', require: false - gem 'simplecov', '~> 0.21.2' - gem 'simplecov-lcov', '~> 0.8.0' + gem 'rack-test', '~> 2.1' + gem 'rspec', '~> 3.13' + gem 'ruby-grape-danger', '~> 0.2', require: false + gem 'simplecov', '~> 0.21' + gem 'simplecov-lcov', '~> 0.8' gem 'test-prof', require: false end diff --git a/gemfiles/dry_validation.gemfile b/gemfiles/dry_validation.gemfile index 65231ea80..ad526a00f 100644 --- a/gemfiles/dry_validation.gemfile +++ b/gemfiles/dry_validation.gemfile @@ -1,40 +1,5 @@ # frozen_string_literal: true -# This file was generated by Appraisal - -source 'https://rubygems.org' +eval_gemfile '../Gemfile' gem 'dry-validation' - -group :development, :test do - gem 'bundler' - gem 'rake' - gem 'rubocop', '1.59.0', require: false - gem 'rubocop-performance', '1.20.1', require: false - gem 'rubocop-rspec', '2.25.0', 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 'rack-contrib', require: false - gem 'rack-test', '< 2.1' - gem 'rspec', '< 4' - gem 'ruby-grape-danger', '~> 0.2.0', require: false - gem 'simplecov', '~> 0.21.2' - gem 'simplecov-lcov', '~> 0.8.0' - gem 'test-prof', require: false -end - -platforms :jruby do - gem 'racc' -end - -gemspec path: '../' diff --git a/gemfiles/grape_entity.gemfile b/gemfiles/grape_entity.gemfile index 241e0adb1..6baf20174 100644 --- a/gemfiles/grape_entity.gemfile +++ b/gemfiles/grape_entity.gemfile @@ -1,40 +1,5 @@ # frozen_string_literal: true -# This file was generated by Appraisal - -source 'https://rubygems.org' +eval_gemfile '../Gemfile' gem 'grape-entity' - -group :development, :test do - gem 'bundler' - gem 'rake' - gem 'rubocop', '1.59.0', require: false - gem 'rubocop-performance', '1.20.1', require: false - gem 'rubocop-rspec', '2.25.0', 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 'rack-contrib', require: false - gem 'rack-test', '< 2.1' - gem 'rspec', '< 4' - gem 'ruby-grape-danger', '~> 0.2.0', require: false - gem 'simplecov', '~> 0.21.2' - gem 'simplecov-lcov', '~> 0.8.0' - gem 'test-prof', require: false -end - -platforms :jruby do - gem 'racc' -end - -gemspec path: '../' diff --git a/gemfiles/hashie.gemfile b/gemfiles/hashie.gemfile index 7bf7b2bbc..c45181b01 100644 --- a/gemfiles/hashie.gemfile +++ b/gemfiles/hashie.gemfile @@ -1,40 +1,5 @@ # frozen_string_literal: true -# This file was generated by Appraisal - -source 'https://rubygems.org' +eval_gemfile '../Gemfile' gem 'hashie' - -group :development, :test do - gem 'bundler' - gem 'rake' - gem 'rubocop', '1.59.0', require: false - gem 'rubocop-performance', '1.20.1', require: false - gem 'rubocop-rspec', '2.25.0', 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 'rack-contrib', require: false - gem 'rack-test', '< 2.1' - gem 'rspec', '< 4' - gem 'ruby-grape-danger', '~> 0.2.0', require: false - gem 'simplecov', '~> 0.21.2' - gem 'simplecov-lcov', '~> 0.8.0' - gem 'test-prof', require: false -end - -platforms :jruby do - gem 'racc' -end - -gemspec path: '../' diff --git a/gemfiles/multi_json.gemfile b/gemfiles/multi_json.gemfile index 20e5e98cb..8574ea89b 100644 --- a/gemfiles/multi_json.gemfile +++ b/gemfiles/multi_json.gemfile @@ -1,40 +1,5 @@ # frozen_string_literal: true -# This file was generated by Appraisal - -source 'https://rubygems.org' +eval_gemfile '../Gemfile' gem 'multi_json', require: 'multi_json' - -group :development, :test do - gem 'bundler' - gem 'rake' - gem 'rubocop', '1.59.0', require: false - gem 'rubocop-performance', '1.20.1', require: false - gem 'rubocop-rspec', '2.25.0', 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 'rack-contrib', require: false - gem 'rack-test', '< 2.1' - gem 'rspec', '< 4' - gem 'ruby-grape-danger', '~> 0.2.0', require: false - gem 'simplecov', '~> 0.21.2' - gem 'simplecov-lcov', '~> 0.8.0' - gem 'test-prof', require: false -end - -platforms :jruby do - gem 'racc' -end - -gemspec path: '../' diff --git a/gemfiles/multi_xml.gemfile b/gemfiles/multi_xml.gemfile index c4f147df1..c0ada8d0d 100644 --- a/gemfiles/multi_xml.gemfile +++ b/gemfiles/multi_xml.gemfile @@ -1,40 +1,5 @@ # frozen_string_literal: true -# This file was generated by Appraisal - -source 'https://rubygems.org' +eval_gemfile '../Gemfile' gem 'multi_xml', require: 'multi_xml' - -group :development, :test do - gem 'bundler' - gem 'rake' - gem 'rubocop', '1.59.0', require: false - gem 'rubocop-performance', '1.20.1', require: false - gem 'rubocop-rspec', '2.25.0', 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 'rack-contrib', require: false - gem 'rack-test', '< 2.1' - gem 'rspec', '< 4' - gem 'ruby-grape-danger', '~> 0.2.0', require: false - gem 'simplecov', '~> 0.21.2' - gem 'simplecov-lcov', '~> 0.8.0' - gem 'test-prof', require: false -end - -platforms :jruby do - gem 'racc' -end - -gemspec path: '../' diff --git a/gemfiles/rack_2_0.gemfile b/gemfiles/rack_2_0.gemfile index 323b23ce7..f43035ba6 100644 --- a/gemfiles/rack_2_0.gemfile +++ b/gemfiles/rack_2_0.gemfile @@ -1,40 +1,5 @@ # frozen_string_literal: true -# This file was generated by Appraisal - -source 'https://rubygems.org' +eval_gemfile '../Gemfile' gem 'rack', '~> 2.0' - -group :development, :test do - gem 'bundler' - gem 'rake' - gem 'rubocop', '1.59.0', require: false - gem 'rubocop-performance', '1.20.1', require: false - gem 'rubocop-rspec', '2.25.0', 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 'rack-contrib', require: false - gem 'rack-test', '< 2.1' - gem 'rspec', '< 4' - gem 'ruby-grape-danger', '~> 0.2.0', require: false - gem 'simplecov', '~> 0.21.2' - gem 'simplecov-lcov', '~> 0.8.0' - gem 'test-prof', require: false -end - -platforms :jruby do - gem 'racc' -end - -gemspec path: '../' diff --git a/gemfiles/rack_3_0.gemfile b/gemfiles/rack_3_0.gemfile index 55188980d..4025f3cc2 100644 --- a/gemfiles/rack_3_0.gemfile +++ b/gemfiles/rack_3_0.gemfile @@ -1,40 +1,5 @@ # frozen_string_literal: true -# This file was generated by Appraisal - -source 'https://rubygems.org' +eval_gemfile '../Gemfile' gem 'rack', '~> 3.0.0' - -group :development, :test do - gem 'bundler' - gem 'rake' - gem 'rubocop', '1.59.0', require: false - gem 'rubocop-performance', '1.20.1', require: false - gem 'rubocop-rspec', '2.25.0', 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 'rack-contrib', require: false - gem 'rack-test', '< 2.1' - gem 'rspec', '< 4' - gem 'ruby-grape-danger', '~> 0.2.0', require: false - gem 'simplecov', '~> 0.21.2' - gem 'simplecov-lcov', '~> 0.8.0' - gem 'test-prof', require: false -end - -platforms :jruby do - gem 'racc' -end - -gemspec path: '../' diff --git a/gemfiles/rack_edge.gemfile b/gemfiles/rack_edge.gemfile index 53ea7d831..975ceedbf 100644 --- a/gemfiles/rack_edge.gemfile +++ b/gemfiles/rack_edge.gemfile @@ -1,40 +1,5 @@ # frozen_string_literal: true -# This file was generated by Appraisal - -source 'https://rubygems.org' +eval_gemfile '../Gemfile' gem 'rack', github: 'rack/rack' - -group :development, :test do - gem 'bundler' - gem 'rake' - gem 'rubocop', '1.59.0', require: false - gem 'rubocop-performance', '1.20.1', require: false - gem 'rubocop-rspec', '2.25.0', 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 'rack-contrib', require: false - gem 'rack-test', '< 2.1' - gem 'rspec', '< 4' - gem 'ruby-grape-danger', '~> 0.2.0', require: false - gem 'simplecov', '~> 0.21.2' - gem 'simplecov-lcov', '~> 0.8.0' - gem 'test-prof', require: false -end - -platforms :jruby do - gem 'racc' -end - -gemspec path: '../' diff --git a/gemfiles/rails_6_0.gemfile b/gemfiles/rails_6_0.gemfile index ae6ae7c42..8a9e3b247 100644 --- a/gemfiles/rails_6_0.gemfile +++ b/gemfiles/rails_6_0.gemfile @@ -1,40 +1,5 @@ # frozen_string_literal: true -# This file was generated by Appraisal - -source 'https://rubygems.org' +eval_gemfile '../Gemfile' gem 'rails', '~> 6.0.0' - -group :development, :test do - gem 'bundler' - gem 'rake' - gem 'rubocop', '1.59.0', require: false - gem 'rubocop-performance', '1.20.1', require: false - gem 'rubocop-rspec', '2.25.0', 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 'rack-contrib', require: false - gem 'rack-test', '< 2.1' - gem 'rspec', '< 4' - gem 'ruby-grape-danger', '~> 0.2.0', require: false - gem 'simplecov', '~> 0.21.2' - gem 'simplecov-lcov', '~> 0.8.0' - gem 'test-prof', require: false -end - -platforms :jruby do - gem 'racc' -end - -gemspec path: '../' diff --git a/gemfiles/rails_6_1.gemfile b/gemfiles/rails_6_1.gemfile index b7553c752..ad33d3a3e 100644 --- a/gemfiles/rails_6_1.gemfile +++ b/gemfiles/rails_6_1.gemfile @@ -1,40 +1,5 @@ # frozen_string_literal: true -# This file was generated by Appraisal - -source 'https://rubygems.org' +eval_gemfile '../Gemfile' gem 'rails', '~> 6.1' - -group :development, :test do - gem 'bundler' - gem 'rake' - gem 'rubocop', '1.59.0', require: false - gem 'rubocop-performance', '1.20.1', require: false - gem 'rubocop-rspec', '2.25.0', 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 'rack-contrib', require: false - gem 'rack-test', '< 2.1' - gem 'rspec', '< 4' - gem 'ruby-grape-danger', '~> 0.2.0', require: false - gem 'simplecov', '~> 0.21.2' - gem 'simplecov-lcov', '~> 0.8.0' - gem 'test-prof', require: false -end - -platforms :jruby do - gem 'racc' -end - -gemspec path: '../' diff --git a/gemfiles/rails_7_0.gemfile b/gemfiles/rails_7_0.gemfile index 118579d80..43db352ec 100644 --- a/gemfiles/rails_7_0.gemfile +++ b/gemfiles/rails_7_0.gemfile @@ -1,40 +1,5 @@ # frozen_string_literal: true -# This file was generated by Appraisal - -source 'https://rubygems.org' +eval_gemfile '../Gemfile' gem 'rails', '~> 7.0.0' - -group :development, :test do - gem 'bundler' - gem 'rake' - gem 'rubocop', '1.59.0', require: false - gem 'rubocop-performance', '1.20.1', require: false - gem 'rubocop-rspec', '2.25.0', 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 'rack-contrib', require: false - gem 'rack-test', '< 2.1' - gem 'rspec', '< 4' - gem 'ruby-grape-danger', '~> 0.2.0', require: false - gem 'simplecov', '~> 0.21.2' - gem 'simplecov-lcov', '~> 0.8.0' - gem 'test-prof', require: false -end - -platforms :jruby do - gem 'racc' -end - -gemspec path: '../' diff --git a/gemfiles/rails_7_1.gemfile b/gemfiles/rails_7_1.gemfile index 610d0b6cf..babb65fd7 100644 --- a/gemfiles/rails_7_1.gemfile +++ b/gemfiles/rails_7_1.gemfile @@ -1,40 +1,5 @@ # frozen_string_literal: true -# This file was generated by Appraisal - -source 'https://rubygems.org' +eval_gemfile '../Gemfile' gem 'rails', '~> 7.1.0' - -group :development, :test do - gem 'bundler' - gem 'rake' - gem 'rubocop', '1.59.0', require: false - gem 'rubocop-performance', '1.20.1', require: false - gem 'rubocop-rspec', '2.25.0', 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 'rack-contrib', require: false - gem 'rack-test', '< 2.1' - gem 'rspec', '< 4' - gem 'ruby-grape-danger', '~> 0.2.0', require: false - gem 'simplecov', '~> 0.21.2' - gem 'simplecov-lcov', '~> 0.8.0' - gem 'test-prof', require: false -end - -platforms :jruby do - gem 'racc' -end - -gemspec path: '../' diff --git a/gemfiles/rails_edge.gemfile b/gemfiles/rails_edge.gemfile index bc18d40f6..80b01d94e 100644 --- a/gemfiles/rails_edge.gemfile +++ b/gemfiles/rails_edge.gemfile @@ -1,40 +1,5 @@ # frozen_string_literal: true -# This file was generated by Appraisal - -source 'https://rubygems.org' +eval_gemfile '../Gemfile' gem 'rails', github: 'rails/rails' - -group :development, :test do - gem 'bundler' - gem 'rake' - gem 'rubocop', '1.59.0', require: false - gem 'rubocop-performance', '1.20.1', require: false - gem 'rubocop-rspec', '2.25.0', 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 'rack-contrib', require: false - gem 'rack-test', '< 2.1' - gem 'rspec', '< 4' - gem 'ruby-grape-danger', '~> 0.2.0', require: false - gem 'simplecov', '~> 0.21.2' - gem 'simplecov-lcov', '~> 0.8.0' - gem 'test-prof', require: false -end - -platforms :jruby do - gem 'racc' -end - -gemspec path: '../' diff --git a/spec/grape/api_remount_spec.rb b/spec/grape/api_remount_spec.rb index 40b550515..2c6c6ae4b 100644 --- a/spec/grape/api_remount_spec.rb +++ b/spec/grape/api_remount_spec.rb @@ -7,9 +7,7 @@ let(:root_api) { Class.new(described_class) } - def app - root_api - end + let(:app) { root_api } describe 'remounting an API' do context 'with a defined route' do @@ -95,7 +93,7 @@ def app expect(last_response.body).to eq 'success' get '/without_conditional/sometimes' - expect(last_response.status).to eq 404 + expect(last_response).to be_not_found end end @@ -167,18 +165,18 @@ def app endpoint: 'test' } get 'test?another_attr=1' - expect(last_response.status).to eq 400 + expect(last_response).to be_bad_request get 'test?my_attr=1' - expect(last_response.status).to eq 200 + expect(last_response).to be_successful root_api.mount a_remounted_api, with: { required_attr_name: 'another_attr', endpoint: 'test_b' } get 'test_b?another_attr=1' - expect(last_response.status).to eq 200 + expect(last_response).to be_successful get 'test_b?my_attr=1' - expect(last_response.status).to eq 400 + expect(last_response).to be_bad_request end end end @@ -264,7 +262,7 @@ def app expect(last_response.body).to eq 'success' get '/different_location' - expect(last_response.status).to eq 404 + expect(last_response).to be_not_found root_api.mount a_remounted_api, with: { endpoint_name: 'new_location' } get '/new_location' @@ -339,13 +337,13 @@ def app expect(last_response.body).to eq 'success' get '/string/location', param_integer: 1 - expect(last_response.status).to eq 400 + expect(last_response).to be_bad_request get '/integer/location', param_integer: 1 expect(last_response.body).to eq 'success' get '/integer/location', param_integer: 'a' - expect(last_response.status).to eq 400 + expect(last_response).to be_bad_request end context 'on dynamic checks' do @@ -368,7 +366,7 @@ def app get '/location', restricted_values: 'sometimes' expect(last_response.body).to eq 'success' get '/location', restricted_values: 'never' - expect(last_response.status).to eq 400 + expect(last_response).to be_bad_request end end end @@ -387,13 +385,13 @@ def app root_api.mount a_remounted_api, with: { path: 'scores', required_param: 'param_key' } end - it 'will use the dynamic configuration on all routes' do + it 'uses the dynamic configuration on all routes' do get 'api/votes', param_key: 'a' expect(last_response.body).to eql '10 votes' get 'api/scores', param_key: 'a' expect(last_response.body).to eql '10 votes' get 'api/votes' - expect(last_response.status).to eq 400 + expect(last_response).to be_bad_request end end @@ -480,7 +478,7 @@ def printed_response end end - it 'will use the dynamic configuration on all routes' do + it 'uses the dynamic configuration on all routes' do root_api.mount(a_remounted_api, with: { some_value: 'response value' }) get '/location' @@ -497,7 +495,7 @@ def printed_response end end - it 'will use the dynamic configuration on all routes' do + it 'uses the dynamic configuration on all routes' do root_api.mount(a_remounted_api, with: { some_value: 'response value' }) get '/location' diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index fcf66ab63..f8aec1c48 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -4285,12 +4285,12 @@ def uniqe_id_route it 'returns an error when the id is bad' do get '/v1/orders/abc' - expect(last_response.body).to be_eql('id is invalid') + expect(last_response.body).to eq('id is invalid') end it 'returns the given id when it is valid' do get '/v1/orders/1-2' - expect(last_response.body).to be_eql('1-2') + expect(last_response.body).to eq('1-2') end end diff --git a/spec/grape/exceptions/validation_errors_spec.rb b/spec/grape/exceptions/validation_errors_spec.rb index 831ad80d7..4bba43d65 100644 --- a/spec/grape/exceptions/validation_errors_spec.rb +++ b/spec/grape/exceptions/validation_errors_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'ostruct' - describe Grape::Exceptions::ValidationErrors do let(:validation_message) { 'FooBar is invalid' } let(:validation_error) { instance_double Grape::Exceptions::Validation, params: [validation_message], message: '' } @@ -80,7 +78,7 @@ def app 'exactly_one_of works!' end get '/exactly_one_of', beer: 'string', wine: 'anotherstring' - expect(last_response.status).to eq(400) + expect(last_response).to be_bad_request expect(JSON.parse(last_response.body)).to eq( [ 'params' => %w[beer wine], diff --git a/spec/grape/middleware/auth/base_spec.rb b/spec/grape/middleware/auth/base_spec.rb index 1bc888f11..7ff7d0ad4 100644 --- a/spec/grape/middleware/auth/base_spec.rb +++ b/spec/grape/middleware/auth/base_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'base64' - describe Grape::Middleware::Auth::Base do subject do Class.new(Grape::API) do @@ -14,18 +12,16 @@ end end - def app - subject - end + let(:app) { subject } it 'authenticates if given valid creds' do get '/authorized', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('admin', 'admin') - expect(last_response.status).to eq(200) + expect(last_response).to be_successful expect(last_response.body).to eq('DONE') end it 'throws a 401 is wrong auth is given' do get '/authorized', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('admin', 'wrong') - expect(last_response.status).to eq(401) + expect(last_response).to be_unauthorized end end diff --git a/spec/grape/middleware/auth/strategies_spec.rb b/spec/grape/middleware/auth/strategies_spec.rb index f6996695b..48c62d3d0 100644 --- a/spec/grape/middleware/auth/strategies_spec.rb +++ b/spec/grape/middleware/auth/strategies_spec.rb @@ -1,10 +1,8 @@ # frozen_string_literal: true -require 'base64' - describe Grape::Middleware::Auth::Strategies do - context 'Basic Auth' do - def app + describe 'Basic Auth' do + let(:app) do proc = ->(u, p) { u && p && u == p } Rack::Builder.new do |b| b.use Grape::Middleware::Error @@ -14,19 +12,18 @@ def app end it 'throws a 401 if no auth is given' do - @proc = -> { false } get '/whatever' - expect(last_response.status).to eq(401) + expect(last_response).to be_unauthorized end it 'authenticates if given valid creds' do get '/whatever', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('admin', 'admin') - expect(last_response.status).to eq(200) + expect(last_response).to be_successful end it 'throws a 401 is wrong auth is given' do get '/whatever', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('admin', 'wrong') - expect(last_response.status).to eq(401) + expect(last_response).to be_unauthorized end end end diff --git a/spec/support/basic_auth_encode_helpers.rb b/spec/support/basic_auth_encode_helpers.rb index 00c3c6149..78e21e6c8 100644 --- a/spec/support/basic_auth_encode_helpers.rb +++ b/spec/support/basic_auth_encode_helpers.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'base64' - module Spec module Support module Helpers diff --git a/spec/support/chunks.rb b/spec/support/chunks.rb index 0506cb7ce..253390238 100644 --- a/spec/support/chunks.rb +++ b/spec/support/chunks.rb @@ -3,7 +3,7 @@ module Chunks def read_chunks(body) buffer = [] - body.each { |chunk| buffer << chunk } + body.each { |chunk| buffer << chunk } # rubocop:disable Style/MapIntoArray buffer end From 11b12b8c0b7735c393a55bcf07b6e90f7dda8841 Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 17 Apr 2024 18:33:27 +0200 Subject: [PATCH 221/304] Fix multi_json and multi_xml Fix rubocop Add CHANGELOG --- .rubocop.yml | 3 ++ .rubocop_todo.yml | 56 ++++++------------------------------- CHANGELOG.md | 1 + docker-compose.yml | 2 ++ gemfiles/multi_json.gemfile | 4 +-- gemfiles/multi_xml.gemfile | 4 +-- spec/support/chunks.rb | 2 +- 7 files changed, 19 insertions(+), 53 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index a58cedd15..8dfa0311a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -83,3 +83,6 @@ RSpec/MultipleMemoizedHelpers: RSpec/ContextWording: Enabled: false + +RSpecRails/HaveHttpStatus: + Enabled: false diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 6569f408c..f8f44abc8 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by -# `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 5000` -# on 2024-04-16 09:59:31 UTC using RuboCop version 1.63.2. +# `rubocop --auto-gen-config` +# on 2024-04-17 16:26:06 UTC using RuboCop version 1.63.2. # 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 @@ -246,52 +246,6 @@ RSpec/VoidExpect: - 'spec/grape/api_spec.rb' - 'spec/grape/dsl/headers_spec.rb' -# Offense count: 598 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: ResponseMethods. -# ResponseMethods: response, last_response -RSpecRails/HaveHttpStatus: - Exclude: - - 'spec/grape/api/custom_validations_spec.rb' - - 'spec/grape/api/deeply_included_options_spec.rb' - - 'spec/grape/api/defines_boolean_in_params_spec.rb' - - 'spec/grape/api/invalid_format_spec.rb' - - 'spec/grape/api/mount_and_helpers_order_spec.rb' - - 'spec/grape/api/mount_and_rescue_from_spec.rb' - - 'spec/grape/api/namespace_parameters_in_route_spec.rb' - - 'spec/grape/api/optional_parameters_in_route_spec.rb' - - 'spec/grape/api/parameters_modification_spec.rb' - - 'spec/grape/api/patch_method_helpers_spec.rb' - - 'spec/grape/api/required_parameters_in_route_spec.rb' - - 'spec/grape/api/required_parameters_with_invalid_method_spec.rb' - - 'spec/grape/api/routes_with_requirements_spec.rb' - - 'spec/grape/api/shared_helpers_exactly_one_of_spec.rb' - - 'spec/grape/api/shared_helpers_spec.rb' - - 'spec/grape/api_spec.rb' - - 'spec/grape/endpoint_spec.rb' - - 'spec/grape/exceptions/body_parse_errors_spec.rb' - - 'spec/grape/exceptions/invalid_accept_header_spec.rb' - - 'spec/grape/exceptions/validation_errors_spec.rb' - - 'spec/grape/extensions/param_builders/hash_spec.rb' - - 'spec/grape/extensions/param_builders/hash_with_indifferent_access_spec.rb' - - 'spec/grape/integration/global_namespace_function_spec.rb' - - 'spec/grape/integration/rack_spec.rb' - - 'spec/grape/middleware/error_spec.rb' - - 'spec/grape/middleware/exception_spec.rb' - - 'spec/grape/validations/params_scope_spec.rb' - - 'spec/grape/validations/validators/all_or_none_spec.rb' - - 'spec/grape/validations/validators/allow_blank_spec.rb' - - 'spec/grape/validations/validators/at_least_one_of_spec.rb' - - 'spec/grape/validations/validators/default_spec.rb' - - 'spec/grape/validations/validators/exactly_one_of_spec.rb' - - 'spec/grape/validations/validators/mutual_exclusion_spec.rb' - - 'spec/grape/validations/validators/presence_spec.rb' - - 'spec/grape/validations/validators/regexp_spec.rb' - - 'spec/grape/validations/validators/same_as_spec.rb' - - 'spec/grape/validations/validators/values_spec.rb' - - 'spec/grape/validations_spec.rb' - - 'spec/shared/versioning_examples.rb' - # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). Style/CombinableLoops: @@ -305,6 +259,12 @@ Style/CombinableLoops: Style/FormatStringToken: EnforcedStyle: template +# Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/MapIntoArray: + Exclude: + - 'spec/support/chunks.rb' + # Offense count: 12 # Configuration parameters: AllowedMethods. # AllowedMethods: respond_to_missing? diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e2c30ae9..bc14e3335 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ * [#2363](https://github.com/ruby-grape/grape/pull/2363): Replace autoload by zeitwerk - [@ericproulx](https://github.com/ericproulx). * [#2425](https://github.com/ruby-grape/grape/pull/2425): Replace `{}` with `Rack::Header` or `Rack::Utils::HeaderHash` - [@dhruvCW](https://github.com/dhruvCW). * [#2430](https://github.com/ruby-grape/grape/pull/2430): Isolate extensions within specific gemfile - [@ericproulx](https://github.com/ericproulx). +* [#2431](https://github.com/ruby-grape/grape/pull/2431): Drop appraisals in favor of eval_gemfile - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/docker-compose.yml b/docker-compose.yml index 2b293708b..a86e58625 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,3 +15,5 @@ services: volumes: - .:/var/grape - gems:/usr/local/bundle + environment: + GEMFILE: multi_xml diff --git a/gemfiles/multi_json.gemfile b/gemfiles/multi_json.gemfile index 8574ea89b..014c2f9c6 100644 --- a/gemfiles/multi_json.gemfile +++ b/gemfiles/multi_json.gemfile @@ -1,5 +1,5 @@ # frozen_string_literal: true -eval_gemfile '../Gemfile' +gem 'multi_json' -gem 'multi_json', require: 'multi_json' +eval_gemfile '../Gemfile' diff --git a/gemfiles/multi_xml.gemfile b/gemfiles/multi_xml.gemfile index c0ada8d0d..3cd7774f9 100644 --- a/gemfiles/multi_xml.gemfile +++ b/gemfiles/multi_xml.gemfile @@ -1,5 +1,5 @@ # frozen_string_literal: true -eval_gemfile '../Gemfile' +gem 'multi_xml' -gem 'multi_xml', require: 'multi_xml' +eval_gemfile '../Gemfile' diff --git a/spec/support/chunks.rb b/spec/support/chunks.rb index 253390238..0506cb7ce 100644 --- a/spec/support/chunks.rb +++ b/spec/support/chunks.rb @@ -3,7 +3,7 @@ module Chunks def read_chunks(body) buffer = [] - body.each { |chunk| buffer << chunk } # rubocop:disable Style/MapIntoArray + body.each { |chunk| buffer << chunk } buffer end From 8e2d2fb876513930fd0406d36e4b04a967933eff Mon Sep 17 00:00:00 2001 From: Andrei Subbota Date: Fri, 26 Apr 2024 21:29:22 +0200 Subject: [PATCH 222/304] Deep Merge for group parameter attributes (#2432) --- CHANGELOG.md | 1 + README.md | 15 ++++--- UPGRADING.md | 29 +++++++++++++- lib/grape/dsl/parameters.rb | 4 +- spec/grape/dsl/parameters_spec.rb | 65 ++++++++++++++++++++++++++++++- 5 files changed, 104 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e2c30ae9..e7d41d4e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ #### Features +* [#2432](https://github.com/ruby-grape/grape/pull/2432): Deep merge for group parameter attributes - [@numbata](https://github.com/numbata). * [#2419](https://github.com/ruby-grape/grape/pull/2419): Add the `contract` DSL - [@dgutov](https://github.com/dgutov). * [#2371](https://github.com/ruby-grape/grape/pull/2371): Use a param value as the `default` value of other param - [@jcagarcia](https://github.com/jcagarcia). * [#2377](https://github.com/ruby-grape/grape/pull/2377): Allow to use instance variables values inside `rescue_from` - [@jcagarcia](https://github.com/jcagarcia). diff --git a/README.md b/README.md index 3e849a1b9..ea2a96443 100644 --- a/README.md +++ b/README.md @@ -1542,13 +1542,16 @@ Note: param in `given` should be the renamed one. In the example, it should be ` ### Group Options -Parameters options can be grouped. It can be useful if you want to extract common validation or types for several parameters. The example below presents a typical case when parameters share common options. +Parameters options can be grouped. It can be useful if you want to extract common validation or types for several parameters. +Within these groups, individual parameters can extend or selectively override the common settings, allowing you to maintain the defaults at the group level while still applying parameter-specific rules where necessary. + +The example below presents a typical case when parameters share common options. ```ruby params do - requires :first_name, type: String, regexp: /w+/, desc: 'First name' - requires :middle_name, type: String, regexp: /w+/, desc: 'Middle name' - requires :last_name, type: String, regexp: /w+/, desc: 'Last name' + requires :first_name, type: String, regexp: /w+/, desc: 'First name', documentation: { in: 'body' } + optional :middle_name, type: String, regexp: /w+/, desc: 'Middle name', documentation: { in: 'body', x: { nullable: true } } + requires :last_name, type: String, regexp: /w+/, desc: 'Last name', documentation: { in: 'body' } end ``` @@ -1556,9 +1559,9 @@ Grape allows you to present the same logic through the `with` method in your par ```ruby params do - with(type: String, regexp: /w+/) do + with(type: String, regexp: /w+/, documentation: { in: 'body' }) do requires :first_name, desc: 'First name' - requires :middle_name, desc: 'Middle name' + optional :middle_name, desc: 'Middle name', documentation: { x: { nullable: true } } requires :last_name, desc: 'Last name' end end diff --git a/UPGRADING.md b/UPGRADING.md index 3bc1fcf31..776d50eb2 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -3,6 +3,33 @@ Upgrading Grape ### Upgrading to >= 2.1.0 +#### Deep Merging of Parameter Attributes + +Grape now uses `deep_merge` to combine parameter attributes within the `with` method. Previously, attributes defined at the parameter level would override those defined at the group level. +With deep merge, attributes are now combined, allowing for more detailed and nuanced API specifications. + +For example: + +```ruby +with(documentation: { in: 'body' }) do + optional :vault, documentation: { default: 33 } +end +``` + +Before it was equivalent to: + +```ruby +optional :vault, documentation: { default: 33 } +``` + +After it is an equivalent of: + +```ruby +optional :vault, documentation: { in: 'body', default: 33 } +``` + +See [#2432](https://github.com/ruby-grape/grape/pull/2432) for more information. + #### Zeitwerk Grape's autoloader has been updated and it's now based on [Zeitwerk](https://github.com/fxn/zeitwerk). @@ -179,7 +206,7 @@ If you are using Rack 3 in your application then the headers will be set to: { "content-type" => "application/json", "secret-password" => "foo"} ``` -This means if you are checking for header values in your application, you would need to change your code to use downcased keys. +This means if you are checking for header values in your application, you would need to change your code to use downcased keys. ```ruby get do diff --git a/lib/grape/dsl/parameters.rb b/lib/grape/dsl/parameters.rb index 561662417..e774105a8 100644 --- a/lib/grape/dsl/parameters.rb +++ b/lib/grape/dsl/parameters.rb @@ -130,7 +130,7 @@ def requires(*attrs, &block) opts = attrs.extract_options!.clone opts[:presence] = { value: true, message: opts[:message] } - opts = @group.merge(opts) if instance_variable_defined?(:@group) && @group + opts = @group.deep_merge(opts) if instance_variable_defined?(:@group) && @group if opts[:using] require_required_and_optional_fields(attrs.first, opts) @@ -149,7 +149,7 @@ def optional(*attrs, &block) opts = attrs.extract_options!.clone type = opts[:type] - opts = @group.merge(opts) if instance_variable_defined?(:@group) && @group + opts = @group.deep_merge(opts) if instance_variable_defined?(:@group) && @group # check type for optional parameter group if attrs && block diff --git a/spec/grape/dsl/parameters_spec.rb b/spec/grape/dsl/parameters_spec.rb index 4fea57d20..5106866d0 100644 --- a/spec/grape/dsl/parameters_spec.rb +++ b/spec/grape/dsl/parameters_spec.rb @@ -7,8 +7,12 @@ class Dummy include Grape::DSL::Parameters attr_accessor :api, :element, :parent + def initialize + @validate_attributes = [] + end + def validate_attributes(*args) - @validate_attributes = *args + @validate_attributes.push(*args) end def validate_attributes_reader @@ -106,6 +110,65 @@ def extract_message_option(attrs) expect(subject.validate_attributes_reader).to eq([[:id], { type: Integer, desc: 'Identity.' }]) expect(subject.push_declared_params_reader).to eq([:id]) end + + it 'merges the group attributes' do + subject.with(documentation: { in: 'body' }) { subject.optional :vault, documentation: { default: 33 } } + + expect(subject.validate_attributes_reader).to eq([[:vault], { documentation: { in: 'body', default: 33 } }]) + expect(subject.push_declared_params_reader).to eq([:vault]) + end + + it 'overrides the group attribute when values not mergable' do + subject.with(type: Integer, documentation: { in: 'body', default: 33 }) do + subject.optional :vault + subject.optional :allowed_vaults, type: [Integer], documentation: { default: [31, 32, 33], is_array: true } + end + + expect(subject.validate_attributes_reader).to eq( + [ + [:vault], { type: Integer, documentation: { in: 'body', default: 33 } }, + [:allowed_vaults], { type: [Integer], documentation: { in: 'body', default: [31, 32, 33], is_array: true } } + ] + ) + end + + it 'allows a primitive type attribite to overwrite a complex type group attribute' do + subject.with(documentation: { x: { nullable: true } }) do + subject.optional :vault, type: Integer, documentation: { x: nil } + end + + expect(subject.validate_attributes_reader).to eq( + [ + [:vault], { type: Integer, documentation: { x: nil } } + ] + ) + end + + it 'does not nest primitives inside existing complex types erroneously' do + subject.with(type: Hash, documentation: { default: { vault: '33' } }) do + subject.optional :info + subject.optional :role, type: String, documentation: { default: 'resident' } + end + + expect(subject.validate_attributes_reader).to eq( + [ + [:info], { type: Hash, documentation: { default: { vault: '33' } } }, + [:role], { type: String, documentation: { default: 'resident' } } + ] + ) + end + + it 'merges deeply nested attributes' do + subject.with(documentation: { details: { in: 'body', hidden: false } }) do + subject.optional :vault, documentation: { details: { desc: 'The vault number' } } + end + + expect(subject.validate_attributes_reader).to eq( + [ + [:vault], { documentation: { details: { in: 'body', hidden: false, desc: 'The vault number' } } } + ] + ) + end end describe '#mutually_exclusive' do From e459e8153d5f0a7144b2d98cecafd31f85dff211 Mon Sep 17 00:00:00 2001 From: Eric Date: Sat, 4 May 2024 18:25:25 +0200 Subject: [PATCH 223/304] Use Rack constants instead of redefining it Use Grape and Rack header constant in specs Drop self.lowercase? and rack2 integration since we're using Grape::Util::Header Change rack_3_0' integration tests to make sure we are returning lowercase headers --- benchmark/large_model.rb | 2 +- benchmark/nested_params.rb | 2 +- benchmark/remounting.rb | 2 +- benchmark/simple.rb | 2 +- docker-compose.yml | 2 - lib/grape/api/instance.rb | 16 +- lib/grape/dsl/inside_route.rb | 13 +- lib/grape/endpoint.rb | 5 +- lib/grape/env.rb | 5 - lib/grape/http/headers.rb | 53 ++-- lib/grape/middleware/error.rb | 2 +- lib/grape/middleware/formatter.rb | 17 +- lib/grape/middleware/globals.rb | 2 +- lib/grape/middleware/versioner/param.rb | 4 +- lib/grape/middleware/versioner/path.rb | 2 +- lib/grape/router.rb | 6 +- spec/grape/api/custom_validations_spec.rb | 2 +- .../api/defines_boolean_in_params_spec.rb | 2 +- spec/grape/api/patch_method_helpers_spec.rb | 8 +- spec/grape/api_spec.rb | 32 +-- spec/grape/dsl/inside_route_spec.rb | 30 +-- spec/grape/dsl/routing_spec.rb | 14 +- spec/grape/endpoint_spec.rb | 21 +- .../exceptions/invalid_accept_header_spec.rb | 46 ++-- spec/grape/integration/rack_sendfile_spec.rb | 2 +- spec/grape/integration/rack_spec.rb | 2 +- spec/grape/middleware/formatter_spec.rb | 228 +++++++++--------- spec/grape/middleware/globals_spec.rb | 8 +- .../versioner/accept_version_header_spec.rb | 22 +- .../grape/middleware/versioner/header_spec.rb | 88 +++---- spec/grape/middleware/versioner/param_spec.rb | 16 +- spec/grape/middleware/versioner/path_spec.rb | 20 +- spec/grape/request_spec.rb | 2 +- spec/grape/util/accept_header_handler_spec.rb | 2 +- .../validations/validators/presence_spec.rb | 4 +- spec/integration/hashie/hashie_spec.rb | 2 +- spec/integration/rack_2_0/headers_spec.rb | 8 - spec/integration/rack_3_0/headers_spec.rb | 77 +++++- spec/shared/versioning_examples.rb | 12 +- spec/support/cookie_jar.rb | 2 +- spec/support/endpoint_faker.rb | 2 +- spec/support/versioned_helpers.rb | 4 +- 42 files changed, 418 insertions(+), 373 deletions(-) delete mode 100644 spec/integration/rack_2_0/headers_spec.rb diff --git a/benchmark/large_model.rb b/benchmark/large_model.rb index 272fc93d1..27dc9a357 100644 --- a/benchmark/large_model.rb +++ b/benchmark/large_model.rb @@ -233,7 +233,7 @@ def self.vrp_request_schedule(this) puts Grape::VERSION options = { - method: 'POST', + method: Rack::POST, params: JSON.parse(File.read('benchmark/resource/vrp_example.json')) } diff --git a/benchmark/nested_params.rb b/benchmark/nested_params.rb index f7cf0798c..35916f920 100644 --- a/benchmark/nested_params.rb +++ b/benchmark/nested_params.rb @@ -21,7 +21,7 @@ class API < Grape::API end options = { - method: 'POST', + method: Rack::POST, params: { address: { street: 'Alexis Pl.', diff --git a/benchmark/remounting.rb b/benchmark/remounting.rb index e174cda75..8b3de34fd 100644 --- a/benchmark/remounting.rb +++ b/benchmark/remounting.rb @@ -28,7 +28,7 @@ class CommentAPI < Grape::API mount VotingApi end -env = Rack::MockRequest.env_for('/votes', method: 'GET') +env = Rack::MockRequest.env_for('/votes', method: Rack::GET) Benchmark.memory do |api| calls = 1000 diff --git a/benchmark/simple.rb b/benchmark/simple.rb index 053c33351..133050ae2 100644 --- a/benchmark/simple.rb +++ b/benchmark/simple.rb @@ -13,7 +13,7 @@ class API < Grape::API end options = { - method: 'GET' + method: Rack::GET } env = Rack::MockRequest.env_for('/api/v1', options) diff --git a/docker-compose.yml b/docker-compose.yml index a86e58625..2b293708b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,5 +15,3 @@ services: volumes: - .:/var/grape - gems:/usr/local/bundle - environment: - GEMFILE: multi_xml diff --git a/lib/grape/api/instance.rb b/lib/grape/api/instance.rb index c0c6ba2fd..8047b4e25 100644 --- a/lib/grape/api/instance.rb +++ b/lib/grape/api/instance.rb @@ -160,9 +160,13 @@ def initialize # 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 + status, headers, response = @router.call(env) + unless cascade? + headers = Grape::Util::Header.new.merge(headers) + headers.delete(Grape::Http::Headers::X_CASCADE) + end + + [status, headers, response] end # Some requests may return a HTTP 404 error if grape cannot find a matching @@ -201,11 +205,11 @@ def add_head_not_allowed_methods_and_options_methods allowed_methods = config[:methods].dup - allowed_methods |= [Grape::Http::Headers::HEAD] if !self.class.namespace_inheritable(:do_not_route_head) && allowed_methods.include?(Grape::Http::Headers::GET) + allowed_methods |= [Rack::HEAD] if !self.class.namespace_inheritable(:do_not_route_head) && allowed_methods.include?(Rack::GET) - allow_header = (self.class.namespace_inheritable(:do_not_route_options) ? allowed_methods : [Grape::Http::Headers::OPTIONS] | allowed_methods) + allow_header = (self.class.namespace_inheritable(:do_not_route_options) ? allowed_methods : [Rack::OPTIONS] | allowed_methods) - config[:endpoint].options[:options_route_enabled] = true unless self.class.namespace_inheritable(:do_not_route_options) || allowed_methods.include?(Grape::Http::Headers::OPTIONS) + config[:endpoint].options[:options_route_enabled] = true unless self.class.namespace_inheritable(:do_not_route_options) || allowed_methods.include?(Rack::OPTIONS) attributes = config.merge(allowed_methods: allowed_methods, allow_header: allow_header) generate_not_allowed_method(config[:pattern], **attributes) diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index d93546061..932640093 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -200,7 +200,7 @@ def redirect(url, permanent: false, body: nil, **_options) if permanent status 301 body_message ||= "This resource has been moved permanently to #{url}." - elsif env[Grape::Http::Headers::HTTP_VERSION] == 'HTTP/1.1' && request.request_method.to_s.upcase != Grape::Http::Headers::GET + elsif http_version == 'HTTP/1.1' && !request.get? status 303 body_message ||= "An alternate resource is located at #{url}." else @@ -226,10 +226,9 @@ def status(status = nil) when nil return @status if instance_variable_defined?(:@status) && @status - case request.request_method.to_s.upcase - when Grape::Http::Headers::POST + if request.post? 201 - when Grape::Http::Headers::DELETE + elsif request.delete? if instance_variable_defined?(:@body) && @body.present? 200 else @@ -351,7 +350,7 @@ def stream(value = nil) return if value.nil? && @stream.nil? header Rack::CONTENT_LENGTH, nil - header Grape::Http::Headers::TRANSFER_ENCODING, nil + header Rack::TRANSFER_ENCODING, nil header Rack::CACHE_CONTROL, 'no-cache' # Skips ETag generation (reading the response up front) if value.is_a?(String) file_body = Grape::ServeStream::FileBody.new(value) @@ -458,6 +457,10 @@ def entity_representation_for(entity_class, object, options) embeds[:version] = env[Grape::Env::API_VERSION] if env.key?(Grape::Env::API_VERSION) entity_class.represent(object, **embeds.merge(options)) end + + def http_version + env['HTTP_VERSION'] || env[Rack::SERVER_PROTOCOL] + end end end end diff --git a/lib/grape/endpoint.rb b/lib/grape/endpoint.rb index a729216bb..cfca9d353 100644 --- a/lib/grape/endpoint.rb +++ b/lib/grape/endpoint.rb @@ -151,7 +151,7 @@ def mount_in(router) reset_routes! routes.each do |route| methods = [route.request_method] - methods << Grape::Http::Headers::HEAD if !namespace_inheritable(:do_not_route_head) && route.request_method == Grape::Http::Headers::GET + methods << Rack::HEAD if !namespace_inheritable(:do_not_route_head) && route.request_method == Rack::GET methods.each do |method| route = Grape::Router::Route.new(method, route.origin, **route.attributes.to_h) unless route.request_method == method router.append(route.apply(self)) @@ -280,6 +280,7 @@ def build_stack(helpers) stack = Grape::Middleware::Stack.new stack.use Rack::Head + stack.use Rack::Lint stack.use Class.new(Grape::Middleware::Error), helpers: helpers, format: namespace_inheritable(:format), @@ -401,7 +402,7 @@ def validations def options? options[:options_route_enabled] && - env[Grape::Http::Headers::REQUEST_METHOD] == Grape::Http::Headers::OPTIONS + env[Rack::REQUEST_METHOD] == Rack::OPTIONS end def method_missing(name, *_args) diff --git a/lib/grape/env.rb b/lib/grape/env.rb index a6023bcc1..09392fd2d 100644 --- a/lib/grape/env.rb +++ b/lib/grape/env.rb @@ -11,11 +11,6 @@ module Env API_VENDOR = 'api.vendor' API_FORMAT = 'api.format' - 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' GRAPE_REQUEST_HEADERS = 'grape.request.headers' GRAPE_REQUEST_PARAMS = 'grape.request.params' diff --git a/lib/grape/http/headers.rb b/lib/grape/http/headers.rb index ae0989a3b..d1d995c57 100644 --- a/lib/grape/http/headers.rb +++ b/lib/grape/http/headers.rb @@ -3,44 +3,25 @@ module Grape module Http module Headers - # https://github.com/rack/rack/blob/master/lib/rack.rb - HTTP_VERSION = 'HTTP_VERSION' - PATH_INFO = 'PATH_INFO' - REQUEST_METHOD = 'REQUEST_METHOD' - QUERY_STRING = 'QUERY_STRING' - - def self.lowercase? - Rack::CONTENT_TYPE == 'content-type' - end - - if lowercase? - ALLOW = 'allow' - LOCATION = 'location' - TRANSFER_ENCODING = 'transfer-encoding' - X_CASCADE = 'x-cascade' - else - ALLOW = 'Allow' - LOCATION = 'Location' - TRANSFER_ENCODING = 'Transfer-Encoding' - X_CASCADE = 'X-Cascade' - end - - 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::Lazy::Object.new { [GET, POST, PUT, PATCH, DELETE, HEAD].freeze } - - HTTP_ACCEPT_VERSION = 'HTTP_ACCEPT_VERSION' + HTTP_ACCEPT_VERSION = 'HTTP_ACCEPT_VERSION' + HTTP_ACCEPT = 'HTTP_ACCEPT' HTTP_TRANSFER_ENCODING = 'HTTP_TRANSFER_ENCODING' - HTTP_ACCEPT = 'HTTP_ACCEPT' - FORMAT = 'format' + ALLOW = 'Allow' + LOCATION = 'Location' + X_CASCADE = 'X-Cascade' + + SUPPORTED_METHODS = [ + Rack::GET, + Rack::POST, + Rack::PUT, + Rack::PATCH, + Rack::DELETE, + Rack::HEAD, + Rack::OPTIONS + ].freeze + + SUPPORTED_METHODS_WITHOUT_OPTIONS = (SUPPORTED_METHODS - [Rack::OPTIONS]).freeze HTTP_HEADERS = Grape::Util::Lazy::Object.new do common_http_headers = %w[ diff --git a/lib/grape/middleware/error.rb b/lib/grape/middleware/error.rb index dedb4cd58..b1fc02767 100644 --- a/lib/grape/middleware/error.rb +++ b/lib/grape/middleware/error.rb @@ -40,7 +40,7 @@ def call!(env) def rack_response(status, headers, message) message = Rack::Utils.escape_html(message) if headers[Rack::CONTENT_TYPE] == TEXT_HTML - Rack::Response.new(Array.wrap(message), Rack::Utils.status_code(status), headers) + Rack::Response.new(Array.wrap(message), Rack::Utils.status_code(status), Grape::Util::Header.new.merge(headers)) end def format_message(message, backtrace, original_exception = nil) diff --git a/lib/grape/middleware/formatter.rb b/lib/grape/middleware/formatter.rb index 2f0f7f0df..a199ce981 100644 --- a/lib/grape/middleware/formatter.rb +++ b/lib/grape/middleware/formatter.rb @@ -4,6 +4,7 @@ module Grape module Middleware class Formatter < Base CHUNKED = 'chunked' + FORMAT = 'format' def default_options { @@ -80,7 +81,7 @@ def read_body_input !request.parseable_data? && (request.content_length.to_i.positive? || request.env[Grape::Http::Headers::HTTP_TRANSFER_ENCODING] == CHUNKED) - return unless (input = env[Grape::Env::RACK_INPUT]) + return unless (input = env[Rack::RACK_INPUT]) input.rewind body = env[Grape::Env::API_REQUEST_INPUT] = input.read @@ -101,12 +102,12 @@ def read_rack_input(body) begin body = (env[Grape::Env::API_REQUEST_BODY] = parser.call(body, env)) if body.is_a?(Hash) - env[Grape::Env::RACK_REQUEST_FORM_HASH] = if env.key?(Grape::Env::RACK_REQUEST_FORM_HASH) - env[Grape::Env::RACK_REQUEST_FORM_HASH].merge(body) - else - body - end - env[Grape::Env::RACK_REQUEST_FORM_INPUT] = env[Grape::Env::RACK_INPUT] + env[Rack::RACK_REQUEST_FORM_HASH] = if env.key?(Rack::RACK_REQUEST_FORM_HASH) + env[Rack::RACK_REQUEST_FORM_HASH].merge(body) + else + body + end + env[Rack::RACK_REQUEST_FORM_INPUT] = env[Rack::RACK_INPUT] end rescue Grape::Exceptions::Base => e raise e @@ -139,7 +140,7 @@ def format_from_extension end def format_from_params - fmt = Rack::Utils.parse_nested_query(env[Grape::Http::Headers::QUERY_STRING])[Grape::Http::Headers::FORMAT] + fmt = Rack::Utils.parse_nested_query(env[Rack::QUERY_STRING])[FORMAT] # avoid symbol memory leak on an unknown format return fmt.to_sym if content_type_for(fmt) diff --git a/lib/grape/middleware/globals.rb b/lib/grape/middleware/globals.rb index 10d5029dc..81fa9b1f6 100644 --- a/lib/grape/middleware/globals.rb +++ b/lib/grape/middleware/globals.rb @@ -7,7 +7,7 @@ def before request = Grape::Request.new(@env, build_params_with: @options[:build_params_with]) @env[Grape::Env::GRAPE_REQUEST] = request @env[Grape::Env::GRAPE_REQUEST_HEADERS] = request.headers - @env[Grape::Env::GRAPE_REQUEST_PARAMS] = request.params if @env[Grape::Env::RACK_INPUT] + @env[Grape::Env::GRAPE_REQUEST_PARAMS] = request.params if @env[Rack::RACK_INPUT] end end end diff --git a/lib/grape/middleware/versioner/param.rb b/lib/grape/middleware/versioner/param.rb index 69b1d0f48..d12690c15 100644 --- a/lib/grape/middleware/versioner/param.rb +++ b/lib/grape/middleware/versioner/param.rb @@ -28,12 +28,12 @@ def default_options end def before - potential_version = Rack::Utils.parse_nested_query(env[Grape::Http::Headers::QUERY_STRING])[paramkey] + potential_version = Rack::Utils.parse_nested_query(env[Rack::QUERY_STRING])[paramkey] return if potential_version.nil? throw :error, status: 404, message: '404 API Version Not Found', headers: { Grape::Http::Headers::X_CASCADE => 'pass' } if options[:versions] && !options[:versions].find { |v| v.to_s == potential_version } env[Grape::Env::API_VERSION] = potential_version - env[Grape::Env::RACK_REQUEST_QUERY_HASH].delete(paramkey) if env.key? Grape::Env::RACK_REQUEST_QUERY_HASH + env[Rack::RACK_REQUEST_QUERY_HASH].delete(paramkey) if env.key? Rack::RACK_REQUEST_QUERY_HASH end private diff --git a/lib/grape/middleware/versioner/path.rb b/lib/grape/middleware/versioner/path.rb index d6c3e90dd..24fc9010a 100644 --- a/lib/grape/middleware/versioner/path.rb +++ b/lib/grape/middleware/versioner/path.rb @@ -24,7 +24,7 @@ def default_options end def before - path = env[Grape::Http::Headers::PATH_INFO].dup + path = env[Rack::PATH_INFO].dup path.sub!(mount_path, '') if mounted_path?(path) if prefix && path.index(prefix) == 0 # rubocop:disable all diff --git a/lib/grape/router.rb b/lib/grape/router.rb index 7d9dd8564..cbed9d87d 100644 --- a/lib/grape/router.rb +++ b/lib/grape/router.rb @@ -96,7 +96,7 @@ def transaction(env) # If last_neighbor_route exists and request method is OPTIONS, # return response by using #call_with_allow_headers. - return call_with_allow_headers(env, last_neighbor_route) if last_neighbor_route && method == Grape::Http::Headers::OPTIONS && !cascade + return call_with_allow_headers(env, last_neighbor_route) if last_neighbor_route && method == Rack::OPTIONS && !cascade route = match?(input, '*') @@ -123,8 +123,8 @@ def make_routing_args(default_args, route, input) end def extract_input_and_method(env) - input = string_for(env[Grape::Http::Headers::PATH_INFO]) - method = env[Grape::Http::Headers::REQUEST_METHOD] + input = string_for(env[Rack::PATH_INFO]) + method = env[Rack::REQUEST_METHOD] [input, method] end diff --git a/spec/grape/api/custom_validations_spec.rb b/spec/grape/api/custom_validations_spec.rb index 558da7576..936971a95 100644 --- a/spec/grape/api/custom_validations_spec.rb +++ b/spec/grape/api/custom_validations_spec.rb @@ -71,7 +71,7 @@ def validate_param!(attr_name, params) let(:in_body_validator) do Class.new(Grape::Validations::Validators::PresenceValidator) do def validate(request) - validate!(request.env['api.request.body']) + validate!(request.env[Grape::Env::API_REQUEST_BODY]) end end end diff --git a/spec/grape/api/defines_boolean_in_params_spec.rb b/spec/grape/api/defines_boolean_in_params_spec.rb index 6d35049cf..e885eff0d 100644 --- a/spec/grape/api/defines_boolean_in_params_spec.rb +++ b/spec/grape/api/defines_boolean_in_params_spec.rb @@ -24,7 +24,7 @@ end context 'Params endpoint type' do - subject { app.new.router.map['POST'].first.options[:params]['message'][:type] } + subject { app.new.router.map[Rack::POST].first.options[:params]['message'][:type] } it 'params type is a boolean' do expect(subject).to eq 'Grape::API::Boolean' diff --git a/spec/grape/api/patch_method_helpers_spec.rb b/spec/grape/api/patch_method_helpers_spec.rb index ad0a162ba..f91f028a5 100644 --- a/spec/grape/api/patch_method_helpers_spec.rb +++ b/spec/grape/api/patch_method_helpers_spec.rb @@ -49,12 +49,12 @@ def app context 'patch' do it 'public' do - patch '/', {}, 'HTTP_ACCEPT' => 'application/vnd.grape-public-v1+json' + patch '/', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.grape-public-v1+json' expect(last_response.status).to eq 405 end it 'private' do - patch '/', {}, 'HTTP_ACCEPT' => 'application/vnd.grape-private-v1+json' + patch '/', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.grape-private-v1+json' expect(last_response.status).to eq 405 end @@ -66,13 +66,13 @@ def app context 'default' do it 'public' do - get '/', {}, 'HTTP_ACCEPT' => 'application/vnd.grape-public-v1+json' + get '/', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.grape-public-v1+json' expect(last_response.status).to eq 200 expect(last_response.body).to eq({ ok: 'public' }.to_json) end it 'private' do - get '/', {}, 'HTTP_ACCEPT' => 'application/vnd.grape-private-v1+json' + get '/', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.grape-private-v1+json' expect(last_response.status).to eq 200 expect(last_response.body).to eq({ ok: 'private' }.to_json) end diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index f8aec1c48..218a65461 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -436,7 +436,7 @@ def to_txt it "allows a(n) #{object.class} json object in params" do subject.format :json subject.send(verb) do - env['api.request.body'] + env[Grape::Env::API_REQUEST_BODY] end send verb, '/', ::Grape::Json.dump(object), 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq(verb == :post ? 201 : 200) @@ -447,7 +447,7 @@ def to_txt it 'stores input in api.request.input' do subject.format :json subject.send(verb) do - env['api.request.input'] + env[Grape::Env::API_REQUEST_INPUT] end send verb, '/', ::Grape::Json.dump(object), 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq(verb == :post ? 201 : 200) @@ -458,9 +458,9 @@ def to_txt it 'stores input in api.request.input' do subject.format :json subject.send(verb) do - env['api.request.input'] + env[Grape::Env::API_REQUEST_INPUT] end - send verb, '/', ::Grape::Json.dump(object), 'CONTENT_TYPE' => 'application/json', 'HTTP_TRANSFER_ENCODING' => 'chunked', 'CONTENT_LENGTH' => nil + send verb, '/', ::Grape::Json.dump(object), 'CONTENT_TYPE' => 'application/json', Grape::Http::Headers::HTTP_TRANSFER_ENCODING => 'chunked', 'CONTENT_LENGTH' => nil expect(last_response.status).to eq(verb == :post ? 201 : 200) expect(last_response.body).to eql ::Grape::Json.dump(object).to_json end @@ -1247,12 +1247,12 @@ def to_txt subject.use Gem::Version.new(Rack.release) < Gem::Version.new('3') ? Rack::Chunked : ChunkedResponse subject.get('/stream') { stream test_stream } - get '/stream', {}, 'HTTP_VERSION' => 'HTTP/1.1', 'SERVER_PROTOCOL' => 'HTTP/1.1' + get '/stream', {}, 'HTTP_VERSION' => 'HTTP/1.1', Rack::SERVER_PROTOCOL => 'HTTP/1.1' expect(last_response.content_type).to eq('text/plain') expect(last_response.content_length).to be_nil expect(last_response.headers[Rack::CACHE_CONTROL]).to eq('no-cache') - expect(last_response.headers[Grape::Http::Headers::TRANSFER_ENCODING]).to eq('chunked') + expect(last_response.headers[Rack::TRANSFER_ENCODING]).to eq('chunked') expect(last_response.body).to eq("c\r\nThis is some\r\nd\r\n file content\r\n0\r\n\r\n") end @@ -1321,7 +1321,7 @@ def to_txt subject.post 'attachment' do filename = params[:file][:filename] content_type ct - env['api.format'] = :binary # there's no formatter for :binary, data will be returned "as is" + env[Grape::Env::API_FORMAT] = :binary # there's no formatter for :binary, data will be returned "as is" header 'Content-Disposition', "attachment; filename*=UTF-8''#{CGI.escape(filename)}" params[:file][:tempfile].read end @@ -2582,7 +2582,7 @@ def self.call(message, _backtrace, _options, _env, _original_exception) end it 'uses custom formatter' do - get '/simple.custom', 'HTTP_ACCEPT' => 'application/custom' + get '/simple.custom', Grape::Http::Headers::HTTP_ACCEPT => 'application/custom' expect(last_response.body).to eql '{"custom_formatter":"hash"}' end end @@ -2611,7 +2611,7 @@ def self.call(object, _env) end it 'uses custom formatter' do - get '/simple.custom', 'HTTP_ACCEPT' => 'application/custom' + get '/simple.custom', Grape::Http::Headers::HTTP_ACCEPT => 'application/custom' expect(last_response.body).to eql '{"custom_formatter":"hash"}' end end @@ -2699,7 +2699,7 @@ def self.call(object, _env) before do subject.parser :json, nil subject.put 'data' do - "body: #{env['api.request.body']}" + "body: #{env[Grape::Env::API_REQUEST_BODY]}" end end @@ -2786,7 +2786,7 @@ def self.call(object, _env) route = subject.routes[0] expect(route.version).to be_nil expect(route.path).to eq('/ping(.:format)') - expect(route.request_method).to eq('GET') + expect(route.request_method).to eq(Rack::GET) end end @@ -3247,7 +3247,7 @@ def self.call(object, _env) it 'is able to cascade' do subject.mount lambda { |env| headers = {} - headers[Grape::Http::Headers::X_CASCADE] == 'pass' if env['PATH_INFO'].exclude?('boo') + headers[Grape::Http::Headers::X_CASCADE] == 'pass' if env[Rack::PATH_INFO].exclude?('boo') [200, headers, ['Farfegnugen']] } => '/' @@ -3747,7 +3747,7 @@ def my_method end it 'forces txt from a non-accepting header' do - get '/meaning_of_life', {}, 'HTTP_ACCEPT' => 'application/json' + get '/meaning_of_life', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/json' expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s) end end @@ -3776,7 +3776,7 @@ def my_method end it 'forces txt from a non-accepting header' do - get '/meaning_of_life', {}, 'HTTP_ACCEPT' => 'application/json' + get '/meaning_of_life', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/json' expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s) end end @@ -3801,7 +3801,7 @@ def my_method end it 'forces json from a non-accepting header' do - get '/meaning_of_life', {}, 'HTTP_ACCEPT' => 'text/html' + get '/meaning_of_life', {}, Grape::Http::Headers::HTTP_ACCEPT => 'text/html' expect(last_response.body).to eq({ meaning_of_life: 42 }.to_json) end @@ -3998,7 +3998,7 @@ def before [true, false].each do |anchor| it "anchor=#{anchor}" do subject.route :any, '*path', anchor: anchor do - error!("Unrecognized request path: #{params[:path]} - #{env['PATH_INFO']}#{env['SCRIPT_NAME']}", 404) + error!("Unrecognized request path: #{params[:path]} - #{env[Rack::PATH_INFO]}#{env[Rack::SCRIPT_NAME]}", 404) end get '/v1/hello' expect(last_response).to be_successful diff --git a/spec/grape/dsl/inside_route_spec.rb b/spec/grape/dsl/inside_route_spec.rb index 7c6d75236..45bc737bd 100644 --- a/spec/grape/dsl/inside_route_spec.rb +++ b/spec/grape/dsl/inside_route_spec.rb @@ -27,7 +27,7 @@ def initialize end it 'returns env[api.version]' do - subject.env['api.version'] = 'dummy' + subject.env[Grape::Env::API_VERSION] = 'dummy' expect(subject.version).to eq 'dummy' end end @@ -96,27 +96,27 @@ def initialize %w[GET PUT OPTIONS].each do |method| it 'defaults to 200 on GET' do request = Grape::Request.new(Rack::MockRequest.env_for('/', method: method)) - expect(subject).to receive(:request).and_return(request) + expect(subject).to receive(:request).and_return(request).twice expect(subject.status).to eq 200 end end it 'defaults to 201 on POST' do - request = Grape::Request.new(Rack::MockRequest.env_for('/', method: 'POST')) + request = Grape::Request.new(Rack::MockRequest.env_for('/', method: Rack::POST)) expect(subject).to receive(:request).and_return(request) expect(subject.status).to eq 201 end it 'defaults to 204 on DELETE' do - request = Grape::Request.new(Rack::MockRequest.env_for('/', method: 'DELETE')) - expect(subject).to receive(:request).and_return(request) + request = Grape::Request.new(Rack::MockRequest.env_for('/', method: Rack::DELETE)) + expect(subject).to receive(:request).and_return(request).twice expect(subject.status).to eq 204 end it 'defaults to 200 on DELETE with a body present' do - request = Grape::Request.new(Rack::MockRequest.env_for('/', method: 'DELETE')) + request = Grape::Request.new(Rack::MockRequest.env_for('/', method: Rack::DELETE)) subject.body 'content here' - expect(subject).to receive(:request).and_return(request) + expect(subject).to receive(:request).and_return(request).twice expect(subject.status).to eq 200 end @@ -247,7 +247,7 @@ def initialize before do subject.header Rack::CACHE_CONTROL, 'cache' subject.header Rack::CONTENT_LENGTH, 123 - subject.header Grape::Http::Headers::TRANSFER_ENCODING, 'base64' + subject.header Rack::TRANSFER_ENCODING, 'base64' end it 'sends no deprecation warnings' do @@ -277,7 +277,7 @@ def initialize it 'does not change the Transfer-Encoding header' do subject.sendfile file_path - expect(subject.header[Grape::Http::Headers::TRANSFER_ENCODING]).to eq 'base64' + expect(subject.header[Rack::TRANSFER_ENCODING]).to eq 'base64' end end @@ -308,7 +308,7 @@ def initialize before do subject.header Rack::CACHE_CONTROL, 'cache' subject.header Rack::CONTENT_LENGTH, 123 - subject.header Grape::Http::Headers::TRANSFER_ENCODING, 'base64' + subject.header Rack::TRANSFER_ENCODING, 'base64' end it 'emits no deprecation warnings' do @@ -344,7 +344,7 @@ def initialize it 'sets Transfer-Encoding header to nil' do subject.stream file_path - expect(subject.header[Grape::Http::Headers::TRANSFER_ENCODING]).to be_nil + expect(subject.header[Rack::TRANSFER_ENCODING]).to be_nil end end @@ -358,7 +358,7 @@ def initialize before do subject.header Rack::CACHE_CONTROL, 'cache' subject.header Rack::CONTENT_LENGTH, 123 - subject.header Grape::Http::Headers::TRANSFER_ENCODING, 'base64' + subject.header Rack::TRANSFER_ENCODING, 'base64' end it 'emits no deprecation warnings' do @@ -388,7 +388,7 @@ def initialize it 'sets Transfer-Encoding header to nil' do subject.stream stream_object - expect(subject.header[Grape::Http::Headers::TRANSFER_ENCODING]).to be_nil + expect(subject.header[Rack::TRANSFER_ENCODING]).to be_nil end end @@ -409,8 +409,8 @@ def initialize describe '#route' do before do - subject.env['grape.routing_args'] = {} - subject.env['grape.routing_args'][:route_info] = 'dummy' + subject.env[Grape::Env::GRAPE_ROUTING_ARGS] = {} + subject.env[Grape::Env::GRAPE_ROUTING_ARGS][:route_info] = 'dummy' end it 'returns route_info' do diff --git a/spec/grape/dsl/routing_spec.rb b/spec/grape/dsl/routing_spec.rb index 8812b05bd..6d6458619 100644 --- a/spec/grape/dsl/routing_spec.rb +++ b/spec/grape/dsl/routing_spec.rb @@ -128,49 +128,49 @@ class Dummy describe '.get' do it 'delegates to .route' do - expect(subject).to receive(:route).with('GET', path, options) + expect(subject).to receive(:route).with(Rack::GET, path, options) subject.get path, options, &proc end end describe '.post' do it 'delegates to .route' do - expect(subject).to receive(:route).with('POST', path, options) + expect(subject).to receive(:route).with(Rack::POST, path, options) subject.post path, options, &proc end end describe '.put' do it 'delegates to .route' do - expect(subject).to receive(:route).with('PUT', path, options) + expect(subject).to receive(:route).with(Rack::PUT, path, options) subject.put path, options, &proc end end describe '.head' do it 'delegates to .route' do - expect(subject).to receive(:route).with('HEAD', path, options) + expect(subject).to receive(:route).with(Rack::HEAD, path, options) subject.head path, options, &proc end end describe '.delete' do it 'delegates to .route' do - expect(subject).to receive(:route).with('DELETE', path, options) + expect(subject).to receive(:route).with(Rack::DELETE, path, options) subject.delete path, options, &proc end end describe '.options' do it 'delegates to .route' do - expect(subject).to receive(:route).with('OPTIONS', path, options) + expect(subject).to receive(:route).with(Rack::OPTIONS, path, options) subject.options path, options, &proc end end describe '.patch' do it 'delegates to .route' do - expect(subject).to receive(:route).with('PATCH', path, options) + expect(subject).to receive(:route).with(Rack::PATCH, path, options) subject.patch path, options, &proc end end diff --git a/spec/grape/endpoint_spec.rb b/spec/grape/endpoint_spec.rb index 8b217af9e..55ae6fd20 100644 --- a/spec/grape/endpoint_spec.rb +++ b/spec/grape/endpoint_spec.rb @@ -75,7 +75,7 @@ def app it 'sets itself in the env upon call' do subject.get('/') { 'Hello world.' } get '/' - expect(last_request.env['api.endpoint']).to be_a(described_class) + expect(last_request.env[Grape::Env::API_ENDPOINT]).to be_a(described_class) end describe '#status' do @@ -136,15 +136,16 @@ def app end end + let(:headers) do + Grape::Util::Header.new.tap do |h| + h['Cookie'] = '' + h['Host'] = 'example.org' + end + end + it 'includes request headers' do get '/headers' - cookie_header = Grape::Http::Headers.lowercase? ? 'cookie' : 'Cookie' - host_header = Grape::Http::Headers.lowercase? ? 'host' : 'Host' - - expect(JSON.parse(last_response.body)).to include( - host_header => 'example.org', - cookie_header => '' - ) + expect(JSON.parse(last_response.body)).to include(headers.to_h) end it 'includes additional request headers' do @@ -969,12 +970,12 @@ def memoized end it 'result in a 406 response if they are invalid' do - get '/test', {}, 'HTTP_ACCEPT' => 'application/vnd.ohanapi.v1+json' + get '/test', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.ohanapi.v1+json' expect(last_response.status).to eq(406) end it 'result in a 406 response if they cannot be parsed' do - get '/test', {}, 'HTTP_ACCEPT' => 'application/vnd.ohanapi.v1+json; version=1' + get '/test', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.ohanapi.v1+json; version=1' expect(last_response.status).to eq(406) end end diff --git a/spec/grape/exceptions/invalid_accept_header_spec.rb b/spec/grape/exceptions/invalid_accept_header_spec.rb index 42edfb526..a0bf11139 100644 --- a/spec/grape/exceptions/invalid_accept_header_spec.rb +++ b/spec/grape/exceptions/invalid_accept_header_spec.rb @@ -19,7 +19,7 @@ shared_examples_for 'a not-cascaded request' do it 'does not include the X-Cascade=pass header' do - expect(last_response.headers[Grape::Http::Headers::X_CASCADE]).to be_nil + expect(last_response.headers).not_to have_key(Grape::Http::Headers::X_CASCADE) end it 'does not accept the request' do @@ -56,7 +56,7 @@ def app end context 'that received a request with correct vendor and version' do - before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } + before { get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendorname-v99' } it_behaves_like 'a valid request' end @@ -64,7 +64,7 @@ def app context 'that receives' do context 'an invalid vendor in the request' do before do - get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99', + get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.invalidvendor-v99', 'CONTENT_TYPE' => 'application/json' end @@ -88,20 +88,20 @@ def app end context 'that received a request with correct vendor and version' do - before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } + before { get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendorname-v99' } it_behaves_like 'a valid request' end context 'that receives' do context 'an invalid version in the request' do - before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77' } + before { get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendorname-v77' } it_behaves_like 'a not-cascaded request' end context 'an invalid vendor in the request' do - before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99' } + before { get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.invalidvendor-v99' } it_behaves_like 'a not-cascaded request' end @@ -131,7 +131,7 @@ def app end context 'that received a request with correct vendor and version' do - before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } + before { get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendorname-v99' } it_behaves_like 'a valid request' end @@ -139,7 +139,7 @@ def app context 'that receives' do context 'an invalid vendor in the request' do before do - get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99', + get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.invalidvendor-v99', 'CONTENT_TYPE' => 'application/json' end @@ -168,20 +168,20 @@ def app end context 'that received a request with correct vendor and version' do - before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } + before { get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendorname-v99' } it_behaves_like 'a valid request' end context 'that receives' do context 'an invalid version in the request' do - before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77' } + before { get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendorname-v77' } it_behaves_like 'a not-cascaded request' end context 'an invalid vendor in the request' do - before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99' } + before { get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.invalidvendor-v99' } it_behaves_like 'a not-cascaded request' end @@ -206,7 +206,7 @@ def app end context 'that received a request with correct vendor and version' do - before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } + before { get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendorname-v99' } it_behaves_like 'a valid request' end @@ -214,7 +214,7 @@ def app context 'that receives' do context 'an invalid version in the request' do before do - get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77', + get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendorname-v77', 'CONTENT_TYPE' => 'application/json' end @@ -223,7 +223,7 @@ def app context 'an invalid vendor in the request' do before do - get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99', + get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.invalidvendor-v99', 'CONTENT_TYPE' => 'application/json' end @@ -247,20 +247,20 @@ def app end context 'that received a request with correct vendor and version' do - before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } + before { get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendorname-v99' } it_behaves_like 'a valid request' end context 'that receives' do context 'an invalid version in the request' do - before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77' } + before { get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendorname-v77' } it_behaves_like 'a cascaded request' end context 'an invalid vendor in the request' do - before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99' } + before { get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.invalidvendor-v99' } it_behaves_like 'a cascaded request' end @@ -290,7 +290,7 @@ def app end context 'that received a request with correct vendor and version' do - before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } + before { get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendorname-v99' } it_behaves_like 'a valid request' end @@ -298,7 +298,7 @@ def app context 'that receives' do context 'an invalid version in the request' do before do - get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77', + get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendorname-v77', 'CONTENT_TYPE' => 'application/json' end @@ -307,7 +307,7 @@ def app context 'an invalid vendor in the request' do before do - get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99', + get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.invalidvendor-v99', 'CONTENT_TYPE' => 'application/json' end @@ -336,20 +336,20 @@ def app end context 'that received a request with correct vendor and version' do - before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } + before { get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendorname-v99' } it_behaves_like 'a valid request' end context 'that receives' do context 'an invalid version in the request' do - before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77' } + before { get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendorname-v77' } it_behaves_like 'a cascaded request' end context 'an invalid vendor in the request' do - before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99' } + before { get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.invalidvendor-v99' } it_behaves_like 'a cascaded request' end diff --git a/spec/grape/integration/rack_sendfile_spec.rb b/spec/grape/integration/rack_sendfile_spec.rb index 87041f7e1..2864ca101 100644 --- a/spec/grape/integration/rack_sendfile_spec.rb +++ b/spec/grape/integration/rack_sendfile_spec.rb @@ -16,7 +16,7 @@ end options = { - method: 'GET', + method: Rack::GET, 'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect', 'HTTP_X_ACCEL_MAPPING' => '/accel/mapping/=/replaced/' } diff --git a/spec/grape/integration/rack_spec.rb b/spec/grape/integration/rack_spec.rb index fc0aca682..0c09b676b 100644 --- a/spec/grape/integration/rack_spec.rb +++ b/spec/grape/integration/rack_spec.rb @@ -14,7 +14,7 @@ input.rewind options = { input: input, - method: 'POST', + method: Rack::POST, 'CONTENT_TYPE' => 'application/json' } env = Rack::MockRequest.env_for('/', options) diff --git a/spec/grape/middleware/formatter_spec.rb b/spec/grape/middleware/formatter_spec.rb index 20600f9d4..0f67d790a 100644 --- a/spec/grape/middleware/formatter_spec.rb +++ b/spec/grape/middleware/formatter_spec.rb @@ -12,7 +12,7 @@ let(:body) { { 'abc' => 'def' } } it 'looks at the bodies for possibly serializable data' do - _, _, bodies = *subject.call('PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/json') + _, _, bodies = *subject.call(Rack::PATH_INFO => '/somewhere', Grape::Http::Headers::HTTP_ACCEPT => 'application/json') bodies.each { |b| expect(b).to eq(::Grape::Json.dump(body)) } # rubocop:disable RSpec/IteratedExpectation end @@ -26,7 +26,7 @@ def to_json(*_args) end end - subject.call('PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/json').to_a.last.each { |b| expect(b).to eq('"bar"') } # rubocop:disable RSpec/IteratedExpectation + subject.call(Rack::PATH_INFO => '/somewhere', Grape::Http::Headers::HTTP_ACCEPT => 'application/json').to_a.last.each { |b| expect(b).to eq('"bar"') } # rubocop:disable RSpec/IteratedExpectation end end @@ -40,7 +40,7 @@ def to_json(*_args) end end - subject.call('PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/vnd.api+json').to_a.last.each { |b| expect(b).to eq('{"foos":[{"bar":"baz"}] }') } # rubocop:disable RSpec/IteratedExpectation + subject.call(Rack::PATH_INFO => '/somewhere', Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.api+json').to_a.last.each { |b| expect(b).to eq('{"foos":[{"bar":"baz"}] }') } # rubocop:disable RSpec/IteratedExpectation end end @@ -53,7 +53,7 @@ def to_xml '' end end - subject.call('PATH_INFO' => '/somewhere.xml', 'HTTP_ACCEPT' => 'application/json').to_a.last.each { |b| expect(b).to eq('') } # rubocop:disable RSpec/IteratedExpectation + subject.call(Rack::PATH_INFO => '/somewhere.xml', Grape::Http::Headers::HTTP_ACCEPT => 'application/json').to_a.last.each { |b| expect(b).to eq('') } # rubocop:disable RSpec/IteratedExpectation end end end @@ -69,7 +69,7 @@ def to_xml allow(formatter).to receive(:call) { raise Grape::Exceptions::InvalidFormatter.new(String, 'xml') } expect do - catch(:error) { subject.call('PATH_INFO' => '/somewhere.xml', 'HTTP_ACCEPT' => 'application/json') } + catch(:error) { subject.call(Rack::PATH_INFO => '/somewhere.xml', Grape::Http::Headers::HTTP_ACCEPT => 'application/json') } end.not_to raise_error end @@ -77,102 +77,102 @@ def to_xml allow(formatter).to receive(:call) { raise StandardError } expect do - catch(:error) { subject.call('PATH_INFO' => '/somewhere.xml', 'HTTP_ACCEPT' => 'application/json') } + catch(:error) { subject.call(Rack::PATH_INFO => '/somewhere.xml', Grape::Http::Headers::HTTP_ACCEPT => 'application/json') } end.to raise_error(StandardError) end end context 'detection' do it 'uses the xml extension if one is provided' do - subject.call('PATH_INFO' => '/info.xml') - expect(subject.env['api.format']).to eq(:xml) + subject.call(Rack::PATH_INFO => '/info.xml') + expect(subject.env[Grape::Env::API_FORMAT]).to eq(:xml) end it 'uses the json extension if one is provided' do - subject.call('PATH_INFO' => '/info.json') - expect(subject.env['api.format']).to eq(:json) + subject.call(Rack::PATH_INFO => '/info.json') + expect(subject.env[Grape::Env::API_FORMAT]).to eq(:json) end it 'uses the format parameter if one is provided' do - subject.call('PATH_INFO' => '/info', 'QUERY_STRING' => 'format=json') - expect(subject.env['api.format']).to eq(:json) - subject.call('PATH_INFO' => '/info', 'QUERY_STRING' => 'format=xml') - expect(subject.env['api.format']).to eq(:xml) + subject.call(Rack::PATH_INFO => '/info', Rack::QUERY_STRING => 'format=json') + expect(subject.env[Grape::Env::API_FORMAT]).to eq(:json) + subject.call(Rack::PATH_INFO => '/info', Rack::QUERY_STRING => 'format=xml') + expect(subject.env[Grape::Env::API_FORMAT]).to eq(:xml) end it 'uses the default format if none is provided' do - subject.call('PATH_INFO' => '/info') - expect(subject.env['api.format']).to eq(:txt) + subject.call(Rack::PATH_INFO => '/info') + expect(subject.env[Grape::Env::API_FORMAT]).to eq(:txt) end it 'uses the requested format if provided in headers' do - subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json') - expect(subject.env['api.format']).to eq(:json) + subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/json') + expect(subject.env[Grape::Env::API_FORMAT]).to eq(:json) end it 'uses the file extension format if provided before headers' do - subject.call('PATH_INFO' => '/info.txt', 'HTTP_ACCEPT' => 'application/json') - expect(subject.env['api.format']).to eq(:txt) + subject.call(Rack::PATH_INFO => '/info.txt', Grape::Http::Headers::HTTP_ACCEPT => 'application/json') + expect(subject.env[Grape::Env::API_FORMAT]).to eq(:txt) end end context 'accept header detection' do it 'detects from the Accept header' do - subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/xml') - expect(subject.env['api.format']).to eq(:xml) + subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/xml') + expect(subject.env[Grape::Env::API_FORMAT]).to eq(:xml) end it 'uses quality rankings to determine formats' do - subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json; q=0.3,application/xml; q=1.0') - expect(subject.env['api.format']).to eq(:xml) + subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/json; q=0.3,application/xml; q=1.0') + expect(subject.env[Grape::Env::API_FORMAT]).to eq(:xml) - subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json; q=1.0,application/xml; q=0.3') - expect(subject.env['api.format']).to eq(:json) + subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/json; q=1.0,application/xml; q=0.3') + expect(subject.env[Grape::Env::API_FORMAT]).to eq(:json) end it 'handles quality rankings mixed with nothing' do - subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json,application/xml; q=1.0') - expect(subject.env['api.format']).to eq(:json) + subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/json,application/xml; q=1.0') + expect(subject.env[Grape::Env::API_FORMAT]).to eq(:json) - subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/xml; q=1.0,application/json') - expect(subject.env['api.format']).to eq(:xml) + subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/xml; q=1.0,application/json') + expect(subject.env[Grape::Env::API_FORMAT]).to eq(:xml) end it 'handles quality rankings that have a default 1.0 value' do - subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json,application/xml;q=0.5') - expect(subject.env['api.format']).to eq(:json) - subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/xml;q=0.5,application/json') - expect(subject.env['api.format']).to eq(:json) + subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/json,application/xml;q=0.5') + expect(subject.env[Grape::Env::API_FORMAT]).to eq(:json) + subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/xml;q=0.5,application/json') + expect(subject.env[Grape::Env::API_FORMAT]).to eq(:json) end it 'parses headers with other attributes' do - subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json; abc=2.3; q=1.0,application/xml; q=0.7') - expect(subject.env['api.format']).to eq(:json) + subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/json; abc=2.3; q=1.0,application/xml; q=0.7') + expect(subject.env[Grape::Env::API_FORMAT]).to eq(:json) end it 'ensures that a quality of 0 is less preferred than any other content type' do - subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json;q=0.0,application/xml') - expect(subject.env['api.format']).to eq(:xml) - subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/xml,application/json;q=0.0') - expect(subject.env['api.format']).to eq(:xml) + subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/json;q=0.0,application/xml') + expect(subject.env[Grape::Env::API_FORMAT]).to eq(:xml) + subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/xml,application/json;q=0.0') + expect(subject.env[Grape::Env::API_FORMAT]).to eq(:xml) end it 'ignores invalid quality rankings' do - subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json;q=invalid,application/xml;q=0.5') - expect(subject.env['api.format']).to eq(:xml) - subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/xml;q=0.5,application/json;q=invalid') - expect(subject.env['api.format']).to eq(:xml) + subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/json;q=invalid,application/xml;q=0.5') + expect(subject.env[Grape::Env::API_FORMAT]).to eq(:xml) + subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/xml;q=0.5,application/json;q=invalid') + expect(subject.env[Grape::Env::API_FORMAT]).to eq(:xml) - subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json;q=,application/xml;q=0.5') - expect(subject.env['api.format']).to eq(:json) + subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/json;q=,application/xml;q=0.5') + expect(subject.env[Grape::Env::API_FORMAT]).to eq(:json) - subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json;q=nil,application/xml;q=0.5') - expect(subject.env['api.format']).to eq(:xml) + subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/json;q=nil,application/xml;q=0.5') + expect(subject.env[Grape::Env::API_FORMAT]).to eq(:xml) end it 'parses headers with vendor and api version' do - subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/vnd.test-v1+xml') - expect(subject.env['api.format']).to eq(:xml) + subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.test-v1+xml') + expect(subject.env[Grape::Env::API_FORMAT]).to eq(:xml) end context 'with custom vendored content types' do @@ -182,50 +182,50 @@ def to_xml end it 'uses the custom type' do - subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/vnd.test+json') - expect(subject.env['api.format']).to eq(:custom) + subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.test+json') + expect(subject.env[Grape::Env::API_FORMAT]).to eq(:custom) end end it 'parses headers with symbols as hash keys' do - subject.call('PATH_INFO' => '/info', 'http_accept' => 'application/xml', system_time: '091293') + subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/xml', system_time: '091293') expect(subject.env[:system_time]).to eq('091293') end end context 'content-type' do it 'is set for json' do - _, headers, = subject.call('PATH_INFO' => '/info.json') - expect(headers['Content-type']).to eq('application/json') + _, headers, = subject.call(Rack::PATH_INFO => '/info.json') + expect(headers[Rack::CONTENT_TYPE]).to eq('application/json') end it 'is set for xml' do - _, headers, = subject.call('PATH_INFO' => '/info.xml') - expect(headers['Content-type']).to eq('application/xml') + _, headers, = subject.call(Rack::PATH_INFO => '/info.xml') + expect(headers[Rack::CONTENT_TYPE]).to eq('application/xml') end it 'is set for txt' do - _, headers, = subject.call('PATH_INFO' => '/info.txt') - expect(headers['Content-type']).to eq('text/plain') + _, headers, = subject.call(Rack::PATH_INFO => '/info.txt') + expect(headers[Rack::CONTENT_TYPE]).to eq('text/plain') end it 'is set for custom' do subject.options[:content_types] = {} subject.options[:content_types][:custom] = 'application/x-custom' - _, headers, = subject.call('PATH_INFO' => '/info.custom') - expect(headers['Content-type']).to eq('application/x-custom') + _, headers, = subject.call(Rack::PATH_INFO => '/info.custom') + expect(headers[Rack::CONTENT_TYPE]).to eq('application/x-custom') end it 'is set for vendored with registered type' do subject.options[:content_types] = {} subject.options[:content_types][:custom] = 'application/vnd.test+json' - _, headers, = subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/vnd.test+json') - expect(headers['Content-type']).to eq('application/vnd.test+json') + _, headers, = subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.test+json') + expect(headers[Rack::CONTENT_TYPE]).to eq('application/vnd.test+json') end it 'is set to closest generic for custom vendored/versioned without registered type' do - _, headers, = subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/vnd.test+json') - expect(headers['Content-type']).to eq('application/json') + _, headers, = subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.test+json') + expect(headers[Rack::CONTENT_TYPE]).to eq('application/json') end end @@ -234,7 +234,7 @@ def to_xml subject.options[:content_types] = {} subject.options[:content_types][:custom] = "don't care" subject.options[:formatters][:custom] = ->(_obj, _env) { 'CUSTOM FORMAT' } - _, _, body = subject.call('PATH_INFO' => '/info.custom') + _, _, body = subject.call(Rack::PATH_INFO => '/info.custom') expect(read_chunks(body)).to eq(['CUSTOM FORMAT']) end @@ -242,14 +242,14 @@ def to_xml let(:body) { ['blah'] } it 'uses default json formatter' do - _, _, body = subject.call('PATH_INFO' => '/info.json') + _, _, body = subject.call(Rack::PATH_INFO => '/info.json') 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') + _, _, body = subject.call(Rack::PATH_INFO => '/info.json') expect(read_chunks(body)).to eq(['CUSTOM JSON FORMAT']) end end @@ -282,14 +282,14 @@ def to_xml it "parses the body from #{method} and copies values into rack.request.form_hash" do subject.call( - 'PATH_INFO' => '/info', - 'REQUEST_METHOD' => method, + Rack::PATH_INFO => '/info', + Rack::REQUEST_METHOD => method, 'CONTENT_TYPE' => content_type, - 'rack.input' => io, + Rack::RACK_INPUT => io, 'CONTENT_LENGTH' => io.length ) - expect(subject.env['rack.request.form_hash']['is_boolean']).to be true - expect(subject.env['rack.request.form_hash']['string']).to eq('thing') + expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]['is_boolean']).to be true + expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]['string']).to eq('thing') end end @@ -300,10 +300,10 @@ def to_xml it 'returns a 415 HTTP error status' do error = catch(:error) do subject.call( - 'PATH_INFO' => '/info', - 'REQUEST_METHOD' => method, + Rack::PATH_INFO => '/info', + Rack::REQUEST_METHOD => method, 'CONTENT_TYPE' => content_type, - 'rack.input' => io, + Rack::RACK_INPUT => io, 'CONTENT_LENGTH' => io.length ) end @@ -323,10 +323,10 @@ def to_xml it 'does not read and parse the body' do expect(subject).not_to receive(:read_rack_input) subject.call( - 'PATH_INFO' => '/info', - 'REQUEST_METHOD' => method, + Rack::PATH_INFO => '/info', + Rack::REQUEST_METHOD => method, 'CONTENT_TYPE' => 'application/json', - 'rack.input' => io, + Rack::RACK_INPUT => io, 'CONTENT_LENGTH' => 0 ) end @@ -342,10 +342,10 @@ def to_xml it 'does not read and parse the body' do expect(subject).not_to receive(:read_rack_input) subject.call( - 'PATH_INFO' => '/info', - 'REQUEST_METHOD' => method, + Rack::PATH_INFO => '/info', + Rack::REQUEST_METHOD => method, 'CONTENT_TYPE' => 'application/json', - 'rack.input' => io, + Rack::RACK_INPUT => io, 'CONTENT_LENGTH' => 0 ) end @@ -356,57 +356,57 @@ def to_xml it "parses the body from #{method} and copies values into rack.request.form_hash" do io = StringIO.new('{"is_boolean":true,"string":"thing"}') subject.call( - 'PATH_INFO' => '/info', - 'REQUEST_METHOD' => method, + Rack::PATH_INFO => '/info', + Rack::REQUEST_METHOD => method, 'CONTENT_TYPE' => content_type, - 'rack.input' => io, + Rack::RACK_INPUT => io, 'CONTENT_LENGTH' => io.length ) - expect(subject.env['rack.request.form_hash']['is_boolean']).to be true - expect(subject.env['rack.request.form_hash']['string']).to eq('thing') + expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]['is_boolean']).to be true + expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]['string']).to eq('thing') end end end it "parses the chunked body from #{method} and copies values into rack.request.from_hash" do io = StringIO.new('{"is_boolean":true,"string":"thing"}') subject.call( - 'PATH_INFO' => '/infol', - 'REQUEST_METHOD' => method, + Rack::PATH_INFO => '/infol', + Rack::REQUEST_METHOD => method, 'CONTENT_TYPE' => 'application/json', - 'rack.input' => io, - 'HTTP_TRANSFER_ENCODING' => 'chunked' + Rack::RACK_INPUT => io, + Grape::Http::Headers::HTTP_TRANSFER_ENCODING => 'chunked' ) - expect(subject.env['rack.request.form_hash']['is_boolean']).to be true - expect(subject.env['rack.request.form_hash']['string']).to eq('thing') + expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]['is_boolean']).to be true + expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]['string']).to eq('thing') end it 'rewinds IO' do io = StringIO.new('{"is_boolean":true,"string":"thing"}') io.read subject.call( - 'PATH_INFO' => '/infol', - 'REQUEST_METHOD' => method, + Rack::PATH_INFO => '/infol', + Rack::REQUEST_METHOD => method, 'CONTENT_TYPE' => 'application/json', - 'rack.input' => io, - 'HTTP_TRANSFER_ENCODING' => 'chunked' + Rack::RACK_INPUT => io, + Grape::Http::Headers::HTTP_TRANSFER_ENCODING => 'chunked' ) - expect(subject.env['rack.request.form_hash']['is_boolean']).to be true - expect(subject.env['rack.request.form_hash']['string']).to eq('thing') + expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]['is_boolean']).to be true + expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]['string']).to eq('thing') end it "parses the body from an xml #{method} and copies values into rack.request.from_hash" do io = StringIO.new('Test') subject.call( - 'PATH_INFO' => '/info.xml', - 'REQUEST_METHOD' => method, + Rack::PATH_INFO => '/info.xml', + Rack::REQUEST_METHOD => method, 'CONTENT_TYPE' => 'application/xml', - 'rack.input' => io, + Rack::RACK_INPUT => io, 'CONTENT_LENGTH' => io.length ) if Object.const_defined? :MultiXml - expect(subject.env['rack.request.form_hash']['thing']['name']).to eq('Test') + expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]['thing']['name']).to eq('Test') else - expect(subject.env['rack.request.form_hash']['thing']['name']['__content__']).to eq('Test') + expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]['thing']['name']['__content__']).to eq('Test') end end @@ -414,13 +414,13 @@ def to_xml it "ignores #{content_type}" do io = StringIO.new('name=Other+Test+Thing') subject.call( - 'PATH_INFO' => '/info', - 'REQUEST_METHOD' => method, + Rack::PATH_INFO => '/info', + Rack::REQUEST_METHOD => method, 'CONTENT_TYPE' => content_type, - 'rack.input' => io, + Rack::RACK_INPUT => io, 'CONTENT_LENGTH' => io.length ) - expect(subject.env['rack.request.form_hash']).to be_nil + expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]).to be_nil end end end @@ -433,10 +433,10 @@ def to_xml it 'returns a file response' do expect(file).to receive(:each).and_yield('data') - env = { 'PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/json' } + env = { Rack::PATH_INFO => '/somewhere', Grape::Http::Headers::HTTP_ACCEPT => 'application/json' } status, headers, body = subject.call(env) expect(status).to eq 200 - expect(headers.transform_keys(&:downcase)).to eq({ 'content-type' => 'application/json' }) + expect(headers).to eq({ Rack::CONTENT_TYPE => 'application/json' }) expect(read_chunks(body)).to eq ['data'] end end @@ -463,7 +463,7 @@ def self.call(_, _) end it 'returns response by invalid formatter' do - env = { 'PATH_INFO' => '/hello.invalid', 'HTTP_ACCEPT' => 'application/x-invalid' } + env = { Rack::PATH_INFO => '/hello.invalid', Grape::Http::Headers::HTTP_ACCEPT => 'application/x-invalid' } _, _, body = *subject.call(env) expect(read_chunks(body).join).to eq({ message: 'invalid' }.to_json) end @@ -479,10 +479,10 @@ def self.call(_, _) io = StringIO.new('{invalid}') error = catch(:error) do subject.call( - 'PATH_INFO' => '/info', - 'REQUEST_METHOD' => 'POST', + Rack::PATH_INFO => '/info', + Rack::REQUEST_METHOD => Rack::POST, 'CONTENT_TYPE' => 'application/json', - 'rack.input' => io, + Rack::RACK_INPUT => io, 'CONTENT_LENGTH' => io.length ) end diff --git a/spec/grape/middleware/globals_spec.rb b/spec/grape/middleware/globals_spec.rb index 272664ef4..18d3e962b 100644 --- a/spec/grape/middleware/globals_spec.rb +++ b/spec/grape/middleware/globals_spec.rb @@ -14,17 +14,17 @@ context 'environment' do it 'sets the grape.request environment' do subject.call({}) - expect(subject.env['grape.request']).to be_a(Grape::Request) + expect(subject.env[Grape::Env::GRAPE_REQUEST]).to be_a(Grape::Request) end it 'sets the grape.request.headers environment' do subject.call({}) - expect(subject.env['grape.request.headers']).to be_a(Hash) + expect(subject.env[Grape::Env::GRAPE_REQUEST_HEADERS]).to be_a(Hash) end it 'sets the grape.request.params environment' do - subject.call('QUERY_STRING' => 'test=1', 'rack.input' => StringIO.new) - expect(subject.env['grape.request.params']).to be_a(Hash) + subject.call(Rack::QUERY_STRING => 'test=1', Rack::RACK_INPUT => StringIO.new) + expect(subject.env[Grape::Env::GRAPE_REQUEST_PARAMS]).to be_a(Hash) end end end diff --git a/spec/grape/middleware/versioner/accept_version_header_spec.rb b/spec/grape/middleware/versioner/accept_version_header_spec.rb index c2a66f215..4f4438498 100644 --- a/spec/grape/middleware/versioner/accept_version_header_spec.rb +++ b/spec/grape/middleware/versioner/accept_version_header_spec.rb @@ -19,20 +19,20 @@ end it 'is set' do - status, _, env = subject.call('HTTP_ACCEPT_VERSION' => 'v1') - expect(env['api.version']).to eql 'v1' + status, _, env = subject.call(Grape::Http::Headers::HTTP_ACCEPT_VERSION => 'v1') + expect(env[Grape::Env::API_VERSION]).to eql 'v1' expect(status).to eq(200) end it 'is set if format provided' do - status, _, env = subject.call('HTTP_ACCEPT_VERSION' => 'v1') - expect(env['api.version']).to eql 'v1' + status, _, env = subject.call(Grape::Http::Headers::HTTP_ACCEPT_VERSION => 'v1') + expect(env[Grape::Env::API_VERSION]).to eql 'v1' expect(status).to eq(200) end it 'fails with 406 Not Acceptable if version is not supported' do expect do - subject.call('HTTP_ACCEPT_VERSION' => 'v2').last + subject.call(Grape::Http::Headers::HTTP_ACCEPT_VERSION => 'v2').last end.to throw_symbol( :error, status: 406, @@ -43,13 +43,13 @@ end it 'succeeds if :strict is not set' do - expect(subject.call('HTTP_ACCEPT_VERSION' => '').first).to eq(200) + expect(subject.call(Grape::Http::Headers::HTTP_ACCEPT_VERSION => '').first).to eq(200) expect(subject.call({}).first).to eq(200) end it 'succeeds if :strict is set to false' do @options[:version_options][:strict] = false - expect(subject.call('HTTP_ACCEPT_VERSION' => '').first).to eq(200) + expect(subject.call(Grape::Http::Headers::HTTP_ACCEPT_VERSION => '').first).to eq(200) expect(subject.call({}).first).to eq(200) end @@ -72,7 +72,7 @@ it 'fails with 406 Not Acceptable if header is empty' do expect do - subject.call('HTTP_ACCEPT_VERSION' => '').last + subject.call(Grape::Http::Headers::HTTP_ACCEPT_VERSION => '').last end.to throw_symbol( :error, status: 406, @@ -82,7 +82,7 @@ end it 'succeeds if proper header is set' do - expect(subject.call('HTTP_ACCEPT_VERSION' => 'v1').first).to eq(200) + expect(subject.call(Grape::Http::Headers::HTTP_ACCEPT_VERSION => 'v1').first).to eq(200) end end @@ -106,7 +106,7 @@ it 'fails with 406 Not Acceptable if header is empty' do expect do - subject.call('HTTP_ACCEPT_VERSION' => '').last + subject.call(Grape::Http::Headers::HTTP_ACCEPT_VERSION => '').last end.to throw_symbol( :error, status: 406, @@ -116,7 +116,7 @@ end it 'succeeds if proper header is set' do - expect(subject.call('HTTP_ACCEPT_VERSION' => 'v1').first).to eq(200) + expect(subject.call(Grape::Http::Headers::HTTP_ACCEPT_VERSION => 'v1').first).to eq(200) end end end diff --git a/spec/grape/middleware/versioner/header_spec.rb b/spec/grape/middleware/versioner/header_spec.rb index ec116a741..1d686aae3 100644 --- a/spec/grape/middleware/versioner/header_spec.rb +++ b/spec/grape/middleware/versioner/header_spec.rb @@ -16,37 +16,37 @@ context 'api.type and api.subtype' do it 'sets type and subtype to first choice of content type if no preference given' do - status, _, env = subject.call('HTTP_ACCEPT' => '*/*') - expect(env['api.type']).to eql 'application' - expect(env['api.subtype']).to eql 'vnd.vendor+xml' + status, _, env = subject.call(Grape::Http::Headers::HTTP_ACCEPT => '*/*') + expect(env[Grape::Env::API_TYPE]).to eql 'application' + expect(env[Grape::Env::API_SUBTYPE]).to eql 'vnd.vendor+xml' expect(status).to eq(200) end it 'sets preferred type' do - status, _, env = subject.call('HTTP_ACCEPT' => 'application/*') - expect(env['api.type']).to eql 'application' - expect(env['api.subtype']).to eql 'vnd.vendor+xml' + status, _, env = subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/*') + expect(env[Grape::Env::API_TYPE]).to eql 'application' + expect(env[Grape::Env::API_SUBTYPE]).to eql 'vnd.vendor+xml' expect(status).to eq(200) end it 'sets preferred type and subtype' do - status, _, env = subject.call('HTTP_ACCEPT' => 'text/plain') - expect(env['api.type']).to eql 'text' - expect(env['api.subtype']).to eql 'plain' + status, _, env = subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'text/plain') + expect(env[Grape::Env::API_TYPE]).to eql 'text' + expect(env[Grape::Env::API_SUBTYPE]).to eql 'plain' expect(status).to eq(200) end end context 'api.format' do it 'is set' do - status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor+json') - expect(env['api.format']).to eql 'json' + status, _, env = subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendor+json') + expect(env[Grape::Env::API_FORMAT]).to eql 'json' expect(status).to eq(200) end it 'is nil if not provided' do - status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor') - expect(env['api.format']).to be_nil + status, _, env = subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendor') + expect(env[Grape::Env::API_FORMAT]).to be_nil expect(status).to eq(200) end @@ -57,14 +57,14 @@ end it 'is set' do - status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json') - expect(env['api.format']).to eql 'json' + status, _, env = subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendor-v1+json') + expect(env[Grape::Env::API_FORMAT]).to eql 'json' expect(status).to eq(200) end it 'is nil if not provided' do - status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1') - expect(env['api.format']).to be_nil + status, _, env = subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendor-v1') + expect(env[Grape::Env::API_FORMAT]).to be_nil expect(status).to eq(200) end end @@ -73,19 +73,19 @@ context 'api.vendor' do it 'is set' do - status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor') - expect(env['api.vendor']).to eql 'vendor' + status, _, env = subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendor') + expect(env[Grape::Env::API_VENDOR]).to eql 'vendor' expect(status).to eq(200) end it 'is set if format provided' do - status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor+json') - expect(env['api.vendor']).to eql 'vendor' + status, _, env = subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendor+json') + expect(env[Grape::Env::API_VENDOR]).to eql 'vendor' expect(status).to eq(200) end it 'fails with 406 Not Acceptable if vendor is invalid' do - expect { subject.call('HTTP_ACCEPT' => 'application/vnd.othervendor+json').last } + expect { subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.othervendor+json').last } .to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) expect(exception.headers).to eql(Grape::Http::Headers::X_CASCADE => 'pass') @@ -100,19 +100,19 @@ end it 'is set' do - status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1') - expect(env['api.vendor']).to eql 'vendor' + status, _, env = subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendor-v1') + expect(env[Grape::Env::API_VENDOR]).to eql 'vendor' expect(status).to eq(200) end it 'is set if format provided' do - status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json') - expect(env['api.vendor']).to eql 'vendor' + status, _, env = subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendor-v1+json') + expect(env[Grape::Env::API_VENDOR]).to eql 'vendor' expect(status).to eq(200) end it 'fails with 406 Not Acceptable if vendor is invalid' do - expect { subject.call('HTTP_ACCEPT' => 'application/vnd.othervendor-v1+json').last } + expect { subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.othervendor-v1+json').last } .to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) expect(exception.headers).to eql(Grape::Http::Headers::X_CASCADE => 'pass') @@ -129,19 +129,19 @@ end it 'is set' do - status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1') - expect(env['api.version']).to eql 'v1' + status, _, env = subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendor-v1') + expect(env[Grape::Env::API_VERSION]).to eql 'v1' expect(status).to eq(200) end it 'is set if format provided' do - status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json') - expect(env['api.version']).to eql 'v1' + status, _, env = subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendor-v1+json') + expect(env[Grape::Env::API_VERSION]).to eql 'v1' expect(status).to eq(200) end it 'fails with 406 Not Acceptable if version is invalid' do - expect { subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v2+json').last }.to raise_exception do |exception| + expect { subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendor-v2+json').last }.to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidVersionHeader) expect(exception.headers).to eql(Grape::Http::Headers::X_CASCADE => 'pass') expect(exception.status).to be 406 @@ -151,19 +151,19 @@ end it 'succeeds if :strict is not set' do - expect(subject.call('HTTP_ACCEPT' => '').first).to eq(200) + expect(subject.call(Grape::Http::Headers::HTTP_ACCEPT => '').first).to eq(200) expect(subject.call({}).first).to eq(200) end it 'succeeds if :strict is set to false' do @options[:version_options][:strict] = false - expect(subject.call('HTTP_ACCEPT' => '').first).to eq(200) + expect(subject.call(Grape::Http::Headers::HTTP_ACCEPT => '').first).to eq(200) expect(subject.call({}).first).to eq(200) end it 'succeeds if :strict is set to false and given an invalid header' do @options[:version_options][:strict] = false - expect(subject.call('HTTP_ACCEPT' => 'yaml').first).to eq(200) + expect(subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'yaml').first).to eq(200) expect(subject.call({}).first).to eq(200) end @@ -183,7 +183,7 @@ end it 'fails with 406 Not Acceptable if header is empty' do - expect { subject.call('HTTP_ACCEPT' => '').last }.to raise_exception do |exception| + expect { subject.call(Grape::Http::Headers::HTTP_ACCEPT => '').last }.to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) expect(exception.headers).to eql(Grape::Http::Headers::X_CASCADE => 'pass') expect(exception.status).to be 406 @@ -192,7 +192,7 @@ end it 'succeeds if proper header is set' do - expect(subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json').first).to eq(200) + expect(subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendor-v1+json').first).to eq(200) end end @@ -213,7 +213,7 @@ end it 'fails with 406 Not Acceptable if header is application/xml' do - expect { subject.call('HTTP_ACCEPT' => 'application/xml').last } + expect { subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/xml').last } .to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) expect(exception.headers).to eql({}) @@ -223,7 +223,7 @@ end it 'fails with 406 Not Acceptable if header is empty' do - expect { subject.call('HTTP_ACCEPT' => '').last }.to raise_exception do |exception| + expect { subject.call(Grape::Http::Headers::HTTP_ACCEPT => '').last }.to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) expect(exception.headers).to eql({}) expect(exception.status).to be 406 @@ -232,7 +232,7 @@ end it 'fails with 406 Not Acceptable if header contains a single invalid accept' do - expect { subject.call('HTTP_ACCEPT' => 'application/json;application/vnd.vendor-v1+json').first } + expect { subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/json;application/vnd.vendor-v1+json').first } .to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) expect(exception.headers).to eql({}) @@ -242,7 +242,7 @@ end it 'succeeds if proper header is set' do - expect(subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json').first).to eq(200) + expect(subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendor-v1+json').first).to eq(200) end end @@ -252,15 +252,15 @@ end it 'succeeds with v1' do - expect(subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json').first).to eq(200) + expect(subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendor-v1+json').first).to eq(200) end it 'succeeds with v2' do - expect(subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v2+json').first).to eq(200) + expect(subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendor-v2+json').first).to eq(200) end it 'fails with another version' do - expect { subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v3+json') }.to raise_exception do |exception| + expect { subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendor-v3+json') }.to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidVersionHeader) expect(exception.headers).to eql(Grape::Http::Headers::X_CASCADE => 'pass') expect(exception.status).to be 406 diff --git a/spec/grape/middleware/versioner/param_spec.rb b/spec/grape/middleware/versioner/param_spec.rb index 9c2e540ef..00099dfc9 100644 --- a/spec/grape/middleware/versioner/param_spec.rb +++ b/spec/grape/middleware/versioner/param_spec.rb @@ -3,19 +3,19 @@ describe Grape::Middleware::Versioner::Param do subject { described_class.new(app, **options) } - let(:app) { ->(env) { [200, env, env['api.version']] } } + let(:app) { ->(env) { [200, env, env[Grape::Env::API_VERSION]] } } let(:options) { {} } it 'sets the API version based on the default param (apiver)' do env = Rack::MockRequest.env_for('/awesome', params: { 'apiver' => 'v1' }) - expect(subject.call(env)[1]['api.version']).to eq('v1') + expect(subject.call(env)[1][Grape::Env::API_VERSION]).to eq('v1') end it 'cuts (only) the version out of the params' do env = Rack::MockRequest.env_for('/awesome', params: { 'apiver' => 'v1', 'other_param' => '5' }) - env['rack.request.query_hash'] = Rack::Utils.parse_nested_query(env['QUERY_STRING']) - expect(subject.call(env)[1]['rack.request.query_hash']['apiver']).to be_nil - expect(subject.call(env)[1]['rack.request.query_hash']['other_param']).to eq('5') + env[Rack::RACK_REQUEST_QUERY_HASH] = Rack::Utils.parse_nested_query(env[Rack::QUERY_STRING]) + expect(subject.call(env)[1][Rack::RACK_REQUEST_QUERY_HASH]['apiver']).to be_nil + expect(subject.call(env)[1][Rack::RACK_REQUEST_QUERY_HASH]['other_param']).to eq('5') end it 'provides a nil version if no version is given' do @@ -28,12 +28,12 @@ it 'sets the API version based on the custom parameter name' do env = Rack::MockRequest.env_for('/awesome', params: { 'v' => 'v1' }) - expect(subject.call(env)[1]['api.version']).to eq('v1') + expect(subject.call(env)[1][Grape::Env::API_VERSION]).to eq('v1') end it 'does not set the API version based on the default param' do env = Rack::MockRequest.env_for('/awesome', params: { 'apiver' => 'v1' }) - expect(subject.call(env)[1]['api.version']).to be_nil + expect(subject.call(env)[1][Grape::Env::API_VERSION]).to be_nil end end @@ -47,7 +47,7 @@ it 'allows versions that have been specified' do env = Rack::MockRequest.env_for('/awesome', params: { 'apiver' => 'v1' }) - expect(subject.call(env)[1]['api.version']).to eq('v1') + expect(subject.call(env)[1][Grape::Env::API_VERSION]).to eq('v1') end end diff --git a/spec/grape/middleware/versioner/path_spec.rb b/spec/grape/middleware/versioner/path_spec.rb index d5b28a88b..79aff5376 100644 --- a/spec/grape/middleware/versioner/path_spec.rb +++ b/spec/grape/middleware/versioner/path_spec.rb @@ -3,30 +3,30 @@ describe Grape::Middleware::Versioner::Path do subject { described_class.new(app, **options) } - let(:app) { ->(env) { [200, env, env['api.version']] } } + let(:app) { ->(env) { [200, env, env[Grape::Env::API_VERSION]] } } let(:options) { {} } it 'sets the API version based on the first path' do - expect(subject.call('PATH_INFO' => '/v1/awesome').last).to eq('v1') + expect(subject.call(Rack::PATH_INFO => '/v1/awesome').last).to eq('v1') end it 'does not cut the version out of the path' do - expect(subject.call('PATH_INFO' => '/v1/awesome')[1]['PATH_INFO']).to eq('/v1/awesome') + expect(subject.call(Rack::PATH_INFO => '/v1/awesome')[1][Rack::PATH_INFO]).to eq('/v1/awesome') end it 'provides a nil version if no path is given' do - expect(subject.call('PATH_INFO' => '/').last).to be_nil + expect(subject.call(Rack::PATH_INFO => '/').last).to be_nil end context 'with a pattern' do let(:options) { { pattern: /v./i } } it 'sets the version if it matches' do - expect(subject.call('PATH_INFO' => '/v1/awesome').last).to eq('v1') + expect(subject.call(Rack::PATH_INFO => '/v1/awesome').last).to eq('v1') end it 'ignores the version if it fails to match' do - expect(subject.call('PATH_INFO' => '/awesome/radical').last).to be_nil + expect(subject.call(Rack::PATH_INFO => '/awesome/radical').last).to be_nil end end @@ -35,11 +35,11 @@ let(:options) { { versions: versions } } it 'throws an error if a non-allowed version is specified' do - expect(catch(:error) { subject.call('PATH_INFO' => '/v3/awesome') }[:status]).to eq(404) + expect(catch(:error) { subject.call(Rack::PATH_INFO => '/v3/awesome') }[:status]).to eq(404) end it 'allows versions that have been specified' do - expect(subject.call('PATH_INFO' => '/v1/asoasd').last).to eq('v1') + expect(subject.call(Rack::PATH_INFO => '/v1/asoasd').last).to eq('v1') end end end @@ -48,7 +48,7 @@ let(:options) { { prefix: '/v1', pattern: /v./i } } it 'recognizes potential version' do - expect(subject.call('PATH_INFO' => '/v3/foo').last).to eq('v3') + expect(subject.call(Rack::PATH_INFO => '/v3/foo').last).to eq('v3') end end @@ -56,7 +56,7 @@ let(:options) { { mount_path: '/mounted', versions: [:v1] } } it 'recognizes potential version' do - expect(subject.call('PATH_INFO' => '/mounted/v1/foo').last).to eq('v1') + expect(subject.call(Rack::PATH_INFO => '/mounted/v1/foo').last).to eq('v1') end end end diff --git a/spec/grape/request_spec.rb b/spec/grape/request_spec.rb index 4c235ebf2..298c70513 100644 --- a/spec/grape/request_spec.rb +++ b/spec/grape/request_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true describe Grape::Request do - let(:default_method) { 'GET' } + let(:default_method) { Rack::GET } let(:default_params) { {} } let(:default_options) do { diff --git a/spec/grape/util/accept_header_handler_spec.rb b/spec/grape/util/accept_header_handler_spec.rb index de85ed26b..6c94f05ec 100644 --- a/spec/grape/util/accept_header_handler_spec.rb +++ b/spec/grape/util/accept_header_handler_spec.rb @@ -84,7 +84,7 @@ context 'when allowed_methods present' do subject { instance.match_best_quality_media_type!(allowed_methods: allowed_methods) } - let(:allowed_methods) { ['OPTIONS'] } + let(:allowed_methods) { [Rack::OPTIONS] } it { is_expected.to match_array(allowed_methods) } end diff --git a/spec/grape/validations/validators/presence_spec.rb b/spec/grape/validations/validators/presence_spec.rb index 349fcf8d0..e49d5f4d0 100644 --- a/spec/grape/validations/validators/presence_spec.rb +++ b/spec/grape/validations/validators/presence_spec.rb @@ -74,12 +74,12 @@ def app expect(last_response.body).to eq('{"error":"id is missing"}') io = StringIO.new('{"id" : "a56b"}') - post '/', {}, 'rack.input' => io, 'CONTENT_TYPE' => 'application/json', 'CONTENT_LENGTH' => io.length + post '/', {}, Rack::RACK_INPUT => io, 'CONTENT_TYPE' => 'application/json', 'CONTENT_LENGTH' => io.length expect(last_response.body).to eq('{"error":"id is invalid"}') expect(last_response.status).to eq(400) io = StringIO.new('{"id" : 56}') - post '/', {}, 'rack.input' => io, 'CONTENT_TYPE' => 'application/json', 'CONTENT_LENGTH' => io.length + post '/', {}, Rack::RACK_INPUT => io, 'CONTENT_TYPE' => 'application/json', 'CONTENT_LENGTH' => io.length expect(last_response.body).to eq('{"ret":56}') expect(last_response.status).to eq(201) end diff --git a/spec/integration/hashie/hashie_spec.rb b/spec/integration/hashie/hashie_spec.rb index 094c686db..8c8314997 100644 --- a/spec/integration/hashie/hashie_spec.rb +++ b/spec/integration/hashie/hashie_spec.rb @@ -108,7 +108,7 @@ end describe 'Grape::Request' do - let(:default_method) { 'GET' } + let(:default_method) { Rack::GET } let(:default_params) { {} } let(:default_options) do { diff --git a/spec/integration/rack_2_0/headers_spec.rb b/spec/integration/rack_2_0/headers_spec.rb deleted file mode 100644 index ad3408019..000000000 --- a/spec/integration/rack_2_0/headers_spec.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -describe Grape::Http::Headers, if: Gem::Version.new(Rack.release) < Gem::Version.new('3.0.0') do - it { expect(described_class::ALLOW).to eq('Allow') } - it { expect(described_class::LOCATION).to eq('Location') } - it { expect(described_class::TRANSFER_ENCODING).to eq('Transfer-Encoding') } - it { expect(described_class::X_CASCADE).to eq('X-Cascade') } -end diff --git a/spec/integration/rack_3_0/headers_spec.rb b/spec/integration/rack_3_0/headers_spec.rb index 1007cd438..dc5118759 100644 --- a/spec/integration/rack_3_0/headers_spec.rb +++ b/spec/integration/rack_3_0/headers_spec.rb @@ -1,8 +1,77 @@ # frozen_string_literal: true describe Grape::Http::Headers, if: Gem::Version.new(Rack.release) >= Gem::Version.new('3') do - it { expect(described_class::ALLOW).to eq('allow') } - it { expect(described_class::LOCATION).to eq('location') } - it { expect(described_class::TRANSFER_ENCODING).to eq('transfer-encoding') } - it { expect(described_class::X_CASCADE).to eq('x-cascade') } + subject { last_response.headers } + + describe 'returned headers should all be in lowercase' do + context 'when setting an header in an API' do + let(:app) do + Class.new(Grape::API) do + get do + header['GRAPE'] = '1' + return_no_content + end + end + end + + before { get '/' } + + it { is_expected.to include('grape' => '1') } + end + + context 'when error!' do + let(:app) do + Class.new(Grape::API) do + rescue_from ArgumentError do + error!('error!', 500, { 'GRAPE' => '1' }) + end + + get { raise ArgumentError } + end + end + + before { get '/' } + + it { is_expected.to include('grape' => '1') } + end + + context 'when redirect' do + let(:app) do + Class.new(Grape::API) do + get do + redirect 'https://www.ruby-grape.org/' + end + end + end + + before { get '/' } + + it { is_expected.to include('location' => 'https://www.ruby-grape.org/') } + end + + context 'when options' do + let(:app) do + Class.new(Grape::API) do + get { return_no_content } + end + end + + before { options '/' } + + it { is_expected.to include('allow' => 'OPTIONS, GET, HEAD') } + end + + context 'when cascade' do + let(:app) do + Class.new(Grape::API) do + version 'v0', using: :path, cascade: true + get { return_no_content } + end + end + + before { get '/v1' } + + it { is_expected.to include('x-cascade' => 'pass') } + end + end end diff --git a/spec/shared/versioning_examples.rb b/spec/shared/versioning_examples.rb index 192d7bd03..0215a7c44 100644 --- a/spec/shared/versioning_examples.rb +++ b/spec/shared/versioning_examples.rb @@ -5,7 +5,7 @@ subject.format :txt subject.version 'v1', macro_options subject.get :hello do - "Version: #{request.env['api.version']}" + "Version: #{request.env[Grape::Env::API_VERSION]}" end versioned_get '/hello', 'v1', **macro_options expect(last_response.body).to eql 'Version: v1' @@ -16,7 +16,7 @@ subject.prefix 'api' subject.version 'v1', macro_options subject.get :hello do - "Version: #{request.env['api.version']}" + "Version: #{request.env[Grape::Env::API_VERSION]}" end versioned_get '/hello', 'v1', **macro_options.merge(prefix: 'api') expect(last_response.body).to eql 'Version: v1' @@ -65,12 +65,12 @@ subject.format :txt subject.version 'v2', macro_options subject.get 'version' do - request.env['api.version'] + request.env[Grape::Env::API_VERSION] end subject.version 'v1', macro_options do get 'version' do - "version #{request.env['api.version']}" + "version #{request.env[Grape::Env::API_VERSION]}" end end @@ -89,12 +89,12 @@ subject.prefix 'api' subject.version 'v2', macro_options subject.get 'version' do - request.env['api.version'] + request.env[Grape::Env::API_VERSION] end subject.version 'v1', macro_options do get 'version' do - "version #{request.env['api.version']}" + "version #{request.env[Grape::Env::API_VERSION]}" end end diff --git a/spec/support/cookie_jar.rb b/spec/support/cookie_jar.rb index 1e67a95a6..e36ec66cc 100644 --- a/spec/support/cookie_jar.rb +++ b/spec/support/cookie_jar.rb @@ -5,7 +5,7 @@ module Rack class MockResponse def cookie_jar - @cookie_jar ||= Array(headers['Set-Cookie']).flat_map { |h| h.split("\n") }.map { |c| Cookie.new(c).to_h } + @cookie_jar ||= Array(headers[Rack::SET_COOKIE]).flat_map { |h| h.split("\n") }.map { |c| Cookie.new(c).to_h } end # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie diff --git a/spec/support/endpoint_faker.rb b/spec/support/endpoint_faker.rb index 773eb85a3..8e597520e 100644 --- a/spec/support/endpoint_faker.rb +++ b/spec/support/endpoint_faker.rb @@ -17,7 +17,7 @@ def call(env) @request = Grape::Request.new(env.dup) end - @app.call(env.merge('api.endpoint' => @endpoint)) + @app.call(env.merge(Grape::Env::API_ENDPOINT => @endpoint)) end end end diff --git a/spec/support/versioned_helpers.rb b/spec/support/versioned_helpers.rb index ea78013d4..e216f7c8f 100644 --- a/spec/support/versioned_helpers.rb +++ b/spec/support/versioned_helpers.rb @@ -29,14 +29,14 @@ def versioned_headers(**options) {} # no-op when :header { - 'HTTP_ACCEPT' => [ + Grape::Http::Headers::HTTP_ACCEPT => [ "application/vnd.#{options[:vendor]}-#{options[:version]}", options[:format] ].compact.join('+') } when :accept_version_header { - 'HTTP_ACCEPT_VERSION' => options[:version].to_s + Grape::Http::Headers::HTTP_ACCEPT_VERSION => options[:version].to_s } else raise ArgumentError.new("unknown versioning strategy: #{options[:using]}") From 18592e5d79ea571693ac6e20fdb3d91e181b37cb Mon Sep 17 00:00:00 2001 From: Eric Date: Sat, 4 May 2024 18:52:52 +0200 Subject: [PATCH 224/304] Remove Rack::Lint Add CHANGELOG entry --- CHANGELOG.md | 1 + lib/grape/endpoint.rb | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 963ad514a..08fb4e266 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ * [#2425](https://github.com/ruby-grape/grape/pull/2425): Replace `{}` with `Rack::Header` or `Rack::Utils::HeaderHash` - [@dhruvCW](https://github.com/dhruvCW). * [#2430](https://github.com/ruby-grape/grape/pull/2430): Isolate extensions within specific gemfile - [@ericproulx](https://github.com/ericproulx). * [#2431](https://github.com/ruby-grape/grape/pull/2431): Drop appraisals in favor of eval_gemfile - [@ericproulx](https://github.com/ericproulx). +* [#2435](https://github.com/ruby-grape/grape/pull/2435): Use rack constants - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/lib/grape/endpoint.rb b/lib/grape/endpoint.rb index cfca9d353..7ec07792d 100644 --- a/lib/grape/endpoint.rb +++ b/lib/grape/endpoint.rb @@ -280,7 +280,6 @@ def build_stack(helpers) stack = Grape::Middleware::Stack.new stack.use Rack::Head - stack.use Rack::Lint stack.use Class.new(Grape::Middleware::Error), helpers: helpers, format: namespace_inheritable(:format), From e0cb8a9b28a5f0fcfdfcdda9e012f367e68d0505 Mon Sep 17 00:00:00 2001 From: Eric Date: Sat, 4 May 2024 18:56:45 +0200 Subject: [PATCH 225/304] Remove Rack_2_0 integrations tests --- .github/workflows/test.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e6c0a7418..59c57de54 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,9 +33,6 @@ jobs: - ruby: '2.7' gemfile: gemfiles/multi_xml.gemfile specs: 'spec/integration/multi_xml' - - ruby: '2.7' - gemfile: gemfiles/rack_2_0.gemfile - specs: 'spec/integration/rack_2_0' - ruby: '2.7' gemfile: gemfiles/rack_3_0.gemfile specs: 'spec/integration/rack_3_0' From 5cc85c30adcc82ec562947b0c732fb63a92d7f3e Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sat, 4 May 2024 21:47:58 +0200 Subject: [PATCH 226/304] Fix nodejs16 within coverallsapp/github-action (#2436) --- .github/workflows/edge.yml | 4 ++-- .github/workflows/test.yml | 4 ++-- CHANGELOG.md | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/edge.yml b/.github/workflows/edge.yml index cfa629b0b..c9d26044d 100644 --- a/.github/workflows/edge.yml +++ b/.github/workflows/edge.yml @@ -30,7 +30,7 @@ jobs: run: bundle exec rake spec - name: Coveralls - uses: coverallsapp/github-action@master + uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.GITHUB_TOKEN }} flag-name: run-${{ matrix.ruby }}-${{ matrix.gemfile }} @@ -41,7 +41,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Coveralls Finished - uses: coverallsapp/github-action@master + uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.github_token }} parallel-finished: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 59c57de54..f1684af13 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -61,7 +61,7 @@ jobs: run: bundle exec rspec ${{ matrix.specs }} - name: Coveralls - uses: coverallsapp/github-action@master + uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.GITHUB_TOKEN }} flag-name: run-${{ matrix.ruby }}-${{ matrix.gemfile }} @@ -72,7 +72,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Coveralls Finished - uses: coverallsapp/github-action@master + uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.github_token }} parallel-finished: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 08fb4e266..7c3c30de0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ * [#2430](https://github.com/ruby-grape/grape/pull/2430): Isolate extensions within specific gemfile - [@ericproulx](https://github.com/ericproulx). * [#2431](https://github.com/ruby-grape/grape/pull/2431): Drop appraisals in favor of eval_gemfile - [@ericproulx](https://github.com/ericproulx). * [#2435](https://github.com/ruby-grape/grape/pull/2435): Use rack constants - [@ericproulx](https://github.com/ericproulx). +* [#2436](https://github.com/ruby-grape/grape/pull/2436): Update coverallsapp github-action - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes From 0c424d2b4604e249f135f559c5339fec011a8b91 Mon Sep 17 00:00:00 2001 From: Andrei Subbota Date: Mon, 6 May 2024 14:15:37 +0200 Subject: [PATCH 227/304] Implement nested `with` support in parameter DSL (#2434) --- CHANGELOG.md | 1 + README.md | 14 ++++ lib/grape/dsl/parameters.rb | 3 +- lib/grape/validations/params_scope.rb | 1 + spec/grape/dsl/parameters_spec.rb | 47 +++++++++++ spec/grape/validations/params_scope_spec.rb | 90 +++++++++++++++++++++ 6 files changed, 155 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c3c30de0..7ef056f65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ * [#2431](https://github.com/ruby-grape/grape/pull/2431): Drop appraisals in favor of eval_gemfile - [@ericproulx](https://github.com/ericproulx). * [#2435](https://github.com/ruby-grape/grape/pull/2435): Use rack constants - [@ericproulx](https://github.com/ericproulx). * [#2436](https://github.com/ruby-grape/grape/pull/2436): Update coverallsapp github-action - [@ericproulx](https://github.com/ericproulx). +* [#2434](https://github.com/ruby-grape/grape/pull/2434): Implement nested `with` support in parameter dsl - [@numbata](https://github.com/numbata). * Your contribution here. #### Fixes diff --git a/README.md b/README.md index ea2a96443..e346aa53d 100644 --- a/README.md +++ b/README.md @@ -1567,6 +1567,20 @@ params do end ``` +You can organize settings into layers using nested `with' blocks. Each layer can use, add to, or change the settings of the layer above it. This helps to keep complex parameters organized and consistent, while still allowing for specific customizations to be made. + +```ruby +params do + with(documentation: { in: 'body' }) do # Applies documentation to all nested parameters + with(type: String, regexp: /\w+/) do # Applies type and validation to names + requires :first_name, desc: 'First name' + requires :last_name, desc: 'Last name' + end + optional :age, type: Integer, desc: 'Age', documentation: { x: { nullable: true } } # Specific settings for 'age' + end +end +``` + ### Renaming You can rename parameters using `as`, which can be useful when refactoring existing APIs: diff --git a/lib/grape/dsl/parameters.rb b/lib/grape/dsl/parameters.rb index e774105a8..bfc9b408e 100644 --- a/lib/grape/dsl/parameters.rb +++ b/lib/grape/dsl/parameters.rb @@ -170,7 +170,8 @@ def optional(*attrs, &block) # @param (see #requires) # @option (see #requires) def with(*attrs, &block) - new_group_scope(attrs.clone, &block) + new_group_attrs = [@group, attrs.clone.first].compact.reduce(&:deep_merge) + new_group_scope([new_group_attrs], &block) end # Disallow the given parameters to be present in the same request. diff --git a/lib/grape/validations/params_scope.rb b/lib/grape/validations/params_scope.rb index 026e7df9c..9e1e28b84 100644 --- a/lib/grape/validations/params_scope.rb +++ b/lib/grape/validations/params_scope.rb @@ -264,6 +264,7 @@ def new_scope(attrs, optional = false, &block) parent: self, optional: optional, type: type || Array, + group: @group, &block ) end diff --git a/spec/grape/dsl/parameters_spec.rb b/spec/grape/dsl/parameters_spec.rb index 5106866d0..97aae0a58 100644 --- a/spec/grape/dsl/parameters_spec.rb +++ b/spec/grape/dsl/parameters_spec.rb @@ -35,9 +35,17 @@ def validates_reader @validates end + def new_scope(args, _, &block) + nested_scope = self.class.new + nested_scope.new_group_scope(args, &block) + nested_scope + end + def new_group_scope(args) + prev_group = @group @group = args.clone.first yield + @group = prev_group end def extract_message_option(attrs) @@ -169,6 +177,45 @@ def extract_message_option(attrs) ] ) end + + it "supports nested 'with' calls" do + subject.with(type: Integer, documentation: { in: 'body' }) do + subject.optional :pipboy_id + subject.with(documentation: { default: 33 }) do + subject.optional :vault + subject.with(type: String) do + subject.with(documentation: { default: 'resident' }) do + subject.optional :role + end + end + subject.optional :age, documentation: { default: 42 } + end + end + + expect(subject.validate_attributes_reader).to eq( + [ + [:pipboy_id], { type: Integer, documentation: { in: 'body' } }, + [:vault], { type: Integer, documentation: { in: 'body', default: 33 } }, + [:role], { type: String, documentation: { in: 'body', default: 'resident' } }, + [:age], { type: Integer, documentation: { in: 'body', default: 42 } } + ] + ) + end + + it "supports Hash parameter inside the 'with' calls" do + subject.with(documentation: { in: 'body' }) do + subject.optional :info, type: Hash, documentation: { x: { nullable: true }, desc: 'The info' } do + subject.optional :vault, type: Integer, documentation: { default: 33, desc: 'The vault number' } + end + end + + expect(subject.validate_attributes_reader).to eq( + [ + [:info], { type: Hash, documentation: { in: 'body', desc: 'The info', x: { nullable: true } } }, + [:vault], { type: Integer, documentation: { in: 'body', default: 33, desc: 'The vault number' } } + ] + ) + end end describe '#mutually_exclusive' do diff --git a/spec/grape/validations/params_scope_spec.rb b/spec/grape/validations/params_scope_spec.rb index 9ef7ad7db..af6a4a2c9 100644 --- a/spec/grape/validations/params_scope_spec.rb +++ b/spec/grape/validations/params_scope_spec.rb @@ -1381,6 +1381,96 @@ def initialize(value) end end end + + context 'with many levels of nested groups' do + before do + subject.params do + requires :first_level, type: Hash do + with(type: Integer) do + requires :value + with(type: String) do + optional :second_level, type: Array do + optional :name, type: String + with(type: Integer) do + optional :third_level, type: Array do + requires :value, type: Integer + optional :position + end + end + end + end + requires :id + end + end + end + subject.put('/nested') { declared(params).to_json } + end + + context 'when data is valid' do + let(:request_params) do + { + first_level: { + value: '10', + second_level: [ + { + name: '13', + third_level: [ + { + value: '2', + position: '1' + } + ] + } + ], + id: '20' + } + } + end + + it 'validates and coerces correctly' do + put '/nested', request_params.to_json, 'CONTENT_TYPE' => 'application/json' + + expect(last_response.status).to eq(200) + expect(JSON.parse(last_response.body, symbolize_names: true)).to eq( + first_level: { + value: 10, + second_level: [ + { name: '13', third_level: [{ value: 2, position: 1 }] } + ], + id: 20 + } + ) + end + end + + context 'when data is invalid' do + let(:request_params) do + { + first_level: { + value: 'wrong', + second_level: [ + { name: 'name', third_level: [{ position: 'wrong' }] } + ] + } + } + end + + it 'responds with HTTP error' do + put '/nested', request_params.to_json, 'CONTENT_TYPE' => 'application/json' + expect(last_response.status).to eq(400) + end + + it 'responds with a validation error' do + put '/nested', request_params.to_json, 'CONTENT_TYPE' => 'application/json' + + expect(last_response.body) + .to include('first_level[value] is invalid') + .and include('first_level[id] is missing') + .and include('first_level[second_level][0][third_level][0][value] is missing') + .and include('first_level[second_level][0][third_level][0][position] is invalid') + end + end + end end context 'with exactly_one_of validation for optional parameters within an Hash param' do From 3f6a70ae47bdf00e398f9327bdf94189d32db8a0 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sat, 11 May 2024 19:11:12 +0200 Subject: [PATCH 228/304] Fix Rack::Lint (#2438) * == Spec == Fix Rack::Lint rack2 and rack3 Use Rack::MockResponse instead spec/support/chunks Use Rack::Builder.app in spec instead of Rack::Builder.new Remove useless Rack::Builder.new == Changes == Returns [] when no entity body instead of received body Grape::ErrorFormatter::Txt forces .to_s since it might be a symbol Try close body in response if possible when dismissing response (cascade) Rewind input only if rewindable * Update router.rb Fix typo * Add CHANGELOG.md * Remove `to_s` --- .rubocop_todo.yml | 18 ++-- CHANGELOG.md | 1 + lib/grape/error_formatter/txt.rb | 21 ++-- lib/grape/middleware/formatter.rb | 10 +- lib/grape/router.rb | 25 +++-- spec/grape/api/custom_validations_spec.rb | 8 +- spec/grape/api_spec.rb | 65 +++++++----- spec/grape/endpoint_spec.rb | 34 +++---- spec/grape/integration/rack_spec.rb | 63 ++++++++---- spec/grape/middleware/auth/strategies_spec.rb | 9 +- spec/grape/middleware/base_spec.rb | 4 +- spec/grape/middleware/exception_spec.rb | 19 ++-- spec/grape/middleware/formatter_spec.rb | 98 ++++++++++++------- .../validations/validators/presence_spec.rb | 12 +-- spec/support/chunks.rb | 14 --- 15 files changed, 230 insertions(+), 171 deletions(-) delete mode 100644 spec/support/chunks.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index f8f44abc8..44b3ed850 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2024-04-17 16:26:06 UTC using RuboCop version 1.63.2. +# on 2024-05-10 16:10:58 UTC using RuboCop version 1.63.2. # 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 @@ -102,6 +102,14 @@ RSpec/DescribeClass: - '**/spec/views/**/*' - 'spec/grape/named_api_spec.rb' +# Offense count: 2 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: SkipBlocks, EnforcedStyle, OnlyStaticConstants. +# SupportedStyles: described_class, explicit +RSpec/DescribedClass: + Exclude: + - 'spec/grape/middleware/exception_spec.rb' + # Offense count: 3 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: CustomTransform, IgnoredWords, DisallowedExamples. @@ -203,7 +211,7 @@ RSpec/ScatteredSetup: Exclude: - 'spec/grape/util/inheritable_setting_spec.rb' -# Offense count: 8 +# Offense count: 5 RSpec/StubbedMock: Exclude: - 'spec/grape/dsl/inside_route_spec.rb' @@ -259,12 +267,6 @@ Style/CombinableLoops: Style/FormatStringToken: EnforcedStyle: template -# Offense count: 1 -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/MapIntoArray: - Exclude: - - 'spec/support/chunks.rb' - # Offense count: 12 # Configuration parameters: AllowedMethods. # AllowedMethods: respond_to_missing? diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ef056f65..6bf46fdb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ * [#2435](https://github.com/ruby-grape/grape/pull/2435): Use rack constants - [@ericproulx](https://github.com/ericproulx). * [#2436](https://github.com/ruby-grape/grape/pull/2436): Update coverallsapp github-action - [@ericproulx](https://github.com/ericproulx). * [#2434](https://github.com/ruby-grape/grape/pull/2434): Implement nested `with` support in parameter dsl - [@numbata](https://github.com/numbata). +* [#2438](https://github.com/ruby-grape/grape/pull/2438): Fix some Rack::Lint - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/lib/grape/error_formatter/txt.rb b/lib/grape/error_formatter/txt.rb index 76d3cb3f1..22fc5c538 100644 --- a/lib/grape/error_formatter/txt.rb +++ b/lib/grape/error_formatter/txt.rb @@ -10,16 +10,17 @@ def call(message, backtrace, options = {}, env = nil, original_exception = nil) message = present(message, env) result = message.is_a?(Hash) ? ::Grape::Json.dump(message) : message - rescue_options = options[:rescue_options] || {} - if rescue_options[:backtrace] && backtrace && !backtrace.empty? - result += "\r\n backtrace:" - result += backtrace.join("\r\n ") - end - if rescue_options[:original_exception] && original_exception - result += "\r\n original exception:" - result += "\r\n #{original_exception.inspect}" - end - result + Array.wrap(result).tap do |final_result| + rescue_options = options[:rescue_options] || {} + if rescue_options[:backtrace] && backtrace.present? + final_result << 'backtrace:' + final_result.concat(backtrace) + end + if rescue_options[:original_exception] && original_exception + final_result << 'original exception:' + final_result << original_exception.inspect + end + end.join("\r\n ") end end end diff --git a/lib/grape/middleware/formatter.rb b/lib/grape/middleware/formatter.rb index a199ce981..35a315001 100644 --- a/lib/grape/middleware/formatter.rb +++ b/lib/grape/middleware/formatter.rb @@ -25,7 +25,7 @@ def after status, headers, bodies = *@app_response if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status) - @app_response + [status, headers, []] else build_formatted_response(status, headers, bodies) end @@ -83,12 +83,12 @@ def read_body_input return unless (input = env[Rack::RACK_INPUT]) - input.rewind + rewind_input input body = env[Grape::Env::API_REQUEST_INPUT] = input.read begin read_rack_input(body) if body && !body.empty? ensure - input.rewind + rewind_input input end end @@ -173,6 +173,10 @@ def mime_array .sort_by { |_, quality_preference| -(quality_preference ? quality_preference.to_f : 1.0) } .flat_map { |mime, _| [mime, mime.sub(vendor_prefix_pattern, '')] } end + + def rewind_input(input) + input.rewind if input.respond_to?(:rewind) + end end end end diff --git a/lib/grape/router.rb b/lib/grape/router.rb index cbed9d87d..ba0207395 100644 --- a/lib/grape/router.rb +++ b/lib/grape/router.rb @@ -88,26 +88,33 @@ def rotation(env, exact_route = nil) def transaction(env) input, method = *extract_input_and_method(env) - response = yield(input, method) - return response if response && !(cascade = cascade?(response)) + # using a Proc is important since `return` will exit the enclosing function + cascade_or_return_response = proc do |response| + if response + cascade?(response).tap do |cascade| + return response unless cascade + # we need to close the body if possible before dismissing + response[2].close if response[2].respond_to?(:close) + end + end + end + + last_response_cascade = cascade_or_return_response.call(yield(input, method)) last_neighbor_route = greedy_match?(input) # If last_neighbor_route exists and request method is OPTIONS, # return response by using #call_with_allow_headers. - return call_with_allow_headers(env, last_neighbor_route) if last_neighbor_route && method == Rack::OPTIONS && !cascade + return call_with_allow_headers(env, last_neighbor_route) if last_neighbor_route && method == Rack::OPTIONS && !last_response_cascade route = match?(input, '*') - return last_neighbor_route.endpoint.call(env) if last_neighbor_route && cascade && route + return last_neighbor_route.endpoint.call(env) if last_neighbor_route && last_response_cascade && route - if route - response = process_route(route, env) - return response if response && !(cascade = cascade?(response)) - end + last_response_cascade = cascade_or_return_response.call(process_route(route, env)) if route - return call_with_allow_headers(env, last_neighbor_route) if !cascade && last_neighbor_route + return call_with_allow_headers(env, last_neighbor_route) if !last_response_cascade && last_neighbor_route nil end diff --git a/spec/grape/api/custom_validations_spec.rb b/spec/grape/api/custom_validations_spec.rb index 936971a95..d16c307fe 100644 --- a/spec/grape/api/custom_validations_spec.rb +++ b/spec/grape/api/custom_validations_spec.rb @@ -33,7 +33,7 @@ def validate_param!(attr_name, params) end end end - let(:app) { Rack::Builder.new(subject) } + let(:app) { subject } before { stub_const('Grape::Validations::Validators::DefaultLengthValidator', default_length_validator) } @@ -75,7 +75,7 @@ def validate(request) end end end - let(:app) { Rack::Builder.new(subject) } + let(:app) { subject } before { stub_const('Grape::Validations::Validators::InBodyValidator', in_body_validator) } @@ -111,7 +111,7 @@ def validate_param!(attr_name, _params) end end end - let(:app) { Rack::Builder.new(subject) } + let(:app) { subject } before { stub_const('Grape::Validations::Validators::WithMessageKeyValidator', message_key_validator) } @@ -156,7 +156,7 @@ def access_header end end - let(:app) { Rack::Builder.new(subject) } + let(:app) { subject } let(:x_access_token_header) { 'x-access-token' } before { stub_const('Grape::Validations::Validators::AdminValidator', admin_validator) } diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index 218a65461..868b1a4f9 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -460,7 +460,7 @@ def to_txt subject.send(verb) do env[Grape::Env::API_REQUEST_INPUT] end - send verb, '/', ::Grape::Json.dump(object), 'CONTENT_TYPE' => 'application/json', Grape::Http::Headers::HTTP_TRANSFER_ENCODING => 'chunked', 'CONTENT_LENGTH' => nil + send verb, '/', ::Grape::Json.dump(object), 'CONTENT_TYPE' => 'application/json', Grape::Http::Headers::HTTP_TRANSFER_ENCODING => 'chunked' expect(last_response.status).to eq(verb == :post ? 201 : 200) expect(last_response.body).to eql ::Grape::Json.dump(object).to_json end @@ -1999,32 +1999,49 @@ def custom_error!(name) end context 'with multiple apis' do - let(:a) { Class.new(described_class) } - let(:b) { Class.new(described_class) } + let(:a) do + Class.new(described_class) do + namespace :a do + helpers do + def foo + error!('foo', 401) + end + end - before do - a.helpers do - def foo - error!('foo', 401) + rescue_from(:all) { foo } + + get { raise 'boo' } end end - a.rescue_from(:all) { foo } - a.get { raise 'boo' } - b.helpers do - def foo - error!('bar', 401) + end + let(:b) do + Class.new(described_class) do + namespace :b do + helpers do + def foo + error!('bar', 401) + end + end + + rescue_from(:all) { foo } + + get { raise 'boo' } end end - b.rescue_from(:all) { foo } - b.get { raise 'boo' } end - it 'avoids polluting global namespace' do - env = Rack::MockRequest.env_for('/') + before do + subject.mount a + subject.mount b + end - 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']) + it 'avoids polluting global namespace' do + get '/a' + expect(last_response.body).to eq('foo') + get '/b' + expect(last_response.body).to eq('bar') + get '/a' + expect(last_response.body).to eq('foo') end end @@ -3817,14 +3834,14 @@ def my_method it 'raised :error from middleware' do middleware = Class.new(Grape::Middleware::Base) do def before - throw :error, message: 'Unauthorized', status: 42 + throw :error, message: 'Unauthorized', status: 500 end end subject.use middleware subject.get do end get '/' - expect(last_response.status).to eq(42) + expect(last_response).to be_server_error expect(last_response.body).to eq({ error: 'Unauthorized' }.to_json) end end @@ -3923,14 +3940,14 @@ def serializable_hash it 'raised :error from middleware' do middleware = Class.new(Grape::Middleware::Base) do def before - throw :error, message: 'Unauthorized', status: 42 + throw :error, message: 'Unauthorized', status: 500 end end subject.use middleware subject.get do end get '/' - expect(last_response.status).to eq(42) + expect(last_response.status).to eq(500) expect(last_response.body).to eq <<~XML @@ -4372,7 +4389,7 @@ def uniqe_id_route let(:app) do Class.new(described_class) do rescue_from :all do - rack_response('deprecated', 500) + rack_response('deprecated', 500, 'Content-Type' => 'text/plain') end get 'test' do diff --git a/spec/grape/endpoint_spec.rb b/spec/grape/endpoint_spec.rb index 55ae6fd20..b932a70d2 100644 --- a/spec/grape/endpoint_spec.rb +++ b/spec/grape/endpoint_spec.rb @@ -153,14 +153,6 @@ def app x_grape_client_header = 'x-grape-client' expect(JSON.parse(last_response.body)[x_grape_client_header]).to eq('1') end - - it 'includes headers passed as symbols' do - env = Rack::MockRequest.env_for('/headers') - env[:HTTP_SYMBOL_HEADER] = 'Goliath passes symbols' - body = read_chunks(subject.call(env)[2]).join - symbol_header = 'symbol-header' - expect(JSON.parse(body)[symbol_header]).to eq('Goliath passes symbols') - end end describe '#cookies' do @@ -662,7 +654,7 @@ def app subject.post('/hey') do redirect '/ha' end - post '/hey', {}, 'HTTP_VERSION' => 'HTTP/1.1' + post '/hey', {}, 'HTTP_VERSION' => 'HTTP/1.1', 'SERVER_PROTOCOL' => 'HTTP/1.1' expect(last_response.status).to eq 303 expect(last_response.location).to eq '/ha' expect(last_response.body).to eq 'An alternate resource is located at /ha.' @@ -842,33 +834,33 @@ def memoized context 'anchoring' do describe 'delete 204' do it 'allows for the anchoring option with a delete method' do - subject.send(:delete, '/example', anchor: true) {} - send(:delete, '/example/and/some/more') - expect(last_response.status).to be 404 + subject.delete('/example', anchor: true) + delete '/example/and/some/more' + expect(last_response).to be_not_found end it 'anchors paths by default for the delete method' do - subject.send(:delete, '/example') {} - send(:delete, '/example/and/some/more') - expect(last_response.status).to be 404 + subject.delete '/example' + delete '/example/and/some/more' + expect(last_response).to be_not_found end it 'responds to /example/and/some/more for the non-anchored delete method' do - subject.send(:delete, '/example', anchor: false) {} - send(:delete, '/example/and/some/more') - expect(last_response.status).to be 204 + subject.delete '/example', anchor: false + delete '/example/and/some/more' + expect(last_response).to be_no_content expect(last_response.body).to be_empty end end describe 'delete 200, with response body' do it 'responds to /example/and/some/more for the non-anchored delete method' do - subject.send(:delete, '/example', anchor: false) do + subject.delete('/example', anchor: false) do status 200 body 'deleted' end - send(:delete, '/example/and/some/more') - expect(last_response.status).to be 200 + delete '/example/and/some/more' + expect(last_response).to be_successful expect(last_response.body).not_to be_empty end end diff --git a/spec/grape/integration/rack_spec.rb b/spec/grape/integration/rack_spec.rb index 0c09b676b..b68b24a60 100644 --- a/spec/grape/integration/rack_spec.rb +++ b/spec/grape/integration/rack_spec.rb @@ -1,28 +1,52 @@ # frozen_string_literal: true describe Rack do - it 'correctly populates params from a Tempfile' do - input = Tempfile.new 'rubbish' - begin - app = Class.new(Grape::API) do + describe 'from a Tempfile' do + subject { last_response.body } + + let(:app) do + Class.new(Grape::API) do format :json + + params do + requires :file, type: File + end + post do - { params_keys: params.keys } + params[:file].then do |file| + { + filename: file[:filename], + type: file[:type], + content: file[:tempfile].read + } + end end end - input.write({ test: '123' * 10_000 }.to_json) - input.rewind - options = { - input: input, - method: Rack::POST, - 'CONTENT_TYPE' => 'application/json' - } - env = Rack::MockRequest.env_for('/', options) - - expect(JSON.parse(read_chunks(app.call(env)[2]).join)['params_keys']).to match_array('test') + end + + let(:response_body) do + { + filename: File.basename(tempfile.path), + type: 'text/plain', + content: 'rubbish' + }.to_json + end + + let(:tempfile) do + Tempfile.new.tap do |t| + t.write('rubbish') + t.rewind + end + end + + before do + post '/', file: Rack::Test::UploadedFile.new(tempfile.path, 'text/plain') + end + + it 'correctly populates params from a Tempfile' do + expect(subject).to eq(response_body) ensure - input.close - input.unlink + tempfile.close! end end @@ -35,17 +59,16 @@ let(:app) do app_to_mount = ping_mount - app = Class.new(Grape::API) do + Class.new(Grape::API) do namespace 'namespace' do mount app_to_mount end end - Rack::Builder.new(app) end it 'finds the app on the namespace' do get '/namespace/ping' - expect(last_response.status).to eq 200 + expect(last_response).to be_successful end end end diff --git a/spec/grape/middleware/auth/strategies_spec.rb b/spec/grape/middleware/auth/strategies_spec.rb index 48c62d3d0..c3efde0f0 100644 --- a/spec/grape/middleware/auth/strategies_spec.rb +++ b/spec/grape/middleware/auth/strategies_spec.rb @@ -4,10 +4,10 @@ describe 'Basic Auth' do let(:app) do proc = ->(u, p) { u && p && u == p } - Rack::Builder.new do |b| - b.use Grape::Middleware::Error - b.use(Grape::Middleware::Auth::Base, type: :http_basic, proc: proc) - b.run ->(_env) { [200, {}, ['Hello there.']] } + Rack::Builder.app do + use Grape::Middleware::Error + use(Grape::Middleware::Auth::Base, type: :http_basic, proc: proc) + run ->(_env) { [200, {}, ['Hello there.']] } end end @@ -19,6 +19,7 @@ it 'authenticates if given valid creds' do get '/whatever', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('admin', 'admin') expect(last_response).to be_successful + expect(last_response.body).to eq('Hello there.') end it 'throws a 401 is wrong auth is given' do diff --git a/spec/grape/middleware/base_spec.rb b/spec/grape/middleware/base_spec.rb index bb3b2d6b8..608c5012e 100644 --- a/spec/grape/middleware/base_spec.rb +++ b/spec/grape/middleware/base_spec.rb @@ -171,7 +171,7 @@ def after end end - def app + let(:app) do context = self Rack::Builder.app do @@ -209,7 +209,7 @@ def after end end - def app + let(:app) do context = self Rack::Builder.app do diff --git a/spec/grape/middleware/exception_spec.rb b/spec/grape/middleware/exception_spec.rb index bc28fa562..b3fe18144 100644 --- a/spec/grape/middleware/exception_spec.rb +++ b/spec/grape/middleware/exception_spec.rb @@ -60,15 +60,18 @@ def call(_env) end let(:app) do - builder = Rack::Builder.new - builder.use Spec::Support::EndpointFaker - if options.any? - builder.use described_class, options - else - builder.use described_class + opts = options + app = running_app + Rack::Builder.app do + use Rack::Lint + use Spec::Support::EndpointFaker + if opts.any? + use Grape::Middleware::Error, opts + else + use Grape::Middleware::Error + end + run app end - builder.run running_app - builder.to_app end context 'with defaults' do diff --git a/spec/grape/middleware/formatter_spec.rb b/spec/grape/middleware/formatter_spec.rb index 0f67d790a..4e19aba96 100644 --- a/spec/grape/middleware/formatter_spec.rb +++ b/spec/grape/middleware/formatter_spec.rb @@ -10,14 +10,20 @@ context 'serialization' do let(:body) { { 'abc' => 'def' } } + let(:env) do + { Rack::PATH_INFO => '/somewhere', Grape::Http::Headers::HTTP_ACCEPT => 'application/json' } + end it 'looks at the bodies for possibly serializable data' do - _, _, bodies = *subject.call(Rack::PATH_INFO => '/somewhere', Grape::Http::Headers::HTTP_ACCEPT => 'application/json') - bodies.each { |b| expect(b).to eq(::Grape::Json.dump(body)) } # rubocop:disable RSpec/IteratedExpectation + r = Rack::MockResponse[*subject.call(env)] + expect(r.body).to eq(::Grape::Json.dump(body)) end context 'default format' do let(:body) { ['foo'] } + let(:env) do + { Rack::PATH_INFO => '/somewhere', Grape::Http::Headers::HTTP_ACCEPT => 'application/json' } + end it 'calls #to_json since default format is json' do body.instance_eval do @@ -25,13 +31,16 @@ def to_json(*_args) '"bar"' end end - - subject.call(Rack::PATH_INFO => '/somewhere', Grape::Http::Headers::HTTP_ACCEPT => 'application/json').to_a.last.each { |b| expect(b).to eq('"bar"') } # rubocop:disable RSpec/IteratedExpectation + r = Rack::MockResponse[*subject.call(env)] + expect(r.body).to eq('"bar"') end end context 'jsonapi' do let(:body) { { 'foos' => [{ 'bar' => 'baz' }] } } + let(:env) do + { Rack::PATH_INFO => '/somewhere', Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.api+json' } + end it 'calls #to_json if the content type is jsonapi' do body.instance_eval do @@ -39,13 +48,16 @@ def to_json(*_args) '{"foos":[{"bar":"baz"}] }' end end - - subject.call(Rack::PATH_INFO => '/somewhere', Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.api+json').to_a.last.each { |b| expect(b).to eq('{"foos":[{"bar":"baz"}] }') } # rubocop:disable RSpec/IteratedExpectation + r = Rack::MockResponse[*subject.call(env)] + expect(r.body).to eq(Grape::Json.dump(body)) end end context 'xml' do let(:body) { +'string' } + let(:env) do + { Rack::PATH_INFO => '/somewhere.xml', Grape::Http::Headers::HTTP_ACCEPT => 'application/json' } + end it 'calls #to_xml if the content type is xml' do body.instance_eval do @@ -53,13 +65,17 @@ def to_xml '' end end - subject.call(Rack::PATH_INFO => '/somewhere.xml', Grape::Http::Headers::HTTP_ACCEPT => 'application/json').to_a.last.each { |b| expect(b).to eq('') } # rubocop:disable RSpec/IteratedExpectation + r = Rack::MockResponse[*subject.call(env)] + expect(r.body).to eq('') end end end context 'error handling' do let(:formatter) { double(:formatter) } + let(:env) do + { Rack::PATH_INFO => '/somewhere.xml', Grape::Http::Headers::HTTP_ACCEPT => 'application/json' } + end before do allow(Grape::Formatter).to receive(:formatter_for) { formatter } @@ -69,7 +85,7 @@ def to_xml allow(formatter).to receive(:call) { raise Grape::Exceptions::InvalidFormatter.new(String, 'xml') } expect do - catch(:error) { subject.call(Rack::PATH_INFO => '/somewhere.xml', Grape::Http::Headers::HTTP_ACCEPT => 'application/json') } + catch(:error) { subject.call(env) } end.not_to raise_error end @@ -177,8 +193,9 @@ def to_xml context 'with custom vendored content types' do before do - subject.options[:content_types] = {} - subject.options[:content_types][:custom] = 'application/vnd.test+json' + subject.options[:content_types] = {}.tap do |ct| + ct[:custom] = 'application/vnd.test+json' + end end it 'uses the custom type' do @@ -210,15 +227,17 @@ def to_xml end it 'is set for custom' do - subject.options[:content_types] = {} - subject.options[:content_types][:custom] = 'application/x-custom' + subject.options[:content_types] = {}.tap do |ct| + ct[:custom] = 'application/x-custom' + end _, headers, = subject.call(Rack::PATH_INFO => '/info.custom') expect(headers[Rack::CONTENT_TYPE]).to eq('application/x-custom') end it 'is set for vendored with registered type' do - subject.options[:content_types] = {} - subject.options[:content_types][:custom] = 'application/vnd.test+json' + subject.options[:content_types] = {}.tap do |ct| + ct[:custom] = 'application/vnd.test+json' + end _, headers, = subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.test+json') expect(headers[Rack::CONTENT_TYPE]).to eq('application/vnd.test+json') end @@ -234,28 +253,28 @@ def to_xml subject.options[:content_types] = {} subject.options[:content_types][:custom] = "don't care" subject.options[:formatters][:custom] = ->(_obj, _env) { 'CUSTOM FORMAT' } - _, _, body = subject.call(Rack::PATH_INFO => '/info.custom') - expect(read_chunks(body)).to eq(['CUSTOM FORMAT']) + r = Rack::MockResponse[*subject.call(Rack::PATH_INFO => '/info.custom')] + expect(r.body).to eq('CUSTOM FORMAT') end context 'default' do let(:body) { ['blah'] } it 'uses default json formatter' do - _, _, body = subject.call(Rack::PATH_INFO => '/info.json') - expect(read_chunks(body)).to eq(['["blah"]']) + r = Rack::MockResponse[*subject.call(Rack::PATH_INFO => '/info.json')] + expect(r.body).to eq(Grape::Json.dump(body)) end end it 'uses custom json formatter' do subject.options[:formatters][:json] = ->(_obj, _env) { 'CUSTOM JSON FORMAT' } - _, _, body = subject.call(Rack::PATH_INFO => '/info.json') - expect(read_chunks(body)).to eq(['CUSTOM JSON FORMAT']) + r = Rack::MockResponse[*subject.call(Rack::PATH_INFO => '/info.json')] + expect(r.body).to eq('CUSTOM JSON FORMAT') end end context 'no content responses' do - let(:no_content_response) { ->(status) { [status, {}, ['']] } } + let(:no_content_response) { ->(status) { [status, {}, []] } } statuses_without_body = if Gem::Version.new(Rack.release) >= Gem::Version.new('2.1.0') Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.keys @@ -286,7 +305,7 @@ def to_xml Rack::REQUEST_METHOD => method, 'CONTENT_TYPE' => content_type, Rack::RACK_INPUT => io, - 'CONTENT_LENGTH' => io.length + 'CONTENT_LENGTH' => io.length.to_s ) expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]['is_boolean']).to be true expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]['string']).to eq('thing') @@ -304,7 +323,7 @@ def to_xml Rack::REQUEST_METHOD => method, 'CONTENT_TYPE' => content_type, Rack::RACK_INPUT => io, - 'CONTENT_LENGTH' => io.length + 'CONTENT_LENGTH' => io.length.to_s ) end expect(error[:status]).to eq(415) @@ -327,7 +346,7 @@ def to_xml Rack::REQUEST_METHOD => method, 'CONTENT_TYPE' => 'application/json', Rack::RACK_INPUT => io, - 'CONTENT_LENGTH' => 0 + 'CONTENT_LENGTH' => '0' ) end end @@ -360,7 +379,7 @@ def to_xml Rack::REQUEST_METHOD => method, 'CONTENT_TYPE' => content_type, Rack::RACK_INPUT => io, - 'CONTENT_LENGTH' => io.length + 'CONTENT_LENGTH' => io.length.to_s ) expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]['is_boolean']).to be true expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]['string']).to eq('thing') @@ -401,7 +420,7 @@ def to_xml Rack::REQUEST_METHOD => method, 'CONTENT_TYPE' => 'application/xml', Rack::RACK_INPUT => io, - 'CONTENT_LENGTH' => io.length + 'CONTENT_LENGTH' => io.length.to_s ) if Object.const_defined? :MultiXml expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]['thing']['name']).to eq('Test') @@ -418,7 +437,7 @@ def to_xml Rack::REQUEST_METHOD => method, 'CONTENT_TYPE' => content_type, Rack::RACK_INPUT => io, - 'CONTENT_LENGTH' => io.length + 'CONTENT_LENGTH' => io.length.to_s ) expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]).to be_nil end @@ -430,14 +449,17 @@ def to_xml let(:file) { double(File) } let(:file_body) { Grape::ServeStream::StreamResponse.new(file) } let(:app) { ->(_env) { [200, {}, file_body] } } + let(:body) { 'data' } + let(:env) do + { Rack::PATH_INFO => '/somewhere', Grape::Http::Headers::HTTP_ACCEPT => 'application/json' } + end it 'returns a file response' do - expect(file).to receive(:each).and_yield('data') - env = { Rack::PATH_INFO => '/somewhere', Grape::Http::Headers::HTTP_ACCEPT => 'application/json' } - status, headers, body = subject.call(env) - expect(status).to eq 200 - expect(headers).to eq({ Rack::CONTENT_TYPE => 'application/json' }) - expect(read_chunks(body)).to eq ['data'] + expect(file).to receive(:each).and_yield(body) + r = Rack::MockResponse[*subject.call(env)] + expect(r).to be_successful + expect(r.headers).to eq({ Rack::CONTENT_TYPE => 'application/json', Rack::CONTENT_LENGTH => body.bytesize.to_s }) + expect(r.body).to eq('data') end end @@ -451,6 +473,9 @@ def self.call(_, _) end let(:app) { ->(_env) { [200, {}, ['']] } } + let(:env) do + { Rack::PATH_INFO => '/hello.invalid', Grape::Http::Headers::HTTP_ACCEPT => 'application/x-invalid' } + end before do Grape::Formatter.register :invalid, invalid_formatter @@ -463,9 +488,8 @@ def self.call(_, _) end it 'returns response by invalid formatter' do - env = { Rack::PATH_INFO => '/hello.invalid', Grape::Http::Headers::HTTP_ACCEPT => 'application/x-invalid' } - _, _, body = *subject.call(env) - expect(read_chunks(body).join).to eq({ message: 'invalid' }.to_json) + r = Rack::MockResponse[*subject.call(env)] + expect(r.body).to eq(Grape::Json.dump('message' => 'invalid')) end end @@ -483,7 +507,7 @@ def self.call(_, _) Rack::REQUEST_METHOD => Rack::POST, 'CONTENT_TYPE' => 'application/json', Rack::RACK_INPUT => io, - 'CONTENT_LENGTH' => io.length + 'CONTENT_LENGTH' => io.length.to_s ) end diff --git a/spec/grape/validations/validators/presence_spec.rb b/spec/grape/validations/validators/presence_spec.rb index e49d5f4d0..3dc2dc8a8 100644 --- a/spec/grape/validations/validators/presence_spec.rb +++ b/spec/grape/validations/validators/presence_spec.rb @@ -70,18 +70,16 @@ def app it 'validates id' do post '/' - expect(last_response.status).to eq(400) + expect(last_response).to be_bad_request expect(last_response.body).to eq('{"error":"id is missing"}') - io = StringIO.new('{"id" : "a56b"}') - post '/', {}, Rack::RACK_INPUT => io, 'CONTENT_TYPE' => 'application/json', 'CONTENT_LENGTH' => io.length + post '/', { id: 'a56b' }.to_json, 'CONTENT_TYPE' => 'application/json' expect(last_response.body).to eq('{"error":"id is invalid"}') - expect(last_response.status).to eq(400) + expect(last_response).to be_bad_request - io = StringIO.new('{"id" : 56}') - post '/', {}, Rack::RACK_INPUT => io, 'CONTENT_TYPE' => 'application/json', 'CONTENT_LENGTH' => io.length + post '/', { id: 56 }.to_json, 'CONTENT_TYPE' => 'application/json' expect(last_response.body).to eq('{"ret":56}') - expect(last_response.status).to eq(201) + expect(last_response).to be_created end end diff --git a/spec/support/chunks.rb b/spec/support/chunks.rb deleted file mode 100644 index 0506cb7ce..000000000 --- a/spec/support/chunks.rb +++ /dev/null @@ -1,14 +0,0 @@ -# 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 From fa188602ebb7efa8f7f820bc1abbd11b686a095c Mon Sep 17 00:00:00 2001 From: Dhruv Paranjape Date: Sun, 12 May 2024 18:21:01 +0200 Subject: [PATCH 229/304] Add length validator (#2437) --- CHANGELOG.md | 1 + README.md | 25 ++ lib/grape/locale/en.yml | 3 + lib/grape/validations/attributes_doc.rb | 3 + .../validators/length_validator.rb | 42 +++ spec/grape/validations/attributes_doc_spec.rb | 7 +- .../validations/validators/length_spec.rb | 301 ++++++++++++++++++ 7 files changed, 380 insertions(+), 2 deletions(-) create mode 100644 lib/grape/validations/validators/length_validator.rb create mode 100644 spec/grape/validations/validators/length_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bf46fdb9..d1027a311 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ * [#2436](https://github.com/ruby-grape/grape/pull/2436): Update coverallsapp github-action - [@ericproulx](https://github.com/ericproulx). * [#2434](https://github.com/ruby-grape/grape/pull/2434): Implement nested `with` support in parameter dsl - [@numbata](https://github.com/numbata). * [#2438](https://github.com/ruby-grape/grape/pull/2438): Fix some Rack::Lint - [@ericproulx](https://github.com/ericproulx). +* [#2437](https://github.com/ruby-grape/grape/pull/2437): Add length validator - [@dhruvCW](https://github.com/dhruvCW). * Your contribution here. #### Fixes diff --git a/README.md b/README.md index e346aa53d..46ab57f3a 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ - [values](#values) - [except_values](#except_values) - [same_as](#same_as) + - [length](#length) - [regexp](#regexp) - [mutually_exclusive](#mutually_exclusive) - [exactly_one_of](#exactly_one_of) @@ -69,6 +70,7 @@ - [Custom Validation messages](#custom-validation-messages) - [presence, allow_blank, values, regexp](#presence-allow_blank-values-regexp) - [same_as](#same_as-1) + - [length](#length-1) - [all_or_none_of](#all_or_none_of-1) - [mutually_exclusive](#mutually_exclusive-1) - [exactly_one_of](#exactly_one_of-1) @@ -1709,6 +1711,20 @@ params do end ``` +#### `length` + +Parameters with types that support `#length` method can be restricted to have a specific length with the `:length` option. + +The validator accepts `:min` or `:max` or both options to validate that the value of the parameter is within the given limits. + +```ruby +params do + requires :str, type: String, length: { min: 3 } + requires :list, type: [Integer], length: { min: 3, max: 5 } + requires :hash, type: Hash, length: { max: 5 } +end +``` + #### `regexp` Parameters can be restricted to match a specific regular expression with the `:regexp` option. If the value does not match the regular expression an error will be returned. Note that this is true for both `requires` and `optional` parameters. @@ -2026,6 +2042,15 @@ params do end ``` +#### `length` + +```ruby +params do + requires :str, type: String, length: { min: 5, message: 'str is expected to be atleast 5 characters long' } + requires :list, type: [Integer], length: { min: 2, max: 3, message: 'list is expected to have between 2 and 3 elements' } +end +``` + #### `all_or_none_of` ```ruby diff --git a/lib/grape/locale/en.yml b/lib/grape/locale/en.yml index 9377e4c4e..3ed7bcc3a 100644 --- a/lib/grape/locale/en.yml +++ b/lib/grape/locale/en.yml @@ -10,6 +10,9 @@ en: values: 'does not have a valid value' except_values: 'has a value not allowed' same_as: 'is not the same as %{parameter}' + length: 'is expected to have length within %{min} and %{max}' + length_min: 'is expected to have length greater than or equal to %{min}' + length_max: 'is expected to have length less than or equal to %{max}' missing_vendor_option: problem: 'missing :vendor option' summary: 'when version using header, you must specify :vendor option' diff --git a/lib/grape/validations/attributes_doc.rb b/lib/grape/validations/attributes_doc.rb index f9e15c148..c0d5ed954 100644 --- a/lib/grape/validations/attributes_doc.rb +++ b/lib/grape/validations/attributes_doc.rb @@ -28,6 +28,9 @@ def extract_details(validations) details[:documentation] = documentation if documentation details[:default] = validations[:default] if validations.key?(:default) + + details[:min_length] = validations[:length][:min] if validations.key?(:length) && validations[:length].key?(:min) + details[:max_length] = validations[:length][:max] if validations.key?(:length) && validations[:length].key?(:max) end def document(attrs) diff --git a/lib/grape/validations/validators/length_validator.rb b/lib/grape/validations/validators/length_validator.rb new file mode 100644 index 000000000..bcd0c9559 --- /dev/null +++ b/lib/grape/validations/validators/length_validator.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Grape + module Validations + module Validators + class LengthValidator < Base + def initialize(attrs, options, required, scope, **opts) + @min = options[:min] + @max = options[:max] + + super + + raise ArgumentError, 'min must be an integer greater than or equal to zero' if !@min.nil? && (!@min.is_a?(Integer) || @min.negative?) + raise ArgumentError, 'max must be an integer greater than or equal to zero' if !@max.nil? && (!@max.is_a?(Integer) || @max.negative?) + raise ArgumentError, "min #{@min} cannot be greater than max #{@max}" if !@min.nil? && !@max.nil? && @min > @max + end + + def validate_param!(attr_name, params) + param = params[attr_name] + + raise ArgumentError, "parameter #{param} does not support #length" unless param.respond_to?(:length) + + return unless (!@min.nil? && param.length < @min) || (!@max.nil? && param.length > @max) + + raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: build_message) + end + + def build_message + if options_key?(:message) + @option[:message] + elsif @min && @max + format I18n.t(:length, scope: 'grape.errors.messages'), min: @min, max: @max + elsif @min + format I18n.t(:length_min, scope: 'grape.errors.messages'), min: @min + else + format I18n.t(:length_max, scope: 'grape.errors.messages'), max: @max + end + end + end + end + end +end diff --git a/spec/grape/validations/attributes_doc_spec.rb b/spec/grape/validations/attributes_doc_spec.rb index f1ae0c93e..d5ae81d2f 100644 --- a/spec/grape/validations/attributes_doc_spec.rb +++ b/spec/grape/validations/attributes_doc_spec.rb @@ -31,7 +31,8 @@ presence: true, desc: 'Age of...', documentation: 'Age is...', - default: 1 + default: 1, + length: { min: 1, max: 13 } } end @@ -77,7 +78,9 @@ documentation: validations[:documentation], default: validations[:default], type: 'Integer', - values: valid_values + values: valid_values, + min_length: validations[:length][:min], + max_length: validations[:length][:max] } end diff --git a/spec/grape/validations/validators/length_spec.rb b/spec/grape/validations/validators/length_spec.rb new file mode 100644 index 000000000..8fa9f8487 --- /dev/null +++ b/spec/grape/validations/validators/length_spec.rb @@ -0,0 +1,301 @@ +# frozen_string_literal: true + +describe Grape::Validations::Validators::LengthValidator do + let_it_be(:app) do + Class.new(Grape::API) do + params do + requires :list, length: { min: 2, max: 3 } + end + post 'with_min_max' do + end + + params do + requires :list, type: [Integer], length: { min: 2 } + end + post 'with_min_only' do + end + + params do + requires :list, type: [Integer], length: { max: 3 } + end + post 'with_max_only' do + end + + params do + requires :list, type: Integer, length: { max: 3 } + end + post 'type_is_not_array' do + end + + params do + requires :list, type: Hash, length: { max: 3 } + end + post 'type_supports_length' do + end + + params do + requires :list, type: [Integer], length: { min: -3 } + end + post 'negative_min' do + end + + params do + requires :list, type: [Integer], length: { max: -3 } + end + post 'negative_max' do + end + + params do + requires :list, type: [Integer], length: { min: 2.5 } + end + post 'float_min' do + end + + params do + requires :list, type: [Integer], length: { max: 2.5 } + end + post 'float_max' do + end + + params do + requires :list, type: [Integer], length: { min: 15, max: 3 } + end + post 'min_greater_than_max' do + end + + params do + requires :list, type: [Integer], length: { min: 3, max: 3 } + end + post 'min_equal_to_max' do + end + + params do + requires :list, type: [JSON], length: { min: 0 } + end + post 'zero_min' do + end + + params do + requires :list, type: [JSON], length: { max: 0 } + end + post 'zero_max' do + end + + params do + requires :list, type: [Integer], length: { min: 2, message: 'not match' } + end + post '/custom-message' do + end + end + end + + describe '/with_min_max' do + context 'when length is within limits' do + it do + post '/with_min_max', list: [1, 2] + expect(last_response.status).to eq(201) + expect(last_response.body).to eq('') + end + end + + context 'when length is exceeded' do + it do + post '/with_min_max', list: [1, 2, 3, 4, 5] + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('list is expected to have length within 2 and 3') + end + end + + context 'when length is less than minimum' do + it do + post '/with_min_max', list: [1] + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('list is expected to have length within 2 and 3') + end + end + end + + describe '/with_max_only' do + context 'when length is less than limits' do + it do + post '/with_max_only', list: [1, 2] + expect(last_response.status).to eq(201) + expect(last_response.body).to eq('') + end + end + + context 'when length is exceeded' do + it do + post '/with_max_only', list: [1, 2, 3, 4, 5] + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('list is expected to have length less than or equal to 3') + end + end + end + + describe '/with_min_only' do + context 'when length is greater than limit' do + it do + post '/with_min_only', list: [1, 2] + expect(last_response.status).to eq(201) + expect(last_response.body).to eq('') + end + end + + context 'when length is less than limit' do + it do + post '/with_min_only', list: [1] + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('list is expected to have length greater than or equal to 2') + end + end + end + + describe '/zero_min' do + context 'when length is equal to the limit' do + it do + post '/zero_min', list: '[]' + expect(last_response.status).to eq(201) + expect(last_response.body).to eq('') + end + end + + context 'when length is greater than limit' do + it do + post '/zero_min', list: [{ key: 'value' }] + expect(last_response.status).to eq(201) + expect(last_response.body).to eq('') + end + end + end + + describe '/zero_max' do + context 'when length is within the limit' do + it do + post '/zero_max', list: '[]' + expect(last_response.status).to eq(201) + expect(last_response.body).to eq('') + end + end + + context 'when length is greater than limit' do + it do + post '/zero_max', list: [{ key: 'value' }] + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('list is expected to have length less than or equal to 0') + end + end + end + + describe '/type_is_not_array' do + context 'raises an error' do + it do + expect do + post 'type_is_not_array', list: 12 + end.to raise_error(ArgumentError, 'parameter 12 does not support #length') + end + end + end + + describe '/type_supports_length' do + context 'when length is within limits' do + it do + post 'type_supports_length', list: { key: 'value' } + expect(last_response.status).to eq(201) + expect(last_response.body).to eq('') + end + end + + context 'when length exceeds the limit' do + it do + post 'type_supports_length', list: { key: 'value', key1: 'value', key3: 'value', key4: 'value' } + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('list is expected to have length less than or equal to 3') + end + end + end + + describe '/negative_min' do + context 'when min is negative' do + it do + expect { post 'negative_min', list: [12] }.to raise_error(ArgumentError, 'min must be an integer greater than or equal to zero') + end + end + end + + describe '/negative_max' do + context 'it raises an error' do + it do + expect { post 'negative_max', list: [12] }.to raise_error(ArgumentError, 'max must be an integer greater than or equal to zero') + end + end + end + + describe '/float_min' do + context 'when min is not an integer' do + it do + expect { post 'float_min', list: [12] }.to raise_error(ArgumentError, 'min must be an integer greater than or equal to zero') + end + end + end + + describe '/float_max' do + context 'when max is not an integer' do + it do + expect { post 'float_max', list: [12] }.to raise_error(ArgumentError, 'max must be an integer greater than or equal to zero') + end + end + end + + describe '/min_greater_than_max' do + context 'raises an error' do + it do + expect { post 'min_greater_than_max', list: [1, 2] }.to raise_error(ArgumentError, 'min 15 cannot be greater than max 3') + end + end + end + + describe '/min_equal_to_max' do + context 'when array meets expectations' do + it do + post 'min_equal_to_max', list: [1, 2, 3] + expect(last_response.status).to eq(201) + expect(last_response.body).to eq('') + end + end + + context 'when array is less than min' do + it do + post 'min_equal_to_max', list: [1, 2] + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('list is expected to have length within 3 and 3') + end + end + + context 'when array is greater than max' do + it do + post 'min_equal_to_max', list: [1, 2, 3, 4] + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('list is expected to have length within 3 and 3') + end + end + end + + describe '/custom-message' do + context 'is within limits' do + it do + post '/custom-message', list: [1, 2, 3] + expect(last_response.status).to eq(201) + expect(last_response.body).to eq('') + end + end + + context 'is outside limit' do + it do + post '/custom-message', list: [1] + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('list not match') + end + end + end +end From 7fd63fecbc1e71662168c616a5a8f5243bc5bbbf Mon Sep 17 00:00:00 2001 From: "Daniel (dB.) Doubrovkine" Date: Fri, 26 Apr 2024 15:34:02 -0400 Subject: [PATCH 230/304] Added Rack version specs to ensure the correct version is loaded. --- spec/integration/rack_3_0/headers_spec.rb | 2 +- spec/integration/rack_3_0/version_spec.rb | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 spec/integration/rack_3_0/version_spec.rb diff --git a/spec/integration/rack_3_0/headers_spec.rb b/spec/integration/rack_3_0/headers_spec.rb index dc5118759..bd270e129 100644 --- a/spec/integration/rack_3_0/headers_spec.rb +++ b/spec/integration/rack_3_0/headers_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe Grape::Http::Headers, if: Gem::Version.new(Rack.release) >= Gem::Version.new('3') do +describe Grape::Http::Headers do subject { last_response.headers } describe 'returned headers should all be in lowercase' do diff --git a/spec/integration/rack_3_0/version_spec.rb b/spec/integration/rack_3_0/version_spec.rb new file mode 100644 index 000000000..1352ee6a5 --- /dev/null +++ b/spec/integration/rack_3_0/version_spec.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +describe Rack do + it { expect(Gem::Version.new(described_class.release).segments.first).to eq 3 } +end From 6fe78d1a8940c72401cdaa0c7f87232edcd290d3 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Mon, 20 May 2024 19:09:16 +0200 Subject: [PATCH 231/304] Replace method_missing by an overrided inspect (#2444) --- CHANGELOG.md | 1 + lib/grape/endpoint.rb | 8 ++------ spec/grape/endpoint_spec.rb | 20 ++++++++++++-------- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1027a311..60f7a77ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ * [#2405](https://github.com/ruby-grape/grape/pull/2405): Fix edge workflow - [@ericproulx](https://github.com/ericproulx). * [#2414](https://github.com/ruby-grape/grape/pull/2414): Fix Rack::Lint missing content-type - [@ericproulx](https://github.com/ericproulx). * [#2378](https://github.com/ruby-grape/grape/pull/2378): Do not overwrite `route_param` with a regular one if they share same name - [@arg](https://github.com/arg). +* [#2444](https://github.com/ruby-grape/grape/pull/2444): Replace method_missing in endpoint - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 2.0.0 (2023/11/11) diff --git a/lib/grape/endpoint.rb b/lib/grape/endpoint.rb index 7ec07792d..b04f47e1c 100644 --- a/lib/grape/endpoint.rb +++ b/lib/grape/endpoint.rb @@ -404,12 +404,8 @@ def options? env[Rack::REQUEST_METHOD] == Rack::OPTIONS end - def method_missing(name, *_args) - raise NoMethodError.new("undefined method `#{name}' for #{self.class} in `#{route.origin}' endpoint") - end - - def respond_to_missing?(method_name, include_private = false) - super + def inspect + "#{self.class} in `#{route.origin}' endpoint" end end end diff --git a/spec/grape/endpoint_spec.rb b/spec/grape/endpoint_spec.rb index b932a70d2..ee4c8986d 100644 --- a/spec/grape/endpoint_spec.rb +++ b/spec/grape/endpoint_spec.rb @@ -679,15 +679,19 @@ def app end end - describe '#method_missing' do - context 'when referencing an undefined local variable' do - it 'raises NoMethodError but stripping the internals of the Grape::Endpoint class and including the API route' do - subject.get('/hey') do - undefined_helper + describe 'NameError' do + context 'when referencing an undefined local variable or method' do + let(:error_message) do + if Gem::Version.new(RUBY_VERSION).release <= Gem::Version.new('3.2') + %r{undefined local variable or method `undefined_helper' for # in `/hey' endpoint} + else + /undefined local variable or method `undefined_helper' for/ end - expect do - get '/hey' - end.to raise_error(NoMethodError, %r{^undefined method `undefined_helper' for # in `/hey' endpoint}) + end + + it 'raises NameError but stripping the internals of the Grape::Endpoint class and including the API route' do + subject.get('/hey') { undefined_helper } + expect { get '/hey' }.to raise_error(NameError, error_message) end end end From 7c3ff27e83a4e91f5112a5b9b70a0ad3367ebd08 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Mon, 20 May 2024 22:51:22 +0200 Subject: [PATCH 232/304] Optimize memory alloc and retained (#2441) * Fix Cache in namespace and path Compile! skip non defined method Pattern, optimize capture_default Use delete_if instead of - * Revert root_prefix and to_regexp * Refactor path * Fix prefix, api string allocation * Drop AttributeTranslator in favor of OrderedOptions Manage Route regexp in Route class * Add cache for capture_index * Drop attribute_translator Remove useless alias * Fix all Rubocop Lint/MissingSuper Add changelog --- .rubocop_todo.yml | 12 +--- CHANGELOG.md | 1 + lib/grape.rb | 2 + lib/grape/api/instance.rb | 3 +- lib/grape/dsl/routing.rb | 4 +- lib/grape/endpoint.rb | 7 ++- .../exceptions/validation_array_errors.rb | 1 + lib/grape/namespace.rb | 5 +- lib/grape/path.rb | 51 +++++++-------- lib/grape/router.rb | 25 ++++---- lib/grape/router/attribute_translator.rb | 63 ------------------- lib/grape/router/base_route.rb | 39 ++++++++++++ lib/grape/router/greedy_route.rb | 17 ++--- lib/grape/router/pattern.rb | 20 +++--- lib/grape/router/route.rb | 15 ++--- lib/grape/util/base_inheritable.rb | 8 +-- lib/grape/util/reverse_stackable_values.rb | 5 +- lib/grape/util/stackable_values.rb | 5 +- lib/grape/validations/params_scope.rb | 13 ++-- spec/grape/path_spec.rb | 2 +- .../grape/router/attribute_translator_spec.rb | 26 -------- spec/grape/router/greedy_route_spec.rb | 10 +-- 22 files changed, 129 insertions(+), 205 deletions(-) delete mode 100644 lib/grape/router/attribute_translator.rb create mode 100644 lib/grape/router/base_route.rb delete mode 100644 spec/grape/router/attribute_translator_spec.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 44b3ed850..74dd51f3c 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2024-05-10 16:10:58 UTC using RuboCop version 1.63.2. +# on 2024-05-20 14:55:33 UTC using RuboCop version 1.63.2. # 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 @@ -40,16 +40,6 @@ Lint/EmptyClass: Exclude: - 'lib/grape/dsl/parameters.rb' -# Offense count: 5 -# Configuration parameters: AllowedParentClasses. -Lint/MissingSuper: - Exclude: - - 'lib/grape/api/instance.rb' - - 'lib/grape/exceptions/validation_array_errors.rb' - - 'lib/grape/namespace.rb' - - 'lib/grape/path.rb' - - 'lib/grape/router/pattern.rb' - # Offense count: 1 # Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns. Metrics/MethodLength: diff --git a/CHANGELOG.md b/CHANGELOG.md index 60f7a77ea..9b409b913 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ * [#2414](https://github.com/ruby-grape/grape/pull/2414): Fix Rack::Lint missing content-type - [@ericproulx](https://github.com/ericproulx). * [#2378](https://github.com/ruby-grape/grape/pull/2378): Do not overwrite `route_param` with a regular one if they share same name - [@arg](https://github.com/arg). * [#2444](https://github.com/ruby-grape/grape/pull/2444): Replace method_missing in endpoint - [@ericproulx](https://github.com/ericproulx). +* [#2441](https://github.com/ruby-grape/grape/pull/2441): Optimize memory alloc and retained - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 2.0.0 (2023/11/11) diff --git a/lib/grape.rb b/lib/grape.rb index 2c99d377a..963e37364 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -16,6 +16,7 @@ require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/hash/reverse_merge' require 'active_support/core_ext/hash/slice' +require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/deep_dup' require 'active_support/core_ext/object/duplicable' @@ -23,6 +24,7 @@ require 'active_support/core_ext/string/exclude' require 'active_support/deprecation' require 'active_support/inflector' +require 'active_support/ordered_options' require 'active_support/notifications' require 'English' diff --git a/lib/grape/api/instance.rb b/lib/grape/api/instance.rb index 8047b4e25..a17c8e63f 100644 --- a/lib/grape/api/instance.rb +++ b/lib/grape/api/instance.rb @@ -125,6 +125,7 @@ def evaluate_as_instance_with_configuration(block, lazy: false) end def inherited(subclass) + super subclass.reset! subclass.logger = logger.clone end @@ -220,7 +221,7 @@ def add_head_not_allowed_methods_and_options_methods 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 } + routes_by_regexp = all_routes.group_by(&:pattern_regexp) # Build the configuration based on the first endpoint and the collection of methods supported. routes_by_regexp.values.map do |routes| diff --git a/lib/grape/dsl/routing.rb b/lib/grape/dsl/routing.rb index a422c34d0..812ddd1d0 100644 --- a/lib/grape/dsl/routing.rb +++ b/lib/grape/dsl/routing.rb @@ -30,7 +30,7 @@ def version(*args, &block) if args.any? options = args.extract_options! options = options.reverse_merge(using: :path) - requested_versions = args.flatten + requested_versions = args.flatten.map(&:to_s) raise Grape::Exceptions::MissingVendorOption.new if options[:using] == :header && !options.key?(:vendor) @@ -54,7 +54,7 @@ def version(*args, &block) # Define a root URL prefix for your entire API. def prefix(prefix = nil) - namespace_inheritable(:root_prefix, prefix) + namespace_inheritable(:root_prefix, prefix&.to_s) end # Create a scope without affecting the URL. diff --git a/lib/grape/endpoint.rb b/lib/grape/endpoint.rb index b04f47e1c..7fc83fc43 100644 --- a/lib/grape/endpoint.rb +++ b/lib/grape/endpoint.rb @@ -190,10 +190,11 @@ def prepare_default_route_attributes end def prepare_version - version = namespace_inheritable(:version) || [] + version = namespace_inheritable(:version) + return unless version return if version.empty? - version.length == 1 ? version.first.to_s : version + version.length == 1 ? version.first : version end def merge_route_options(**default) @@ -206,7 +207,7 @@ def map_routes def prepare_path(path) path_settings = inheritable_setting.to_hash[:namespace_stackable].merge(inheritable_setting.to_hash[:namespace_inheritable]) - Path.prepare(path, namespace, path_settings) + Path.new(path, namespace, path_settings) end def namespace diff --git a/lib/grape/exceptions/validation_array_errors.rb b/lib/grape/exceptions/validation_array_errors.rb index d596c8877..d7815b1f6 100644 --- a/lib/grape/exceptions/validation_array_errors.rb +++ b/lib/grape/exceptions/validation_array_errors.rb @@ -6,6 +6,7 @@ class ValidationArrayErrors < Base attr_reader :errors def initialize(errors) + super() @errors = errors end end diff --git a/lib/grape/namespace.rb b/lib/grape/namespace.rb index 8375e3a56..537e7ff66 100644 --- a/lib/grape/namespace.rb +++ b/lib/grape/namespace.rb @@ -31,13 +31,14 @@ def self.joined_space(settings) # 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(JoinedSpaceCache[joined_space(settings)]) + JoinedSpaceCache[joined_space(settings)] end class JoinedSpaceCache < Grape::Util::Cache def initialize + super @cache = Hash.new do |h, joined_space| - h[joined_space] = -joined_space.join('/') + h[joined_space] = Grape::Router.normalize_path(joined_space.join('/')) end end end diff --git a/lib/grape/path.rb b/lib/grape/path.rb index e6fa540e7..fd0577893 100644 --- a/lib/grape/path.rb +++ b/lib/grape/path.rb @@ -3,10 +3,6 @@ module Grape # Represents a path to an endpoint. class Path - def self.prepare(raw_path, namespace, settings) - Path.new(raw_path, namespace, settings) - end - attr_reader :raw_path, :namespace, :settings def initialize(raw_path, namespace, settings) @@ -20,31 +16,27 @@ def mount_path end def root_prefix - split_setting(:root_prefix) + settings[:root_prefix] end def uses_specific_format? - if settings.key?(:format) && settings.key?(:content_types) - settings[:format] && Array(settings[:content_types]).size == 1 - else - false - end + return false unless settings.key?(:format) && settings.key?(:content_types) + + settings[:format] && Array(settings[:content_types]).size == 1 end def uses_path_versioning? - if settings.key?(:version) && settings[:version_options] && settings[:version_options].key?(:using) - settings[:version] && settings[:version_options][:using] == :path - else - false - end + return false unless settings.key?(:version) && settings[:version_options]&.key?(:using) + + settings[:version] && settings[:version_options][:using] == :path end def namespace? - namespace&.match?(/^\S/) && namespace != '/' + namespace&.match?(/^\S/) && not_slash?(namespace) end def path? - raw_path&.match?(/^\S/) && raw_path != '/' + raw_path&.match?(/^\S/) && not_slash?(raw_path) end def suffix @@ -58,7 +50,7 @@ def suffix end def path - Grape::Router.normalize_path(PartsCache[parts]) + PartsCache[parts] end def path_with_suffix @@ -73,24 +65,29 @@ def to_s class PartsCache < Grape::Util::Cache def initialize + super @cache = Hash.new do |h, parts| - h[parts] = -parts.join('/') + h[parts] = Grape::Router.normalize_path(parts.join('/')) end end end def parts - parts = [mount_path, root_prefix].compact - parts << ':version' if uses_path_versioning? - parts << namespace.to_s - parts << raw_path.to_s - parts.flatten.reject { |part| part == '/' } + [].tap do |parts| + add_part(parts, mount_path) + add_part(parts, root_prefix) + parts << ':version' if uses_path_versioning? + add_part(parts, namespace) + add_part(parts, raw_path) + end end - def split_setting(key) - return if settings[key].nil? + def add_part(parts, value) + parts << value if value && not_slash?(value) + end - settings[key].to_s.split('/') + def not_slash?(value) + value != '/' end end end diff --git a/lib/grape/router.rb b/lib/grape/router.rb index ba0207395..e1adf63bb 100644 --- a/lib/grape/router.rb +++ b/lib/grape/router.rb @@ -12,10 +12,6 @@ def self.normalize_path(path) path end - def self.supported_methods - @supported_methods ||= Grape::Http::Headers::SUPPORTED_METHODS + ['*'] - end - def initialize @neutral_map = [] @neutral_regexes = [] @@ -28,13 +24,12 @@ def compile! @union = Regexp.union(@neutral_regexes) @neutral_regexes = nil - self.class.supported_methods.each do |method| + (Grape::Http::Headers::SUPPORTED_METHODS + ['*']).each do |method| + next unless map.key?(method) + routes = map[method] - @optimized_map[method] = routes.map.with_index do |route, index| - route.index = index - Regexp.new("(?<_#{index}>#{route.pattern.to_regexp})") - end - @optimized_map[method] = Regexp.union(@optimized_map[method]) + optimized_map = routes.map.with_index { |route, index| route.to_regexp(index) } + @optimized_map[method] = Regexp.union(optimized_map) end @compiled = true end @@ -44,8 +39,10 @@ def append(route) end def associate_routes(pattern, **options) - @neutral_regexes << Regexp.new("(?<_#{@neutral_map.length}>)#{pattern.to_regexp}") - @neutral_map << Grape::Router::GreedyRoute.new(pattern: pattern, index: @neutral_map.length, **options) + Grape::Router::GreedyRoute.new(pattern: pattern, **options).then do |greedy_route| + @neutral_regexes << greedy_route.to_regexp(@neutral_map.length) + @neutral_map << greedy_route + end end def call(env) @@ -145,11 +142,11 @@ def default_response end def match?(input, method) - @optimized_map[method].match(input) { |m| @map[method].detect { |route| m["_#{route.index}"] } } + @optimized_map[method].match(input) { |m| @map[method].detect { |route| m[route.regexp_capture_index] } } end def greedy_match?(input) - @union.match(input) { |m| @neutral_map.detect { |route| m["_#{route.index}"] } } + @union.match(input) { |m| @neutral_map.detect { |route| m[route.regexp_capture_index] } } end def call_with_allow_headers(env, route) diff --git a/lib/grape/router/attribute_translator.rb b/lib/grape/router/attribute_translator.rb deleted file mode 100644 index 2197e0efe..000000000 --- a/lib/grape/router/attribute_translator.rb +++ /dev/null @@ -1,63 +0,0 @@ -# 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 - # fixed >= 3.0 - class AttributeTranslator - ROUTE_ATTRIBUTES = (%i[ - allow_header - anchor - endpoint - format - forward_match - namespace - not_allowed_method - prefix - request_method - requirements - settings - suffix - version - ] | Grape::DSL::Desc::ROUTE_ATTRIBUTES).freeze - - def initialize(**attributes) - @attributes = attributes - end - - ROUTE_ATTRIBUTES.each do |attr| - define_method attr do - @attributes[attr] - end - - define_method(:"#{attr}=") do |val| - @attributes[attr] = val - end - end - - def to_h - @attributes - end - - def method_missing(method_name, *args) - if setter?(method_name) - @attributes[method_name.to_s.chomp('=').to_sym] = args.first - else - @attributes[method_name] - end - end - - def respond_to_missing?(method_name, _include_private = false) - return true if setter?(method_name) - - @attributes.key?(method_name) - end - - private - - def setter?(method_name) - method_name.end_with?('=') - end - end - end -end diff --git a/lib/grape/router/base_route.rb b/lib/grape/router/base_route.rb new file mode 100644 index 000000000..9d19c8720 --- /dev/null +++ b/lib/grape/router/base_route.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Grape + class Router + class BaseRoute + delegate_missing_to :@options + + attr_reader :index, :pattern, :options + + def initialize(**options) + @options = ActiveSupport::OrderedOptions.new.update(options) + end + + alias attributes options + + def regexp_capture_index + CaptureIndexCache[index] + end + + def pattern_regexp + pattern.to_regexp + end + + def to_regexp(index) + @index = index + Regexp.new("(?<#{regexp_capture_index}>#{pattern_regexp})") + end + + class CaptureIndexCache < Grape::Util::Cache + def initialize + super + @cache = Hash.new do |h, index| + h[index] = "_#{index}" + end + end + end + end + end +end diff --git a/lib/grape/router/greedy_route.rb b/lib/grape/router/greedy_route.rb index 787c1265b..a999c1b90 100644 --- a/lib/grape/router/greedy_route.rb +++ b/lib/grape/router/greedy_route.rb @@ -5,24 +5,15 @@ module Grape class Router - class GreedyRoute - extend Forwardable - - attr_reader :index, :pattern, :options, :attributes - - # params must be handled in this class to avoid method redefined warning - delegate Grape::Router::AttributeTranslator::ROUTE_ATTRIBUTES - [:params] => :@attributes - - def initialize(index:, pattern:, **options) - @index = index + class GreedyRoute < BaseRoute + def initialize(pattern:, **options) @pattern = pattern - @options = options - @attributes = Grape::Router::AttributeTranslator.new(**options) + super(**options) end # Grape::Router:Route defines params as a function def params(_input = nil) - @attributes.params || {} + options[:params] || {} end end end diff --git a/lib/grape/router/pattern.rb b/lib/grape/router/pattern.rb index 0f646bea9..7b5b5276f 100644 --- a/lib/grape/router/pattern.rb +++ b/lib/grape/router/pattern.rb @@ -3,9 +3,12 @@ module Grape class Router class Pattern - attr_reader :origin, :path, :pattern, :to_regexp, :captures_default - extend Forwardable + + DEFAULT_CAPTURES = %w[format version].freeze + + attr_reader :origin, :path, :pattern, :to_regexp + def_delegators :pattern, :named_captures, :params def_delegators :to_regexp, :=== alias match? === @@ -15,7 +18,12 @@ def initialize(pattern, **options) @path = build_path(pattern, anchor: options[:anchor], suffix: options[:suffix]) @pattern = build_pattern(@path, options) @to_regexp = @pattern.to_regexp - @captures_default = regex_captures_default(@to_regexp) + end + + def captures_default + to_regexp.names + .delete_if { |n| DEFAULT_CAPTURES.include?(n) } + .to_h { |k| [k, ''] } end private @@ -43,11 +51,6 @@ def extract_capture(**options) options[:requirements].merge(sliced_options) end - def regex_captures_default(regex) - names = regex.names - %w[format version] # remove default format and version - names.to_h { |k| [k, ''] } - end - def build_path_from_pattern(pattern, anchor: false) if pattern.end_with?('*path') pattern.dup.insert(pattern.rindex('/') + 1, '?') @@ -62,6 +65,7 @@ def build_path_from_pattern(pattern, anchor: false) class PatternCache < Grape::Util::Cache def initialize + super @cache = Hash.new do |h, (pattern, suffix)| h[[pattern, suffix]] = -"#{pattern}#{suffix}" end diff --git a/lib/grape/router/route.rb b/lib/grape/router/route.rb index 2ee3bb89f..335ecb712 100644 --- a/lib/grape/router/route.rb +++ b/lib/grape/router/route.rb @@ -2,20 +2,17 @@ module Grape class Router - class Route + class Route < BaseRoute extend Forwardable - attr_reader :app, :pattern, :options, :attributes - attr_accessor :index + attr_reader :app, :request_method def_delegators :pattern, :path, :origin - # params must be handled in this class to avoid method redefined warning - delegate Grape::Router::AttributeTranslator::ROUTE_ATTRIBUTES - [:params] => :attributes def initialize(method, pattern, **options) - @options = options + @request_method = upcase_method(method) @pattern = Grape::Router::Pattern.new(pattern, **options) - @attributes = Grape::Router::AttributeTranslator.new(**options, request_method: upcase_method(method)) + super(**options) end def exec(env) @@ -30,7 +27,7 @@ def apply(app) def match?(input) return false if input.blank? - attributes.forward_match ? input.start_with?(pattern.origin) : pattern.match?(input) + options[:forward_match] ? input.start_with?(pattern.origin) : pattern.match?(input) end def params(input = nil) @@ -50,7 +47,7 @@ def params_without_input def upcase_method(method) method_s = method.to_s - Grape::Http::Headers.find_supported_method(method_s) || method_s.upcase + Grape::Http::Headers::SUPPORTED_METHODS.detect { |m| m.casecmp(method_s).zero? } || method_s.upcase end end end diff --git a/lib/grape/util/base_inheritable.rb b/lib/grape/util/base_inheritable.rb index 5db6e3455..cc68ababf 100644 --- a/lib/grape/util/base_inheritable.rb +++ b/lib/grape/util/base_inheritable.rb @@ -26,10 +26,10 @@ def initialize_copy(other) def keys if new_values.any? - combined = inherited_values.keys - combined.concat(new_values.keys) - combined.uniq! - combined + inherited_values.keys.tap do |combined| + combined.concat(new_values.keys) + combined.uniq! + end else inherited_values.keys end diff --git a/lib/grape/util/reverse_stackable_values.rb b/lib/grape/util/reverse_stackable_values.rb index 3b008a926..43da1ead5 100644 --- a/lib/grape/util/reverse_stackable_values.rb +++ b/lib/grape/util/reverse_stackable_values.rb @@ -8,10 +8,7 @@ class ReverseStackableValues < StackableValues def concat_values(inherited_value, new_value) return inherited_value unless new_value - [].tap do |value| - value.concat(new_value) - value.concat(inherited_value) - end + new_value + inherited_value end end end diff --git a/lib/grape/util/stackable_values.rb b/lib/grape/util/stackable_values.rb index c19e2f582..64336182c 100644 --- a/lib/grape/util/stackable_values.rb +++ b/lib/grape/util/stackable_values.rb @@ -29,10 +29,7 @@ def to_hash def concat_values(inherited_value, new_value) return inherited_value unless new_value - [].tap do |value| - value.concat(inherited_value) - value.concat(new_value) - end + inherited_value + new_value end end end diff --git a/lib/grape/validations/params_scope.rb b/lib/grape/validations/params_scope.rb index 9e1e28b84..403b0ef09 100644 --- a/lib/grape/validations/params_scope.rb +++ b/lib/grape/validations/params_scope.rb @@ -209,11 +209,11 @@ def push_renamed_param(path, new_name) def require_required_and_optional_fields(context, opts) if context == :all - optional_fields = Array(opts[:except]) - required_fields = opts[:using].keys - optional_fields + optional_fields = Array.wrap(opts[:except]) + required_fields = opts[:using].keys.delete_if { |f| optional_fields.include?(f) } else # context == :none - required_fields = Array(opts[:except]) - optional_fields = opts[:using].keys - required_fields + required_fields = Array.wrap(opts[:except]) + optional_fields = opts[:using].keys.delete_if { |f| required_fields.include?(f) } end required_fields.each do |field| field_opts = opts[:using][field] @@ -229,7 +229,10 @@ def require_required_and_optional_fields(context, opts) def require_optional_fields(context, opts) optional_fields = opts[:using].keys - optional_fields -= Array(opts[:except]) unless context == :all + unless context == :all + except_fields = Array.wrap(opts[:except]) + optional_fields.delete_if { |f| except_fields.include?(f) } + end optional_fields.each do |field| field_opts = opts[:using][field] optional(field, field_opts) if field_opts diff --git a/spec/grape/path_spec.rb b/spec/grape/path_spec.rb index a222a2c40..afad438df 100644 --- a/spec/grape/path_spec.rb +++ b/spec/grape/path_spec.rb @@ -49,7 +49,7 @@ module Grape it 'splits the mount path' do path = described_class.new(anything, anything, root_prefix: 'hello/world') - expect(path.root_prefix).to eql(%w[hello world]) + expect(path.root_prefix).to eql('hello/world') end end diff --git a/spec/grape/router/attribute_translator_spec.rb b/spec/grape/router/attribute_translator_spec.rb deleted file mode 100644 index 54e22dd64..000000000 --- a/spec/grape/router/attribute_translator_spec.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -describe Grape::Router::AttributeTranslator do - described_class::ROUTE_ATTRIBUTES.each do |attribute| - describe "##{attribute}" do - it "returns value from #{attribute} key if present" do - translator = described_class.new(attribute => 'value') - expect(translator.public_send(attribute)).to eq('value') - end - - it "returns nil from #{attribute} key if missing" do - translator = described_class.new - expect(translator.public_send(attribute)).to be_nil - end - end - - describe "##{attribute}=" do - it "sets value for #{attribute}", :aggregate_failures do - translator = described_class.new(attribute => 'value') - expect do - translator.public_send(:"#{attribute}=", 'new_value') - end.to change(translator, attribute).from('value').to('new_value') - end - end - end -end diff --git a/spec/grape/router/greedy_route_spec.rb b/spec/grape/router/greedy_route_spec.rb index add108ac5..29e966280 100644 --- a/spec/grape/router/greedy_route_spec.rb +++ b/spec/grape/router/greedy_route_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.describe Grape::Router::GreedyRoute do - let(:instance) { described_class.new(index: index, pattern: pattern, **options) } + let(:instance) { described_class.new(pattern: pattern, **options) } let(:index) { 0 } let(:pattern) { :pattern } let(:params) do @@ -11,12 +11,6 @@ { params: params }.freeze end - describe '#index' do - subject { instance.index } - - it { is_expected.to eq(index) } - end - describe '#pattern' do subject { instance.pattern } @@ -38,6 +32,6 @@ describe '#attributes' do subject { instance.attributes } - it { is_expected.to be_a(Grape::Router::AttributeTranslator) } + it { is_expected.to eq(options) } end end From 848e97ab7dda44edef4e1d3e67c6ba52d3657aff Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Tue, 21 May 2024 20:32:27 +0200 Subject: [PATCH 233/304] Remove builder as a dependency (#2445) --- CHANGELOG.md | 1 + Gemfile | 1 + UPGRADING.md | 6 ++++++ grape.gemspec | 1 - 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b409b913..cfc87189e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ * [#2434](https://github.com/ruby-grape/grape/pull/2434): Implement nested `with` support in parameter dsl - [@numbata](https://github.com/numbata). * [#2438](https://github.com/ruby-grape/grape/pull/2438): Fix some Rack::Lint - [@ericproulx](https://github.com/ericproulx). * [#2437](https://github.com/ruby-grape/grape/pull/2437): Add length validator - [@dhruvCW](https://github.com/dhruvCW). +* [#2445](https://github.com/ruby-grape/grape/pull/2445): Remove builder as a dependency - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/Gemfile b/Gemfile index cf63280c5..120567b69 100644 --- a/Gemfile +++ b/Gemfile @@ -7,6 +7,7 @@ source('https://rubygems.org') gemspec group :development, :test do + gem 'builder', require: false gem 'bundler' gem 'rake' gem 'rubocop', '1.63.2', require: false diff --git a/UPGRADING.md b/UPGRADING.md index 776d50eb2..be2fb564d 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -3,6 +3,12 @@ Upgrading Grape ### Upgrading to >= 2.1.0 +#### Optional Builder + +The `builder` gem dependency has been made optional as it's only used when generating XML. If your code does, add `builder` to your `Gemfile`. + +See [#2445](https://github.com/ruby-grape/grape/pull/2445) for more information. + #### Deep Merging of Parameter Attributes Grape now uses `deep_merge` to combine parameter attributes within the `with` method. Previously, attributes defined at the parameter level would override those defined at the group level. diff --git a/grape.gemspec b/grape.gemspec index 92c2d7fd0..e3383f927 100644 --- a/grape.gemspec +++ b/grape.gemspec @@ -21,7 +21,6 @@ Gem::Specification.new do |s| } s.add_runtime_dependency 'activesupport', '>= 6' - s.add_runtime_dependency 'builder' s.add_runtime_dependency 'dry-types', '>= 1.1' s.add_runtime_dependency 'mustermann-grape', '~> 1.1.0' s.add_runtime_dependency 'rack', '>= 2' From 1b4f510f79aed993eef019f564d2851969af0213 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Fri, 14 Jun 2024 22:25:02 +0200 Subject: [PATCH 234/304] Rack 3.1 fixes (#2449) * Add Grape::Http::Headers::TRANSFER_ENCODING Fix spec regarding content-length on stream * Add CHANGELOG.md --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 1 + gemfiles/rack_3_1.gemfile | 5 +++++ lib/grape/dsl/inside_route.rb | 2 +- lib/grape/http/headers.rb | 1 + spec/grape/api_spec.rb | 2 +- spec/grape/dsl/inside_route_spec.rb | 12 ++++++------ spec/grape/middleware/formatter_spec.rb | 9 ++++++++- spec/support/chunked_response.rb | 4 ++-- 9 files changed, 26 insertions(+), 12 deletions(-) create mode 100644 gemfiles/rack_3_1.gemfile diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f1684af13..fbc6cc259 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,7 +24,7 @@ jobs: fail-fast: false matrix: ruby: ['2.7', '3.0', '3.1', '3.2', '3.3'] - gemfile: [Gemfile, gemfiles/rack_2_0.gemfile, gemfiles/rack_3_0.gemfile, gemfiles/rails_6_0.gemfile, gemfiles/rails_6_1.gemfile, gemfiles/rails_7_0.gemfile, gemfiles/rails_7_1.gemfile] + gemfile: [Gemfile, gemfiles/rack_2_0.gemfile, gemfiles/rack_3_0.gemfile, gemfiles/rack_3_1.gemfile, gemfiles/rails_6_0.gemfile, gemfiles/rails_6_1.gemfile, gemfiles/rails_7_0.gemfile, gemfiles/rails_7_1.gemfile] specs: ['spec --exclude-pattern=spec/integration/**/*_spec.rb'] include: - ruby: '2.7' diff --git a/CHANGELOG.md b/CHANGELOG.md index cfc87189e..71dbde410 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ * [#2378](https://github.com/ruby-grape/grape/pull/2378): Do not overwrite `route_param` with a regular one if they share same name - [@arg](https://github.com/arg). * [#2444](https://github.com/ruby-grape/grape/pull/2444): Replace method_missing in endpoint - [@ericproulx](https://github.com/ericproulx). * [#2441](https://github.com/ruby-grape/grape/pull/2441): Optimize memory alloc and retained - [@ericproulx](https://github.com/ericproulx). +* [#2449](https://github.com/ruby-grape/grape/pull/2449): Rack 3.1 fixes - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 2.0.0 (2023/11/11) diff --git a/gemfiles/rack_3_1.gemfile b/gemfiles/rack_3_1.gemfile new file mode 100644 index 000000000..c57807e9f --- /dev/null +++ b/gemfiles/rack_3_1.gemfile @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +eval_gemfile '../Gemfile' + +gem 'rack', '~> 3.1' diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index 932640093..8a927550f 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -350,7 +350,7 @@ def stream(value = nil) return if value.nil? && @stream.nil? header Rack::CONTENT_LENGTH, nil - header Rack::TRANSFER_ENCODING, nil + header Grape::Http::Headers::TRANSFER_ENCODING, nil header Rack::CACHE_CONTROL, 'no-cache' # Skips ETag generation (reading the response up front) if value.is_a?(String) file_body = Grape::ServeStream::FileBody.new(value) diff --git a/lib/grape/http/headers.rb b/lib/grape/http/headers.rb index d1d995c57..ab8770ab4 100644 --- a/lib/grape/http/headers.rb +++ b/lib/grape/http/headers.rb @@ -10,6 +10,7 @@ module Headers ALLOW = 'Allow' LOCATION = 'Location' X_CASCADE = 'X-Cascade' + TRANSFER_ENCODING = 'Transfer-Encoding' SUPPORTED_METHODS = [ Rack::GET, diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index 868b1a4f9..df8bdbb43 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -1252,7 +1252,7 @@ def to_txt expect(last_response.content_type).to eq('text/plain') expect(last_response.content_length).to be_nil expect(last_response.headers[Rack::CACHE_CONTROL]).to eq('no-cache') - expect(last_response.headers[Rack::TRANSFER_ENCODING]).to eq('chunked') + expect(last_response.headers[Grape::Http::Headers::TRANSFER_ENCODING]).to eq('chunked') expect(last_response.body).to eq("c\r\nThis is some\r\nd\r\n file content\r\n0\r\n\r\n") end diff --git a/spec/grape/dsl/inside_route_spec.rb b/spec/grape/dsl/inside_route_spec.rb index 45bc737bd..8963a73a2 100644 --- a/spec/grape/dsl/inside_route_spec.rb +++ b/spec/grape/dsl/inside_route_spec.rb @@ -247,7 +247,7 @@ def initialize before do subject.header Rack::CACHE_CONTROL, 'cache' subject.header Rack::CONTENT_LENGTH, 123 - subject.header Rack::TRANSFER_ENCODING, 'base64' + subject.header Grape::Http::Headers::TRANSFER_ENCODING, 'base64' end it 'sends no deprecation warnings' do @@ -277,7 +277,7 @@ def initialize it 'does not change the Transfer-Encoding header' do subject.sendfile file_path - expect(subject.header[Rack::TRANSFER_ENCODING]).to eq 'base64' + expect(subject.header[Grape::Http::Headers::TRANSFER_ENCODING]).to eq 'base64' end end @@ -308,7 +308,7 @@ def initialize before do subject.header Rack::CACHE_CONTROL, 'cache' subject.header Rack::CONTENT_LENGTH, 123 - subject.header Rack::TRANSFER_ENCODING, 'base64' + subject.header Grape::Http::Headers::TRANSFER_ENCODING, 'base64' end it 'emits no deprecation warnings' do @@ -344,7 +344,7 @@ def initialize it 'sets Transfer-Encoding header to nil' do subject.stream file_path - expect(subject.header[Rack::TRANSFER_ENCODING]).to be_nil + expect(subject.header[Grape::Http::Headers::TRANSFER_ENCODING]).to be_nil end end @@ -358,7 +358,7 @@ def initialize before do subject.header Rack::CACHE_CONTROL, 'cache' subject.header Rack::CONTENT_LENGTH, 123 - subject.header Rack::TRANSFER_ENCODING, 'base64' + subject.header Grape::Http::Headers::TRANSFER_ENCODING, 'base64' end it 'emits no deprecation warnings' do @@ -388,7 +388,7 @@ def initialize it 'sets Transfer-Encoding header to nil' do subject.stream stream_object - expect(subject.header[Rack::TRANSFER_ENCODING]).to be_nil + expect(subject.header[Grape::Http::Headers::TRANSFER_ENCODING]).to be_nil end end diff --git a/spec/grape/middleware/formatter_spec.rb b/spec/grape/middleware/formatter_spec.rb index 4e19aba96..61a2a67bf 100644 --- a/spec/grape/middleware/formatter_spec.rb +++ b/spec/grape/middleware/formatter_spec.rb @@ -453,12 +453,19 @@ def to_xml let(:env) do { Rack::PATH_INFO => '/somewhere', Grape::Http::Headers::HTTP_ACCEPT => 'application/json' } end + let(:headers) do + if Gem::Version.new(Rack.release) < Gem::Version.new('3.1') + { Rack::CONTENT_TYPE => 'application/json', Rack::CONTENT_LENGTH => body.bytesize.to_s } + else + { Rack::CONTENT_TYPE => 'application/json' } + end + end it 'returns a file response' do expect(file).to receive(:each).and_yield(body) r = Rack::MockResponse[*subject.call(env)] expect(r).to be_successful - expect(r.headers).to eq({ Rack::CONTENT_TYPE => 'application/json', Rack::CONTENT_LENGTH => body.bytesize.to_s }) + expect(r.headers).to eq(headers) expect(r.body).to eq('data') end end diff --git a/spec/support/chunked_response.rb b/spec/support/chunked_response.rb index 41defe87e..4e118d1dc 100644 --- a/spec/support/chunked_response.rb +++ b/spec/support/chunked_response.rb @@ -58,9 +58,9 @@ def call(env) if !Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) && !headers[Rack::CONTENT_LENGTH] && - !headers[Rack::TRANSFER_ENCODING] + !headers[Grape::Http::Headers::TRANSFER_ENCODING] - headers[Rack::TRANSFER_ENCODING] = 'chunked' + headers[Grape::Http::Headers::TRANSFER_ENCODING] = 'chunked' response[2] = if headers['trailer'] TrailerBody.new(body) else From 71b93d9f1181486896ac444a6e90182e6200cd34 Mon Sep 17 00:00:00 2001 From: Eric Date: Sat, 15 Jun 2024 16:53:17 +0200 Subject: [PATCH 235/304] Preparing for release, 2.1.0. --- CHANGELOG.md | 4 +--- README.md | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71dbde410..c9a28ec45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -### 2.1.0 (Next) +### 2.1.0 (2024/06/15) #### Features @@ -31,7 +31,6 @@ * [#2438](https://github.com/ruby-grape/grape/pull/2438): Fix some Rack::Lint - [@ericproulx](https://github.com/ericproulx). * [#2437](https://github.com/ruby-grape/grape/pull/2437): Add length validator - [@dhruvCW](https://github.com/dhruvCW). * [#2445](https://github.com/ruby-grape/grape/pull/2445): Remove builder as a dependency - [@ericproulx](https://github.com/ericproulx). -* Your contribution here. #### Fixes @@ -47,7 +46,6 @@ * [#2444](https://github.com/ruby-grape/grape/pull/2444): Replace method_missing in endpoint - [@ericproulx](https://github.com/ericproulx). * [#2441](https://github.com/ruby-grape/grape/pull/2441): Optimize memory alloc and retained - [@ericproulx](https://github.com/ericproulx). * [#2449](https://github.com/ruby-grape/grape/pull/2449): Rack 3.1 fixes - [@ericproulx](https://github.com/ericproulx). -* Your contribution here. ### 2.0.0 (2023/11/11) diff --git a/README.md b/README.md index 46ab57f3a..28906ec59 100644 --- a/README.md +++ b/README.md @@ -157,10 +157,8 @@ Grape is a REST-like API framework for Ruby. It's designed to run on Rack or com ## Stable Release -You're reading the documentation for the next release of Grape, which should be **2.1.0**. +You're reading the documentation for the stable release of Grape, **2.1.0**. Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. -The current stable release is [2.0.0](https://github.com/ruby-grape/grape/blob/v2.0.0/README.md). - ## Project Resources From f73811a08c06bbd3d917e72ac2972bbf32a22867 Mon Sep 17 00:00:00 2001 From: Eric Date: Sat, 15 Jun 2024 16:57:50 +0200 Subject: [PATCH 236/304] Preparing for next development iteration, 2.2.0. --- CHANGELOG.md | 10 ++++++++++ README.md | 3 ++- lib/grape/version.rb | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9a28ec45..4b27d60b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +### 2.2.0 (Next) + +#### Features + +* Your contribution here. + +#### Fixes + +* Your contribution here. + ### 2.1.0 (2024/06/15) #### Features diff --git a/README.md b/README.md index 28906ec59..38f164a2d 100644 --- a/README.md +++ b/README.md @@ -157,8 +157,9 @@ Grape is a REST-like API framework for Ruby. It's designed to run on Rack or com ## Stable Release -You're reading the documentation for the stable release of Grape, **2.1.0**. +You're reading the documentation for the stable release of Grape, 2.2.0. Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. +The current stable release is [2.1.0](https://github.com/ruby-grape/grape/blob/v2.1.0/README.md). ## Project Resources diff --git a/lib/grape/version.rb b/lib/grape/version.rb index 1013166e1..5083e38bd 100644 --- a/lib/grape/version.rb +++ b/lib/grape/version.rb @@ -2,5 +2,5 @@ module Grape # The current version of Grape. - VERSION = '2.1.0' + VERSION = '2.2.0' end From 3df163a114bbcbae5e4943380fa2ac699c068e47 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Mon, 17 Jun 2024 23:45:06 +0200 Subject: [PATCH 237/304] Update rubocop and its todo (#2450) * Update rubocop and its todo * Add CHANGELOG.md * Fix changelog --- .rubocop.yml | 12 +++--------- .rubocop_todo.yml | 19 ++++++++++++++++++- CHANGELOG.md | 1 + Gemfile | 4 ++-- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 8dfa0311a..9e4cc8bd4 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -57,21 +57,18 @@ Metrics/ModuleLength: Metrics/PerceivedComplexity: Max: 15 -RSpec/Capybara/FeatureMethods: - Enabled: false - RSpec/ExampleLength: Max: 60 RSpec/NestedGroups: Max: 6 -RSpec/FilePath: - SpecSuffixOnly: true - RSpec/SpecFilePathFormat: Enabled: false +RSpec/SpecFilePathSuffix: + Enabled: true + RSpec/MultipleExpectations: Enabled: false @@ -83,6 +80,3 @@ RSpec/MultipleMemoizedHelpers: RSpec/ContextWording: Enabled: false - -RSpecRails/HaveHttpStatus: - Enabled: false diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 74dd51f3c..a3956b5bc 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2024-05-20 14:55:33 UTC using RuboCop version 1.63.2. +# on 2024-06-15 15:12:21 UTC using RuboCop version 1.64.1. # 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 @@ -197,6 +197,7 @@ RSpec/RepeatedExampleGroupDescription: # Offense count: 2 # This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AutoCorrect. RSpec/ScatteredSetup: Exclude: - 'spec/grape/util/inheritable_setting_spec.rb' @@ -296,6 +297,22 @@ Style/Semicolon: Exclude: - 'spec/grape/api_spec.rb' +# Offense count: 2 +# This cop supports safe autocorrection (--autocorrect). +Style/SuperArguments: + Exclude: + - 'lib/grape/api.rb' + - 'spec/support/deprecated_warning_handlers.rb' + +# Offense count: 2 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: AllowMethodsWithArguments, AllowedMethods, AllowedPatterns, AllowComments. +# AllowedMethods: define_method +Style/SymbolProc: + Exclude: + - 'benchmark/large_model.rb' + - 'spec/grape/validations/params_scope_spec.rb' + # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b27d60b4..114d5762e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ #### Features +* [#2450](https://github.com/ruby-grape/grape/pull/2450): Update RuboCop to 1.64.1 - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/Gemfile b/Gemfile index 120567b69..eaf603c1d 100644 --- a/Gemfile +++ b/Gemfile @@ -10,9 +10,9 @@ group :development, :test do gem 'builder', require: false gem 'bundler' gem 'rake' - gem 'rubocop', '1.63.2', require: false + gem 'rubocop', '1.64.1', require: false gem 'rubocop-performance', '1.21.0', require: false - gem 'rubocop-rspec', '2.29.1', require: false + gem 'rubocop-rspec', '3.0.1', require: false end group :development do From 3a26c2ca2368b464f556cf93a0af1f5e6405d527 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Wed, 19 Jun 2024 22:35:09 +0200 Subject: [PATCH 238/304] Add context in endpoint dsl (#2453) * Add context in endpoint dsl like Grape::Middleware::Helpers * Add CHANGELOG Fix rubocop * Change to self --- CHANGELOG.md | 1 + lib/grape/dsl/inside_route.rb | 4 ++++ spec/grape/api_spec.rb | 19 +++++++++++++++++++ 3 files changed, 24 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 114d5762e..6934d245b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ #### Fixes +* [#2453](https://github.com/ruby-grape/grape/pull/2453): Fix context in rescue_from - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 2.1.0 (2024/06/15) diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index 8a927550f..c73eb7c77 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -461,6 +461,10 @@ def entity_representation_for(entity_class, object, options) def http_version env['HTTP_VERSION'] || env[Rack::SERVER_PROTOCOL] end + + def context + self + end end end end diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index df8bdbb43..fbd0ee38c 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -4404,4 +4404,23 @@ def uniqe_id_route expect(last_response.body).to eq('deprecated') end end + + context 'rescue_from context' do + subject { last_response } + + let(:api) do + Class.new(described_class) do + rescue_from :all do + error!(context.env, 400) + end + get { raise ArgumentError, 'Oops!' } + end + end + + let(:app) { api } + + before { get '/' } + + it { is_expected.to be_bad_request } + end end From b1123d8094a99f41e5e7071b68361aa5eeeeeb5f Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Fri, 21 Jun 2024 00:19:17 +0200 Subject: [PATCH 239/304] Fix default response headers to work with Rack 3 (#2455) * Default response headers are using Grape::Util::Header for rack 3 compatibility Add gcompat for nokogiri in Dockerfile. Needed for testing a Rails app Add tzinfo-data in Rails's Gemfiles. Needed for testing a Rails app Add integration test rails thats mounts a Grape API within a Rails App Move railtie_spec.rb to rails integration * Add CHANGELOG.md Add rails integrations tests * Fix config.load_defaults * Fix config.load_defaults in railtie_spec.rb * Change anonymous class to named class with stub_const * Reset Singleton ActiveSupport::Dependencies.autoload_paths and autoload_once_paths * Add comment about ActiveSupport::Dependencies * Replace responds by cascades --- .github/workflows/test.yml | 9 +++++ CHANGELOG.md | 1 + docker/Dockerfile | 4 +- gemfiles/rails_6_0.gemfile | 1 + gemfiles/rails_6_1.gemfile | 1 + gemfiles/rails_7_0.gemfile | 1 + gemfiles/rails_7_1.gemfile | 1 + gemfiles/rails_edge.gemfile | 1 + lib/grape/router.rb | 3 +- spec/grape/railtie_spec.rb | 20 ---------- spec/integration/rails/mounting_spec.rb | 51 +++++++++++++++++++++++++ spec/integration/rails/railtie_spec.rb | 25 ++++++++++++ 12 files changed, 95 insertions(+), 23 deletions(-) delete mode 100644 spec/grape/railtie_spec.rb create mode 100644 spec/integration/rails/mounting_spec.rb create mode 100644 spec/integration/rails/railtie_spec.rb diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fbc6cc259..cd47f23cb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,6 +45,15 @@ jobs: - ruby: '3.3' gemfile: gemfiles/dry_validation.gemfile specs: 'spec/integration/dry_validation' + - ruby: '3.3' + gemfile: gemfiles/rails_6_1.gemfile + specs: 'spec/integration/rails' + - ruby: '3.3' + gemfile: gemfiles/rails_7_0.gemfile + specs: 'spec/integration/rails' + - ruby: '3.3' + gemfile: gemfiles/rails_7_1.gemfile + specs: 'spec/integration/rails' runs-on: ubuntu-latest env: BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 6934d245b..7b2b2bc8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ #### Fixes * [#2453](https://github.com/ruby-grape/grape/pull/2453): Fix context in rescue_from - [@ericproulx](https://github.com/ericproulx). +* [#2455](https://github.com/ruby-grape/grape/pull/2455): Fix default response headers to work with Rack 3 - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 2.1.0 (2024/06/15) diff --git a/docker/Dockerfile b/docker/Dockerfile index bf5c75088..ad41be490 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -4,7 +4,7 @@ FROM ruby:$RUBY_VERSION-alpine ENV BUNDLE_PATH /usr/local/bundle/gems ENV LIB_PATH /var/grape -RUN apk add --update --no-cache make gcc git libc-dev && \ +RUN apk add --update --no-cache make gcc git libc-dev gcompat && \ gem update --system && gem install bundler WORKDIR $LIB_PATH @@ -12,4 +12,4 @@ WORKDIR $LIB_PATH COPY /docker/entrypoint.sh /usr/local/bin/docker-entrypoint.sh RUN chmod +x /usr/local/bin/docker-entrypoint.sh -ENTRYPOINT ["docker-entrypoint.sh"] \ No newline at end of file +ENTRYPOINT ["docker-entrypoint.sh"] diff --git a/gemfiles/rails_6_0.gemfile b/gemfiles/rails_6_0.gemfile index 8a9e3b247..0e775d785 100644 --- a/gemfiles/rails_6_0.gemfile +++ b/gemfiles/rails_6_0.gemfile @@ -3,3 +3,4 @@ eval_gemfile '../Gemfile' gem 'rails', '~> 6.0.0' +gem 'tzinfo-data', require: false diff --git a/gemfiles/rails_6_1.gemfile b/gemfiles/rails_6_1.gemfile index ad33d3a3e..f6ae64477 100644 --- a/gemfiles/rails_6_1.gemfile +++ b/gemfiles/rails_6_1.gemfile @@ -3,3 +3,4 @@ eval_gemfile '../Gemfile' gem 'rails', '~> 6.1' +gem 'tzinfo-data', require: false diff --git a/gemfiles/rails_7_0.gemfile b/gemfiles/rails_7_0.gemfile index 43db352ec..e9c87639d 100644 --- a/gemfiles/rails_7_0.gemfile +++ b/gemfiles/rails_7_0.gemfile @@ -3,3 +3,4 @@ eval_gemfile '../Gemfile' gem 'rails', '~> 7.0.0' +gem 'tzinfo-data', require: false diff --git a/gemfiles/rails_7_1.gemfile b/gemfiles/rails_7_1.gemfile index babb65fd7..81358706c 100644 --- a/gemfiles/rails_7_1.gemfile +++ b/gemfiles/rails_7_1.gemfile @@ -3,3 +3,4 @@ eval_gemfile '../Gemfile' gem 'rails', '~> 7.1.0' +gem 'tzinfo-data', require: false diff --git a/gemfiles/rails_edge.gemfile b/gemfiles/rails_edge.gemfile index 80b01d94e..ed2aab3e1 100644 --- a/gemfiles/rails_edge.gemfile +++ b/gemfiles/rails_edge.gemfile @@ -3,3 +3,4 @@ eval_gemfile '../Gemfile' gem 'rails', github: 'rails/rails' +gem 'tzinfo-data', require: false diff --git a/lib/grape/router.rb b/lib/grape/router.rb index e1adf63bb..61722011a 100644 --- a/lib/grape/router.rb +++ b/lib/grape/router.rb @@ -138,7 +138,8 @@ def with_optimization end def default_response - [404, { Grape::Http::Headers::X_CASCADE => 'pass' }, ['404 Not Found']] + headers = Grape::Util::Header.new.merge(Grape::Http::Headers::X_CASCADE => 'pass') + [404, headers, ['404 Not Found']] end def match?(input, method) diff --git a/spec/grape/railtie_spec.rb b/spec/grape/railtie_spec.rb deleted file mode 100644 index 4a9555108..000000000 --- a/spec/grape/railtie_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -if defined?(Rails::Railtie) && ActiveSupport.gem_version >= Gem::Version.new('7.1') - describe Grape::Railtie do - describe '.railtie' do - subject { test_app.deprecators[:grape] } - - let(:test_app) do - Class.new(Rails::Application) do - config.eager_load = false - config.load_defaults 7.1 - end - end - - before { test_app.initialize! } - - it { is_expected.to be(Grape.deprecator) } - end - end -end diff --git a/spec/integration/rails/mounting_spec.rb b/spec/integration/rails/mounting_spec.rb new file mode 100644 index 000000000..3d8288b93 --- /dev/null +++ b/spec/integration/rails/mounting_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +describe 'Rails', if: defined?(Rails) do + context 'rails mounted' do + let(:api) do + Class.new(Grape::API) do + get('/test_grape') { 'rails mounted' } + end + end + + let(:app) do + require 'rails' + require 'action_controller/railtie' + + # https://github.com/rails/rails/issues/51784 + # same error as described if not redefining the following + ActiveSupport::Dependencies.autoload_paths = [] + ActiveSupport::Dependencies.autoload_once_paths = [] + + Class.new(Rails::Application) do + config.eager_load = false + config.load_defaults "#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}" + config.api_only = true + config.consider_all_requests_local = true + config.hosts << 'example.org' + + routes.append do + mount GrapeApi => '/' + + get 'up', to: lambda { |_env| + ['200', {}, ['hello world']] + } + end + end + end + + before do + stub_const('GrapeApi', api) + app.initialize! + end + + it 'cascades' do + get '/test_grape' + expect(last_response).to be_successful + expect(last_response.body).to eq('rails mounted') + get '/up' + expect(last_response).to be_successful + expect(last_response.body).to eq('hello world') + end + end +end diff --git a/spec/integration/rails/railtie_spec.rb b/spec/integration/rails/railtie_spec.rb new file mode 100644 index 000000000..7583fc29c --- /dev/null +++ b/spec/integration/rails/railtie_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +if defined?(Rails) && ActiveSupport.gem_version >= Gem::Version.new('7.1') + describe Grape::Railtie do + describe '.railtie' do + subject { test_app.deprecators[:grape] } + + let(:test_app) do + # https://github.com/rails/rails/issues/51784 + # same error as described if not redefining the following + ActiveSupport::Dependencies.autoload_paths = [] + ActiveSupport::Dependencies.autoload_once_paths = [] + + Class.new(Rails::Application) do + config.eager_load = false + config.load_defaults "#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}" + end + end + + before { test_app.initialize! } + + it { is_expected.to be(Grape.deprecator) } + end + end +end From 19ab6a2754a992f4b55f6760425962bc5b6b70d4 Mon Sep 17 00:00:00 2001 From: Eric Date: Sat, 22 Jun 2024 17:57:54 +0200 Subject: [PATCH 240/304] Preparing for release, 2.1.1. --- CHANGELOG.md | 4 +--- README.md | 3 +-- lib/grape/version.rb | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b2b2bc8e..86c354b70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,13 @@ -### 2.2.0 (Next) +### 2.1.1 (2024-06-22) #### Features * [#2450](https://github.com/ruby-grape/grape/pull/2450): Update RuboCop to 1.64.1 - [@ericproulx](https://github.com/ericproulx). -* Your contribution here. #### Fixes * [#2453](https://github.com/ruby-grape/grape/pull/2453): Fix context in rescue_from - [@ericproulx](https://github.com/ericproulx). * [#2455](https://github.com/ruby-grape/grape/pull/2455): Fix default response headers to work with Rack 3 - [@ericproulx](https://github.com/ericproulx). -* Your contribution here. ### 2.1.0 (2024/06/15) diff --git a/README.md b/README.md index 38f164a2d..0267ee31b 100644 --- a/README.md +++ b/README.md @@ -157,9 +157,8 @@ Grape is a REST-like API framework for Ruby. It's designed to run on Rack or com ## Stable Release -You're reading the documentation for the stable release of Grape, 2.2.0. +You're reading the documentation for the stable release of Grape, **2.1.1**. Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. -The current stable release is [2.1.0](https://github.com/ruby-grape/grape/blob/v2.1.0/README.md). ## Project Resources diff --git a/lib/grape/version.rb b/lib/grape/version.rb index 5083e38bd..832f8ad6d 100644 --- a/lib/grape/version.rb +++ b/lib/grape/version.rb @@ -2,5 +2,5 @@ module Grape # The current version of Grape. - VERSION = '2.2.0' + VERSION = '2.1.1' end From bd76c1fa2669609d2619d80f715dc202b7695fb4 Mon Sep 17 00:00:00 2001 From: Eric Date: Sat, 22 Jun 2024 18:04:24 +0200 Subject: [PATCH 241/304] Preparing for next development iteration, 2.2.0. --- CHANGELOG.md | 10 ++++++++++ README.md | 3 ++- lib/grape/version.rb | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86c354b70..8634e3657 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +### 2.2.0 (Next) + +#### Features + +* Your contribution here. + +#### Fixes + +* Your contribution here. + ### 2.1.1 (2024-06-22) #### Features diff --git a/README.md b/README.md index 0267ee31b..dd156e2c9 100644 --- a/README.md +++ b/README.md @@ -157,8 +157,9 @@ Grape is a REST-like API framework for Ruby. It's designed to run on Rack or com ## Stable Release -You're reading the documentation for the stable release of Grape, **2.1.1**. +You're reading the documentation for the next release of Grape, which should be 2.2.0. Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. +The current stable release is [2.1.1](https://github.com/ruby-grape/grape/blob/v2.1.1/README.md). ## Project Resources diff --git a/lib/grape/version.rb b/lib/grape/version.rb index 832f8ad6d..5083e38bd 100644 --- a/lib/grape/version.rb +++ b/lib/grape/version.rb @@ -2,5 +2,5 @@ module Grape # The current version of Grape. - VERSION = '2.1.1' + VERSION = '2.2.0' end From cc948bddc058c08ed519c1154d405c8ec0170b5b Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sat, 22 Jun 2024 19:31:44 +0200 Subject: [PATCH 242/304] Remove Grape::Util::Accept::Header (#2458) * Remove Grape::Util::Accept::Header * Add CHANGELOG.md --- CHANGELOG.md | 1 + lib/grape/util/accept/header.rb | 19 ------------------- 2 files changed, 1 insertion(+), 19 deletions(-) delete mode 100644 lib/grape/util/accept/header.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 8634e3657..4dec503c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ #### Fixes +* [#3458](https://github.com/ruby-grape/grape/pull/2458): Remove unused Grape::Util::Accept::Header - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 2.1.1 (2024-06-22) diff --git a/lib/grape/util/accept/header.rb b/lib/grape/util/accept/header.rb deleted file mode 100644 index 3f6b3a67a..000000000 --- a/lib/grape/util/accept/header.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -module Grape - module Util - module Accept - module Header - ALLOWED_CHARACTERS = %r{^([a-z*]+)/([a-z0-9*&\^\-_#{$ERROR_INFO}.+]+)(?:;([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&.match(ALLOWED_CHARACTERS) - m ? [m[1], m[2], m[3] || ''] : [] - end - end - end - end - end -end From 69d14ee467a6151b25a7540637af85bba159f9c8 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Tue, 25 Jun 2024 21:50:12 +0200 Subject: [PATCH 243/304] Autocorrect cops (#2459) * Autocorrect rubocop whenever possible Fix Lint/AmbiguousBlockAssociation: Fix Lint/DuplicateBranch Fix Lint/EmptyClass: Fix Naming/MemoizedInstanceVariableName Fix Naming/MethodParameterName Fix RSpec/NoExpectationExample Fix RSpec/ScatteredSetup: Fix Style/RedundantConstantBase Fix Style/Semicolon Fix Style/SuperArguments Fix Style/SymbolProc Fix Style/YodaCondition Fix Style/ZeroLengthPredicate * Add CHANGELOG.md --- .rubocop.yml | 3 + .rubocop_todo.yml | 131 +------------ CHANGELOG.md | 1 + benchmark/large_model.rb | 2 +- lib/grape/api.rb | 4 +- lib/grape/api/instance.rb | 2 +- lib/grape/dsl/parameters.rb | 2 +- lib/grape/endpoint.rb | 4 +- lib/grape/exceptions/validation_errors.rb | 2 +- lib/grape/middleware/base.rb | 2 +- lib/grape/middleware/error.rb | 4 +- lib/grape/middleware/stack.rb | 4 +- .../validators/exactly_one_of_validator.rb | 2 +- spec/grape/api/invalid_format_spec.rb | 6 +- spec/grape/api_remount_spec.rb | 4 +- spec/grape/api_spec.rb | 177 ++++++++++++------ spec/grape/dsl/logger_spec.rb | 2 +- spec/grape/dsl/routing_spec.rb | 2 +- spec/grape/endpoint/declared_spec.rb | 4 +- spec/grape/endpoint_spec.rb | 8 +- spec/grape/middleware/formatter_spec.rb | 6 +- spec/grape/util/inheritable_setting_spec.rb | 5 +- spec/grape/validations/params_scope_spec.rb | 2 +- .../validations/validators/coerce_spec.rb | 8 +- .../validations/validators/default_spec.rb | 4 +- spec/grape/validations_spec.rb | 24 +-- spec/integration/multi_json/json_spec.rb | 4 +- spec/integration/multi_xml/xml_spec.rb | 2 +- spec/support/deprecated_warning_handlers.rb | 2 +- spec/support/versioned_helpers.rb | 12 +- 30 files changed, 176 insertions(+), 259 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 9e4cc8bd4..44b9d7e87 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -80,3 +80,6 @@ RSpec/MultipleMemoizedHelpers: RSpec/ContextWording: Enabled: false + +RSpec/MessageSpies: + EnforcedStyle: receive diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index a3956b5bc..f4482aff4 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2024-06-15 15:12:21 UTC using RuboCop version 1.64.1. +# on 2024-06-22 17:26:10 UTC using RuboCop version 1.64.1. # 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 @@ -14,13 +14,6 @@ Gemspec/RequireMFA: Exclude: - 'grape.gemspec' -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowedMethods, AllowedPatterns. -Lint/AmbiguousBlockAssociation: - Exclude: - - 'spec/grape/dsl/routing_spec.rb' - # Offense count: 1 # Configuration parameters: AllowedMethods. # AllowedMethods: enums @@ -28,43 +21,12 @@ Lint/ConstantDefinitionInBlock: Exclude: - 'spec/grape/validations/validators/except_values_spec.rb' -# Offense count: 3 -# Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches. -Lint/DuplicateBranch: - Exclude: - - 'spec/support/versioned_helpers.rb' - -# Offense count: 1 -# Configuration parameters: AllowComments. -Lint/EmptyClass: - Exclude: - - 'lib/grape/dsl/parameters.rb' - # Offense count: 1 # Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns. Metrics/MethodLength: Exclude: - 'lib/grape/endpoint.rb' -# Offense count: 2 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: EnforcedStyleForLeadingUnderscores. -# SupportedStylesForLeadingUnderscores: disallowed, required, optional -Naming/MemoizedInstanceVariableName: - Exclude: - - 'lib/grape/api/instance.rb' - - 'lib/grape/middleware/base.rb' - -# Offense count: 5 -# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. -# AllowedNames: as, at, by, cc, db, id, if, in, io, ip, of, on, os, pp, to -Naming/MethodParameterName: - Exclude: - - '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, AllowedPatterns. # SupportedStyles: snake_case, normalcase, non_integer @@ -117,10 +79,9 @@ RSpec/ExpectActual: - 'spec/grape/endpoint/declared_spec.rb' - 'spec/grape/middleware/exception_spec.rb' -# Offense count: 3 +# Offense count: 1 RSpec/ExpectInHook: Exclude: - - 'spec/grape/api_spec.rb' - 'spec/grape/validations/validators/values_spec.rb' # Offense count: 6 @@ -147,31 +108,16 @@ RSpec/LeakyConstantDeclaration: Exclude: - 'spec/grape/validations/validators/except_values_spec.rb' -# Offense count: 2 +# Offense count: 1 RSpec/MessageChain: Exclude: - 'spec/grape/middleware/formatter_spec.rb' -# Offense count: 144 -# Configuration parameters: . -# SupportedStyles: have_received, receive -RSpec/MessageSpies: - EnforcedStyle: receive - # Offense count: 12 RSpec/MissingExampleGroupArgument: Exclude: - 'spec/grape/middleware/exception_spec.rb' -# Offense count: 17 -# Configuration parameters: AllowedPatterns. -# AllowedPatterns: ^expect_, ^assert_ -RSpec/NoExpectationExample: - Exclude: - - 'spec/grape/api_remount_spec.rb' - - 'spec/grape/api_spec.rb' - - 'spec/grape/validations_spec.rb' - # Offense count: 12 RSpec/RepeatedDescription: Exclude: @@ -180,10 +126,9 @@ RSpec/RepeatedDescription: - 'spec/grape/validations/validators/allow_blank_spec.rb' - 'spec/grape/validations/validators/values_spec.rb' -# Offense count: 8 +# Offense count: 6 RSpec/RepeatedExample: Exclude: - - 'spec/grape/api_spec.rb' - 'spec/grape/middleware/versioner/accept_version_header_spec.rb' - 'spec/grape/validations/validators/allow_blank_spec.rb' @@ -195,13 +140,6 @@ RSpec/RepeatedExampleGroupDescription: - 'spec/grape/util/inheritable_setting_spec.rb' - 'spec/grape/validations/validators/values_spec.rb' -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AutoCorrect. -RSpec/ScatteredSetup: - Exclude: - - 'spec/grape/util/inheritable_setting_spec.rb' - # Offense count: 5 RSpec/StubbedMock: Exclude: @@ -228,7 +166,7 @@ RSpec/SubjectStub: - 'spec/grape/middleware/stack_spec.rb' - 'spec/grape/parser_spec.rb' -# Offense count: 24 +# Offense count: 23 # Configuration parameters: IgnoreNameless, IgnoreSymbolicNames. RSpec/VerifiedDoubles: Exclude: @@ -251,13 +189,6 @@ Style/CombinableLoops: Exclude: - 'spec/grape/endpoint_spec.rb' -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: MaxUnannotatedPlaceholdersAllowed, AllowedMethods, AllowedPatterns. -# SupportedStyles: annotated, template, unannotated -Style/FormatStringToken: - EnforcedStyle: template - # Offense count: 12 # Configuration parameters: AllowedMethods. # AllowedMethods: respond_to_missing? @@ -274,55 +205,3 @@ Style/OptionalBooleanParameter: - 'lib/grape/validations/types/dry_type_coercer.rb' - 'lib/grape/validations/types/primitive_coercer.rb' - 'lib/grape/validations/types/set_coercer.rb' - -# Offense count: 29 -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantConstantBase: - Exclude: - - 'spec/grape/api/invalid_format_spec.rb' - - 'spec/grape/api_spec.rb' - - 'spec/grape/dsl/logger_spec.rb' - - 'spec/grape/endpoint/declared_spec.rb' - - 'spec/grape/endpoint_spec.rb' - - 'spec/grape/middleware/formatter_spec.rb' - - 'spec/grape/validations/validators/coerce_spec.rb' - - 'spec/grape/validations/validators/default_spec.rb' - - 'spec/integration/multi_json/json_spec.rb' - - 'spec/integration/multi_xml/xml_spec.rb' - -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowAsExpressionSeparator. -Style/Semicolon: - Exclude: - - 'spec/grape/api_spec.rb' - -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -Style/SuperArguments: - Exclude: - - 'lib/grape/api.rb' - - 'spec/support/deprecated_warning_handlers.rb' - -# Offense count: 2 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AllowMethodsWithArguments, AllowedMethods, AllowedPatterns, AllowComments. -# AllowedMethods: define_method -Style/SymbolProc: - Exclude: - - 'benchmark/large_model.rb' - - 'spec/grape/validations/params_scope_spec.rb' - -# Offense count: 1 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: forbid_for_all_comparison_operators, forbid_for_equality_operators_only, require_for_all_comparison_operators, require_for_equality_operators_only -Style/YodaCondition: - Exclude: - - 'lib/grape/api.rb' - -# Offense count: 1 -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/ZeroLengthPredicate: - Exclude: - - 'lib/grape/validations/validators/exactly_one_of_validator.rb' diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dec503c8..f384013f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ #### Fixes +* [#2459](https://github.com/ruby-grape/grape/pull/2459): Autocorrect cops - [@ericproulx](https://github.com/ericproulx). * [#3458](https://github.com/ruby-grape/grape/pull/2458): Remove unused Grape::Util::Accept::Header - [@ericproulx](https://github.com/ericproulx). * Your contribution here. diff --git a/benchmark/large_model.rb b/benchmark/large_model.rb index 27dc9a357..9eaa1a528 100644 --- a/benchmark/large_model.rb +++ b/benchmark/large_model.rb @@ -140,7 +140,7 @@ def self.vrp_request_configuration(this) 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(:entity, type: Symbol, values: %i[vehicle work_day], coerce_with: lambda(&:to_sym)) this.optional(:threshold, type: Integer) end diff --git a/lib/grape/api.rb b/lib/grape/api.rb index 38f17dcc5..eaa351c9b 100644 --- a/lib/grape/api.rb +++ b/lib/grape/api.rb @@ -32,7 +32,7 @@ def new(...) def inherited(api) super - api.initial_setup(Grape::API == self ? Grape::API::Instance : @base_instance) + api.initial_setup(self == Grape::API ? Grape::API::Instance : @base_instance) api.override_all_methods! end @@ -108,7 +108,7 @@ def replay_setup_on(instance) end def respond_to?(method, include_private = false) - super(method, include_private) || base_instance.respond_to?(method, include_private) + super || base_instance.respond_to?(method, include_private) end def respond_to_missing?(method, include_private = false) diff --git a/lib/grape/api/instance.rb b/lib/grape/api/instance.rb index a17c8e63f..ce290df7c 100644 --- a/lib/grape/api/instance.rb +++ b/lib/grape/api/instance.rb @@ -46,7 +46,7 @@ def reset! # Parses the API's definition and compiles it into an instance of # Grape::API. def compile - @instance ||= new + @instance ||= new # rubocop:disable Naming/MemoizedInstanceVariableName end # Wipe the compiled API so we can recompile after changes were made. diff --git a/lib/grape/dsl/parameters.rb b/lib/grape/dsl/parameters.rb index bfc9b408e..821da5d79 100644 --- a/lib/grape/dsl/parameters.rb +++ b/lib/grape/dsl/parameters.rb @@ -231,7 +231,7 @@ def declared_param?(param) alias group requires - class EmptyOptionalValue; end + class EmptyOptionalValue; end # rubocop:disable Lint/EmptyClass def map_params(params, element, is_array = false) if params.is_a?(Array) diff --git a/lib/grape/endpoint.rb b/lib/grape/endpoint.rb index 7fc83fc43..cc1fdba83 100644 --- a/lib/grape/endpoint.rb +++ b/lib/grape/endpoint.rb @@ -231,8 +231,8 @@ def endpoints options[:app].endpoints if options[:app].respond_to?(:endpoints) end - def equals?(e) - (options == e.options) && (inheritable_setting.to_hash == e.inheritable_setting.to_hash) + def equals?(endpoint) + (options == endpoint.options) && (inheritable_setting.to_hash == endpoint.inheritable_setting.to_hash) end protected diff --git a/lib/grape/exceptions/validation_errors.rb b/lib/grape/exceptions/validation_errors.rb index 09e4a37b0..b8a843b1a 100644 --- a/lib/grape/exceptions/validation_errors.rb +++ b/lib/grape/exceptions/validation_errors.rb @@ -4,7 +4,7 @@ module Grape module Exceptions class ValidationErrors < Grape::Exceptions::Base ERRORS_FORMAT_KEY = 'grape.errors.format' - DEFAULT_ERRORS_FORMAT = '%{attributes} %{message}' + DEFAULT_ERRORS_FORMAT = '%s %s' include Enumerable diff --git a/lib/grape/middleware/base.rb b/lib/grape/middleware/base.rb index 87f2429ff..2ee9cbeae 100644 --- a/lib/grape/middleware/base.rb +++ b/lib/grape/middleware/base.rb @@ -74,7 +74,7 @@ def content_type end def mime_types - @mime_type ||= content_types.each_pair.with_object({}) do |(k, v), types_without_params| + @mime_types ||= content_types.each_pair.with_object({}) do |(k, v), types_without_params| types_without_params[v.split(';').first] = k end end diff --git a/lib/grape/middleware/error.rb b/lib/grape/middleware/error.rb index b1fc02767..2dc71c1da 100644 --- a/lib/grape/middleware/error.rb +++ b/lib/grape/middleware/error.rb @@ -74,8 +74,8 @@ def error_response(error = {}) rack_response(status, headers, format_message(message, backtrace, original_exception)) end - def default_rescue_handler(e) - error_response(message: e.message, backtrace: e.backtrace, original_exception: e) + def default_rescue_handler(exception) + error_response(message: exception.message, backtrace: exception.backtrace, original_exception: exception) end def rescue_handler_for_base_only_class(klass) diff --git a/lib/grape/middleware/stack.rb b/lib/grape/middleware/stack.rb index 9ba339a78..8e25af385 100644 --- a/lib/grape/middleware/stack.rb +++ b/lib/grape/middleware/stack.rb @@ -57,8 +57,8 @@ def last middlewares.last end - def [](i) - middlewares[i] + def [](index) + middlewares[index] end def insert(index, *args, &block) diff --git a/lib/grape/validations/validators/exactly_one_of_validator.rb b/lib/grape/validations/validators/exactly_one_of_validator.rb index 735c45701..aa1c54711 100644 --- a/lib/grape/validations/validators/exactly_one_of_validator.rb +++ b/lib/grape/validations/validators/exactly_one_of_validator.rb @@ -7,7 +7,7 @@ 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: all_keys, message: message(:exactly_one)) if keys.empty? raise Grape::Exceptions::Validation.new(params: keys, message: message(:mutual_exclusion)) end diff --git a/spec/grape/api/invalid_format_spec.rb b/spec/grape/api/invalid_format_spec.rb index 79da1ac1d..0e6399d32 100644 --- a/spec/grape/api/invalid_format_spec.rb +++ b/spec/grape/api/invalid_format_spec.rb @@ -27,19 +27,19 @@ def app it 'no format' do get '/foo' expect(last_response.status).to eq 200 - expect(last_response.body).to eq(::Grape::Json.dump(id: 'foo', format: nil)) + expect(last_response.body).to eq(Grape::Json.dump(id: 'foo', format: nil)) end it 'json format' do get '/foo.json' expect(last_response.status).to eq 200 - expect(last_response.body).to eq(::Grape::Json.dump(id: 'foo', format: 'json')) + expect(last_response.body).to eq(Grape::Json.dump(id: 'foo', format: 'json')) end it 'invalid format' do get '/foo.invalid' expect(last_response.status).to eq 200 - expect(last_response.body).to eq(::Grape::Json.dump(id: 'foo', format: 'invalid')) + expect(last_response.body).to eq(Grape::Json.dump(id: 'foo', format: 'invalid')) end end end diff --git a/spec/grape/api_remount_spec.rb b/spec/grape/api_remount_spec.rb index 2c6c6ae4b..793d49d9f 100644 --- a/spec/grape/api_remount_spec.rb +++ b/spec/grape/api_remount_spec.rb @@ -306,13 +306,15 @@ tags ['not_configurable_tag', configuration[:a_configurable_tag]] end get 'location' do - 'success' + route.tags end end end it 'mounts the endpoint with the appropiate tags' do root_api.mount({ a_remounted_api => 'integer' }, with: { a_configurable_tag: 'a configured tag' }) + get '/integer/location', param_key: 'a' + expect(JSON.parse(last_response.body)).to eq ['not_configurable_tag', 'a configured tag'] end end diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index fbd0ee38c..dd280c9a5 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -287,8 +287,10 @@ def subject.enable_root_route! end end - after do - expect(last_response.body).to eql 'root' + shared_examples_for 'a root route' do + it 'returns root' do + expect(last_response.body).to eql 'root' + end end describe 'path versioned APIs' do @@ -300,56 +302,104 @@ def subject.enable_root_route! context 'when a single version provided' do let(:version) { 'v1' } - it 'without a format' do - versioned_get '/', 'v1', using: :path + context 'without a format' do + before do + versioned_get '/', 'v1', using: :path + end + + it_behaves_like 'a root route' end - it 'with a format' do - get '/v1/.json' + context 'with a format' do + before do + get '/v1/.json' + end + + it_behaves_like 'a root route' end end context 'when array of versions provided' do let(:version) { %w[v1 v2] } - it { versioned_get '/', 'v1', using: :path } - it { versioned_get '/', 'v2', using: :path } + context 'when v1' do + before do + versioned_get '/', 'v1', using: :path + end + + it_behaves_like 'a root route' + end + + context 'when v2' do + before do + versioned_get '/', 'v2', using: :path + end + + it_behaves_like 'a root route' + end end end - it 'header versioned APIs' do - subject.version 'v1', using: :header, vendor: 'test' - subject.enable_root_route! + context 'when header versioned APIs' do + before do + subject.version 'v1', using: :header, vendor: 'test' + subject.enable_root_route! + versioned_get '/', 'v1', using: :header, vendor: 'test' + end - versioned_get '/', 'v1', using: :header, vendor: 'test' + it_behaves_like 'a root route' end - it 'header versioned APIs with multiple headers' do - subject.version %w[v1 v2], using: :header, vendor: 'test' - subject.enable_root_route! + context 'when header versioned APIs with multiple headers' do + before do + subject.version %w[v1 v2], using: :header, vendor: 'test' + subject.enable_root_route! + end + + context 'when v1' do + before do + versioned_get '/', 'v1', using: :header, vendor: 'test' + end - versioned_get '/', 'v1', using: :header, vendor: 'test' - versioned_get '/', 'v2', using: :header, vendor: 'test' + it_behaves_like 'a root route' + end + + context 'when v2' do + before do + versioned_get '/', 'v2', using: :header, vendor: 'test' + end + + it_behaves_like 'a root route' + end end - it 'param versioned APIs' do - subject.version 'v1', using: :param - subject.enable_root_route! + context 'param versioned APIs' do + before do + subject.version 'v1', using: :param + subject.enable_root_route! + versioned_get '/', 'v1', using: :param + end - versioned_get '/', 'v1', using: :param + it_behaves_like 'a root route' end - it 'Accept-Version header versioned APIs' do - subject.version 'v1', using: :accept_version_header - subject.enable_root_route! + context 'when Accept-Version header versioned APIs' do + before do + subject.version 'v1', using: :accept_version_header + subject.enable_root_route! + versioned_get '/', 'v1', using: :accept_version_header + end - versioned_get '/', 'v1', using: :accept_version_header + it_behaves_like 'a root route' end - it 'unversioned APIs' do - subject.enable_root_route! + context 'unversioned APIss' do + before do + subject.enable_root_route! + get '/' + end - get '/' + it_behaves_like 'a root route' end end @@ -438,9 +488,9 @@ def to_txt subject.send(verb) do env[Grape::Env::API_REQUEST_BODY] end - send verb, '/', ::Grape::Json.dump(object), 'CONTENT_TYPE' => 'application/json' + send verb, '/', Grape::Json.dump(object), 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq(verb == :post ? 201 : 200) - expect(last_response.body).to eql ::Grape::Json.dump(object) + expect(last_response.body).to eql Grape::Json.dump(object) expect(last_request.params).to eql({}) end @@ -449,9 +499,9 @@ def to_txt subject.send(verb) do env[Grape::Env::API_REQUEST_INPUT] end - send verb, '/', ::Grape::Json.dump(object), 'CONTENT_TYPE' => 'application/json' + send verb, '/', Grape::Json.dump(object), 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq(verb == :post ? 201 : 200) - expect(last_response.body).to eql ::Grape::Json.dump(object).to_json + expect(last_response.body).to eql Grape::Json.dump(object).to_json end context 'chunked transfer encoding' do @@ -460,9 +510,9 @@ def to_txt subject.send(verb) do env[Grape::Env::API_REQUEST_INPUT] end - send verb, '/', ::Grape::Json.dump(object), 'CONTENT_TYPE' => 'application/json', Grape::Http::Headers::HTTP_TRANSFER_ENCODING => 'chunked' + send verb, '/', Grape::Json.dump(object), 'CONTENT_TYPE' => 'application/json', Grape::Http::Headers::HTTP_TRANSFER_ENCODING => 'chunked' expect(last_response.status).to eq(verb == :post ? 201 : 200) - expect(last_response.body).to eql ::Grape::Json.dump(object).to_json + expect(last_response.body).to eql Grape::Json.dump(object).to_json end end end @@ -993,9 +1043,14 @@ def to_txt end describe '.compile!' do - it 'compiles the instance for rack!' do - stubbed_object = double(:instance_for_rack) - allow(app).to receive(:instance_for_rack) { stubbed_object } + let(:base_instance) { app.base_instance } + + before do + allow(base_instance).to receive(:compile!).and_return(:compiled!) + end + + it 'returns compiled!' do + expect(app.send(:compile!)).to eq(:compiled!) end end @@ -1593,8 +1648,8 @@ def call(env) it 'has access to helper methods' do subject.helpers do - def authorize(u, p) - u == 'allow' && p == 'whatever' + def authorize(user, password) + user == 'allow' && password == 'whatever' end end @@ -1634,7 +1689,7 @@ def authorize(u, p) def self.io @io ||= StringIO.new end - logger ::Logger.new(io) + logger Logger.new(io) end end @@ -2496,7 +2551,7 @@ def self.call(message, _backtrace, _options, _env, _original_exception) raise 'rain!' end get '/exception' - json = ::Grape::Json.load(last_response.body) + json = Grape::Json.load(last_response.body) expect(json['error']).to eql 'rain!' expect(json['backtrace'].length).to be > 0 end @@ -2511,24 +2566,26 @@ def self.call(message, _backtrace, _options, _env, _original_exception) end context 'with json format' do - before { subject.format :json } + shared_examples_for 'a json format api' do |error_message| + subject { JSON.parse(last_response.body) } - after do - get '/error' - expect(last_response.body).to eql('{"error":"failure"}') - end + before { get '/error' } - it 'rescues error! called with a string and returns json' do - subject.get('/error') { error!(:failure, 401) } - end + let(:app) do + Class.new(Grape::API) do + format :json + get('/error') { error!(error_message, 401) } + end + end - it 'rescues error! called with a symbol and returns json' do - subject.get('/error') { error!(:failure, 401) } + context "when error! called with #{error_message.class.name}" do + it { is_expected.to eq('error' => 'failure') } + end end - it 'rescues error! called with a hash and returns json' do - subject.get('/error') { error!({ error: :failure }, 401) } - end + it_behaves_like 'a json format api', 'failure' + it_behaves_like 'a json format api', :failure + it_behaves_like 'a json format api', { error: :failure } end end @@ -3083,13 +3140,13 @@ def self.call(object, _env) optional :param2 end subject.namespace 'ns1' do - get { ; } + get {} end subject.params do optional :param2 end subject.namespace 'ns2' do - get { ; } + get {} end routes_doc = subject.routes.map do |route| { description: route.description, params: route.params } @@ -3216,6 +3273,12 @@ def self.call(object, _env) optional :bar end end + subject.get 'method' + expect(subject.routes.map do |route| + { description: route.description, params: route.params } + end).to eq [ + { description: nil, params: { 'foo' => { required: true, type: 'Array' }, 'foo[bar]' => { required: false } } } + ] end it 'parses parameters when no description is given' do @@ -3693,7 +3756,7 @@ def my_method it 'path' do get '/endpoint/options' - options = ::Grape::Json.load(last_response.body) + options = Grape::Json.load(last_response.body) expect(options['path']).to eq(['/endpoint/options']) expect(options['source_location'][0]).to include 'api_spec.rb' expect(options['source_location'][1].to_i).to be > 0 diff --git a/spec/grape/dsl/logger_spec.rb b/spec/grape/dsl/logger_spec.rb index 2c9739f75..ae1bab567 100644 --- a/spec/grape/dsl/logger_spec.rb +++ b/spec/grape/dsl/logger_spec.rb @@ -9,7 +9,7 @@ end end - let(:logger) { instance_double(::Logger) } + let(:logger) { instance_double(Logger) } describe '.logger' do it 'sets a logger' do diff --git a/spec/grape/dsl/routing_spec.rb b/spec/grape/dsl/routing_spec.rb index 6d6458619..e763428f6 100644 --- a/spec/grape/dsl/routing_spec.rb +++ b/spec/grape/dsl/routing_spec.rb @@ -259,7 +259,7 @@ class Dummy it 'does not modify options parameter' do allow(subject).to receive(:namespace) expect { subject.route_param('foo', options, &proc {}) } - .not_to change { options } + .not_to(change { options }) end end diff --git a/spec/grape/endpoint/declared_spec.rb b/spec/grape/endpoint/declared_spec.rb index dcf2fe9d6..6bf5d206c 100644 --- a/spec/grape/endpoint/declared_spec.rb +++ b/spec/grape/endpoint/declared_spec.rb @@ -281,7 +281,7 @@ '' end - post '/declared', ::Grape::Json.dump(first: 'one', boolean: false), 'CONTENT_TYPE' => 'application/json' + post '/declared', Grape::Json.dump(first: 'one', boolean: false), 'CONTENT_TYPE' => 'application/json' expect(last_response).to be_created end @@ -296,7 +296,7 @@ '' end - post '/declared', ::Grape::Json.dump(first: 'one', second: nil), 'CONTENT_TYPE' => 'application/json' + post '/declared', Grape::Json.dump(first: 'one', second: nil), 'CONTENT_TYPE' => 'application/json' expect(last_response).to be_created end diff --git a/spec/grape/endpoint_spec.rb b/spec/grape/endpoint_spec.rb index ee4c8986d..ee44efc39 100644 --- a/spec/grape/endpoint_spec.rb +++ b/spec/grape/endpoint_spec.rb @@ -371,7 +371,7 @@ def app end it 'converts JSON bodies to params' do - post '/request_body', ::Grape::Json.dump(user: 'Bobby T.'), 'CONTENT_TYPE' => 'application/json' + post '/request_body', Grape::Json.dump(user: 'Bobby T.'), 'CONTENT_TYPE' => 'application/json' expect(last_response.body).to eq('Bobby T.') end @@ -407,7 +407,7 @@ def app error! 400, 'expected nil' if params[:version] params[:user] end - post '/omitted_params', ::Grape::Json.dump(user: 'Bob'), 'CONTENT_TYPE' => 'application/json' + post '/omitted_params', Grape::Json.dump(user: 'Bob'), 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq(201) expect(last_response.body).to eq('Bob') end @@ -464,7 +464,7 @@ def app subject.put '/request_body' do params[:user] end - put '/request_body', ::Grape::Json.dump(user: 'Bob'), 'CONTENT_TYPE' => 'text/plain' + put '/request_body', Grape::Json.dump(user: 'Bob'), 'CONTENT_TYPE' => 'text/plain' expect(last_response.status).to eq(415) expect(last_response.body).to eq('{"error":"The provided content-type \'text/plain\' is not supported."}') @@ -478,7 +478,7 @@ def app subject.post do params[:data] end - post '/', ::Grape::Json.dump(data: { some: 'payload' }), 'CONTENT_TYPE' => 'application/json' + post '/', Grape::Json.dump(data: { some: 'payload' }), 'CONTENT_TYPE' => 'application/json' end it 'does not response with 406 for same type without params' do diff --git a/spec/grape/middleware/formatter_spec.rb b/spec/grape/middleware/formatter_spec.rb index 61a2a67bf..9b7dc9b56 100644 --- a/spec/grape/middleware/formatter_spec.rb +++ b/spec/grape/middleware/formatter_spec.rb @@ -16,7 +16,7 @@ it 'looks at the bodies for possibly serializable data' do r = Rack::MockResponse[*subject.call(env)] - expect(r.body).to eq(::Grape::Json.dump(body)) + expect(r.body).to eq(Grape::Json.dump(body)) end context 'default format' do @@ -336,7 +336,7 @@ def to_xml let(:io) { double } before do - allow(io).to receive_message_chain(:rewind, :read).and_return(nil) + allow(io).to receive_message_chain(rewind: nil, read: nil) end it 'does not read and parse the body' do @@ -355,7 +355,7 @@ def to_xml let(:io) { double } before do - allow(io).to receive_message_chain(:rewind, :read).and_return('') + allow(io).to receive_messages(rewind: nil, read: '') end it 'does not read and parse the body' do diff --git a/spec/grape/util/inheritable_setting_spec.rb b/spec/grape/util/inheritable_setting_spec.rb index 2941b3181..c9ad93bd9 100644 --- a/spec/grape/util/inheritable_setting_spec.rb +++ b/spec/grape/util/inheritable_setting_spec.rb @@ -5,6 +5,7 @@ module Util describe InheritableSetting do before do described_class.reset_global! + subject.inherit_from parent end let(:parent) do @@ -28,10 +29,6 @@ module Util end end - before do - subject.inherit_from parent - end - describe '#global' do it 'sets a global value' do subject.global[:some_thing] = :foo_bar diff --git a/spec/grape/validations/params_scope_spec.rb b/spec/grape/validations/params_scope_spec.rb index af6a4a2c9..454fdf6c0 100644 --- a/spec/grape/validations/params_scope_spec.rb +++ b/spec/grape/validations/params_scope_spec.rb @@ -58,7 +58,7 @@ def initialize(value) it do subject.params do - requires :foo, as: :bar, type: String, coerce_with: ->(c) { c.strip } + requires :foo, as: :bar, type: String, coerce_with: lambda(&:strip) end subject.get('/renaming-coerced') { "#{params['bar']}-#{params['foo']}" } get '/renaming-coerced', foo: ' there we go ' diff --git a/spec/grape/validations/validators/coerce_spec.rb b/spec/grape/validations/validators/coerce_spec.rb index 6b8f50068..81cd4b511 100644 --- a/spec/grape/validations/validators/coerce_spec.rb +++ b/spec/grape/validations/validators/coerce_spec.rb @@ -655,19 +655,19 @@ def self.parse(_val) params[:values] end - post '/coerce_nested_strings', ::Grape::Json.dump(values: 'a,b,c,d'), 'CONTENT_TYPE' => 'application/json' + post '/coerce_nested_strings', Grape::Json.dump(values: 'a,b,c,d'), 'CONTENT_TYPE' => 'application/json' expect(last_response).to be_created expect(JSON.parse(last_response.body)).to eq([%w[a b c d]]) - post '/coerce_nested_strings', ::Grape::Json.dump(values: [%w[a c], %w[b]]), 'CONTENT_TYPE' => 'application/json' + post '/coerce_nested_strings', Grape::Json.dump(values: [%w[a c], %w[b]]), 'CONTENT_TYPE' => 'application/json' expect(last_response).to be_created expect(JSON.parse(last_response.body)).to eq([%w[a c], %w[b]]) - post '/coerce_nested_strings', ::Grape::Json.dump(values: [[]]), 'CONTENT_TYPE' => 'application/json' + post '/coerce_nested_strings', Grape::Json.dump(values: [[]]), 'CONTENT_TYPE' => 'application/json' expect(last_response).to be_created expect(JSON.parse(last_response.body)).to eq([[]]) - post '/coerce_nested_strings', ::Grape::Json.dump(values: [['a', { bar: 0 }], ['b']]), 'CONTENT_TYPE' => 'application/json' + post '/coerce_nested_strings', Grape::Json.dump(values: [['a', { bar: 0 }], ['b']]), 'CONTENT_TYPE' => 'application/json' expect(last_response).to be_bad_request end diff --git a/spec/grape/validations/validators/default_spec.rb b/spec/grape/validations/validators/default_spec.rb index 2bb59792d..df84f06ee 100644 --- a/spec/grape/validations/validators/default_spec.rb +++ b/spec/grape/validations/validators/default_spec.rb @@ -382,8 +382,8 @@ def app [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], + [File, ''], + [File, { test: 'non-empty-string' }.to_json], [Rack::Multipart::UploadedFile, ''], [Rack::Multipart::UploadedFile, { test: 'non-empty-string' }.to_json] ].each do |type, default| diff --git a/spec/grape/validations_spec.rb b/spec/grape/validations_spec.rb index 12acb33d3..8f7db50c4 100644 --- a/spec/grape/validations_spec.rb +++ b/spec/grape/validations_spec.rb @@ -4,11 +4,7 @@ subject { Class.new(Grape::API) } let(:app) { subject } - let(:declard_params) {} - - def declared_params - subject.namespace_stackable(:declared_params).flatten - end + let(:declared_params) { subject.namespace_stackable(:declared_params).flatten } describe 'params' do context 'optional' do @@ -1365,24 +1361,6 @@ def validate_param!(attr_name, params) end context 'named' do - context 'can be defined' do - it 'in helpers' do - subject.helpers do - params :pagination do - end - end - end - - it 'in helper module which kind of Grape::DSL::Helpers::BaseHelper' do - shared_params = Module.new do - extend Grape::DSL::Helpers::BaseHelper - params :pagination do - end - end - subject.helpers shared_params - end - end - context 'can be included in usual params' do before do shared_params = Module.new do diff --git a/spec/integration/multi_json/json_spec.rb b/spec/integration/multi_json/json_spec.rb index 994a0aeb2..56ded26b0 100644 --- a/spec/integration/multi_json/json_spec.rb +++ b/spec/integration/multi_json/json_spec.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true # grape_entity depends on multi-json and it breaks the test. -describe Grape::Json, if: defined?(::MultiJson) && !defined?(Grape::Entity) do +describe Grape::Json, if: defined?(MultiJson) && !defined?(Grape::Entity) do subject { described_class } - it { is_expected.to eq(::MultiJson) } + it { is_expected.to eq(MultiJson) } end diff --git a/spec/integration/multi_xml/xml_spec.rb b/spec/integration/multi_xml/xml_spec.rb index cda102b1d..a5d998847 100644 --- a/spec/integration/multi_xml/xml_spec.rb +++ b/spec/integration/multi_xml/xml_spec.rb @@ -3,5 +3,5 @@ describe Grape::Xml, if: defined?(MultiXml) do subject { described_class } - it { is_expected.to eq(::MultiXml) } + it { is_expected.to eq(MultiXml) } end diff --git a/spec/support/deprecated_warning_handlers.rb b/spec/support/deprecated_warning_handlers.rb index 85c4bb78d..040410fb1 100644 --- a/spec/support/deprecated_warning_handlers.rb +++ b/spec/support/deprecated_warning_handlers.rb @@ -8,7 +8,7 @@ class DeprecationWarning < StandardError; end DEPRECATION_REGEX = /is deprecated/.freeze def warn(message) - return super(message) unless message.match?(DEPRECATION_REGEX) + return super unless message.match?(DEPRECATION_REGEX) exception = DeprecationWarning.new(message) exception.set_backtrace(caller) diff --git a/spec/support/versioned_helpers.rb b/spec/support/versioned_helpers.rb index e216f7c8f..75e56e7d1 100644 --- a/spec/support/versioned_helpers.rb +++ b/spec/support/versioned_helpers.rb @@ -10,11 +10,7 @@ def versioned_path(**options) case options[:using] when :path File.join('/', options[:prefix] || '', options[:version], options[:path]) - when :param - File.join('/', options[:prefix] || '', options[:path]) - when :header - File.join('/', options[:prefix] || '', options[:path]) - when :accept_version_header + when :param, :header, :accept_version_header File.join('/', options[:prefix] || '', options[:path]) else raise ArgumentError.new("unknown versioning strategy: #{options[:using]}") @@ -23,10 +19,8 @@ def versioned_path(**options) def versioned_headers(**options) case options[:using] - when :path - {} # no-op - when :param - {} # no-op + when :path, :param + {} when :header { Grape::Http::Headers::HTTP_ACCEPT => [ From f3dd0beecdd33df188094b413539120f79c2ea83 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Wed, 26 Jun 2024 00:08:48 +0200 Subject: [PATCH 244/304] Fix error message indices (#2463) * Reset index before iterating Add reset_index to facilitate * Fix rubocop ClassLength Add CHANGELOG.md --- CHANGELOG.md | 1 + lib/grape/validations/attributes_iterator.rb | 1 + lib/grape/validations/params_scope.rb | 14 +++---- spec/grape/validations_spec.rb | 42 ++++++++++++++++++++ 4 files changed, 49 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f384013f8..90ad82e7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * [#2459](https://github.com/ruby-grape/grape/pull/2459): Autocorrect cops - [@ericproulx](https://github.com/ericproulx). * [#3458](https://github.com/ruby-grape/grape/pull/2458): Remove unused Grape::Util::Accept::Header - [@ericproulx](https://github.com/ericproulx). +* [#2463](https://github.com/ruby-grape/grape/pull/2463): Fix error message indices - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 2.1.1 (2024-06-22) diff --git a/lib/grape/validations/attributes_iterator.rb b/lib/grape/validations/attributes_iterator.rb index c16d44f3f..97d7f0280 100644 --- a/lib/grape/validations/attributes_iterator.rb +++ b/lib/grape/validations/attributes_iterator.rb @@ -21,6 +21,7 @@ def each(&block) private def do_each(params_to_process, parent_indicies = [], &block) + @scope.reset_index # gets updated depending on the size of params_to_process params_to_process.each_with_index do |resource_params, index| # when we get arrays of arrays it means that target element located inside array # we need this because we want to know parent arrays indicies diff --git a/lib/grape/validations/params_scope.rb b/lib/grape/validations/params_scope.rb index 403b0ef09..ef8d3ec1b 100644 --- a/lib/grape/validations/params_scope.rb +++ b/lib/grape/validations/params_scope.rb @@ -93,9 +93,7 @@ def should_validate?(parameters) def meets_dependency?(params, request_params) return true unless @dependent_on - return false if @parent.present? && !@parent.meets_dependency?(@parent.params(request_params), request_params) - return params.any? { |param| meets_dependency?(param, request_params) } if params.is_a?(Array) meets_hash_dependency?(params) @@ -103,7 +101,6 @@ def meets_dependency?(params, request_params) def attr_meets_dependency?(params) return true unless @dependent_on - return false if @parent.present? && !@parent.attr_meets_dependency?(params) meets_hash_dependency?(params) @@ -169,6 +166,10 @@ def required? !@optional end + def reset_index + @index = nil + end + protected # Adds a parameter declaration to our list of validations. @@ -297,12 +298,7 @@ def new_lateral_scope(options, &block) # `optional` invocation that opened this scope. # @yield parameter scope def new_group_scope(attrs, &block) - self.class.new( - api: @api, - parent: self, - group: attrs.first, - &block - ) + self.class.new(api: @api, parent: self, group: attrs.first, &block) end # Pushes declared params to parent or settings diff --git a/spec/grape/validations_spec.rb b/spec/grape/validations_spec.rb index 8f7db50c4..19dd130f0 100644 --- a/spec/grape/validations_spec.rb +++ b/spec/grape/validations_spec.rb @@ -1966,6 +1966,48 @@ def validate_param!(attr_name, params) expect(last_response.status).to eq(400) end end + + # Ensure there is no leakage of indices between requests + context 'required with a hash inside an array' do + before do + subject.params do + requires :items, type: Array do + requires :item, type: Hash do + requires :name, type: String + end + end + end + subject.post '/required' do + 'required works' + end + end + + let(:valid_item) { { item: { name: 'foo' } } } + + let(:params) do + { + items: [ + valid_item, + valid_item, + {} + ] + } + end + + it 'makes sure the error message is independent of the previous request' do + post_with_json '/required', {} + expect(last_response).to be_bad_request + expect(last_response.body).to eq('items is missing, items[item][name] is missing') + + post_with_json '/required', params + expect(last_response).to be_bad_request + expect(last_response.body).to eq('items[2][item] is missing, items[2][item][name] is missing') + + post_with_json '/required', {} + expect(last_response).to be_bad_request + expect(last_response.body).to eq('items is missing, items[item][name] is missing') + end + end end describe 'require_validator' do From f1560cd7edbc7cf53b4d66fc6a5451cf942ad0ed Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 28 Jun 2024 09:31:13 +0200 Subject: [PATCH 245/304] Preparing for release, 2.1.2. --- CHANGELOG.md | 7 +------ README.md | 3 +-- lib/grape/version.rb | 2 +- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90ad82e7f..25c203f8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,10 @@ -### 2.2.0 (Next) - -#### Features - -* Your contribution here. +### 2.1.2 (2024-06-28) #### Fixes * [#2459](https://github.com/ruby-grape/grape/pull/2459): Autocorrect cops - [@ericproulx](https://github.com/ericproulx). * [#3458](https://github.com/ruby-grape/grape/pull/2458): Remove unused Grape::Util::Accept::Header - [@ericproulx](https://github.com/ericproulx). * [#2463](https://github.com/ruby-grape/grape/pull/2463): Fix error message indices - [@ericproulx](https://github.com/ericproulx). -* Your contribution here. ### 2.1.1 (2024-06-22) diff --git a/README.md b/README.md index dd156e2c9..6f568149d 100644 --- a/README.md +++ b/README.md @@ -157,9 +157,8 @@ Grape is a REST-like API framework for Ruby. It's designed to run on Rack or com ## Stable Release -You're reading the documentation for the next release of Grape, which should be 2.2.0. +You're reading the documentation for the stable release of Grape, **2.1.2**. Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. -The current stable release is [2.1.1](https://github.com/ruby-grape/grape/blob/v2.1.1/README.md). ## Project Resources diff --git a/lib/grape/version.rb b/lib/grape/version.rb index 5083e38bd..f3bdb18f8 100644 --- a/lib/grape/version.rb +++ b/lib/grape/version.rb @@ -2,5 +2,5 @@ module Grape # The current version of Grape. - VERSION = '2.2.0' + VERSION = '2.1.2' end From 987b9f95cd5076660914969ff11f276792ee13ca Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 28 Jun 2024 09:36:21 +0200 Subject: [PATCH 246/304] Preparing for next development iteration, 2.2.0. --- CHANGELOG.md | 10 ++++++++++ README.md | 3 ++- lib/grape/version.rb | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25c203f8d..fb34e9846 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +### 2.2.0 (Next) + +#### Features + +* Your contribution here. + +#### Fixes + +* Your contribution here. + ### 2.1.2 (2024-06-28) #### Fixes diff --git a/README.md b/README.md index 6f568149d..3be2dcead 100644 --- a/README.md +++ b/README.md @@ -157,8 +157,9 @@ Grape is a REST-like API framework for Ruby. It's designed to run on Rack or com ## Stable Release -You're reading the documentation for the stable release of Grape, **2.1.2**. +You're reading the documentation for the next release of Grape, which should be 2.2.0. Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. +The current stable release is [2.1.2](https://github.com/ruby-grape/grape/blob/v2.1.2/README.md). ## Project Resources diff --git a/lib/grape/version.rb b/lib/grape/version.rb index f3bdb18f8..5083e38bd 100644 --- a/lib/grape/version.rb +++ b/lib/grape/version.rb @@ -2,5 +2,5 @@ module Grape # The current version of Grape. - VERSION = '2.1.2' + VERSION = '2.2.0' end From b47d9adec9a1dcaff8d4e21a6560acd71492265a Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sat, 29 Jun 2024 21:12:21 +0200 Subject: [PATCH 247/304] Fix repo coverage (#2467) * simplecov libraries are now require false simplecov required at top of spec_helper.rb Use simplecov configuration file lcov will be used only on CI, default is a html remove duplicate warnings in spec_helper remove some $LOAD_PATH.unshift( at top of spec_helper * Add changelog --- .simplecov | 15 +++++++++++++++ CHANGELOG.md | 1 + Gemfile | 4 ++-- spec/spec_helper.rb | 18 +----------------- 4 files changed, 19 insertions(+), 19 deletions(-) create mode 100644 .simplecov diff --git a/.simplecov b/.simplecov new file mode 100644 index 000000000..dd8c5cd4d --- /dev/null +++ b/.simplecov @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +if ENV['GITHUB_USER'] # only when running CI + require 'simplecov-lcov' + SimpleCov::Formatter::LcovFormatter.config do |c| + c.report_with_single_file = true + c.single_report_path = 'coverage/lcov.info' + end + + SimpleCov.formatter = SimpleCov::Formatter::LcovFormatter +end + +SimpleCov.start do + add_filter '/spec/' +end diff --git a/CHANGELOG.md b/CHANGELOG.md index fb34e9846..46defa267 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ #### Fixes +* [#2467](https://github.com/ruby-grape/grape/pull/2467): Fix repo coverage - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 2.1.2 (2024-06-28) diff --git a/Gemfile b/Gemfile index eaf603c1d..8df676a00 100644 --- a/Gemfile +++ b/Gemfile @@ -28,8 +28,8 @@ group :test do gem 'rack-test', '~> 2.1' gem 'rspec', '~> 3.13' gem 'ruby-grape-danger', '~> 0.2', require: false - gem 'simplecov', '~> 0.21' - gem 'simplecov-lcov', '~> 0.8' + gem 'simplecov', '~> 0.21', require: false + gem 'simplecov-lcov', '~> 0.8', require: false gem 'test-prof', require: false end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index af3ff8256..1589a0881 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,15 +1,10 @@ # 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')) - +require 'simplecov' require 'rubygems' require 'bundler' Bundler.require :default, :test -require 'grape' - Grape.deprecator.behavior = :raise %w[config support].each do |dir| @@ -27,7 +22,6 @@ config.include Spec::Support::Helpers config.raise_errors_for_deprecations! config.filter_run_when_matching :focus - config.warnings = true config.before(:all) { Grape::Util::InheritableSetting.reset_global! } config.before { Grape::Util::InheritableSetting.reset_global! } @@ -35,13 +29,3 @@ # Enable flags like --only-failures and --next-failure config.example_status_persistence_file_path = '.rspec_status' end - -require 'simplecov' -require 'simplecov-lcov' -SimpleCov::Formatter::LcovFormatter.config do |c| - c.report_with_single_file = true - c.single_report_path = 'coverage/lcov.info' -end - -SimpleCov.formatter = SimpleCov::Formatter::LcovFormatter -SimpleCov.start From 5affa8f17ed25787f4b2d0d54d6310a691b778ee Mon Sep 17 00:00:00 2001 From: Andrei Subbota Date: Wed, 3 Jul 2024 17:52:50 +0200 Subject: [PATCH 248/304] Align `error!` method signatures across different places. (#2468) * Add spec with calling `error!` helper inside the `rescue_from` block * Align the signature of Grape::DSL#error! method * Update CHANGELOG.md --- CHANGELOG.md | 1 + lib/grape/dsl/inside_route.rb | 8 +++++--- spec/grape/api_spec.rb | 12 ++++++++++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46defa267..abc785274 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ #### Fixes * [#2467](https://github.com/ruby-grape/grape/pull/2467): Fix repo coverage - [@ericproulx](https://github.com/ericproulx). +* [#2468](https://github.com/ruby-grape/grape/pull/2468): Align `error!` method signatures across different places - [@numbata](https://github.com/numbata). * Your contribution here. ### 2.1.2 (2024-06-28) diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index c73eb7c77..320b45a6d 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -163,12 +163,14 @@ def configuration # end user with the specified message. # # @param message [String] The message to display. - # @param status [Integer] the HTTP Status Code. Defaults to default_error_status, 500 if not set. + # @param status [Integer] The HTTP Status Code. Defaults to default_error_status, 500 if not set. # @param additional_headers [Hash] Addtional headers for the response. - def error!(message, status = nil, additional_headers = nil) + # @param backtrace [Array] The backtrace of the exception that caused the error. + # @param original_exception [Exception] The original exception that caused the error. + def error!(message, status = nil, additional_headers = nil, backtrace = nil, original_exception = nil) status = self.status(status || namespace_inheritable(:default_error_status)) headers = additional_headers.present? ? header.merge(additional_headers) : header - throw :error, message: message, status: status, headers: headers + throw :error, message: message, status: status, headers: headers, backtrace: backtrace, original_exception: original_exception end # Creates a Rack response based on the provided message, status, and headers. diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index dd280c9a5..ca26b812a 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -2532,6 +2532,18 @@ def self.call(message, _backtrace, _options, _env, _original_exception) get '/exception' expect(last_response.body).to eq('message: rain! @backtrace') end + + it 'returns a modified error with a custom error format' do + subject.rescue_from :all, backtrace: true do |e| + error!('raining dogs and cats', 418, {}, e.backtrace, e) + end + subject.error_formatter :txt, with: custom_error_formatter + subject.get '/exception' do + raise 'rain!' + end + get '/exception' + expect(last_response.body).to eq('message: raining dogs and cats @backtrace') + end end it 'rescues all errors and return :json' do From da9815d68e14248ba8f33d83871b251f73aa99c3 Mon Sep 17 00:00:00 2001 From: Andrei Subbota Date: Sun, 7 Jul 2024 02:21:58 +0200 Subject: [PATCH 249/304] Fixes #2347 - Correct full path building for lateral scopes (#2469) * add spec for renamed parameter in given block * Fix the full_path for the lateral scope * Update CHANGELOG.md --------- Co-authored-by: Boris Drovnin --- .rubocop.yml | 2 +- CHANGELOG.md | 1 + lib/grape/validations/params_scope.rb | 8 +++++++- spec/grape/endpoint/declared_spec.rb | 26 ++++++++++++++++++++++++++ 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 44b9d7e87..432e84f55 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -40,7 +40,7 @@ Metrics/BlockLength: - spec/**/*_spec.rb Metrics/ClassLength: - Max: 300 + Max: 305 Metrics/CyclomaticComplexity: Max: 15 diff --git a/CHANGELOG.md b/CHANGELOG.md index abc785274..634a172f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * [#2467](https://github.com/ruby-grape/grape/pull/2467): Fix repo coverage - [@ericproulx](https://github.com/ericproulx). * [#2468](https://github.com/ruby-grape/grape/pull/2468): Align `error!` method signatures across different places - [@numbata](https://github.com/numbata). +* [#2469](https://github.com/ruby-grape/grape/pull/2469): Fix full path building for lateral scopes - [@numbata](https://github.com/numbata). * Your contribution here. ### 2.1.2 (2024-06-28) diff --git a/lib/grape/validations/params_scope.rb b/lib/grape/validations/params_scope.rb index ef8d3ec1b..1d3384883 100644 --- a/lib/grape/validations/params_scope.rb +++ b/lib/grape/validations/params_scope.rb @@ -190,7 +190,13 @@ def push_declared_params(attrs, **opts) # # @return [Array] the nesting/path of the current parameter scope def full_path - nested? ? @parent.full_path + [@element] : [] + if nested? + (@parent.full_path + [@element]) + elsif lateral? + @parent.full_path + else + [] + end end private diff --git a/spec/grape/endpoint/declared_spec.rb b/spec/grape/endpoint/declared_spec.rb index 6bf5d206c..9b561d886 100644 --- a/spec/grape/endpoint/declared_spec.rb +++ b/spec/grape/endpoint/declared_spec.rb @@ -829,5 +829,31 @@ expect(JSON.parse(last_response.body)).to match({}) end end + + context 'with a renamed field inside `given` block nested in hashes' do + before do + subject.format :json + subject.params do + requires :a, type: Hash do + optional :c, type: String + given :c do + requires :b, type: Hash do + requires :input_field, as: :output_field + end + end + end + end + subject.post '/test' do + declared(params) + end + end + + it 'renames parameter input_field to output_field' do + post '/test', { a: { b: { input_field: 'value' }, c: 'value2' } } + + expect(JSON.parse(last_response.body)).to \ + match('a' => { 'b' => { 'output_field' => 'value' }, 'c' => 'value2' }) + end + end end end From dfc0e16c94d942118db3ff23f0a955f48b553bc3 Mon Sep 17 00:00:00 2001 From: Eric Date: Sat, 13 Jul 2024 15:49:40 +0200 Subject: [PATCH 250/304] Preparing for release, 2.1.3. --- CHANGELOG.md | 7 +------ README.md | 3 +-- lib/grape/version.rb | 2 +- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 634a172f9..d99411397 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,10 @@ -### 2.2.0 (Next) - -#### Features - -* Your contribution here. +### 2.1.3 (2024-07-13) #### Fixes * [#2467](https://github.com/ruby-grape/grape/pull/2467): Fix repo coverage - [@ericproulx](https://github.com/ericproulx). * [#2468](https://github.com/ruby-grape/grape/pull/2468): Align `error!` method signatures across different places - [@numbata](https://github.com/numbata). * [#2469](https://github.com/ruby-grape/grape/pull/2469): Fix full path building for lateral scopes - [@numbata](https://github.com/numbata). -* Your contribution here. ### 2.1.2 (2024-06-28) diff --git a/README.md b/README.md index 3be2dcead..7ecffc271 100644 --- a/README.md +++ b/README.md @@ -157,9 +157,8 @@ Grape is a REST-like API framework for Ruby. It's designed to run on Rack or com ## Stable Release -You're reading the documentation for the next release of Grape, which should be 2.2.0. +You're reading the documentation for the stable release of Grape, **2.1.3**. Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. -The current stable release is [2.1.2](https://github.com/ruby-grape/grape/blob/v2.1.2/README.md). ## Project Resources diff --git a/lib/grape/version.rb b/lib/grape/version.rb index 5083e38bd..80f48e112 100644 --- a/lib/grape/version.rb +++ b/lib/grape/version.rb @@ -2,5 +2,5 @@ module Grape # The current version of Grape. - VERSION = '2.2.0' + VERSION = '2.1.3' end From 47eb702895fc42675b46980d3dd1be28c40f1d44 Mon Sep 17 00:00:00 2001 From: Eric Date: Sat, 13 Jul 2024 15:53:13 +0200 Subject: [PATCH 251/304] Preparing for next development iteration, 2.2.0 --- CHANGELOG.md | 10 ++++++++++ README.md | 3 ++- lib/grape/version.rb | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d99411397..34c72c728 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +### 2.2.0 (Next) + +#### Features + +* Your contribution here. + +#### Fixes + +* Your contribution here. + ### 2.1.3 (2024-07-13) #### Fixes diff --git a/README.md b/README.md index 7ecffc271..add3905b8 100644 --- a/README.md +++ b/README.md @@ -157,8 +157,9 @@ Grape is a REST-like API framework for Ruby. It's designed to run on Rack or com ## Stable Release -You're reading the documentation for the stable release of Grape, **2.1.3**. +You're reading the documentation for the next release of Grape, which should be 2.2.0. Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. +The current stable release is [2.1.3](https://github.com/ruby-grape/grape/blob/v2.1.3/README.md). ## Project Resources diff --git a/lib/grape/version.rb b/lib/grape/version.rb index 80f48e112..5083e38bd 100644 --- a/lib/grape/version.rb +++ b/lib/grape/version.rb @@ -2,5 +2,5 @@ module Grape # The current version of Grape. - VERSION = '2.1.3' + VERSION = '2.2.0' end From 56719693012269b0132d09e5e8fd41a906e38a6d Mon Sep 17 00:00:00 2001 From: Andrei Subbota Date: Mon, 15 Jul 2024 22:06:55 +0200 Subject: [PATCH 252/304] Fix absence of original_exception and/or backtrace even if passed in error! (#2471) * Fixes #2470 Expose original_exception and/or backtrace if present * Update CHANGELOG.md --- CHANGELOG.md | 2 +- lib/grape/dsl/inside_route.rb | 7 +- lib/grape/middleware/error.rb | 10 ++- spec/grape/api_spec.rb | 144 ++++++++++++++++++++++++++++++++++ 4 files changed, 157 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34c72c728..1d95cf9ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ #### Fixes -* Your contribution here. +* [#2471](https://github.com/ruby-grape/grape/pull/2471): Fix absence of original_exception and/or backtrace even if passed in error! - [@numbata](https://github.com/numbata). ### 2.1.3 (2024-07-13) diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index 320b45a6d..ba1715de2 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -170,7 +170,12 @@ def configuration def error!(message, status = nil, additional_headers = nil, backtrace = nil, original_exception = nil) status = self.status(status || namespace_inheritable(:default_error_status)) headers = additional_headers.present? ? header.merge(additional_headers) : header - throw :error, message: message, status: status, headers: headers, backtrace: backtrace, original_exception: original_exception + throw :error, + message: message, + status: status, + headers: headers, + backtrace: backtrace, + original_exception: original_exception end # Creates a Rack response based on the provided message, status, and headers. diff --git a/lib/grape/middleware/error.rb b/lib/grape/middleware/error.rb index 2dc71c1da..573db672c 100644 --- a/lib/grape/middleware/error.rb +++ b/lib/grape/middleware/error.rb @@ -120,9 +120,9 @@ def run_rescue_handler(handler, error, endpoint) handler.arity.zero? ? endpoint.instance_exec(&handler) : endpoint.instance_exec(error, &handler) end - response = error!(response[:message], response[:status], response[:headers]) if error?(response) - - if response.is_a?(Rack::Response) + if error?(response) + error_response(response) + elsif response.is_a?(Rack::Response) response else run_rescue_handler(:default_rescue_handler, Grape::Exceptions::InvalidResponse.new, endpoint) @@ -137,7 +137,9 @@ def error!(message, status = options[:default_status], headers = {}, backtrace = end def error?(response) - response.is_a?(Hash) && response[:message] && response[:status] && response[:headers] + return false unless response.is_a?(Hash) + + response.key?(:message) && response.key?(:status) && response.key?(:headers) end end end diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index ca26b812a..3ac2fe745 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -2599,6 +2599,150 @@ def self.call(message, _backtrace, _options, _env, _original_exception) it_behaves_like 'a json format api', :failure it_behaves_like 'a json format api', { error: :failure } end + + context 'when rescue_from enables backtrace without original exception' do + let(:app) do + response_type = response_format + + Class.new(Grape::API) do + format response_type + + rescue_from :all, backtrace: true, original_exception: false do |e| + error!('raining dogs and cats!', 418, {}, e.backtrace, e) + end + + get '/exception' do + raise 'rain!' + end + end + end + + before do + get '/exception' + end + + context 'with json response type format' do + subject { JSON.parse(last_response.body) } + + let(:response_format) { :json } + + it { is_expected.to include('error' => a_kind_of(String), 'backtrace' => a_kind_of(Array)) } + it { is_expected.not_to include('original_exception') } + end + + context 'with txt response type format' do + subject { last_response.body } + + let(:response_format) { :txt } + + it { is_expected.to include('backtrace') } + it { is_expected.not_to include('original_exception') } + end + + context 'with xml response type format' do + subject { Grape::Xml.parse(last_response.body)['error'] } + + let(:response_format) { :xml } + + it { is_expected.to have_key('backtrace') } + it { is_expected.not_to have_key('original-exception') } + end + end + + context 'when rescue_from enables original exception without backtrace' do + let(:app) do + response_type = response_format + + Class.new(Grape::API) do + format response_type + + rescue_from :all, backtrace: false, original_exception: true do |e| + error!('raining dogs and cats!', 418, {}, e.backtrace, e) + end + + get '/exception' do + raise 'rain!' + end + end + end + + before do + get '/exception' + end + + context 'with json response type format' do + subject { JSON.parse(last_response.body) } + + let(:response_format) { :json } + + it { is_expected.to include('error' => a_kind_of(String), 'original_exception' => a_kind_of(String)) } + it { is_expected.not_to include('backtrace') } + end + + context 'with txt response type format' do + subject { last_response.body } + + let(:response_format) { :txt } + + it { is_expected.to include('original exception') } + it { is_expected.not_to include('backtrace') } + end + + context 'with xml response type format' do + subject { Grape::Xml.parse(last_response.body)['error'] } + + let(:response_format) { :xml } + + it { is_expected.to have_key('original-exception') } + it { is_expected.not_to have_key('backtrace') } + end + end + + context 'when rescue_from include backtrace and original exception' do + let(:app) do + response_type = response_format + + Class.new(Grape::API) do + format response_type + + rescue_from :all, backtrace: true, original_exception: true do |e| + error!('raining dogs and cats!', 418, {}, e.backtrace, e) + end + + get '/exception' do + raise 'rain!' + end + end + end + + before do + get '/exception' + end + + context 'with json response type format' do + subject { JSON.parse(last_response.body) } + + let(:response_format) { :json } + + it { is_expected.to include('error' => a_kind_of(String), 'backtrace' => a_kind_of(Array), 'original_exception' => a_kind_of(String)) } + end + + context 'with txt response type format' do + subject { last_response.body } + + let(:response_format) { :txt } + + it { is_expected.to include('backtrace', 'original exception') } + end + + context 'with xml response type format' do + subject { Grape::Xml.parse(last_response.body)['error'] } + + let(:response_format) { :xml } + + it { is_expected.to have_key('backtrace') & have_key('original-exception') } + end + end end describe '.content_type' do From fb67ea9940ea47d353cc9b413a7e25fc8da0983e Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Mon, 22 Jul 2024 22:10:10 +0200 Subject: [PATCH 253/304] Remove Grape::Util::Registrable + Small refactors (#2475) * Refactor ErrorFormatter, Parser and Formatter Remove Grape::Util::Registrable Add Grape::MimeTypes + spec Small refactors * Add CHANGELOG.md --- CHANGELOG.md | 2 + lib/grape/content_types.rb | 21 ++++--- lib/grape/dsl/request_response.rb | 32 +++++----- lib/grape/error_formatter.rb | 38 +++++------- lib/grape/formatter.rb | 40 +++++-------- lib/grape/middleware/base.rb | 22 +++---- lib/grape/middleware/error.rb | 2 +- lib/grape/middleware/formatter.rb | 4 +- lib/grape/parser.rb | 32 +++------- lib/grape/util/accept_header_handler.rb | 2 +- lib/grape/util/registrable.rb | 15 ----- spec/grape/content_types_spec.rb | 78 +++++++++++++++++++++++++ spec/grape/middleware/formatter_spec.rb | 40 ++++--------- spec/grape/parser_spec.rb | 69 +++++----------------- 14 files changed, 183 insertions(+), 214 deletions(-) delete mode 100644 lib/grape/util/registrable.rb create mode 100644 spec/grape/content_types_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d95cf9ee..54284527e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,13 @@ #### Features +* [#2475](https://github.com/ruby-grape/grape/pull/2475): Remove Grape::Util::Registrable - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes * [#2471](https://github.com/ruby-grape/grape/pull/2471): Fix absence of original_exception and/or backtrace even if passed in error! - [@numbata](https://github.com/numbata). +* Your contribution here. ### 2.1.3 (2024-07-13) diff --git a/lib/grape/content_types.rb b/lib/grape/content_types.rb index cc0cc2cab..336948b9b 100644 --- a/lib/grape/content_types.rb +++ b/lib/grape/content_types.rb @@ -2,10 +2,10 @@ module Grape module ContentTypes - extend Util::Registrable + module_function # Content types are listed in order of preference. - CONTENT_TYPES = { + DEFAULTS = { xml: 'application/xml', serializable_hash: 'application/json', json: 'application/json', @@ -13,13 +13,18 @@ module ContentTypes txt: 'text/plain' }.freeze - class << self - def content_types_for_settings(settings) - settings&.inject(:merge!) - end + MIME_TYPES = Grape::ContentTypes::DEFAULTS.except(:serializable_hash).invert.freeze + + def content_types_for(from_settings) + from_settings.presence || DEFAULTS + end + + def mime_types_for(from_settings) + return MIME_TYPES if from_settings == Grape::ContentTypes::DEFAULTS - def content_types_for(from_settings) - from_settings.presence || Grape::ContentTypes::CONTENT_TYPES.merge(default_elements) + from_settings.each_with_object({}) do |(k, v), types_without_params| + # remove optional parameter + types_without_params[v.split(';', 2).first] = k end end end diff --git a/lib/grape/dsl/request_response.rb b/lib/grape/dsl/request_response.rb index 7e23c9956..7f0a0d27d 100644 --- a/lib/grape/dsl/request_response.rb +++ b/lib/grape/dsl/request_response.rb @@ -17,18 +17,16 @@ def default_format(new_format = nil) # Specify the format for the API's serializers. # May be `:json`, `:xml`, `:txt`, etc. 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, **{})) - # define a single mime type - mime_type = content_types[new_format.to_sym] - raise Grape::Exceptions::MissingMimeType.new(new_format) unless mime_type - - namespace_stackable(:content_types, new_format.to_sym => mime_type) - else - namespace_inheritable(:format) - end + return namespace_inheritable(:format) unless new_format + + symbolic_new_format = new_format.to_sym + namespace_inheritable(:format, symbolic_new_format) + namespace_inheritable(:default_error_formatter, Grape::ErrorFormatter.formatter_for(symbolic_new_format)) + + content_type = content_types[symbolic_new_format] + raise Grape::Exceptions::MissingMimeType.new(new_format) unless content_type + + namespace_stackable(:content_types, symbolic_new_format => content_type) end # Specify a custom formatter for a content-type. @@ -43,12 +41,10 @@ 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, **{}) - namespace_inheritable(:default_error_formatter, new_formatter) - else - namespace_inheritable(:default_error_formatter) - end + return namespace_inheritable(:default_error_formatter) unless new_formatter_name + + new_formatter = Grape::ErrorFormatter.formatter_for(new_formatter_name) + namespace_inheritable(:default_error_formatter, new_formatter) end def error_formatter(format, options) diff --git a/lib/grape/error_formatter.rb b/lib/grape/error_formatter.rb index 4d76fe296..7784055b6 100644 --- a/lib/grape/error_formatter.rb +++ b/lib/grape/error_formatter.rb @@ -2,34 +2,22 @@ module Grape module ErrorFormatter - extend Util::Registrable + module_function - class << self - def builtin_formatters - @builtin_formatters ||= { - serializable_hash: Grape::ErrorFormatter::Json, - json: Grape::ErrorFormatter::Json, - jsonapi: Grape::ErrorFormatter::Json, - txt: Grape::ErrorFormatter::Txt, - xml: Grape::ErrorFormatter::Xml - } - end + DEFAULTS = { + serializable_hash: Grape::ErrorFormatter::Json, + json: Grape::ErrorFormatter::Json, + jsonapi: Grape::ErrorFormatter::Json, + txt: Grape::ErrorFormatter::Txt, + xml: Grape::ErrorFormatter::Xml + }.freeze - def formatters(**options) - builtin_formatters.merge(default_elements).merge!(options[:error_formatters] || {}) - end + def formatter_for(format, error_formatters = nil, default_error_formatter = nil) + select_formatter(error_formatters, format) || default_error_formatter || DEFAULTS[:txt] + end - def formatter_for(api_format, **options) - spec = formatters(**options)[api_format] - case spec - when nil - options[:default_error_formatter] || Grape::ErrorFormatter::Txt - when Symbol - method(spec) - else - spec - end - end + def select_formatter(error_formatters, format) + error_formatters&.key?(format) ? error_formatters[format] : DEFAULTS[format] end end end diff --git a/lib/grape/formatter.rb b/lib/grape/formatter.rb index 4a84f0e2b..d586b0bd6 100644 --- a/lib/grape/formatter.rb +++ b/lib/grape/formatter.rb @@ -2,34 +2,24 @@ module Grape module Formatter - extend Util::Registrable + module_function - class << self - def builtin_formatters - @builtin_formatters ||= { - json: Grape::Formatter::Json, - jsonapi: Grape::Formatter::Json, - serializable_hash: Grape::Formatter::SerializableHash, - txt: Grape::Formatter::Txt, - xml: Grape::Formatter::Xml - } - end + DEFAULTS = { + json: Grape::Formatter::Json, + jsonapi: Grape::Formatter::Json, + serializable_hash: Grape::Formatter::SerializableHash, + txt: Grape::Formatter::Txt, + xml: Grape::Formatter::Xml + }.freeze - def formatters(**options) - builtin_formatters.merge(default_elements).merge!(options[:formatters] || {}) - end + DEFAULT_LAMBDA_FORMATTER = ->(obj, _env) { obj } - def formatter_for(api_format, **options) - spec = formatters(**options)[api_format] - case spec - when nil - ->(obj, _env) { obj } - when Symbol - method(spec) - else - spec - end - end + def formatter_for(api_format, formatters) + select_formatter(formatters, api_format) || DEFAULT_LAMBDA_FORMATTER + end + + def select_formatter(formatters, api_format) + formatters&.key?(api_format) ? formatters[api_format] : DEFAULTS[api_format] end end end diff --git a/lib/grape/middleware/base.rb b/lib/grape/middleware/base.rb index 2ee9cbeae..6a54aa6d7 100644 --- a/lib/grape/middleware/base.rb +++ b/lib/grape/middleware/base.rb @@ -61,22 +61,20 @@ def response @app_response = Rack::Response.new(@app_response[2], @app_response[0], @app_response[1]) end - def content_type_for(format) - HashWithIndifferentAccess.new(content_types)[format] + def content_types + @content_types ||= Grape::ContentTypes.content_types_for(options[:content_types]) end - def content_types - ContentTypes.content_types_for(options[:content_types]) + def mime_types + @mime_types ||= Grape::ContentTypes.mime_types_for(content_types) end - def content_type - content_type_for(env[Grape::Env::API_FORMAT] || options[:format]) || TEXT_HTML + def content_type_for(format) + content_types_indifferent_access[format] end - def mime_types - @mime_types ||= content_types.each_pair.with_object({}) do |(k, v), types_without_params| - types_without_params[v.split(';').first] = k - end + def content_type + content_type_for(env[Grape::Env::API_FORMAT] || options[:format]) || TEXT_HTML end private @@ -89,6 +87,10 @@ def merge_headers(response) when Array then response[1].merge!(headers) end end + + def content_types_indifferent_access + @content_types_indifferent_access ||= content_types.with_indifferent_access + end end end end diff --git a/lib/grape/middleware/error.rb b/lib/grape/middleware/error.rb index 573db672c..b798a1921 100644 --- a/lib/grape/middleware/error.rb +++ b/lib/grape/middleware/error.rb @@ -45,7 +45,7 @@ def rack_response(status, headers, message) 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[:error_formatters], options[:default_error_formatter]) return formatter.call(message, backtrace, options, env, original_exception) if formatter throw :error, diff --git a/lib/grape/middleware/formatter.rb b/lib/grape/middleware/formatter.rb index 35a315001..4de1af02e 100644 --- a/lib/grape/middleware/formatter.rb +++ b/lib/grape/middleware/formatter.rb @@ -54,7 +54,7 @@ def build_formatted_response(status, headers, bodies) def fetch_formatter(headers, options) api_format = mime_types[headers[Rack::CONTENT_TYPE]] || env[Grape::Env::API_FORMAT] - Grape::Formatter.formatter_for(api_format, **options) + Grape::Formatter.formatter_for(api_format, options[:formatters]) end # Set the content type header for the API format if it is not already present. @@ -97,7 +97,7 @@ def read_rack_input(body) fmt = request.media_type ? mime_types[request.media_type] : options[:default_format] throw :error, status: 415, message: "The provided content-type '#{request.media_type}' is not supported." unless content_type_for(fmt) - parser = Grape::Parser.parser_for fmt, **options + parser = Grape::Parser.parser_for fmt, options[:parsers] if parser begin body = (env[Grape::Env::API_REQUEST_BODY] = parser.call(body, env)) diff --git a/lib/grape/parser.rb b/lib/grape/parser.rb index 3676a45a7..a446b4da2 100644 --- a/lib/grape/parser.rb +++ b/lib/grape/parser.rb @@ -2,32 +2,16 @@ module Grape module Parser - extend Util::Registrable + module_function - class << self - def builtin_parsers - @builtin_parsers ||= { - json: Grape::Parser::Json, - jsonapi: Grape::Parser::Json, - xml: Grape::Parser::Xml - } - end + DEFAULTS = { + json: Grape::Parser::Json, + jsonapi: Grape::Parser::Json, + xml: Grape::Parser::Xml + }.freeze - def parsers(**options) - builtin_parsers.merge(default_elements).merge!(options[:parsers] || {}) - end - - def parser_for(api_format, **options) - spec = parsers(**options)[api_format] - case spec - when nil - nil - when Symbol - method(spec) - else - spec - end - end + def parser_for(format, parsers = nil) + parsers&.key?(format) ? parsers[format] : DEFAULTS[format] end end end diff --git a/lib/grape/util/accept_header_handler.rb b/lib/grape/util/accept_header_handler.rb index c7fc7bee9..3098f3a82 100644 --- a/lib/grape/util/accept_header_handler.rb +++ b/lib/grape/util/accept_header_handler.rb @@ -13,7 +13,7 @@ def initialize(accept_header:, versions:, **options) @cascade = options.fetch(:cascade, true) end - def match_best_quality_media_type!(content_types: Grape::ContentTypes::CONTENT_TYPES, allowed_methods: nil) + def match_best_quality_media_type!(content_types: Grape::ContentTypes::DEFAULTS, allowed_methods: nil) return unless vendor strict_header_checks! diff --git a/lib/grape/util/registrable.rb b/lib/grape/util/registrable.rb deleted file mode 100644 index e154456f8..000000000 --- a/lib/grape/util/registrable.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -module Grape - module Util - module Registrable - def default_elements - @default_elements ||= {} - end - - def register(format, element) - default_elements[format] = element unless default_elements[format] - end - end - end -end diff --git a/spec/grape/content_types_spec.rb b/spec/grape/content_types_spec.rb new file mode 100644 index 000000000..19040bbeb --- /dev/null +++ b/spec/grape/content_types_spec.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +describe Grape::ContentTypes do + describe 'DEFAULTS' do + subject { described_class::DEFAULTS } + + let(:expected_value) do + { + xml: 'application/xml', + serializable_hash: 'application/json', + json: 'application/json', + binary: 'application/octet-stream', + txt: 'text/plain' + }.freeze + end + + it { is_expected.to eq(expected_value) } + end + + describe 'MIME_TYPES' do + subject { described_class::MIME_TYPES } + + let(:expected_value) do + { + 'application/xml' => :xml, + 'application/json' => :json, + 'application/octet-stream' => :binary, + 'text/plain' => :txt + }.freeze + end + + it { is_expected.to eq(expected_value) } + end + + describe '.content_types_for' do + subject { described_class.content_types_for(from_settings) } + + context 'when from_settings is present' do + let(:from_settings) { { a: :b } } + + it { is_expected.to eq(from_settings) } + end + + context 'when from_settings is not present' do + let(:from_settings) { nil } + + it { is_expected.to be(described_class::DEFAULTS) } + end + end + + describe '.mime_types_for' do + subject { described_class.mime_types_for(from_settings) } + + context 'when from_settings is equal to Grape::ContentTypes::DEFAULTS' do + let(:from_settings) do + { + xml: 'application/xml', + serializable_hash: 'application/json', + json: 'application/json', + binary: 'application/octet-stream', + txt: 'text/plain' + }.freeze + end + + it { is_expected.to be(described_class::MIME_TYPES) } + end + + context 'when from_settings is not equal to Grape::ContentTypes::DEFAULTS' do + let(:from_settings) do + { + xml: 'application/xml;charset=utf-8' + } + end + + it { is_expected.to eq('application/xml' => :xml) } + end + end +end diff --git a/spec/grape/middleware/formatter_spec.rb b/spec/grape/middleware/formatter_spec.rb index 9b7dc9b56..28ec6c9aa 100644 --- a/spec/grape/middleware/formatter_spec.rb +++ b/spec/grape/middleware/formatter_spec.rb @@ -192,11 +192,7 @@ def to_xml end context 'with custom vendored content types' do - before do - subject.options[:content_types] = {}.tap do |ct| - ct[:custom] = 'application/vnd.test+json' - end - end + subject { described_class.new(app, content_types: { custom: 'application/vnd.test+json' }) } it 'uses the custom type' do subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.test+json') @@ -227,18 +223,14 @@ def to_xml end it 'is set for custom' do - subject.options[:content_types] = {}.tap do |ct| - ct[:custom] = 'application/x-custom' - end - _, headers, = subject.call(Rack::PATH_INFO => '/info.custom') + s = described_class.new(app, content_types: { custom: 'application/x-custom' }) + _, headers, = s.call(Rack::PATH_INFO => '/info.custom') expect(headers[Rack::CONTENT_TYPE]).to eq('application/x-custom') end it 'is set for vendored with registered type' do - subject.options[:content_types] = {}.tap do |ct| - ct[:custom] = 'application/vnd.test+json' - end - _, headers, = subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.test+json') + s = described_class.new(app, content_types: { custom: 'application/vnd.test+json' }) + _, headers, = s.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.test+json') expect(headers[Rack::CONTENT_TYPE]).to eq('application/vnd.test+json') end @@ -250,10 +242,8 @@ def to_xml context 'format' do it 'uses custom formatter' do - subject.options[:content_types] = {} - subject.options[:content_types][:custom] = "don't care" - subject.options[:formatters][:custom] = ->(_obj, _env) { 'CUSTOM FORMAT' } - r = Rack::MockResponse[*subject.call(Rack::PATH_INFO => '/info.custom')] + s = described_class.new(app, content_types: { custom: "don't care" }, formatters: { custom: ->(_obj, _env) { 'CUSTOM FORMAT' } }) + r = Rack::MockResponse[*s.call(Rack::PATH_INFO => '/info.custom')] expect(r.body).to eq('CUSTOM FORMAT') end @@ -471,6 +461,8 @@ def to_xml end context 'inheritable formatters' do + subject { described_class.new(app, formatters: { invalid: invalid_formatter }, content_types: { invalid: 'application/x-invalid' }) } + let(:invalid_formatter) do Class.new do def self.call(_, _) @@ -481,22 +473,12 @@ def self.call(_, _) let(:app) { ->(_env) { [200, {}, ['']] } } let(:env) do - { Rack::PATH_INFO => '/hello.invalid', Grape::Http::Headers::HTTP_ACCEPT => 'application/x-invalid' } - end - - before do - Grape::Formatter.register :invalid, invalid_formatter - Grape::ContentTypes.register :invalid, 'application/x-invalid' - end - - after do - Grape::ContentTypes.default_elements.delete(:invalid) - Grape::Formatter.default_elements.delete(:invalid) + Rack::MockRequest.env_for('/hello.invalid', Grape::Http::Headers::HTTP_ACCEPT => 'application/x-invalid') end it 'returns response by invalid formatter' do r = Rack::MockResponse[*subject.call(env)] - expect(r.body).to eq(Grape::Json.dump('message' => 'invalid')) + expect(JSON.parse(r.body)).to eq('message' => 'invalid') end end diff --git a/spec/grape/parser_spec.rb b/spec/grape/parser_spec.rb index a3b43856f..ecc5fdfa4 100644 --- a/spec/grape/parser_spec.rb +++ b/spec/grape/parser_spec.rb @@ -3,77 +3,34 @@ describe Grape::Parser do subject { described_class } - describe '.builtin_parsers' do - it 'returns an instance of Hash' do - expect(subject.builtin_parsers).to be_an_instance_of(Hash) - end - - it 'includes json and xml parsers by default' do - expect(subject.builtin_parsers).to include(json: Grape::Parser::Json, xml: Grape::Parser::Xml) - end - end - - describe '.parsers' do - it 'returns an instance of Hash' do - expect(subject.parsers(**{})).to be_an_instance_of(Hash) - end + describe 'DEFAULTS' do + subject { described_class::DEFAULTS } - it 'includes built-in parsers' do - expect(subject.parsers(**{})).to include(subject.builtin_parsers) + let(:expected_defaults) do + { + json: Grape::Parser::Json, + jsonapi: Grape::Parser::Json, + xml: Grape::Parser::Xml + } end - context 'with :parsers option' do - let(:parsers) { { customized: Class.new } } - - it 'includes passed :parsers values' do - expect(subject.parsers(parsers: parsers)).to include(parsers) - end - end - - context 'with added parser by using `register` keyword' do - let(:added_parser) { Class.new } - - before { subject.register :added, added_parser } - - it 'includes added parser' do - expect(subject.parsers(**{})).to include(added: added_parser) - end - end + it { is_expected.to eq(expected_defaults) } end describe '.parser_for' do let(:options) { {} } - it 'calls .parsers' do - expect(subject).to receive(:parsers).with(any_args).and_return(subject.builtin_parsers) - subject.parser_for(:json, **options) - end - it 'returns parser correctly' do expect(subject.parser_for(:json)).to eq(Grape::Parser::Json) end context 'when parser is available' do - before { subject.register :customized_json, Grape::Parser::Json } - - it 'returns registered parser if available' do - expect(subject.parser_for(:customized_json)).to eq(Grape::Parser::Json) - end - end - - context 'when parser is an instance of Symbol' do - before do - allow(subject).to receive(:foo).and_return(:bar) - subject.register :foo, :foo + let(:parsers) do + { customized_json: Grape::Parser::Json } end - it 'returns an instance of Method' do - expect(subject.parser_for(:foo)).to be_an_instance_of(Method) - end - - it 'returns object which can be called' do - method = subject.parser_for(:foo) - expect(method.call).to eq(:bar) + it 'returns registered parser if available' do + expect(subject.parser_for(:customized_json, parsers)).to eq(Grape::Parser::Json) end end From 838c75e7a302ea6ca48b681ac08f665cc4e07891 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Tue, 23 Jul 2024 22:53:09 +0200 Subject: [PATCH 254/304] Fix rescue_from with invalid response (#2478) * Remove expect_any_instance_of(...) Wrap `default_rescue_handler` with `method` * Add CHANGELOG entry * Fix RSpec/AnyInstance Regenerate Rubocop's todo * Update spec wording * Update spec/grape/api_spec.rb Co-authored-by: Manuel Jacob --------- Co-authored-by: Manuel Jacob --- .rubocop_todo.yml | 14 +++----------- CHANGELOG.md | 1 + lib/grape/middleware/error.rb | 2 +- spec/grape/api_spec.rb | 16 ++++++++-------- spec/grape/middleware/base_spec.rb | 2 +- 5 files changed, 14 insertions(+), 21 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index f4482aff4..10cc2c7ed 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2024-06-22 17:26:10 UTC using RuboCop version 1.64.1. +# on 2024-07-23 11:24:53 UTC using RuboCop version 1.64.1. # 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 @@ -37,12 +37,6 @@ Naming/VariableNumber: - 'spec/grape/exceptions/validation_errors_spec.rb' - 'spec/grape/validations_spec.rb' -# Offense count: 2 -RSpec/AnyInstance: - Exclude: - - 'spec/grape/api_spec.rb' - - 'spec/grape/middleware/base_spec.rb' - # Offense count: 1 # Configuration parameters: IgnoredMetadata. RSpec/DescribeClass: @@ -140,15 +134,14 @@ RSpec/RepeatedExampleGroupDescription: - 'spec/grape/util/inheritable_setting_spec.rb' - 'spec/grape/validations/validators/values_spec.rb' -# Offense count: 5 +# Offense count: 4 RSpec/StubbedMock: Exclude: - 'spec/grape/dsl/inside_route_spec.rb' - 'spec/grape/dsl/routing_spec.rb' - 'spec/grape/middleware/formatter_spec.rb' - - 'spec/grape/parser_spec.rb' -# Offense count: 122 +# Offense count: 121 RSpec/SubjectStub: Exclude: - 'spec/grape/api_spec.rb' @@ -164,7 +157,6 @@ RSpec/SubjectStub: - 'spec/grape/middleware/formatter_spec.rb' - 'spec/grape/middleware/globals_spec.rb' - 'spec/grape/middleware/stack_spec.rb' - - 'spec/grape/parser_spec.rb' # Offense count: 23 # Configuration parameters: IgnoreNameless, IgnoreSymbolicNames. diff --git a/CHANGELOG.md b/CHANGELOG.md index 54284527e..f6d681243 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ #### Fixes * [#2471](https://github.com/ruby-grape/grape/pull/2471): Fix absence of original_exception and/or backtrace even if passed in error! - [@numbata](https://github.com/numbata). +* [#2478](https://github.com/ruby-grape/grape/pull/2478): Fix rescue_from with invalid response - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 2.1.3 (2024-07-13) diff --git a/lib/grape/middleware/error.rb b/lib/grape/middleware/error.rb index b798a1921..6e5304607 100644 --- a/lib/grape/middleware/error.rb +++ b/lib/grape/middleware/error.rb @@ -125,7 +125,7 @@ def run_rescue_handler(handler, error, endpoint) elsif response.is_a?(Rack::Response) response else - run_rescue_handler(:default_rescue_handler, Grape::Exceptions::InvalidResponse.new, endpoint) + run_rescue_handler(method(:default_rescue_handler), Grape::Exceptions::InvalidResponse.new, endpoint) end end diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index 3ac2fe745..d12f29d76 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -2193,14 +2193,14 @@ def foo 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).to be_server_error - expect(last_response.body).to eql 'Invalid response' + context 'when rescue_from block returns an invalid response' do + it 'returns a formatted response' do + subject.rescue_from(:all) { 'error' } + subject.get('/') { raise } + get '/' + expect(last_response).to be_server_error + expect(last_response.body).to eql 'Invalid response' + end end end diff --git a/spec/grape/middleware/base_spec.rb b/spec/grape/middleware/base_spec.rb index 608c5012e..a3acc39d5 100644 --- a/spec/grape/middleware/base_spec.rb +++ b/spec/grape/middleware/base_spec.rb @@ -57,7 +57,7 @@ context 'with patched warnings' do before do @warnings = warnings = [] - allow_any_instance_of(described_class).to receive(:warn) { |m| warnings << m } + allow(subject).to receive(:warn) { |m| warnings << m } allow(subject).to receive(:after).and_raise(StandardError) end From 2b8567a1ca49b3d0236fa82deb2edef34c8839cf Mon Sep 17 00:00:00 2001 From: Andrei Subbota Date: Wed, 24 Jul 2024 05:37:15 +0200 Subject: [PATCH 255/304] Fix rescue_from ValidationErrors exception (#2480) * Fix rescue_from ValidationErrors exception * Update CHANGELOG --- CHANGELOG.md | 1 + lib/grape/error_formatter/json.rb | 17 ++++++++--- spec/grape/api_spec.rb | 51 +++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6d681243..1f22f7228 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * [#2471](https://github.com/ruby-grape/grape/pull/2471): Fix absence of original_exception and/or backtrace even if passed in error! - [@numbata](https://github.com/numbata). * [#2478](https://github.com/ruby-grape/grape/pull/2478): Fix rescue_from with invalid response - [@ericproulx](https://github.com/ericproulx). +* [#2480](https://github.com/ruby-grape/grape/pull/2480): Fix rescue_from ValidationErrors exception - [@numbata](https://github.com/numbata). * Your contribution here. ### 2.1.3 (2024-07-13) diff --git a/lib/grape/error_formatter/json.rb b/lib/grape/error_formatter/json.rb index 535919234..f4df46e84 100644 --- a/lib/grape/error_formatter/json.rb +++ b/lib/grape/error_formatter/json.rb @@ -9,17 +9,18 @@ class << self def call(message, backtrace, options = {}, env = nil, original_exception = nil) result = wrap_message(present(message, env)) - rescue_options = options[:rescue_options] || {} - result = result.merge(backtrace: backtrace) if rescue_options[:backtrace] && backtrace && !backtrace.empty? - result = result.merge(original_exception: original_exception.inspect) if rescue_options[:original_exception] && original_exception + result = merge_rescue_options(result, backtrace, options, original_exception) if result.is_a?(Hash) + ::Grape::Json.dump(result) end private def wrap_message(message) - if message.is_a?(Exceptions::ValidationErrors) || message.is_a?(Hash) + if message.is_a?(Hash) message + elsif message.is_a?(Exceptions::ValidationErrors) + message.as_json else { error: ensure_utf8(message) } end @@ -30,6 +31,14 @@ def ensure_utf8(message) message.encode('UTF-8', invalid: :replace, undef: :replace) end + + def merge_rescue_options(result, backtrace, options, original_exception) + rescue_options = options[:rescue_options] || {} + result = result.merge(backtrace: backtrace) if rescue_options[:backtrace] && backtrace && !backtrace.empty? + result = result.merge(original_exception: original_exception.inspect) if rescue_options[:original_exception] && original_exception + + result + end end end end diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index d12f29d76..14e2c9257 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -2743,6 +2743,57 @@ def self.call(message, _backtrace, _options, _env, _original_exception) it { is_expected.to have_key('backtrace') & have_key('original-exception') } end end + + context 'when rescue validation errors include backtrace and original exception' do + let(:app) do + response_type = response_format + + Class.new(Grape::API) do + format response_type + + rescue_from Grape::Exceptions::ValidationErrors, backtrace: true, original_exception: true do |e| + error!(e, 418, {}, e.backtrace, e) + end + + params do + requires :weather + end + get '/forecast' do + 'sunny' + end + end + end + + before do + get '/forecast' + end + + context 'with json response type format' do + subject { JSON.parse(last_response.body) } + + let(:response_format) { :json } + + it 'does not include backtrace or original exception' do + expect(subject).to match([{ 'messages' => ['is missing'], 'params' => ['weather'] }]) + end + end + + context 'with txt response type format' do + subject { last_response.body } + + let(:response_format) { :txt } + + it { is_expected.to include('backtrace', 'original exception') } + end + + context 'with xml response type format' do + subject { Grape::Xml.parse(last_response.body)['error'] } + + let(:response_format) { :xml } + + it { is_expected.to have_key('backtrace') & have_key('original-exception') } + end + end end describe '.content_type' do From 229248601f2e1dde205b1671ccc29d154183c16f Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sat, 27 Jul 2024 23:39:09 +0300 Subject: [PATCH 256/304] Revisit versioner middlewares (#2484) * AcceptHeaderHandler is now part of Grape::Middleware::Versioner::Header Use `const_get` to find versioner Grape::Middleware::Versioner::* uses `default_options` like other middlewares Add versioner_helpers for Grape::Middleware::Versioner::* Replace `merge` by `deep_merge` in Grape::Middleware::Base initialize Add specs * Add CHANGELOG entry * Remove prefix throw_ and add! Use `camelize` instead of `classify` --- CHANGELOG.md | 1 + lib/grape/endpoint.rb | 7 +- lib/grape/middleware/base.rb | 5 +- lib/grape/middleware/versioner.rb | 19 +-- .../versioner/accept_version_header.rb | 39 ++---- lib/grape/middleware/versioner/header.rb | 105 ++++++++++++++-- lib/grape/middleware/versioner/param.rb | 26 +--- lib/grape/middleware/versioner/path.rb | 42 ++----- lib/grape/middleware/versioner_helpers.rb | 75 ++++++++++++ lib/grape/util/accept_header_handler.rb | 105 ---------------- spec/grape/middleware/versioner_spec.rb | 34 ++++-- spec/grape/util/accept_header_handler_spec.rb | 112 ------------------ 12 files changed, 230 insertions(+), 340 deletions(-) create mode 100644 lib/grape/middleware/versioner_helpers.rb delete mode 100644 lib/grape/util/accept_header_handler.rb delete mode 100644 spec/grape/util/accept_header_handler_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f22f7228..22ea580d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ #### Features * [#2475](https://github.com/ruby-grape/grape/pull/2475): Remove Grape::Util::Registrable - [@ericproulx](https://github.com/ericproulx). +* [#2484](https://github.com/ruby-grape/grape/pull/2484): Refactor versioner middlewares - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/lib/grape/endpoint.rb b/lib/grape/endpoint.rb index cc1fdba83..88605d13b 100644 --- a/lib/grape/endpoint.rb +++ b/lib/grape/endpoint.rb @@ -191,8 +191,7 @@ def prepare_default_route_attributes def prepare_version version = namespace_inheritable(:version) - return unless version - return if version.empty? + return if version.blank? version.length == 1 ? version.first : version end @@ -298,9 +297,9 @@ def build_stack(helpers) stack.concat namespace_stackable(:middleware) - if namespace_inheritable(:version) + if namespace_inheritable(:version).present? stack.use Grape::Middleware::Versioner.using(namespace_inheritable(:version_options)[:using]), - versions: namespace_inheritable(:version)&.flatten, + versions: namespace_inheritable(:version).flatten, version_options: namespace_inheritable(:version_options), prefix: namespace_inheritable(:root_prefix), mount_path: namespace_stackable(:mount_path).first diff --git a/lib/grape/middleware/base.rb b/lib/grape/middleware/base.rb index 6a54aa6d7..af7195f86 100644 --- a/lib/grape/middleware/base.rb +++ b/lib/grape/middleware/base.rb @@ -4,18 +4,17 @@ module Grape module Middleware class Base include Helpers + include Grape::DSL::Headers attr_reader :app, :env, :options 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) @app = app - @options = options.any? ? default_options.merge(options.shift) : default_options + @options = options.any? ? default_options.deep_merge(options.shift) : default_options @app_response = nil end diff --git a/lib/grape/middleware/versioner.rb b/lib/grape/middleware/versioner.rb index d40c87a27..1589f9b76 100644 --- a/lib/grape/middleware/versioner.rb +++ b/lib/grape/middleware/versioner.rb @@ -4,30 +4,21 @@ # on the requests. The current methods for determining version are: # # :header - version from HTTP Accept header. +# :accept_version_header - version from HTTP Accept-Version header # :path - version from uri. e.g. /v1/resource # :param - version from uri query string, e.g. /v1/resource?apiver=v1 -# # See individual classes for details. module Grape module Middleware module Versioner module_function - # @param strategy [Symbol] :path, :header or :param + # @param strategy [Symbol] :path, :header, :accept_version_header or :param # @return a middleware class based on strategy def using(strategy) - case strategy - when :path - Path - when :header - Header - when :param - Param - when :accept_version_header - AcceptVersionHeader - else - raise Grape::Exceptions::InvalidVersionerOption.new(strategy) - end + Grape::Middleware::Versioner.const_get(:"#{strategy.to_s.camelize}") + rescue NameError + raise Grape::Exceptions::InvalidVersionerOption, strategy end end end diff --git a/lib/grape/middleware/versioner/accept_version_header.rb b/lib/grape/middleware/versioner/accept_version_header.rb index 98237e947..1cf2bb674 100644 --- a/lib/grape/middleware/versioner/accept_version_header.rb +++ b/lib/grape/middleware/versioner/accept_version_header.rb @@ -17,45 +17,22 @@ module Versioner # X-Cascade header to alert Grape::Router to attempt the next matched # route. class AcceptVersionHeader < Base - def before - potential_version = (env[Grape::Http::Headers::HTTP_ACCEPT_VERSION] || '').strip - - if strict? && potential_version.empty? - # If no Accept-Version header: - throw :error, status: 406, headers: error_headers, message: 'Accept-Version header must be set.' - end + include VersionerHelpers - return if potential_version.empty? + def before + potential_version = env[Grape::Http::Headers::HTTP_ACCEPT_VERSION]&.strip + not_acceptable!('Accept-Version header must be set.') if strict? && potential_version.blank? - # If the requested version is not supported: - throw :error, status: 406, headers: error_headers, message: 'The requested version is not supported.' unless versions.any? { |v| v.to_s == potential_version } + return if potential_version.blank? + not_acceptable!('The requested version is not supported.') unless potential_version_match?(potential_version) env[Grape::Env::API_VERSION] = potential_version end private - def versions - options[:versions] || [] - end - - def strict? - options[:version_options] && options[:version_options][:strict] - end - - # By default those errors contain an `X-Cascade` header set to `pass`, which allows nesting and stacking - # of routes (see Grape::Router) for more information). To prevent - # this behavior, and not add the `X-Cascade` header, one can set the `:cascade` option to `false`. - def cascade? - if options[:version_options]&.key?(:cascade) - options[:version_options][:cascade] - else - true - end - end - - def error_headers - cascade? ? { Grape::Http::Headers::X_CASCADE => 'pass' } : {} + def not_acceptable!(message) + throw :error, status: 406, headers: error_headers, message: message end end end diff --git a/lib/grape/middleware/versioner/header.rb b/lib/grape/middleware/versioner/header.rb index 33ea25660..619549304 100644 --- a/lib/grape/middleware/versioner/header.rb +++ b/lib/grape/middleware/versioner/header.rb @@ -22,17 +22,10 @@ module Versioner # X-Cascade header to alert Grape::Router to attempt the next matched # route. class Header < Base + include VersionerHelpers + def before - handler = Grape::Util::AcceptHeaderHandler.new( - accept_header: env[Grape::Http::Headers::HTTP_ACCEPT], - versions: options[:versions], - **options.fetch(:version_options) { {} } - ) - - handler.match_best_quality_media_type!( - content_types: content_types, - allowed_methods: env[Grape::Env::GRAPE_ALLOWED_METHODS] - ) do |media_type| + match_best_quality_media_type! do |media_type| env.update( Grape::Env::API_TYPE => media_type.type, Grape::Env::API_SUBTYPE => media_type.subtype, @@ -42,6 +35,98 @@ def before ) end end + + private + + def match_best_quality_media_type! + return unless vendor + + strict_header_checks! + media_type = Grape::Util::MediaType.best_quality(accept_header, available_media_types) + if media_type + yield media_type + else + fail!(allowed_methods) + end + end + + def allowed_methods + env[Grape::Env::GRAPE_ALLOWED_METHODS] + end + + def accept_header + env[Grape::Http::Headers::HTTP_ACCEPT] + end + + def strict_header_checks! + return unless strict? + + accept_header_check! + version_and_vendor_check! + end + + def accept_header_check! + return if accept_header.present? + + invalid_accept_header!('Accept header must be set.') + end + + def version_and_vendor_check! + return if versions.blank? || version_and_vendor? + + invalid_accept_header!('API vendor or version not found.') + end + + def q_values_mime_types + @q_values_mime_types ||= Rack::Utils.q_values(accept_header).map(&:first) + end + + def version_and_vendor? + q_values_mime_types.any? { |mime_type| Grape::Util::MediaType.match?(mime_type) } + end + + def invalid_accept_header!(message) + raise Grape::Exceptions::InvalidAcceptHeader.new(message, error_headers) + end + + def invalid_version_header!(message) + raise Grape::Exceptions::InvalidVersionHeader.new(message, error_headers) + end + + def fail!(grape_allowed_methods) + return grape_allowed_methods if grape_allowed_methods.present? + + media_types = q_values_mime_types.map { |mime_type| Grape::Util::MediaType.parse(mime_type) } + vendor_not_found!(media_types) || version_not_found!(media_types) + end + + def vendor_not_found!(media_types) + return unless media_types.all? { |media_type| media_type&.vendor && media_type.vendor != vendor } + + invalid_accept_header!('API vendor not found.') + end + + def version_not_found!(media_types) + return unless media_types.all? { |media_type| media_type&.version && versions&.exclude?(media_type.version) } + + invalid_version_header!('API version not found.') + end + + def available_media_types + [].tap do |available_media_types| + base_media_type = "application/vnd.#{vendor}" + content_types.each_key do |extension| + versions&.reverse_each do |version| + available_media_types << "#{base_media_type}-#{version}+#{extension}" + available_media_types << "#{base_media_type}-#{version}" + end + available_media_types << "#{base_media_type}+#{extension}" + end + + available_media_types << base_media_type + available_media_types.concat(content_types.values.flatten) + end + end end end end diff --git a/lib/grape/middleware/versioner/param.rb b/lib/grape/middleware/versioner/param.rb index d12690c15..0c8f88a47 100644 --- a/lib/grape/middleware/versioner/param.rb +++ b/lib/grape/middleware/versioner/param.rb @@ -19,31 +19,15 @@ module Versioner # # env['api.version'] => 'v1' class Param < Base - def default_options - { - version_options: { - parameter: 'apiver' - } - } - end + include VersionerHelpers def before - potential_version = Rack::Utils.parse_nested_query(env[Rack::QUERY_STRING])[paramkey] - return if potential_version.nil? + potential_version = Rack::Utils.parse_nested_query(env[Rack::QUERY_STRING])[parameter_key] + return if potential_version.blank? - throw :error, status: 404, message: '404 API Version Not Found', headers: { Grape::Http::Headers::X_CASCADE => 'pass' } if options[:versions] && !options[:versions].find { |v| v.to_s == potential_version } + version_not_found! unless potential_version_match?(potential_version) env[Grape::Env::API_VERSION] = potential_version - env[Rack::RACK_REQUEST_QUERY_HASH].delete(paramkey) if env.key? Rack::RACK_REQUEST_QUERY_HASH - end - - private - - def paramkey - version_options[:parameter] || default_options[:version_options][:parameter] - end - - def version_options - options[:version_options] + env[Rack::RACK_REQUEST_QUERY_HASH].delete(parameter_key) if env.key? Rack::RACK_REQUEST_QUERY_HASH end end end diff --git a/lib/grape/middleware/versioner/path.rb b/lib/grape/middleware/versioner/path.rb index 24fc9010a..c824f2df8 100644 --- a/lib/grape/middleware/versioner/path.rb +++ b/lib/grape/middleware/versioner/path.rb @@ -17,44 +17,24 @@ module Versioner # env['api.version'] => 'v1' # class Path < Base - def default_options - { - pattern: /.*/i - } - end + include VersionerHelpers def before - path = env[Rack::PATH_INFO].dup - path.sub!(mount_path, '') if mounted_path?(path) + path_info = Grape::Router.normalize_path(env[Rack::PATH_INFO]) + return if path_info == '/' - if prefix && path.index(prefix) == 0 # rubocop:disable all - path.sub!(prefix, '') - path = Grape::Router.normalize_path(path) + [mount_path, Grape::Router.normalize_path(prefix)].each do |path| + path_info.delete_prefix!(path) if path.present? && path != '/' && path_info.start_with?(path) end - pieces = path.split('/') - potential_version = pieces[1] - 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 - - private + slash_position = path_info.index('/', 1) # omit the first one + return unless slash_position - def mounted_path?(path) - return false unless mount_path && path.start_with?(mount_path) + potential_version = path_info[1..slash_position - 1] + return unless potential_version.match?(pattern) - rest = path.slice(mount_path.length..-1) - rest.start_with?('/') || rest.empty? - end - - def mount_path - @mount_path ||= options[:mount_path] && options[:mount_path] != '/' ? options[:mount_path] : '' - end - - def prefix - Grape::Router.normalize_path(options[:prefix].to_s) if options[:prefix] + version_not_found! unless potential_version_match?(potential_version) + env[Grape::Env::API_VERSION] = potential_version end end end diff --git a/lib/grape/middleware/versioner_helpers.rb b/lib/grape/middleware/versioner_helpers.rb new file mode 100644 index 000000000..0cce2055c --- /dev/null +++ b/lib/grape/middleware/versioner_helpers.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +module Grape + module Middleware + module VersionerHelpers + DEFAULT_PATTERN = /.*/i.freeze + DEFAULT_PARAMETER = 'apiver' + + def default_options + { + versions: nil, + prefix: nil, + mount_path: nil, + pattern: DEFAULT_PATTERN, + version_options: { + strict: false, + cascade: true, + parameter: DEFAULT_PARAMETER + } + } + end + + def versions + options[:versions] + end + + def prefix + options[:prefix] + end + + def mount_path + options[:mount_path] + end + + def pattern + options[:pattern] + end + + def version_options + options[:version_options] + end + + def strict? + version_options[:strict] + end + + # By default those errors contain an `X-Cascade` header set to `pass`, which allows nesting and stacking + # of routes (see Grape::Router) for more information). To prevent + # this behavior, and not add the `X-Cascade` header, one can set the `:cascade` option to `false`. + def cascade? + version_options[:cascade] + end + + def parameter_key + version_options[:parameter] + end + + def vendor + version_options[:vendor] + end + + def error_headers + cascade? ? { Grape::Http::Headers::X_CASCADE => 'pass' } : {} + end + + def potential_version_match?(potential_version) + versions.blank? || versions.any? { |v| v.to_s == potential_version } + end + + def version_not_found! + throw :error, status: 404, message: '404 API Version Not Found', headers: { Grape::Http::Headers::X_CASCADE => 'pass' } + end + end + end +end diff --git a/lib/grape/util/accept_header_handler.rb b/lib/grape/util/accept_header_handler.rb deleted file mode 100644 index 3098f3a82..000000000 --- a/lib/grape/util/accept_header_handler.rb +++ /dev/null @@ -1,105 +0,0 @@ -# frozen_string_literal: true - -module Grape - module Util - class AcceptHeaderHandler - attr_reader :accept_header, :versions, :vendor, :strict, :cascade - - def initialize(accept_header:, versions:, **options) - @accept_header = accept_header - @versions = versions - @vendor = options.fetch(:vendor, nil) - @strict = options.fetch(:strict, false) - @cascade = options.fetch(:cascade, true) - end - - def match_best_quality_media_type!(content_types: Grape::ContentTypes::DEFAULTS, allowed_methods: nil) - return unless vendor - - strict_header_checks! - media_type = Grape::Util::MediaType.best_quality(accept_header, available_media_types(content_types)) - if media_type - yield media_type - else - fail!(allowed_methods) - end - end - - private - - def strict_header_checks! - return unless strict - - accept_header_check! - version_and_vendor_check! - end - - def accept_header_check! - return if accept_header.present? - - invalid_accept_header!('Accept header must be set.') - end - - def version_and_vendor_check! - return if versions.blank? || version_and_vendor? - - invalid_accept_header!('API vendor or version not found.') - end - - def q_values_mime_types - @q_values_mime_types ||= Rack::Utils.q_values(accept_header).map(&:first) - end - - def version_and_vendor? - q_values_mime_types.any? { |mime_type| Grape::Util::MediaType.match?(mime_type) } - end - - def invalid_accept_header!(message) - raise Grape::Exceptions::InvalidAcceptHeader.new(message, error_headers) - end - - def invalid_version_header!(message) - raise Grape::Exceptions::InvalidVersionHeader.new(message, error_headers) - end - - def fail!(grape_allowed_methods) - return grape_allowed_methods if grape_allowed_methods.present? - - media_types = q_values_mime_types.map { |mime_type| Grape::Util::MediaType.parse(mime_type) } - vendor_not_found!(media_types) || version_not_found!(media_types) - end - - def vendor_not_found!(media_types) - return unless media_types.all? { |media_type| media_type&.vendor && media_type.vendor != vendor } - - invalid_accept_header!('API vendor not found.') - end - - def version_not_found!(media_types) - return unless media_types.all? { |media_type| media_type&.version && versions.exclude?(media_type.version) } - - invalid_version_header!('API version not found.') - end - - def error_headers - cascade ? { Grape::Http::Headers::X_CASCADE => 'pass' } : {} - end - - def available_media_types(content_types) - [].tap do |available_media_types| - base_media_type = "application/vnd.#{vendor}" - content_types.each_key do |extension| - versions&.reverse_each do |version| - available_media_types << "#{base_media_type}-#{version}+#{extension}" - available_media_types << "#{base_media_type}-#{version}" - end - available_media_types << "#{base_media_type}+#{extension}" - end - - available_media_types << base_media_type - available_media_types.concat(content_types.values.flatten) - end - end - end - end -end diff --git a/spec/grape/middleware/versioner_spec.rb b/spec/grape/middleware/versioner_spec.rb index f42eaa5fe..eb7b95b3c 100644 --- a/spec/grape/middleware/versioner_spec.rb +++ b/spec/grape/middleware/versioner_spec.rb @@ -1,21 +1,37 @@ # frozen_string_literal: true describe Grape::Middleware::Versioner do - let(:klass) { described_class } + subject { described_class.using(strategy) } - it 'recognizes :path' do - expect(klass.using(:path)).to eq(Grape::Middleware::Versioner::Path) + context 'when :path' do + let(:strategy) { :path } + + it { is_expected.to eq(Grape::Middleware::Versioner::Path) } end - it 'recognizes :header' do - expect(klass.using(:header)).to eq(Grape::Middleware::Versioner::Header) + context 'when :header' do + let(:strategy) { :header } + + it { is_expected.to eq(Grape::Middleware::Versioner::Header) } end - it 'recognizes :param' do - expect(klass.using(:param)).to eq(Grape::Middleware::Versioner::Param) + context 'when :param' do + let(:strategy) { :param } + + it { is_expected.to eq(Grape::Middleware::Versioner::Param) } end - it 'recognizes :accept_version_header' do - expect(klass.using(:accept_version_header)).to eq(Grape::Middleware::Versioner::AcceptVersionHeader) + context 'when :accept_version_header' do + let(:strategy) { :accept_version_header } + + it { is_expected.to eq(Grape::Middleware::Versioner::AcceptVersionHeader) } + end + + context 'when unknown' do + let(:strategy) { :unknown } + + it 'raises an error' do + expect { subject }.to raise_error Grape::Exceptions::InvalidVersionerOption, Grape::Exceptions::InvalidVersionerOption.new(strategy).message + end end end diff --git a/spec/grape/util/accept_header_handler_spec.rb b/spec/grape/util/accept_header_handler_spec.rb deleted file mode 100644 index 6c94f05ec..000000000 --- a/spec/grape/util/accept_header_handler_spec.rb +++ /dev/null @@ -1,112 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Grape::Util::AcceptHeaderHandler do - subject(:match_best_quality_media_type!) { instance.match_best_quality_media_type! } - - let(:instance) do - described_class.new( - accept_header: accept_header, - versions: versions, - **options - ) - end - let(:accept_header) { '*/*' } - let(:versions) { ['v1'] } - let(:options) { {} } - - shared_examples 'an invalid accept header exception' do |message| - before do - allow(Grape::Exceptions::InvalidAcceptHeader).to receive(:new) - .with(message, { Grape::Http::Headers::X_CASCADE => 'pass' }) - .and_call_original - end - - it 'raises a Grape::Exceptions::InvalidAcceptHeader' do - expect { match_best_quality_media_type! }.to raise_error(Grape::Exceptions::InvalidAcceptHeader) - end - end - - describe '#match_best_quality_media_type!' do - context 'when no vendor set' do - let(:options) do - { - vendor: nil - } - end - - it { is_expected.to be_nil } - end - - context 'when strict header check' do - let(:options) do - { - vendor: 'vendor', - strict: true - } - end - - context 'when accept_header blank' do - let(:accept_header) { nil } - - it_behaves_like 'an invalid accept header exception', 'Accept header must be set.' - end - - context 'when vendor not found' do - let(:accept_header) { '*/*' } - - it_behaves_like 'an invalid accept header exception', 'API vendor or version not found.' - end - end - - context 'when media_type found' do - let(:options) do - { - vendor: 'vendor' - } - end - - let(:accept_header) { 'application/vnd.vendor-v1+json' } - - it 'yields a media type' do - expect { |b| instance.match_best_quality_media_type!(&b) }.to yield_with_args(Grape::Util::MediaType.new(type: 'application', subtype: 'vnd.vendor-v1+json')) - end - end - - context 'when media_type is not found' do - let(:options) do - { - vendor: 'vendor' - } - end - - let(:accept_header) { 'application/vnd.another_vendor-v1+json' } - - context 'when allowed_methods present' do - subject { instance.match_best_quality_media_type!(allowed_methods: allowed_methods) } - - let(:allowed_methods) { [Rack::OPTIONS] } - - it { is_expected.to match_array(allowed_methods) } - end - - context 'when vendor not found' do - it_behaves_like 'an invalid accept header exception', 'API vendor not found.' - end - - context 'when version not found' do - let(:versions) { ['v2'] } - let(:accept_header) { 'application/vnd.vendor-v1+json' } - - before do - allow(Grape::Exceptions::InvalidVersionHeader).to receive(:new) - .with('API version not found.', { Grape::Http::Headers::X_CASCADE => 'pass' }) - .and_call_original - end - - it 'raises a Grape::Exceptions::InvalidAcceptHeader' do - expect { match_best_quality_media_type! }.to raise_error(Grape::Exceptions::InvalidVersionHeader) - end - end - end - end -end From 04e69ea120ad2e32348932a82930b7526456766e Mon Sep 17 00:00:00 2001 From: ouyangjinting Date: Sun, 28 Jul 2024 04:39:43 +0800 Subject: [PATCH 257/304] The `length` validator only takes effect for parameters with types that support `#length` method (#2464) --- CHANGELOG.md | 1 + UPGRADING.md | 8 ++++++++ lib/grape/validations/validators/length_validator.rb | 2 +- spec/grape/validations/validators/length_spec.rb | 4 ++-- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22ea580d9..48d1b804e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ * [#2471](https://github.com/ruby-grape/grape/pull/2471): Fix absence of original_exception and/or backtrace even if passed in error! - [@numbata](https://github.com/numbata). * [#2478](https://github.com/ruby-grape/grape/pull/2478): Fix rescue_from with invalid response - [@ericproulx](https://github.com/ericproulx). * [#2480](https://github.com/ruby-grape/grape/pull/2480): Fix rescue_from ValidationErrors exception - [@numbata](https://github.com/numbata). +* [#2464](https://github.com/ruby-grape/grape/pull/2464): The `length` validator only takes effect for parameters with types that support `#length` method - [@OuYangJinTing](https://github.com/OuYangJinTing). * Your contribution here. ### 2.1.3 (2024-07-13) diff --git a/UPGRADING.md b/UPGRADING.md index be2fb564d..cba4774bb 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,6 +1,14 @@ Upgrading Grape =============== +### Upgrading to >= 2.2.0 + +### `Length` validator + +After Grape 2.2.0, `length` validator will only take effect for parameters with types that support `#length` method, will not throw `ArgumentError` exception. + +See [#2464](https://github.com/ruby-grape/grape/pull/2464) for more information. + ### Upgrading to >= 2.1.0 #### Optional Builder diff --git a/lib/grape/validations/validators/length_validator.rb b/lib/grape/validations/validators/length_validator.rb index bcd0c9559..ed266fe84 100644 --- a/lib/grape/validations/validators/length_validator.rb +++ b/lib/grape/validations/validators/length_validator.rb @@ -18,7 +18,7 @@ def initialize(attrs, options, required, scope, **opts) def validate_param!(attr_name, params) param = params[attr_name] - raise ArgumentError, "parameter #{param} does not support #length" unless param.respond_to?(:length) + return unless param.respond_to?(:length) return unless (!@min.nil? && param.length < @min) || (!@max.nil? && param.length > @max) diff --git a/spec/grape/validations/validators/length_spec.rb b/spec/grape/validations/validators/length_spec.rb index 8fa9f8487..9334b7a20 100644 --- a/spec/grape/validations/validators/length_spec.rb +++ b/spec/grape/validations/validators/length_spec.rb @@ -188,11 +188,11 @@ end describe '/type_is_not_array' do - context 'raises an error' do + context 'does not raise an error' do it do expect do post 'type_is_not_array', list: 12 - end.to raise_error(ArgumentError, 'parameter 12 does not support #length') + end.not_to raise_error end end end From 12dc739e6aa8be56e4b5adb39a9ffd9df03eec72 Mon Sep 17 00:00:00 2001 From: "David A. K. Ad." <3106338+Dakad@users.noreply.github.com> Date: Tue, 30 Jul 2024 14:40:44 +0200 Subject: [PATCH 258/304] Add `is:` parameter to the `length` validator (#2485) --- CHANGELOG.md | 1 + README.md | 4 +- lib/grape/locale/en.yml | 1 + .../validators/length_validator.rb | 11 ++- .../validations/validators/length_spec.rb | 68 +++++++++++++++++++ 5 files changed, 82 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48d1b804e..f1000c1a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ * [#2478](https://github.com/ruby-grape/grape/pull/2478): Fix rescue_from with invalid response - [@ericproulx](https://github.com/ericproulx). * [#2480](https://github.com/ruby-grape/grape/pull/2480): Fix rescue_from ValidationErrors exception - [@numbata](https://github.com/numbata). * [#2464](https://github.com/ruby-grape/grape/pull/2464): The `length` validator only takes effect for parameters with types that support `#length` method - [@OuYangJinTing](https://github.com/OuYangJinTing). +* [#2485](https://github.com/ruby-grape/grape/pull/2485): Add `is:` param to length validator - [@dakad](https://github.com/dakad). * Your contribution here. ### 2.1.3 (2024-07-13) diff --git a/README.md b/README.md index add3905b8..ef738c9ff 100644 --- a/README.md +++ b/README.md @@ -1714,10 +1714,11 @@ end Parameters with types that support `#length` method can be restricted to have a specific length with the `:length` option. -The validator accepts `:min` or `:max` or both options to validate that the value of the parameter is within the given limits. +The validator accepts `:min` or `:max` or both options or only `:is` to validate that the value of the parameter is within the given limits. ```ruby params do + requires :code, type: String, length: { is: 2 } requires :str, type: String, length: { min: 3 } requires :list, type: [Integer], length: { min: 3, max: 5 } requires :hash, type: Hash, length: { max: 5 } @@ -2045,6 +2046,7 @@ end ```ruby params do + requires :code, type: String, length: { is: 2, message: 'code is expected to be exactly 2 characters long' } requires :str, type: String, length: { min: 5, message: 'str is expected to be atleast 5 characters long' } requires :list, type: [Integer], length: { min: 2, max: 3, message: 'list is expected to have between 2 and 3 elements' } end diff --git a/lib/grape/locale/en.yml b/lib/grape/locale/en.yml index 3ed7bcc3a..6b1b6ae06 100644 --- a/lib/grape/locale/en.yml +++ b/lib/grape/locale/en.yml @@ -11,6 +11,7 @@ en: except_values: 'has a value not allowed' same_as: 'is not the same as %{parameter}' length: 'is expected to have length within %{min} and %{max}' + length_is: 'is expected to have length exactly equal to %{is}' length_min: 'is expected to have length greater than or equal to %{min}' length_max: 'is expected to have length less than or equal to %{max}' missing_vendor_option: diff --git a/lib/grape/validations/validators/length_validator.rb b/lib/grape/validations/validators/length_validator.rb index ed266fe84..f844f047e 100644 --- a/lib/grape/validations/validators/length_validator.rb +++ b/lib/grape/validations/validators/length_validator.rb @@ -7,12 +7,17 @@ class LengthValidator < Base def initialize(attrs, options, required, scope, **opts) @min = options[:min] @max = options[:max] + @is = options[:is] super raise ArgumentError, 'min must be an integer greater than or equal to zero' if !@min.nil? && (!@min.is_a?(Integer) || @min.negative?) raise ArgumentError, 'max must be an integer greater than or equal to zero' if !@max.nil? && (!@max.is_a?(Integer) || @max.negative?) raise ArgumentError, "min #{@min} cannot be greater than max #{@max}" if !@min.nil? && !@max.nil? && @min > @max + + return if @is.nil? + raise ArgumentError, 'is must be an integer greater than zero' if !@is.is_a?(Integer) || !@is.positive? + raise ArgumentError, 'is cannot be combined with min or max' if !@min.nil? || !@max.nil? end def validate_param!(attr_name, params) @@ -20,7 +25,7 @@ def validate_param!(attr_name, params) return unless param.respond_to?(:length) - return unless (!@min.nil? && param.length < @min) || (!@max.nil? && param.length > @max) + return unless (!@min.nil? && param.length < @min) || (!@max.nil? && param.length > @max) || (!@is.nil? && param.length != @is) raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: build_message) end @@ -32,8 +37,10 @@ def build_message format I18n.t(:length, scope: 'grape.errors.messages'), min: @min, max: @max elsif @min format I18n.t(:length_min, scope: 'grape.errors.messages'), min: @min - else + elsif @max format I18n.t(:length_max, scope: 'grape.errors.messages'), max: @max + else + format I18n.t(:length_is, scope: 'grape.errors.messages'), is: @is end end end diff --git a/spec/grape/validations/validators/length_spec.rb b/spec/grape/validations/validators/length_spec.rb index 9334b7a20..7e85b4dd8 100644 --- a/spec/grape/validations/validators/length_spec.rb +++ b/spec/grape/validations/validators/length_spec.rb @@ -86,6 +86,24 @@ end post '/custom-message' do end + + params do + requires :code, length: { is: 2 } + end + post 'is' do + end + + params do + requires :code, length: { is: -2 } + end + post 'negative_is' do + end + + params do + requires :code, length: { is: 2, max: 10 } + end + post 'is_with_max' do + end end end @@ -298,4 +316,54 @@ end end end + + describe '/is' do + context 'when length is exact' do + it do + post 'is', code: 'ZZ' + expect(last_response.status).to eq(201) + expect(last_response.body).to eq('') + end + end + + context 'when length exceeds the limit' do + it do + post 'is', code: 'aze' + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('code is expected to have length exactly equal to 2') + end + end + + context 'when length is less than the limit' do + it do + post 'is', code: 'a' + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('code is expected to have length exactly equal to 2') + end + end + + context 'when length is zero' do + it do + post 'is', code: '' + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('code is expected to have length exactly equal to 2') + end + end + end + + describe '/negative_is' do + context 'when `is` is negative' do + it do + expect { post 'negative_is', code: 'ZZ' }.to raise_error(ArgumentError, 'is must be an integer greater than zero') + end + end + end + + describe '/is_with_max' do + context 'when `is` is combined with max' do + it do + expect { post 'is_with_max', code: 'ZZ' }.to raise_error(ArgumentError, 'is cannot be combined with min or max') + end + end + end end From c33f93e4000ba183c679c73a7043f1f13a407bc3 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Mon, 12 Aug 2024 00:44:58 +0200 Subject: [PATCH 259/304] Add Rails 7.2 in CI workflow (#2489) * Add Rails 7.2 in workflow * Add CHANGELOG entry --- .github/workflows/test.yml | 10 +++++++++- CHANGELOG.md | 1 + gemfiles/rails_7_2.gemfile | 6 ++++++ 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 gemfiles/rails_7_2.gemfile diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cd47f23cb..6951c9432 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,7 +24,7 @@ jobs: fail-fast: false matrix: ruby: ['2.7', '3.0', '3.1', '3.2', '3.3'] - gemfile: [Gemfile, gemfiles/rack_2_0.gemfile, gemfiles/rack_3_0.gemfile, gemfiles/rack_3_1.gemfile, gemfiles/rails_6_0.gemfile, gemfiles/rails_6_1.gemfile, gemfiles/rails_7_0.gemfile, gemfiles/rails_7_1.gemfile] + gemfile: [Gemfile, gemfiles/rack_2_0.gemfile, gemfiles/rack_3_0.gemfile, gemfiles/rack_3_1.gemfile, gemfiles/rails_6_0.gemfile, gemfiles/rails_6_1.gemfile, gemfiles/rails_7_0.gemfile, gemfiles/rails_7_1.gemfile, gemfiles/rails_7_2.gemfile] specs: ['spec --exclude-pattern=spec/integration/**/*_spec.rb'] include: - ruby: '2.7' @@ -54,6 +54,14 @@ jobs: - ruby: '3.3' gemfile: gemfiles/rails_7_1.gemfile specs: 'spec/integration/rails' + - ruby: '3.3' + gemfile: gemfiles/rails_7_2.gemfile + specs: 'spec/integration/rails' + exclude: + - ruby: '2.7' + gemfile: gemfiles/rails_7_2.gemfile + - ruby: '3.0' + gemfile: gemfiles/rails_7_2.gemfile runs-on: ubuntu-latest env: BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }} diff --git a/CHANGELOG.md b/CHANGELOG.md index f1000c1a4..465f6fc89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * [#2475](https://github.com/ruby-grape/grape/pull/2475): Remove Grape::Util::Registrable - [@ericproulx](https://github.com/ericproulx). * [#2484](https://github.com/ruby-grape/grape/pull/2484): Refactor versioner middlewares - [@ericproulx](https://github.com/ericproulx). +* [#2489](https://github.com/ruby-grape/grape/pull/2489): Add Rails 7.2 in CI workflow - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/gemfiles/rails_7_2.gemfile b/gemfiles/rails_7_2.gemfile new file mode 100644 index 000000000..eb3d029f2 --- /dev/null +++ b/gemfiles/rails_7_2.gemfile @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +eval_gemfile '../Gemfile' + +gem 'rails', '~> 7.2.0' +gem 'tzinfo-data', require: false From 26a2742cf4a36dce499eb11b3aaa06991d834519 Mon Sep 17 00:00:00 2001 From: Eric Date: Sun, 1 Sep 2024 14:54:08 +0200 Subject: [PATCH 260/304] Add jemalloc and use it including yjit, frozen_string in Dockerfile Use `exec` in entrypoint.sh --- docker/Dockerfile | 9 ++++++--- docker/entrypoint.sh | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index ad41be490..2bc484ecf 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,10 +1,13 @@ -ARG RUBY_VERSION -FROM ruby:$RUBY_VERSION-alpine +ARG RUBY_VERSION=3 +FROM ruby:${RUBY_VERSION}-alpine ENV BUNDLE_PATH /usr/local/bundle/gems ENV LIB_PATH /var/grape +ENV RUBYOPT --enable-frozen-string-literal --yjit +ENV LD_PRELOAD libjemalloc.so.2 +ENV MALLOC_CONF dirty_decay_ms:1000,narenas:2,background_thread:true -RUN apk add --update --no-cache make gcc git libc-dev gcompat && \ +RUN apk add --update --no-cache make gcc git libc-dev gcompat jemalloc && \ gem update --system && gem install bundler WORKDIR $LIB_PATH diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index b7674f276..bc492f395 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -13,4 +13,4 @@ else fi # Keep gems in the latest possible state -(bundle check || bundle install) && bundle update && bundle exec ${@} +(bundle check || bundle install) && bundle update && exec bundle exec ${@} From dc31f1ce96ca4f4a968c7503f910a7334b998bc1 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sun, 1 Sep 2024 18:43:56 +0200 Subject: [PATCH 261/304] Adds rubygems_mfa_required to gemspec (#2493) * Adds rubygems_mfa_required to gemspec * Add CHANGELOG.md entry --- .rubocop_todo.yml | 8 -------- CHANGELOG.md | 1 + grape.gemspec | 3 ++- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 10cc2c7ed..81384c026 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -6,14 +6,6 @@ # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Severity, Include. -# Include: **/*.gemspec -Gemspec/RequireMFA: - Exclude: - - 'grape.gemspec' - # Offense count: 1 # Configuration parameters: AllowedMethods. # AllowedMethods: enums diff --git a/CHANGELOG.md b/CHANGELOG.md index 465f6fc89..16182f635 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * [#2475](https://github.com/ruby-grape/grape/pull/2475): Remove Grape::Util::Registrable - [@ericproulx](https://github.com/ericproulx). * [#2484](https://github.com/ruby-grape/grape/pull/2484): Refactor versioner middlewares - [@ericproulx](https://github.com/ericproulx). * [#2489](https://github.com/ruby-grape/grape/pull/2489): Add Rails 7.2 in CI workflow - [@ericproulx](https://github.com/ericproulx). +* [#2493](https://github.com/ruby-grape/grape/pull/2493): MFA required when releasing - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/grape.gemspec b/grape.gemspec index e3383f927..bdb4ba1c1 100644 --- a/grape.gemspec +++ b/grape.gemspec @@ -17,7 +17,8 @@ Gem::Specification.new do |s| '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}" + 'source_code_uri' => "https://github.com/ruby-grape/grape/tree/v#{s.version}", + 'rubygems_mfa_required' => 'true' } s.add_runtime_dependency 'activesupport', '>= 6' From 1cf4a801efc22e65c4fb342ead57fe3c428a4b8d Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sun, 1 Sep 2024 19:56:08 +0200 Subject: [PATCH 262/304] Fix Grape::Endpoint's inspect method when not called in the context of an API (#2492) * Move inspect method to public Calls super if env is not defined Add spec * Add CHANGELOG.md entry * Fix rubocop * Fix comments * Change backtick for single quote in test --- CHANGELOG.md | 1 + lib/grape/endpoint.rb | 13 +++++++++---- spec/grape/endpoint_spec.rb | 22 +++++++++++++++++++++- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16182f635..2eda07b1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ * [#2480](https://github.com/ruby-grape/grape/pull/2480): Fix rescue_from ValidationErrors exception - [@numbata](https://github.com/numbata). * [#2464](https://github.com/ruby-grape/grape/pull/2464): The `length` validator only takes effect for parameters with types that support `#length` method - [@OuYangJinTing](https://github.com/OuYangJinTing). * [#2485](https://github.com/ruby-grape/grape/pull/2485): Add `is:` param to length validator - [@dakad](https://github.com/dakad). +* [#2492](https://github.com/ruby-grape/grape/pull/2492): Fix `Grape::Endpoint#inspect` method - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 2.1.3 (2024-07-13) diff --git a/lib/grape/endpoint.rb b/lib/grape/endpoint.rb index 88605d13b..9072ba877 100644 --- a/lib/grape/endpoint.rb +++ b/lib/grape/endpoint.rb @@ -234,6 +234,15 @@ def equals?(endpoint) (options == endpoint.options) && (inheritable_setting.to_hash == endpoint.inheritable_setting.to_hash) end + # The purpose of this override is solely for stripping internals when an error occurs while calling + # an endpoint through an api. See https://github.com/ruby-grape/grape/issues/2398 + # Otherwise, it calls super. + def inspect + return super unless env + + "#{self.class} in '#{route.origin}' endpoint" + end + protected def run @@ -403,9 +412,5 @@ def options? options[:options_route_enabled] && env[Rack::REQUEST_METHOD] == Rack::OPTIONS end - - def inspect - "#{self.class} in `#{route.origin}' endpoint" - end end end diff --git a/spec/grape/endpoint_spec.rb b/spec/grape/endpoint_spec.rb index ee44efc39..f51e58213 100644 --- a/spec/grape/endpoint_spec.rb +++ b/spec/grape/endpoint_spec.rb @@ -683,7 +683,7 @@ def app context 'when referencing an undefined local variable or method' do let(:error_message) do if Gem::Version.new(RUBY_VERSION).release <= Gem::Version.new('3.2') - %r{undefined local variable or method `undefined_helper' for # in `/hey' endpoint} + %r{undefined local variable or method `undefined_helper' for # in '/hey' endpoint} else /undefined local variable or method `undefined_helper' for/ end @@ -1088,4 +1088,24 @@ def memoized ) end end + + describe '#inspect' do + subject { described_class.new(settings, options).inspect } + + let(:options) do + { + method: :path, + path: '/path', + app: {}, + route_options: { anchor: false }, + forward_match: true, + for: Class.new + } + end + let(:settings) { Grape::Util::InheritableSetting.new } + + it 'does not raise an error' do + expect { subject }.not_to raise_error + end + end end From 2d94dd8b705e2f5dfaa97cc6f706e1ceb0b9e87c Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sun, 8 Sep 2024 18:03:05 +0200 Subject: [PATCH 263/304] Reduce object allocation while compiling (#2496) * Only 1 to_hash to prepare_path * Use inheritable_setting.namespace_stackable and inheritable_setting.namespace_inheritable instead * Use `include` instead of `send(:include)` * Use `include` instead of `send(:include)` * small refactor * Add CHANGELOG entry * Return if helpers.empty? instead. * Update CHANGELOG.md Object allocation instead of just hash --- CHANGELOG.md | 1 + lib/grape/dsl/helpers.rb | 10 ++++-- lib/grape/endpoint.rb | 33 +++++++++++-------- lib/grape/middleware/error.rb | 6 ++-- .../extensions/param_builders/hash_spec.rb | 2 +- .../hash_with_indifferent_access_spec.rb | 2 +- spec/integration/hashie/hashie_spec.rb | 2 +- 7 files changed, 34 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2eda07b1b..01057484f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ * [#2464](https://github.com/ruby-grape/grape/pull/2464): The `length` validator only takes effect for parameters with types that support `#length` method - [@OuYangJinTing](https://github.com/OuYangJinTing). * [#2485](https://github.com/ruby-grape/grape/pull/2485): Add `is:` param to length validator - [@dakad](https://github.com/dakad). * [#2492](https://github.com/ruby-grape/grape/pull/2492): Fix `Grape::Endpoint#inspect` method - [@ericproulx](https://github.com/ericproulx). +* [#2496](https://github.com/ruby-grape/grape/pull/2496): Reduce object allocation when compiling - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 2.1.3 (2024-07-13) diff --git a/lib/grape/dsl/helpers.rb b/lib/grape/dsl/helpers.rb index ef8118c0e..180712282 100644 --- a/lib/grape/dsl/helpers.rb +++ b/lib/grape/dsl/helpers.rb @@ -33,18 +33,22 @@ module ClassMethods # end # def helpers(*new_modules, &block) - include_new_modules(new_modules) if new_modules.any? - include_block(block) if block + include_new_modules(new_modules) + include_block(block) include_all_in_scope if !block && new_modules.empty? end protected def include_new_modules(modules) + return if modules.empty? + modules.each { |mod| make_inclusion(mod) } end def include_block(block) + return unless block + Module.new.tap do |mod| make_inclusion(mod) { mod.class_eval(&block) } end @@ -58,7 +62,7 @@ def make_inclusion(mod, &block) def include_all_in_scope Module.new.tap do |mod| - namespace_stackable(:helpers).each { |mod_to_include| mod.send :include, mod_to_include } + namespace_stackable(:helpers).each { |mod_to_include| mod.include mod_to_include } change! end end diff --git a/lib/grape/endpoint.rb b/lib/grape/endpoint.rb index 9072ba877..5fdb03567 100644 --- a/lib/grape/endpoint.rb +++ b/lib/grape/endpoint.rb @@ -114,10 +114,10 @@ def initialize(new_settings, options = {}, &block) # Update our settings from a given set of stackable parameters. Used when # the endpoint's API is mounted under another one. def inherit_settings(namespace_stackable) - inheritable_setting.route[:saved_validations] += namespace_stackable[:validations] + parent_validations = namespace_stackable[:validations] + inheritable_setting.route[:saved_validations].concat(parent_validations) if parent_validations.any? parent_declared_params = namespace_stackable[:declared_params] - - inheritable_setting.route[:declared_params].concat(parent_declared_params.flatten) if parent_declared_params + inheritable_setting.route[:declared_params].concat(parent_declared_params.flatten) if parent_declared_params.any? endpoints&.each { |e| e.inherit_settings(namespace_stackable) } end @@ -205,7 +205,9 @@ def map_routes end def prepare_path(path) - path_settings = inheritable_setting.to_hash[:namespace_stackable].merge(inheritable_setting.to_hash[:namespace_inheritable]) + namespace_stackable_hash = inheritable_setting.namespace_stackable.to_hash + namespace_inheritable_hash = inheritable_setting.namespace_inheritable.to_hash + path_settings = namespace_stackable_hash.merge!(namespace_inheritable_hash) Path.new(path, namespace, path_settings) end @@ -288,19 +290,22 @@ def run def build_stack(helpers) stack = Grape::Middleware::Stack.new + content_types = namespace_stackable_with_hash(:content_types) + format = namespace_inheritable(:format) + stack.use Rack::Head stack.use Class.new(Grape::Middleware::Error), helpers: helpers, - format: namespace_inheritable(:format), - content_types: namespace_stackable_with_hash(:content_types), + format: format, + content_types: content_types, default_status: namespace_inheritable(:default_error_status), rescue_all: namespace_inheritable(:rescue_all), rescue_grape_exceptions: namespace_inheritable(:rescue_grape_exceptions), default_error_formatter: namespace_inheritable(:default_error_formatter), error_formatters: namespace_stackable_with_hash(:error_formatters), - rescue_options: namespace_stackable_with_hash(:rescue_options) || {}, - rescue_handlers: namespace_reverse_stackable_with_hash(:rescue_handlers) || {}, - base_only_rescue_handlers: namespace_stackable_with_hash(:base_only_rescue_handlers) || {}, + rescue_options: namespace_stackable_with_hash(:rescue_options), + rescue_handlers: namespace_reverse_stackable_with_hash(:rescue_handlers), + base_only_rescue_handlers: namespace_stackable_with_hash(:base_only_rescue_handlers), all_rescue_handler: namespace_inheritable(:all_rescue_handler), grape_exceptions_rescue_handler: namespace_inheritable(:grape_exceptions_rescue_handler) @@ -315,9 +320,9 @@ def build_stack(helpers) end stack.use Grape::Middleware::Formatter, - format: namespace_inheritable(:format), + format: format, default_format: namespace_inheritable(:default_format) || :txt, - content_types: namespace_stackable_with_hash(:content_types), + content_types: content_types, formatters: namespace_stackable_with_hash(:formatters), parsers: namespace_stackable_with_hash(:parsers) @@ -328,7 +333,9 @@ def build_stack(helpers) def build_helpers helpers = namespace_stackable(:helpers) - Module.new { helpers&.each { |mod_to_include| include mod_to_include } } + return if helpers.empty? + + Module.new { helpers.each { |mod_to_include| include mod_to_include } } end private :build_stack, :build_helpers @@ -347,7 +354,7 @@ def lazy_initialize! @lazy_initialize_lock.synchronize do return true if @lazy_initialized - @helpers = build_helpers.tap { |mod| self.class.send(:include, mod) } + @helpers = build_helpers&.tap { |mod| self.class.include mod } @app = options[:app] || build_stack(@helpers) @lazy_initialized = true diff --git a/lib/grape/middleware/error.rb b/lib/grape/middleware/error.rb index 6e5304607..f201ee8ef 100644 --- a/lib/grape/middleware/error.rb +++ b/lib/grape/middleware/error.rb @@ -26,7 +26,7 @@ def default_options def initialize(app, *options) super - self.class.send(:include, @options[:helpers]) if @options[:helpers] + self.class.include(@options[:helpers]) if @options[:helpers] end def call!(env) @@ -79,7 +79,7 @@ def default_rescue_handler(exception) end def rescue_handler_for_base_only_class(klass) - error, handler = options[:base_only_rescue_handlers].find { |err, _handler| klass == err } + error, handler = options[:base_only_rescue_handlers]&.find { |err, _handler| klass == err } return unless error @@ -87,7 +87,7 @@ def rescue_handler_for_base_only_class(klass) end def rescue_handler_for_class_or_its_ancestor(klass) - error, handler = options[:rescue_handlers].find { |err, _handler| klass <= err } + error, handler = options[:rescue_handlers]&.find { |err, _handler| klass <= err } return unless error diff --git a/spec/grape/extensions/param_builders/hash_spec.rb b/spec/grape/extensions/param_builders/hash_spec.rb index 872330bb4..fdce95226 100644 --- a/spec/grape/extensions/param_builders/hash_spec.rb +++ b/spec/grape/extensions/param_builders/hash_spec.rb @@ -29,7 +29,7 @@ def app describe 'in an api' do before do - subject.send(:include, Grape::Extensions::Hash::ParamBuilder) # rubocop:disable RSpec/DescribedClass + subject.include Grape::Extensions::Hash::ParamBuilder # rubocop:disable RSpec/DescribedClass end describe '#params' 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 0f741193b..97b4e56cd 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 @@ -29,7 +29,7 @@ def app describe 'in an api' do before do - subject.send(:include, Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder) # rubocop:disable RSpec/DescribedClass + subject.include Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder # rubocop:disable RSpec/DescribedClass end describe '#params' do diff --git a/spec/integration/hashie/hashie_spec.rb b/spec/integration/hashie/hashie_spec.rb index 8c8314997..73c97ce1e 100644 --- a/spec/integration/hashie/hashie_spec.rb +++ b/spec/integration/hashie/hashie_spec.rb @@ -28,7 +28,7 @@ describe 'in an api' do before do - subject.send(:include, Grape::Extensions::Hashie::Mash::ParamBuilder) + subject.include Grape::Extensions::Hashie::Mash::ParamBuilder end describe '#params' do From f8ca202f0fb5b2ba8d62903a30439b08252912b2 Mon Sep 17 00:00:00 2001 From: Eric Date: Sat, 14 Sep 2024 12:01:22 +0200 Subject: [PATCH 264/304] Preparing for release, 2.2.0 --- CHANGELOG.md | 4 +--- README.md | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01057484f..e338f1a3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -### 2.2.0 (Next) +### 2.2.0 (2024-09-14) #### Features @@ -6,7 +6,6 @@ * [#2484](https://github.com/ruby-grape/grape/pull/2484): Refactor versioner middlewares - [@ericproulx](https://github.com/ericproulx). * [#2489](https://github.com/ruby-grape/grape/pull/2489): Add Rails 7.2 in CI workflow - [@ericproulx](https://github.com/ericproulx). * [#2493](https://github.com/ruby-grape/grape/pull/2493): MFA required when releasing - [@ericproulx](https://github.com/ericproulx). -* Your contribution here. #### Fixes @@ -17,7 +16,6 @@ * [#2485](https://github.com/ruby-grape/grape/pull/2485): Add `is:` param to length validator - [@dakad](https://github.com/dakad). * [#2492](https://github.com/ruby-grape/grape/pull/2492): Fix `Grape::Endpoint#inspect` method - [@ericproulx](https://github.com/ericproulx). * [#2496](https://github.com/ruby-grape/grape/pull/2496): Reduce object allocation when compiling - [@ericproulx](https://github.com/ericproulx). -* Your contribution here. ### 2.1.3 (2024-07-13) diff --git a/README.md b/README.md index ef738c9ff..a81b8e04e 100644 --- a/README.md +++ b/README.md @@ -157,9 +157,7 @@ Grape is a REST-like API framework for Ruby. It's designed to run on Rack or com ## Stable Release -You're reading the documentation for the next release of Grape, which should be 2.2.0. -Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. -The current stable release is [2.1.3](https://github.com/ruby-grape/grape/blob/v2.1.3/README.md). +You're reading the documentation for the stable release of Grape, 2.2.0. Please read UPGRADING when upgrading from a previous version. ## Project Resources From ea9c76dc33ec4e13863f3f45428f582cdbe0a30c Mon Sep 17 00:00:00 2001 From: Eric Date: Sat, 14 Sep 2024 12:04:41 +0200 Subject: [PATCH 265/304] Preparing for next development iteration, 2.3.0. --- CHANGELOG.md | 10 ++++++++++ README.md | 3 ++- lib/grape/version.rb | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e338f1a3a..27ee59e3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +### 2.3.0 (Next) + +#### Features + +* Your contribution here. + +#### Fixes + +* Your contribution here. + ### 2.2.0 (2024-09-14) #### Features diff --git a/README.md b/README.md index a81b8e04e..9d46af651 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,8 @@ Grape is a REST-like API framework for Ruby. It's designed to run on Rack or com ## Stable Release -You're reading the documentation for the stable release of Grape, 2.2.0. Please read UPGRADING when upgrading from a previous version. +You're reading the documentation for the next release of Grape, which should be 2.3.0. +The current stable release is [2.2.0](https://github.com/ruby-grape/grape/blob/v2.2.0/README.md). ## Project Resources diff --git a/lib/grape/version.rb b/lib/grape/version.rb index 5083e38bd..246308b4a 100644 --- a/lib/grape/version.rb +++ b/lib/grape/version.rb @@ -2,5 +2,5 @@ module Grape # The current version of Grape. - VERSION = '2.2.0' + VERSION = '2.3.0' end From 4a8b8c40d2f882890dc19e1d13a728b9ab9ab1f6 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sun, 15 Sep 2024 12:34:52 +0200 Subject: [PATCH 266/304] Update rubocop to latest (#2497) * Update rubocop to 1.66.1 Update rubocop-performance 1.21.1 Update rubocop-rspec 3.0.5 Change add_runtime_dependency to add_dependency Gemspec/AddRuntimeDependency * Add CHANGELOG.md * Update CHANGELOG.md Co-authored-by: Daniel (dB.) Doubrovkine --------- Co-authored-by: Daniel (dB.) Doubrovkine --- CHANGELOG.md | 1 + Gemfile | 6 +++--- grape.gemspec | 10 +++++----- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27ee59e3b..aa56c6c11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ #### Features +* [#2497](https://github.com/ruby-grape/grape/pull/2497): Update RuboCop to 1.66.1 - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/Gemfile b/Gemfile index 8df676a00..17972e469 100644 --- a/Gemfile +++ b/Gemfile @@ -10,9 +10,9 @@ group :development, :test do gem 'builder', require: false gem 'bundler' gem 'rake' - gem 'rubocop', '1.64.1', require: false - gem 'rubocop-performance', '1.21.0', require: false - gem 'rubocop-rspec', '3.0.1', require: false + gem 'rubocop', '1.66.1', require: false + gem 'rubocop-performance', '1.21.1', require: false + gem 'rubocop-rspec', '3.0.5', require: false end group :development do diff --git a/grape.gemspec b/grape.gemspec index bdb4ba1c1..a5d7c57a0 100644 --- a/grape.gemspec +++ b/grape.gemspec @@ -21,11 +21,11 @@ Gem::Specification.new do |s| 'rubygems_mfa_required' => 'true' } - s.add_runtime_dependency 'activesupport', '>= 6' - s.add_runtime_dependency 'dry-types', '>= 1.1' - s.add_runtime_dependency 'mustermann-grape', '~> 1.1.0' - s.add_runtime_dependency 'rack', '>= 2' - s.add_runtime_dependency 'zeitwerk' + s.add_dependency 'activesupport', '>= 6' + s.add_dependency 'dry-types', '>= 1.1' + s.add_dependency 'mustermann-grape', '~> 1.1.0' + s.add_dependency 'rack', '>= 2' + s.add_dependency 'zeitwerk' s.files = Dir['lib/**/*', 'CHANGELOG.md', 'CONTRIBUTING.md', 'README.md', 'grape.png', 'UPGRADING.md', 'LICENSE', 'grape.gemspec'] s.require_paths = ['lib'] From 75e8c5bef618572d86fc3283494fd717819db989 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Thu, 26 Sep 2024 17:35:48 +0200 Subject: [PATCH 267/304] Remove file method (#2500) * Remove file method and specs A * Add CHANGELOG.md and UPGRADING.md * Remove leaky dummy class in inside_route_spec.rb * Use match operator instead of multiple examples * Use match operator instead of multiple examples * Fix rubocop --- CHANGELOG.md | 1 + UPGRADING.md | 8 ++ lib/grape/dsl/inside_route.rb | 14 --- spec/grape/dsl/inside_route_spec.rb | 149 +++++----------------------- 4 files changed, 33 insertions(+), 139 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa56c6c11..09b7d2e88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ #### Features * [#2497](https://github.com/ruby-grape/grape/pull/2497): Update RuboCop to 1.66.1 - [@ericproulx](https://github.com/ericproulx). +* [#2500](https://github.com/ruby-grape/grape/pull/2500): Remove deprecated `file` method - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/UPGRADING.md b/UPGRADING.md index cba4774bb..acc74fbac 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,6 +1,14 @@ Upgrading Grape =============== +### Upgrading to >= 2.3.0 + +#### Remove deprecated methods + +Deprecated `file` method has been removed. Use `send_file` or `stream`. + +See [#2500](https://github.com/ruby-grape/grape/pull/2500) + ### Upgrading to >= 2.2.0 ### `Length` validator diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index ba1715de2..72b365bc4 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -305,20 +305,6 @@ def return_no_content body false end - # Deprecated method to send files to the client. Use `sendfile` or `stream` - def file(value = nil) - if value.is_a?(String) - Grape.deprecator.warn('Use sendfile or stream to send files.') - sendfile(value) - elsif !value.is_a?(NilClass) - Grape.deprecator.warn('Use stream to use a Stream object.') - stream(value) - else - Grape.deprecator.warn('Use sendfile or stream to send files.') - sendfile - end - end - # Allows you to send a file to the client via sendfile. # # @example diff --git a/spec/grape/dsl/inside_route_spec.rb b/spec/grape/dsl/inside_route_spec.rb index 8963a73a2..038e20314 100644 --- a/spec/grape/dsl/inside_route_spec.rb +++ b/spec/grape/dsl/inside_route_spec.rb @@ -1,25 +1,21 @@ # frozen_string_literal: true -module Grape - module DSL - module InsideRouteSpec - class Dummy - include Grape::DSL::InsideRoute - - attr_reader :env, :request, :new_settings - - def initialize - @env = {} - @header = {} - @new_settings = { namespace_inheritable: {}, namespace_stackable: {} } - end +describe Grape::Endpoint do + subject { dummy_class.new } + + let(:dummy_class) do + Class.new do + include Grape::DSL::InsideRoute + + attr_reader :env, :request, :new_settings + + def initialize + @env = {} + @header = {} + @new_settings = { namespace_inheritable: {}, namespace_stackable: {} } end end end -end - -describe Grape::Endpoint do - subject { Grape::DSL::InsideRouteSpec::Dummy.new } describe '#version' do it 'defaults to nil' do @@ -202,38 +198,6 @@ def initialize end end - describe '#file' do - describe 'set' do - context 'as file path' do - let(:file_path) { '/some/file/path' } - - it 'emits a warning that this method is deprecated' do - expect(Grape.deprecator).to receive(:warn).with(/Use sendfile or stream/) - expect(subject).to receive(:sendfile).with(file_path) - subject.file file_path - end - end - - context 'as object (backward compatibility)' do - let(:file_object) { double('StreamerObject', each: nil) } - - it 'emits a warning that this method is deprecated' do - expect(Grape.deprecator).to receive(:warn).with(/Use stream to use a Stream object/) - 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(Grape.deprecator).to receive(:warn).with(/Use sendfile or stream/) - expect(subject).to receive(:sendfile) - subject.file - end - end - end - describe '#sendfile' do describe 'set' do context 'as file path' do @@ -248,36 +212,19 @@ def initialize subject.header Rack::CACHE_CONTROL, 'cache' subject.header Rack::CONTENT_LENGTH, 123 subject.header Grape::Http::Headers::TRANSFER_ENCODING, 'base64' - end - - it 'sends no deprecation warnings' do - expect(Grape.deprecator).not_to 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[Rack::CACHE_CONTROL]).to eq 'cache' - end - - it 'does not change the Content-Length header' do - subject.sendfile file_path - - expect(subject.header[Rack::CONTENT_LENGTH]).to eq 123 - end - - it 'does not change the Transfer-Encoding header' do - subject.sendfile file_path - - expect(subject.header[Grape::Http::Headers::TRANSFER_ENCODING]).to eq 'base64' + it 'set the correct headers' do + expect(subject.header).to match( + Rack::CACHE_CONTROL => 'cache', + Rack::CONTENT_LENGTH => 123, + Grape::Http::Headers::TRANSFER_ENCODING => 'base64' + ) end end @@ -309,42 +256,15 @@ def initialize subject.header Rack::CACHE_CONTROL, 'cache' subject.header Rack::CONTENT_LENGTH, 123 subject.header Grape::Http::Headers::TRANSFER_ENCODING, 'base64' - end - - it 'emits no deprecation warnings' do - expect(Grape.deprecator).not_to 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[Rack::CACHE_CONTROL]).to eq 'no-cache' - end - - it 'does not change Cache-Control header' do - subject.stream - - expect(subject.header[Rack::CACHE_CONTROL]).to eq 'cache' - end - - it 'sets Content-Length header to nil' do - subject.stream file_path - - expect(subject.header[Rack::CONTENT_LENGTH]).to be_nil - end - - it 'sets Transfer-Encoding header to nil' do - subject.stream file_path - - expect(subject.header[Grape::Http::Headers::TRANSFER_ENCODING]).to be_nil + it 'sets only the cache-control header' do + expect(subject.header).to match(Rack::CACHE_CONTROL => 'no-cache') end end @@ -359,36 +279,15 @@ def initialize subject.header Rack::CACHE_CONTROL, 'cache' subject.header Rack::CONTENT_LENGTH, 123 subject.header Grape::Http::Headers::TRANSFER_ENCODING, 'base64' - end - - it 'emits no deprecation warnings' do - expect(Grape.deprecator).not_to 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[Rack::CACHE_CONTROL]).to eq 'no-cache' - end - - it 'sets Content-Length header to nil' do - subject.stream stream_object - - expect(subject.header[Rack::CONTENT_LENGTH]).to be_nil - end - - it 'sets Transfer-Encoding header to nil' do - subject.stream stream_object - - expect(subject.header[Grape::Http::Headers::TRANSFER_ENCODING]).to be_nil + it 'set only the cache-control header' do + expect(subject.header).to match(Rack::CACHE_CONTROL => 'no-cache') end end @@ -403,7 +302,7 @@ def initialize it 'returns default' do expect(subject.stream).to be_nil - expect(subject.header[Rack::CACHE_CONTROL]).to be_nil + expect(subject.header).to be_empty end end From 7f0595cb8e79353c45b82cc88989d87dedde7c08 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Fri, 27 Sep 2024 20:41:44 +0200 Subject: [PATCH 268/304] Remove deprecated except and proc in values validator (#2501) --- CHANGELOG.md | 1 + UPGRADING.md | 8 ++- .../validators/values_validator.rb | 68 ++++--------------- 3 files changed, 18 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09b7d2e88..14f5ce81f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * [#2497](https://github.com/ruby-grape/grape/pull/2497): Update RuboCop to 1.66.1 - [@ericproulx](https://github.com/ericproulx). * [#2500](https://github.com/ruby-grape/grape/pull/2500): Remove deprecated `file` method - [@ericproulx](https://github.com/ericproulx). +* [#2501](https://github.com/ruby-grape/grape/pull/2501): Remove deprecated `except` and `proc` options in values validator - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/UPGRADING.md b/UPGRADING.md index acc74fbac..8039f3af5 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -3,11 +3,13 @@ Upgrading Grape ### Upgrading to >= 2.3.0 -#### Remove deprecated methods +#### Remove Deprecated Methods and Options -Deprecated `file` method has been removed. Use `send_file` or `stream`. +- Deprecated `file` method has been removed. Use `send_file` or `stream`. +See [#2500](https://github.com/ruby-grape/grape/pull/2500) for more information. -See [#2500](https://github.com/ruby-grape/grape/pull/2500) +- The `except` and `proc` options have been removed from the `values` validator. Use `except_values` validator or assign `proc` directly to `values`. +See [#2501](https://github.com/ruby-grape/grape/pull/2501) for more information. ### Upgrading to >= 2.2.0 diff --git a/lib/grape/validations/validators/values_validator.rb b/lib/grape/validations/validators/values_validator.rb index 8475ffb82..cc2a64172 100644 --- a/lib/grape/validations/validators/values_validator.rb +++ b/lib/grape/validations/validators/values_validator.rb @@ -5,22 +5,7 @@ module Validations module Validators class ValuesValidator < Base def initialize(attrs, options, required, scope, **opts) - if options.is_a?(Hash) - @excepts = options[:except] - @values = options[:value] - @proc = options[:proc] - - Grape.deprecator.warn('The values validator except option is deprecated. Use the except validator instead.') if @excepts - - raise ArgumentError, 'proc must be a Proc' if @proc && !@proc.is_a?(Proc) - - Grape.deprecator.warn('The values validator proc option is deprecated. The lambda expression can now be assigned directly to values.') if @proc - else - @excepts = nil - @values = nil - @proc = nil - @values = options - end + @values = options.is_a?(Hash) ? options[:value] : options super end @@ -34,54 +19,29 @@ def validate_param!(attr_name, params) # don't forget that +false.blank?+ is true return if val != false && val.blank? && @allow_blank - param_array = val.nil? ? [nil] : Array.wrap(val) - - raise validation_exception(attr_name, except_message) \ - unless check_excepts(param_array) - - raise validation_exception(attr_name, message(:values)) \ - unless check_values(param_array, attr_name) + return if check_values?(val, attr_name) - raise validation_exception(attr_name, message(:values)) \ - if @proc && !validate_proc(@proc, param_array) + raise Grape::Exceptions::Validation.new( + params: [@scope.full_name(attr_name)], + message: message(:values) + ) end private - def check_values(param_array, attr_name) + def check_values?(val, attr_name) values = @values.is_a?(Proc) && @values.arity.zero? ? @values.call : @values return true if values.nil? + param_array = val.nil? ? [nil] : Array.wrap(val) + return param_array.all? { |param| values.include?(param) } unless values.is_a?(Proc) + begin - return param_array.all? { |param| values.call(param) } if values.is_a? Proc + param_array.all? { |param| values.call(param) } rescue StandardError => e warn "Error '#{e}' raised while validating attribute '#{attr_name}'" - return false + false end - param_array.all? { |param| values.include?(param) } - end - - def check_excepts(param_array) - excepts = @excepts.is_a?(Proc) ? @excepts.call : @excepts - return true if excepts.nil? - - param_array.none? { |param| excepts.include?(param) } - end - - def validate_proc(proc, param_array) - case proc.arity - when 0 - param_array.all? { |_param| proc.call } - when 1 - param_array.all? { |param| proc.call(param) } - else - raise ArgumentError, 'proc arity must be 0 or 1' - end - end - - def except_message - options = instance_variable_get(:@option) - options_key?(:except_message) ? options[:except_message] : message(:except_values) end def required_for_root_scope? @@ -92,10 +52,6 @@ def required_for_root_scope? 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 From 5b0066cc7cb80b920d5b59e9577c271f7c692855 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Tue, 1 Oct 2024 22:21:11 +0200 Subject: [PATCH 269/304] Remove deprecation msg + small refactor (#2502) * Remove deprecation msg + small refactor * Add CHANGELOG.md and UPGRADING.md --- CHANGELOG.md | 1 + UPGRADING.md | 3 +++ lib/grape/dsl/desc.rb | 51 ++++++++++++++++++++----------------- spec/grape/dsl/desc_spec.rb | 13 ---------- 4 files changed, 31 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14f5ce81f..344f34934 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * [#2497](https://github.com/ruby-grape/grape/pull/2497): Update RuboCop to 1.66.1 - [@ericproulx](https://github.com/ericproulx). * [#2500](https://github.com/ruby-grape/grape/pull/2500): Remove deprecated `file` method - [@ericproulx](https://github.com/ericproulx). * [#2501](https://github.com/ruby-grape/grape/pull/2501): Remove deprecated `except` and `proc` options in values validator - [@ericproulx](https://github.com/ericproulx). +* [#2502](https://github.com/ruby-grape/grape/pull/2502): Remove deprecation `options` in `desc` - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/UPGRADING.md b/UPGRADING.md index 8039f3af5..ab80fe4cb 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -11,6 +11,9 @@ See [#2500](https://github.com/ruby-grape/grape/pull/2500) for more information. - The `except` and `proc` options have been removed from the `values` validator. Use `except_values` validator or assign `proc` directly to `values`. See [#2501](https://github.com/ruby-grape/grape/pull/2501) for more information. +- `Passing an options hash and a block to 'desc'` deprecation has been removed. Move all hash options to block instead. +See [#2502](https://github.com/ruby-grape/grape/pull/2502) for more information. + ### Upgrading to >= 2.2.0 ### `Length` validator diff --git a/lib/grape/dsl/desc.rb b/lib/grape/dsl/desc.rb index f83eb8b00..2d3155026 100644 --- a/lib/grape/dsl/desc.rb +++ b/lib/grape/dsl/desc.rb @@ -70,33 +70,23 @@ module Desc # # ... # end # - def desc(description, options = {}, &config_block) - if config_block - 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) + def desc(description, options = nil, &config_block) + opts = + if config_block + desc_container(endpoint_configuration).then do |config_class| + config_class.configure do + description(description) + end - config_class.configure do - description description + config_class.configure(&config_block) + config_class.settings + end + else + options&.merge(description: description) || { description: description } end - config_class.configure(&config_block) - Grape.deprecator.warn('Passing a options hash and a block to `desc` is deprecated. Move all hash options to block.') if options.any? - options = config_class.settings - else - options = options.merge(description: description) - end - - namespace_setting :description, options - route_setting :description, options + namespace_setting :description, opts + route_setting :description, opts end # Returns an object which configures itself via an instance-context DSL. @@ -116,6 +106,19 @@ def config_context.failure(*args) end end end + + private + + def endpoint_configuration + return {} unless defined?(configuration) + + 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 end end end diff --git a/spec/grape/dsl/desc_spec.rb b/spec/grape/dsl/desc_spec.rb index fa3ae7f9e..aa2aa4c33 100644 --- a/spec/grape/dsl/desc_spec.rb +++ b/spec/grape/dsl/desc_spec.rb @@ -81,18 +81,5 @@ expect(subject.namespace_setting(:description)).to eq(expected_options) expect(subject.route_setting(:description)).to eq(expected_options) end - - it 'can be set with options and a block' do - expect(Grape.deprecator).to receive(:warn).with('Passing a options hash and a block to `desc` is deprecated. Move all hash options to block.') - - desc_text = 'The description' - detail_text = 'more details' - options = { message: 'none' } - subject.desc desc_text, options do - detail detail_text - end - expect(subject.namespace_setting(:description)).to eq(description: desc_text, detail: detail_text) - expect(subject.route_setting(:description)).to eq(description: desc_text, detail: detail_text) - end end end From 277fe3e927f6f5174b4c0054fb20333facc31f52 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sun, 6 Oct 2024 20:37:48 +0200 Subject: [PATCH 270/304] Fix leaky modules in specs (#2504) * Replace modules/classes declaration by anonymous modules/classes. * Rename validator spec with suffix validator_spec. Rubocop autocorrect * Fix RSpec/DescribeClass * Fix rubocop * Add CHANGELOG.md --- .rubocop_todo.yml | 42 +- CHANGELOG.md | 1 + .../grape/api/deeply_included_options_spec.rb | 43 +- spec/grape/dsl/callbacks_spec.rb | 64 ++- spec/grape/dsl/headers_spec.rb | 93 ++-- spec/grape/dsl/helpers_spec.rb | 141 +++--- spec/grape/dsl/inside_route_spec.rb | 2 +- spec/grape/dsl/middleware_spec.rb | 82 ++- spec/grape/dsl/parameters_spec.rb | 454 +++++++++-------- spec/grape/dsl/request_response_spec.rb | 362 +++++++------- spec/grape/dsl/routing_spec.rb | 472 +++++++++--------- spec/grape/dsl/settings_spec.rb | 386 +++++++------- spec/grape/named_api_spec.rb | 8 +- spec/grape/path_spec.rb | 352 +++++++------ spec/grape/presenters/presenter_spec.rb | 98 ++-- spec/grape/util/inheritable_setting_spec.rb | 405 ++++++++------- spec/grape/util/inheritable_values_spec.rb | 116 +++-- .../util/reverse_stackable_values_spec.rb | 212 ++++---- spec/grape/util/stackable_values_spec.rb | 200 ++++---- .../util/strict_hash_configuration_spec.rb | 50 +- ..._spec.rb => all_or_none_validator_spec.rb} | 0 ..._spec.rb => allow_blank_validator_spec.rb} | 0 ...c.rb => at_least_one_of_validator_spec.rb} | 0 ...oerce_spec.rb => coerce_validator_spec.rb} | 0 ...ault_spec.rb => default_validator_spec.rb} | 0 ...ec.rb => exactly_one_of_validator_spec.rb} | 0 .../validators/except_values_spec.rb | 192 ------- .../except_values_validator_spec.rb | 194 +++++++ ...ength_spec.rb => length_validator_spec.rb} | 0 ....rb => mutual_exclusion_validator_spec.rb} | 0 ...nce_spec.rb => presence_validator_spec.rb} | 0 ...egexp_spec.rb => regexp_validator_spec.rb} | 0 ...e_as_spec.rb => same_as_validator_spec.rb} | 0 ...alues_spec.rb => values_validator_spec.rb} | 0 34 files changed, 1951 insertions(+), 2018 deletions(-) rename spec/grape/validations/validators/{all_or_none_spec.rb => all_or_none_validator_spec.rb} (100%) rename spec/grape/validations/validators/{allow_blank_spec.rb => allow_blank_validator_spec.rb} (100%) rename spec/grape/validations/validators/{at_least_one_of_spec.rb => at_least_one_of_validator_spec.rb} (100%) rename spec/grape/validations/validators/{coerce_spec.rb => coerce_validator_spec.rb} (100%) rename spec/grape/validations/validators/{default_spec.rb => default_validator_spec.rb} (100%) rename spec/grape/validations/validators/{exactly_one_of_spec.rb => exactly_one_of_validator_spec.rb} (100%) delete mode 100644 spec/grape/validations/validators/except_values_spec.rb create mode 100644 spec/grape/validations/validators/except_values_validator_spec.rb rename spec/grape/validations/validators/{length_spec.rb => length_validator_spec.rb} (100%) rename spec/grape/validations/validators/{mutual_exclusion_spec.rb => mutual_exclusion_validator_spec.rb} (100%) rename spec/grape/validations/validators/{presence_spec.rb => presence_validator_spec.rb} (100%) rename spec/grape/validations/validators/{regexp_spec.rb => regexp_validator_spec.rb} (100%) rename spec/grape/validations/validators/{same_as_spec.rb => same_as_validator_spec.rb} (100%) rename spec/grape/validations/validators/{values_spec.rb => values_validator_spec.rb} (100%) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 81384c026..11eb9f51e 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,18 +1,11 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2024-07-23 11:24:53 UTC using RuboCop version 1.64.1. +# on 2024-10-06 16:00:59 UTC using RuboCop version 1.66.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 1 -# Configuration parameters: AllowedMethods. -# AllowedMethods: enums -Lint/ConstantDefinitionInBlock: - Exclude: - - 'spec/grape/validations/validators/except_values_spec.rb' - # Offense count: 1 # Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns. Metrics/MethodLength: @@ -29,17 +22,6 @@ Naming/VariableNumber: - 'spec/grape/exceptions/validation_errors_spec.rb' - 'spec/grape/validations_spec.rb' -# Offense count: 1 -# Configuration parameters: IgnoredMetadata. -RSpec/DescribeClass: - Exclude: - - '**/spec/features/**/*' - - '**/spec/requests/**/*' - - '**/spec/routing/**/*' - - '**/spec/system/**/*' - - '**/spec/views/**/*' - - 'spec/grape/named_api_spec.rb' - # Offense count: 2 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: SkipBlocks, EnforcedStyle, OnlyStaticConstants. @@ -68,7 +50,7 @@ RSpec/ExpectActual: # Offense count: 1 RSpec/ExpectInHook: Exclude: - - 'spec/grape/validations/validators/values_spec.rb' + - 'spec/grape/validations/validators/values_validator_spec.rb' # Offense count: 6 # Configuration parameters: Max, AllowedIdentifiers, AllowedPatterns. @@ -78,7 +60,7 @@ RSpec/IndexedLet: - 'spec/grape/presenters/presenter_spec.rb' - 'spec/shared/versioning_examples.rb' -# Offense count: 39 +# Offense count: 38 # Configuration parameters: AssignmentOnly. RSpec/InstanceVariable: Exclude: @@ -87,12 +69,6 @@ RSpec/InstanceVariable: - 'spec/grape/middleware/base_spec.rb' - 'spec/grape/middleware/versioner/accept_version_header_spec.rb' - 'spec/grape/middleware/versioner/header_spec.rb' - - 'spec/grape/validations/validators/except_values_spec.rb' - -# Offense count: 6 -RSpec/LeakyConstantDeclaration: - Exclude: - - 'spec/grape/validations/validators/except_values_spec.rb' # Offense count: 1 RSpec/MessageChain: @@ -109,14 +85,14 @@ RSpec/RepeatedDescription: Exclude: - 'spec/grape/api_spec.rb' - 'spec/grape/endpoint_spec.rb' - - 'spec/grape/validations/validators/allow_blank_spec.rb' - - 'spec/grape/validations/validators/values_spec.rb' + - 'spec/grape/validations/validators/allow_blank_validator_spec.rb' + - 'spec/grape/validations/validators/values_validator_spec.rb' # Offense count: 6 RSpec/RepeatedExample: Exclude: - 'spec/grape/middleware/versioner/accept_version_header_spec.rb' - - 'spec/grape/validations/validators/allow_blank_spec.rb' + - 'spec/grape/validations/validators/allow_blank_validator_spec.rb' # Offense count: 10 RSpec/RepeatedExampleGroupDescription: @@ -124,7 +100,7 @@ RSpec/RepeatedExampleGroupDescription: - 'spec/grape/api_spec.rb' - 'spec/grape/endpoint_spec.rb' - 'spec/grape/util/inheritable_setting_spec.rb' - - 'spec/grape/validations/validators/values_spec.rb' + - 'spec/grape/validations/validators/values_validator_spec.rb' # Offense count: 4 RSpec/StubbedMock: @@ -133,7 +109,7 @@ RSpec/StubbedMock: - 'spec/grape/dsl/routing_spec.rb' - 'spec/grape/middleware/formatter_spec.rb' -# Offense count: 121 +# Offense count: 118 RSpec/SubjectStub: Exclude: - 'spec/grape/api_spec.rb' @@ -150,7 +126,7 @@ RSpec/SubjectStub: - 'spec/grape/middleware/globals_spec.rb' - 'spec/grape/middleware/stack_spec.rb' -# Offense count: 23 +# Offense count: 22 # Configuration parameters: IgnoreNameless, IgnoreSymbolicNames. RSpec/VerifiedDoubles: Exclude: diff --git a/CHANGELOG.md b/CHANGELOG.md index 344f34934..e5d85dfec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ #### Fixes +* [#2504](https://github.com/ruby-grape/grape/pull/2504): Fix leaky modules in specs - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 2.2.0 (2024-09-14) diff --git a/spec/grape/api/deeply_included_options_spec.rb b/spec/grape/api/deeply_included_options_spec.rb index 08084811b..940e11560 100644 --- a/spec/grape/api/deeply_included_options_spec.rb +++ b/spec/grape/api/deeply_included_options_spec.rb @@ -1,21 +1,17 @@ # frozen_string_literal: true -module DeeplyIncludedOptionsSpec - module Defaults - extend ActiveSupport::Concern - included do - format :json +describe Grape::API do + let(:app) do + main_api = api + Class.new(Grape::API) do + mount main_api end end - module Admin - module Defaults - extend ActiveSupport::Concern - include DeeplyIncludedOptionsSpec::Defaults - end - - class Users < Grape::API - include DeeplyIncludedOptionsSpec::Admin::Defaults + let(:api) do + deeply_included_options = options + Class.new(Grape::API) do + include deeply_included_options resource :users do get do @@ -25,16 +21,21 @@ class Users < Grape::API end end - class Main < Grape::API - mount DeeplyIncludedOptionsSpec::Admin::Users + let(:options) do + deep_included_options_default = default + Module.new do + extend ActiveSupport::Concern + include deep_included_options_default + end end -end - -describe Grape::API do - subject { DeeplyIncludedOptionsSpec::Main } - def app - subject + let(:default) do + Module.new do + extend ActiveSupport::Concern + included do + format :json + end + end end it 'works for unspecified format' do diff --git a/spec/grape/dsl/callbacks_spec.rb b/spec/grape/dsl/callbacks_spec.rb index bec2d823f..7fc444fe2 100644 --- a/spec/grape/dsl/callbacks_spec.rb +++ b/spec/grape/dsl/callbacks_spec.rb @@ -1,45 +1,41 @@ # frozen_string_literal: true -module Grape - module DSL - module CallbacksSpec - class Dummy - include Grape::DSL::Callbacks - end - end +describe Grape::DSL::Callbacks do + subject { dummy_class } - describe Callbacks do - subject { Class.new(CallbacksSpec::Dummy) } + let(:dummy_class) do + Class.new do + include Grape::DSL::Callbacks + end + end - let(:proc) { -> {} } + let(:proc) { -> {} } - describe '.before' do - it 'adds a block to "before"' do - expect(subject).to receive(:namespace_stackable).with(:befores, proc) - subject.before(&proc) - end - end + describe '.before' do + it 'adds a block to "before"' do + expect(subject).to receive(:namespace_stackable).with(:befores, proc) + subject.before(&proc) + end + end - describe '.before_validation' do - it 'adds a block to "before_validation"' do - expect(subject).to receive(:namespace_stackable).with(:before_validations, proc) - subject.before_validation(&proc) - end - end + describe '.before_validation' do + it 'adds a block to "before_validation"' do + expect(subject).to receive(:namespace_stackable).with(:before_validations, proc) + subject.before_validation(&proc) + end + end - describe '.after_validation' do - it 'adds a block to "after_validation"' do - expect(subject).to receive(:namespace_stackable).with(:after_validations, proc) - subject.after_validation(&proc) - end - end + describe '.after_validation' do + it 'adds a block to "after_validation"' do + expect(subject).to receive(:namespace_stackable).with(:after_validations, proc) + subject.after_validation(&proc) + end + end - describe '.after' do - it 'adds a block to "after"' do - expect(subject).to receive(:namespace_stackable).with(:afters, proc) - subject.after(&proc) - end - end + describe '.after' do + it 'adds a block to "after"' do + expect(subject).to receive(:namespace_stackable).with(:afters, proc) + subject.after(&proc) end end end diff --git a/spec/grape/dsl/headers_spec.rb b/spec/grape/dsl/headers_spec.rb index 154fbf82b..1502176bd 100644 --- a/spec/grape/dsl/headers_spec.rb +++ b/spec/grape/dsl/headers_spec.rb @@ -1,62 +1,59 @@ # frozen_string_literal: true -module Grape - module DSL - module HeadersSpec - class Dummy - include Grape::DSL::Headers - end +describe Grape::DSL::Headers do + subject { dummy_class.new } + + let(:dummy_class) do + Class.new do + include Grape::DSL::Headers end - describe Headers do - subject { HeadersSpec::Dummy.new } + end + + let(:header_data) do + { 'first key' => 'First Value', + 'second key' => 'Second Value' } + end + + context 'when headers are set' do + describe '#header' do + before do + header_data.each { |k, v| subject.header(k, v) } + end - let(:header_data) do - { 'first key' => 'First Value', - 'second key' => 'Second Value' } + describe 'get' do + it 'returns a specifc value' do + expect(subject.header['first key']).to eq 'First Value' + expect(subject.header['second key']).to eq 'Second Value' + end + + it 'returns all set headers' do + expect(subject.header).to eq header_data + expect(subject.headers).to eq header_data + end end - context 'when headers are set' do - describe '#header' do - before do - header_data.each { |k, v| subject.header(k, v) } - end - - describe 'get' do - it 'returns a specifc value' do - expect(subject.header['first key']).to eq 'First Value' - expect(subject.header['second key']).to eq 'Second Value' - end - - it 'returns all set headers' do - expect(subject.header).to eq header_data - expect(subject.headers).to eq header_data - end - end - - describe 'set' do - it 'returns value' do - expect(subject.header('third key', 'Third Value')) - expect(subject.header['third key']).to eq 'Third Value' - end - end - - describe 'delete' do - it 'deletes a header key-value pair' do - expect(subject.header('first key')).to eq header_data['first key'] - expect(subject.header).not_to have_key('first key') - end - end + describe 'set' do + it 'returns value' do + expect(subject.header('third key', 'Third Value')) + expect(subject.header['third key']).to eq 'Third Value' end end - context 'when no headers are set' do - describe '#header' do - it 'returns nil' do - expect(subject.header['first key']).to be_nil - expect(subject.header('first key')).to be_nil - end + describe 'delete' do + it 'deletes a header key-value pair' do + expect(subject.header('first key')).to eq header_data['first key'] + expect(subject.header).not_to have_key('first key') end end end end + + context 'when no headers are set' do + describe '#header' do + it 'returns nil' do + expect(subject.header['first key']).to be_nil + expect(subject.header('first key')).to be_nil + end + end + end end diff --git a/spec/grape/dsl/helpers_spec.rb b/spec/grape/dsl/helpers_spec.rb index ce122e953..1c4d539ee 100644 --- a/spec/grape/dsl/helpers_spec.rb +++ b/spec/grape/dsl/helpers_spec.rb @@ -1,100 +1,103 @@ # frozen_string_literal: true -module Grape - module DSL - module HelpersSpec - class Dummy - include Grape::DSL::Helpers - - def self.mods - namespace_stackable(:helpers) - end +describe Grape::DSL::Helpers do + subject { dummy_class } - def self.first_mod - mods.first - end - end - end + let(:dummy_class) do + Class.new do + include Grape::DSL::Helpers - module BooleanParam - extend Grape::API::Helpers + def self.mods + namespace_stackable(:helpers) + end - params :requires_toggle_prm do - requires :toggle_prm, type: Boolean + def self.first_mod + mods.first end end + end - class Base < Grape::API - helpers BooleanParam + let(:proc) do + lambda do |*| + def test + :test + end end + end - class Child < Base; end + describe '.helpers' do + it 'adds a module with the given block' do + expect(subject).to receive(:namespace_stackable).with(:helpers, kind_of(Grape::DSL::Helpers::BaseHelper)).and_call_original + expect(subject).to receive(:namespace_stackable).with(:helpers).and_call_original + subject.helpers(&proc) - describe Helpers do - subject { Class.new(HelpersSpec::Dummy) } + expect(subject.first_mod.instance_methods).to include(:test) + end - let(:proc) do - lambda do |*| - def test - :test - end - end - end + it 'uses provided modules' do + mod = Module.new - describe '.helpers' do - it 'adds a module with the given block' do - expect(subject).to receive(:namespace_stackable).with(:helpers, kind_of(Grape::DSL::Helpers::BaseHelper)).and_call_original - expect(subject).to receive(:namespace_stackable).with(:helpers).and_call_original - subject.helpers(&proc) + expect(subject).to receive(:namespace_stackable).with(:helpers, kind_of(Grape::DSL::Helpers::BaseHelper)).and_call_original.twice + expect(subject).to receive(:namespace_stackable).with(:helpers).and_call_original + subject.helpers(mod, &proc) - expect(subject.first_mod.instance_methods).to include(:test) - end - - it 'uses provided modules' do - mod = Module.new + expect(subject.first_mod).to eq mod + end - expect(subject).to receive(:namespace_stackable).with(:helpers, kind_of(Grape::DSL::Helpers::BaseHelper)).and_call_original.twice - expect(subject).to receive(:namespace_stackable).with(:helpers).and_call_original - subject.helpers(mod, &proc) + it 'uses many provided modules' do + mod = Module.new + mod2 = Module.new + mod3 = Module.new - expect(subject.first_mod).to eq mod - end + expect(subject).to receive(:namespace_stackable).with(:helpers, kind_of(Grape::DSL::Helpers::BaseHelper)).and_call_original.exactly(4).times + expect(subject).to receive(:namespace_stackable).with(:helpers).and_call_original.exactly(3).times - it 'uses many provided modules' do - mod = Module.new - mod2 = Module.new - mod3 = Module.new + subject.helpers(mod, mod2, mod3, &proc) - expect(subject).to receive(:namespace_stackable).with(:helpers, kind_of(Grape::DSL::Helpers::BaseHelper)).and_call_original.exactly(4).times - expect(subject).to receive(:namespace_stackable).with(:helpers).and_call_original.exactly(3).times + expect(subject.mods).to include(mod) + expect(subject.mods).to include(mod2) + expect(subject.mods).to include(mod3) + end - subject.helpers(mod, mod2, mod3, &proc) + context 'with an external file' do + let(:boolean_helper) do + Module.new do + extend Grape::API::Helpers - expect(subject.mods).to include(mod) - expect(subject.mods).to include(mod2) - expect(subject.mods).to include(mod3) + params :requires_toggle_prm do + requires :toggle_prm, type: Boolean + end end + end - context 'with an external file' do - it 'sets Boolean as a Grape::API::Boolean' do - subject.helpers BooleanParam - expect(subject.first_mod::Boolean).to eq Grape::API::Boolean + it 'sets Boolean as a Grape::API::Boolean' do + subject.helpers boolean_helper + expect(subject.first_mod::Boolean).to eq Grape::API::Boolean + end + end + + context 'in child classes' do + let(:base_class) do + Class.new(Grape::API) do + helpers do + params :requires_toggle_prm do + requires :toggle_prm, type: Integer + end end end + end - context 'in child classes' do - it 'is available' do - klass = Child - expect do - klass.instance_eval do - params do - use :requires_toggle_prm - end - end - end.not_to raise_exception + let(:api_class) do + Class.new(base_class) do + params do + use :requires_toggle_prm end end end + + it 'is available' do + expect { api_class }.not_to raise_exception + end end end end diff --git a/spec/grape/dsl/inside_route_spec.rb b/spec/grape/dsl/inside_route_spec.rb index 038e20314..ea8d6d987 100644 --- a/spec/grape/dsl/inside_route_spec.rb +++ b/spec/grape/dsl/inside_route_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe Grape::Endpoint do +describe Grape::DSL::InsideRoute do subject { dummy_class.new } let(:dummy_class) do diff --git a/spec/grape/dsl/middleware_spec.rb b/spec/grape/dsl/middleware_spec.rb index 8413eaaaf..1b363697a 100644 --- a/spec/grape/dsl/middleware_spec.rb +++ b/spec/grape/dsl/middleware_spec.rb @@ -1,60 +1,56 @@ # frozen_string_literal: true -module Grape - module DSL - module MiddlewareSpec - class Dummy - include Grape::DSL::Middleware - end - end +describe Grape::DSL::Middleware do + subject { dummy_class } - describe Middleware do - subject { Class.new(MiddlewareSpec::Dummy) } + let(:dummy_class) do + Class.new do + include Grape::DSL::Middleware + end + end - let(:proc) { -> {} } - let(:foo_middleware) { Class.new } - let(:bar_middleware) { Class.new } + let(:proc) { -> {} } + let(:foo_middleware) { Class.new } + let(:bar_middleware) { Class.new } - describe '.use' do - it 'adds a middleware with the right operation' do - expect(subject).to receive(:namespace_stackable).with(:middleware, [:use, foo_middleware, :arg1, proc]) + describe '.use' do + it 'adds a middleware with the right operation' do + expect(subject).to receive(:namespace_stackable).with(:middleware, [:use, foo_middleware, :arg1, proc]) - subject.use foo_middleware, :arg1, &proc - end - end + subject.use foo_middleware, :arg1, &proc + 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]) + 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 + 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]) + 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]) - subject.insert_before foo_middleware, :arg1, &proc - end - end + subject.insert_before foo_middleware, :arg1, &proc + end + end - describe '.insert_after' do - it 'adds a middleware with the right operation' do - expect(subject).to receive(:namespace_stackable).with(:middleware, [:insert_after, foo_middleware, :arg1, proc]) + describe '.insert_after' do + it 'adds a middleware with the right operation' do + expect(subject).to receive(:namespace_stackable).with(:middleware, [:insert_after, foo_middleware, :arg1, proc]) - subject.insert_after foo_middleware, :arg1, &proc - end - end + subject.insert_after foo_middleware, :arg1, &proc + end + end - describe '.middleware' do - it 'returns the middleware stack' do - subject.use foo_middleware, :arg1, &proc - subject.insert_before bar_middleware, :arg1, :arg2 + describe '.middleware' do + it 'returns the middleware stack' do + subject.use foo_middleware, :arg1, &proc + subject.insert_before bar_middleware, :arg1, :arg2 - expect(subject.middleware).to eq [[:use, foo_middleware, :arg1, proc], [:insert_before, bar_middleware, :arg1, :arg2]] - end - end + expect(subject.middleware).to eq [[:use, foo_middleware, :arg1, proc], [:insert_before, bar_middleware, :arg1, :arg2]] end end end diff --git a/spec/grape/dsl/parameters_spec.rb b/spec/grape/dsl/parameters_spec.rb index 97aae0a58..28ff1cce0 100644 --- a/spec/grape/dsl/parameters_spec.rb +++ b/spec/grape/dsl/parameters_spec.rb @@ -1,289 +1,285 @@ # frozen_string_literal: true -module Grape - module DSL - module ParametersSpec - class Dummy - include Grape::DSL::Parameters - attr_accessor :api, :element, :parent - - def initialize - @validate_attributes = [] - end - - def validate_attributes(*args) - @validate_attributes.push(*args) - end +describe Grape::DSL::Parameters do + subject { dummy_class.new } - def validate_attributes_reader - @validate_attributes - end + let(:dummy_class) do + Class.new do + include Grape::DSL::Parameters + attr_accessor :api, :element, :parent - def push_declared_params(args, **_opts) - @push_declared_params = args - end + def initialize + @validate_attributes = [] + end - def push_declared_params_reader - @push_declared_params - end + def validate_attributes(*args) + @validate_attributes.push(*args) + end - def validates(*args) - @validates = *args - end + def validate_attributes_reader + @validate_attributes + end - def validates_reader - @validates - end + def push_declared_params(args, **_opts) + @push_declared_params = args + end - def new_scope(args, _, &block) - nested_scope = self.class.new - nested_scope.new_group_scope(args, &block) - nested_scope - end + def push_declared_params_reader + @push_declared_params + end - def new_group_scope(args) - prev_group = @group - @group = args.clone.first - yield - @group = prev_group - end + def validates(*args) + @validates = *args + end - def extract_message_option(attrs) - return nil unless attrs.is_a?(Array) + def validates_reader + @validates + end - opts = attrs.last.is_a?(Hash) ? attrs.pop : {} - opts.key?(:message) && !opts[:message].nil? ? opts.delete(:message) : nil - end + def new_scope(args, _, &block) + nested_scope = self.class.new + nested_scope.new_group_scope(args, &block) + nested_scope end - end - describe Parameters do - subject { ParametersSpec::Dummy.new } + def new_group_scope(args) + prev_group = @group + @group = args.clone.first + yield + @group = prev_group + end - describe '#use' do - before do - allow_message_expectations_on_nil - allow(subject.api).to receive(:namespace_stackable).with(:named_params) - end + def extract_message_option(attrs) + return nil unless attrs.is_a?(Array) - let(:options) { { option: 'value' } } - let(:named_params) { { params_group: proc {} } } + opts = attrs.last.is_a?(Hash) ? attrs.pop : {} + opts.key?(:message) && !opts[:message].nil? ? opts.delete(:message) : nil + end + end + end - it 'calls processes associated with named params' do - allow(subject.api).to receive(:namespace_stackable_with_hash).and_return(named_params) - expect(subject).to receive(:instance_exec).with(options).and_yield - subject.use :params_group, options - end + describe '#use' do + before do + allow_message_expectations_on_nil + allow(subject.api).to receive(:namespace_stackable).with(:named_params) + end - it 'raises error when non-existent named param is called' do - allow(subject.api).to receive(:namespace_stackable_with_hash).and_return({}) - expect { subject.use :params_group }.to raise_error('Params :params_group not found!') - end - end + let(:options) { { option: 'value' } } + let(:named_params) { { params_group: proc {} } } - describe '#use_scope' do - it 'is alias to #use' do - expect(subject.method(:use_scope)).to eq subject.method(:use) - end - end + it 'calls processes associated with named params' do + allow(subject.api).to receive(:namespace_stackable_with_hash).and_return(named_params) + expect(subject).to receive(:instance_exec).with(options).and_yield + subject.use :params_group, options + end - describe '#includes' do - it 'is alias to #use' do - expect(subject.method(:includes)).to eq subject.method(:use) - end - end + it 'raises error when non-existent named param is called' do + allow(subject.api).to receive(:namespace_stackable_with_hash).and_return({}) + expect { subject.use :params_group }.to raise_error('Params :params_group not found!') + end + end - describe '#requires' do - it 'adds a required parameter' do - subject.requires :id, type: Integer, desc: 'Identity.' + describe '#use_scope' do + it 'is alias to #use' do + expect(subject.method(:use_scope)).to eq subject.method(:use) + end + end - expect(subject.validate_attributes_reader).to eq([[:id], { type: Integer, desc: 'Identity.', presence: { value: true, message: nil } }]) - expect(subject.push_declared_params_reader).to eq([:id]) - end - end + describe '#includes' do + it 'is alias to #use' do + expect(subject.method(:includes)).to eq subject.method(:use) + end + end - describe '#optional' do - it 'adds an optional parameter' do - subject.optional :id, type: Integer, desc: 'Identity.' + describe '#requires' do + it 'adds a required parameter' do + subject.requires :id, type: Integer, desc: 'Identity.' - expect(subject.validate_attributes_reader).to eq([[:id], { type: Integer, desc: 'Identity.' }]) - expect(subject.push_declared_params_reader).to eq([:id]) - end - end + expect(subject.validate_attributes_reader).to eq([[:id], { type: Integer, desc: 'Identity.', presence: { value: true, message: nil } }]) + expect(subject.push_declared_params_reader).to eq([:id]) + end + end - describe '#with' do - it 'creates a scope with group attributes' do - subject.with(type: Integer) { subject.optional :id, desc: 'Identity.' } + describe '#optional' do + it 'adds an optional parameter' do + subject.optional :id, type: Integer, desc: 'Identity.' - expect(subject.validate_attributes_reader).to eq([[:id], { type: Integer, desc: 'Identity.' }]) - expect(subject.push_declared_params_reader).to eq([:id]) - end + expect(subject.validate_attributes_reader).to eq([[:id], { type: Integer, desc: 'Identity.' }]) + expect(subject.push_declared_params_reader).to eq([:id]) + end + end - it 'merges the group attributes' do - subject.with(documentation: { in: 'body' }) { subject.optional :vault, documentation: { default: 33 } } + describe '#with' do + it 'creates a scope with group attributes' do + subject.with(type: Integer) { subject.optional :id, desc: 'Identity.' } - expect(subject.validate_attributes_reader).to eq([[:vault], { documentation: { in: 'body', default: 33 } }]) - expect(subject.push_declared_params_reader).to eq([:vault]) - end + expect(subject.validate_attributes_reader).to eq([[:id], { type: Integer, desc: 'Identity.' }]) + expect(subject.push_declared_params_reader).to eq([:id]) + end - it 'overrides the group attribute when values not mergable' do - subject.with(type: Integer, documentation: { in: 'body', default: 33 }) do - subject.optional :vault - subject.optional :allowed_vaults, type: [Integer], documentation: { default: [31, 32, 33], is_array: true } - end + it 'merges the group attributes' do + subject.with(documentation: { in: 'body' }) { subject.optional :vault, documentation: { default: 33 } } - expect(subject.validate_attributes_reader).to eq( - [ - [:vault], { type: Integer, documentation: { in: 'body', default: 33 } }, - [:allowed_vaults], { type: [Integer], documentation: { in: 'body', default: [31, 32, 33], is_array: true } } - ] - ) - end + expect(subject.validate_attributes_reader).to eq([[:vault], { documentation: { in: 'body', default: 33 } }]) + expect(subject.push_declared_params_reader).to eq([:vault]) + end - it 'allows a primitive type attribite to overwrite a complex type group attribute' do - subject.with(documentation: { x: { nullable: true } }) do - subject.optional :vault, type: Integer, documentation: { x: nil } - end + it 'overrides the group attribute when values not mergable' do + subject.with(type: Integer, documentation: { in: 'body', default: 33 }) do + subject.optional :vault + subject.optional :allowed_vaults, type: [Integer], documentation: { default: [31, 32, 33], is_array: true } + end - expect(subject.validate_attributes_reader).to eq( - [ - [:vault], { type: Integer, documentation: { x: nil } } - ] - ) - end + expect(subject.validate_attributes_reader).to eq( + [ + [:vault], { type: Integer, documentation: { in: 'body', default: 33 } }, + [:allowed_vaults], { type: [Integer], documentation: { in: 'body', default: [31, 32, 33], is_array: true } } + ] + ) + end - it 'does not nest primitives inside existing complex types erroneously' do - subject.with(type: Hash, documentation: { default: { vault: '33' } }) do - subject.optional :info - subject.optional :role, type: String, documentation: { default: 'resident' } - end + it 'allows a primitive type attribite to overwrite a complex type group attribute' do + subject.with(documentation: { x: { nullable: true } }) do + subject.optional :vault, type: Integer, documentation: { x: nil } + end - expect(subject.validate_attributes_reader).to eq( - [ - [:info], { type: Hash, documentation: { default: { vault: '33' } } }, - [:role], { type: String, documentation: { default: 'resident' } } - ] - ) - end + expect(subject.validate_attributes_reader).to eq( + [ + [:vault], { type: Integer, documentation: { x: nil } } + ] + ) + end - it 'merges deeply nested attributes' do - subject.with(documentation: { details: { in: 'body', hidden: false } }) do - subject.optional :vault, documentation: { details: { desc: 'The vault number' } } - end + it 'does not nest primitives inside existing complex types erroneously' do + subject.with(type: Hash, documentation: { default: { vault: '33' } }) do + subject.optional :info + subject.optional :role, type: String, documentation: { default: 'resident' } + end - expect(subject.validate_attributes_reader).to eq( - [ - [:vault], { documentation: { details: { in: 'body', hidden: false, desc: 'The vault number' } } } - ] - ) - end + expect(subject.validate_attributes_reader).to eq( + [ + [:info], { type: Hash, documentation: { default: { vault: '33' } } }, + [:role], { type: String, documentation: { default: 'resident' } } + ] + ) + end - it "supports nested 'with' calls" do - subject.with(type: Integer, documentation: { in: 'body' }) do - subject.optional :pipboy_id - subject.with(documentation: { default: 33 }) do - subject.optional :vault - subject.with(type: String) do - subject.with(documentation: { default: 'resident' }) do - subject.optional :role - end - end - subject.optional :age, documentation: { default: 42 } - end - end + it 'merges deeply nested attributes' do + subject.with(documentation: { details: { in: 'body', hidden: false } }) do + subject.optional :vault, documentation: { details: { desc: 'The vault number' } } + end - expect(subject.validate_attributes_reader).to eq( - [ - [:pipboy_id], { type: Integer, documentation: { in: 'body' } }, - [:vault], { type: Integer, documentation: { in: 'body', default: 33 } }, - [:role], { type: String, documentation: { in: 'body', default: 'resident' } }, - [:age], { type: Integer, documentation: { in: 'body', default: 42 } } - ] - ) - end + expect(subject.validate_attributes_reader).to eq( + [ + [:vault], { documentation: { details: { in: 'body', hidden: false, desc: 'The vault number' } } } + ] + ) + end - it "supports Hash parameter inside the 'with' calls" do - subject.with(documentation: { in: 'body' }) do - subject.optional :info, type: Hash, documentation: { x: { nullable: true }, desc: 'The info' } do - subject.optional :vault, type: Integer, documentation: { default: 33, desc: 'The vault number' } + it "supports nested 'with' calls" do + subject.with(type: Integer, documentation: { in: 'body' }) do + subject.optional :pipboy_id + subject.with(documentation: { default: 33 }) do + subject.optional :vault + subject.with(type: String) do + subject.with(documentation: { default: 'resident' }) do + subject.optional :role end end - - expect(subject.validate_attributes_reader).to eq( - [ - [:info], { type: Hash, documentation: { in: 'body', desc: 'The info', x: { nullable: true } } }, - [:vault], { type: Integer, documentation: { in: 'body', default: 33, desc: 'The vault number' } } - ] - ) + subject.optional :age, documentation: { default: 42 } end end - describe '#mutually_exclusive' do - it 'adds an mutally exclusive parameter validation' do - subject.mutually_exclusive :media, :audio + expect(subject.validate_attributes_reader).to eq( + [ + [:pipboy_id], { type: Integer, documentation: { in: 'body' } }, + [:vault], { type: Integer, documentation: { in: 'body', default: 33 } }, + [:role], { type: String, documentation: { in: 'body', default: 'resident' } }, + [:age], { type: Integer, documentation: { in: 'body', default: 42 } } + ] + ) + end - expect(subject.validates_reader).to eq([%i[media audio], { mutual_exclusion: { value: true, message: nil } }]) + it "supports Hash parameter inside the 'with' calls" do + subject.with(documentation: { in: 'body' }) do + subject.optional :info, type: Hash, documentation: { x: { nullable: true }, desc: 'The info' } do + subject.optional :vault, type: Integer, documentation: { default: 33, desc: 'The vault number' } end end - describe '#exactly_one_of' do - it 'adds an exactly of one parameter validation' do - subject.exactly_one_of :media, :audio + expect(subject.validate_attributes_reader).to eq( + [ + [:info], { type: Hash, documentation: { in: 'body', desc: 'The info', x: { nullable: true } } }, + [:vault], { type: Integer, documentation: { in: 'body', default: 33, desc: 'The vault number' } } + ] + ) + end + end - expect(subject.validates_reader).to eq([%i[media audio], { exactly_one_of: { value: true, message: nil } }]) - end - end + describe '#mutually_exclusive' do + it 'adds an mutally exclusive parameter validation' do + subject.mutually_exclusive :media, :audio - describe '#at_least_one_of' do - it 'adds an at least one of parameter validation' do - subject.at_least_one_of :media, :audio + expect(subject.validates_reader).to eq([%i[media audio], { mutual_exclusion: { value: true, message: nil } }]) + end + end - expect(subject.validates_reader).to eq([%i[media audio], { at_least_one_of: { value: true, message: nil } }]) - end - end + describe '#exactly_one_of' do + it 'adds an exactly of one parameter validation' do + subject.exactly_one_of :media, :audio - describe '#all_or_none_of' do - it 'adds an all or none of parameter validation' do - subject.all_or_none_of :media, :audio + expect(subject.validates_reader).to eq([%i[media audio], { exactly_one_of: { value: true, message: nil } }]) + end + end - expect(subject.validates_reader).to eq([%i[media audio], { all_or_none_of: { value: true, message: nil } }]) - end - end + describe '#at_least_one_of' do + it 'adds an at least one of parameter validation' do + subject.at_least_one_of :media, :audio - describe '#group' do - it 'is alias to #requires' do - expect(subject.method(:group)).to eq subject.method(:requires) - end - end + expect(subject.validates_reader).to eq([%i[media audio], { at_least_one_of: { value: true, message: nil } }]) + end + end - describe '#params' do - it 'inherits params from parent' do - parent_params = { foo: 'bar' } - subject.parent = Object.new - allow(subject.parent).to receive(:params).and_return(parent_params) - expect(subject.params({})).to eq parent_params - end + describe '#all_or_none_of' do + it 'adds an all or none of parameter validation' do + subject.all_or_none_of :media, :audio - describe 'when params argument is an array of hashes' do - it 'returns values of each hash for @element key' do - subject.element = :foo - expect(subject.params([{ foo: 'bar' }, { foo: 'baz' }])).to eq(%w[bar baz]) - end - end + expect(subject.validates_reader).to eq([%i[media audio], { all_or_none_of: { value: true, message: nil } }]) + end + end - describe 'when params argument is a hash' do - it 'returns value for @element key' do - subject.element = :foo - expect(subject.params(foo: 'bar')).to eq('bar') - end - end + describe '#group' do + it 'is alias to #requires' do + expect(subject.method(:group)).to eq subject.method(:requires) + end + end - describe 'when params argument is not a array or a hash' do - it 'returns empty hash' do - subject.element = Object.new - expect(subject.params(Object.new)).to eq({}) - end - end + describe '#params' do + it 'inherits params from parent' do + parent_params = { foo: 'bar' } + subject.parent = Object.new + allow(subject.parent).to receive(:params).and_return(parent_params) + expect(subject.params({})).to eq parent_params + end + + describe 'when params argument is an array of hashes' do + it 'returns values of each hash for @element key' do + subject.element = :foo + expect(subject.params([{ foo: 'bar' }, { foo: 'baz' }])).to eq(%w[bar baz]) + end + end + + describe 'when params argument is a hash' do + it 'returns value for @element key' do + subject.element = :foo + expect(subject.params(foo: 'bar')).to eq('bar') + end + end + + describe 'when params argument is not a array or a hash' do + it 'returns empty hash' do + subject.element = Object.new + expect(subject.params(Object.new)).to eq({}) end end end diff --git a/spec/grape/dsl/request_response_spec.rb b/spec/grape/dsl/request_response_spec.rb index 832ceafcf..24bedc65b 100644 --- a/spec/grape/dsl/request_response_spec.rb +++ b/spec/grape/dsl/request_response_spec.rb @@ -1,225 +1,221 @@ # frozen_string_literal: true -module Grape - module DSL - module RequestResponseSpec - class Dummy - include Grape::DSL::RequestResponse +describe Grape::DSL::RequestResponse do + subject { dummy_class } - def self.set(key, value) - settings[key.to_sym] = value - end + let(:dummy_class) do + Class.new do + include Grape::DSL::RequestResponse - def self.imbue(key, value) - settings.imbue(key, value) - end + def self.set(key, value) + settings[key.to_sym] = value + end + + def self.imbue(key, value) + settings.imbue(key, value) end end + end - describe RequestResponse do - subject { Class.new(RequestResponseSpec::Dummy) } + let(:c_type) { 'application/json' } + let(:format) { 'txt' } - let(:c_type) { 'application/json' } - let(:format) { 'txt' } + describe '.default_format' do + it 'sets the default format' do + expect(subject).to receive(:namespace_inheritable).with(:default_format, :format) + subject.default_format :format + end - describe '.default_format' do - it 'sets the default format' do - expect(subject).to receive(:namespace_inheritable).with(:default_format, :format) - subject.default_format :format - end + it 'returns the format without paramter' do + subject.default_format :format - it 'returns the format without paramter' do - subject.default_format :format + expect(subject.default_format).to eq :format + end + end - expect(subject.default_format).to eq :format - end - end + describe '.format' do + it 'sets a new format' do + expect(subject).to receive(:namespace_inheritable).with(:format, format.to_sym) + expect(subject).to receive(:namespace_inheritable).with(:default_error_formatter, Grape::ErrorFormatter::Txt) - describe '.format' do - it 'sets a new format' do - expect(subject).to receive(:namespace_inheritable).with(:format, format.to_sym) - expect(subject).to receive(:namespace_inheritable).with(:default_error_formatter, Grape::ErrorFormatter::Txt) + subject.format format + end + end - subject.format format - end - end + describe '.formatter' do + it 'sets the formatter for a content type' do + expect(subject).to receive(:namespace_stackable).with(:formatters, c_type.to_sym => :formatter) + subject.formatter c_type, :formatter + end + end - describe '.formatter' do - it 'sets the formatter for a content type' do - expect(subject).to receive(:namespace_stackable).with(:formatters, c_type.to_sym => :formatter) - subject.formatter c_type, :formatter - end - end + describe '.parser' do + it 'sets a parser for a content type' do + expect(subject).to receive(:namespace_stackable).with(:parsers, c_type.to_sym => :parser) + subject.parser c_type, :parser + end + end - describe '.parser' do - it 'sets a parser for a content type' do - expect(subject).to receive(:namespace_stackable).with(:parsers, c_type.to_sym => :parser) - subject.parser c_type, :parser - end - end + describe '.default_error_formatter' do + it 'sets a new error formatter' do + expect(subject).to receive(:namespace_inheritable).with(:default_error_formatter, Grape::ErrorFormatter::Json) + subject.default_error_formatter :json + end + end - describe '.default_error_formatter' do - it 'sets a new error formatter' do - expect(subject).to receive(:namespace_inheritable).with(:default_error_formatter, Grape::ErrorFormatter::Json) - subject.default_error_formatter :json - end - end + describe '.error_formatter' do + it 'sets a error_formatter' do + format = 'txt' + expect(subject).to receive(:namespace_stackable).with(:error_formatters, format.to_sym => :error_formatter) + subject.error_formatter format, :error_formatter + end - describe '.error_formatter' do - it 'sets a error_formatter' do - format = 'txt' - expect(subject).to receive(:namespace_stackable).with(:error_formatters, format.to_sym => :error_formatter) - subject.error_formatter format, :error_formatter - end + it 'understands syntactic sugar' do + expect(subject).to receive(:namespace_stackable).with(:error_formatters, format.to_sym => :error_formatter) + subject.error_formatter format, with: :error_formatter + end + end - it 'understands syntactic sugar' do - expect(subject).to receive(:namespace_stackable).with(:error_formatters, format.to_sym => :error_formatter) - subject.error_formatter format, with: :error_formatter - end - end + describe '.content_type' do + it 'sets a content type for a format' do + expect(subject).to receive(:namespace_stackable).with(:content_types, format.to_sym => c_type) + subject.content_type format, c_type + end + end - describe '.content_type' do - it 'sets a content type for a format' do - expect(subject).to receive(:namespace_stackable).with(:content_types, format.to_sym => c_type) - subject.content_type format, c_type - end - end + describe '.content_types' do + it 'returns all content types' do + expect(subject.content_types).to eq(xml: 'application/xml', + serializable_hash: 'application/json', + json: 'application/json', + txt: 'text/plain', + binary: 'application/octet-stream') + end + end - describe '.content_types' do - it 'returns all content types' do - expect(subject.content_types).to eq(xml: 'application/xml', - serializable_hash: 'application/json', - json: 'application/json', - txt: 'text/plain', - binary: 'application/octet-stream') - end - end + describe '.default_error_status' do + it 'sets a default error status' do + expect(subject).to receive(:namespace_inheritable).with(:default_error_status, 500) + subject.default_error_status 500 + end + end - describe '.default_error_status' do - it 'sets a default error status' do - expect(subject).to receive(:namespace_inheritable).with(:default_error_status, 500) - subject.default_error_status 500 - end + describe '.rescue_from' do + describe ':all' do + it 'sets rescue all to true' do + expect(subject).to receive(:namespace_inheritable).with(:rescue_all, true) + expect(subject).to receive(:namespace_inheritable).with(:all_rescue_handler, nil) + subject.rescue_from :all end - describe '.rescue_from' do - describe ':all' do - it 'sets rescue all to true' do - expect(subject).to receive(:namespace_inheritable).with(:rescue_all, true) - expect(subject).to receive(:namespace_inheritable).with(:all_rescue_handler, nil) - subject.rescue_from :all - end - - it 'sets given proc as rescue handler' do - rescue_handler_proc = proc {} - expect(subject).to receive(:namespace_inheritable).with(:rescue_all, true) - expect(subject).to receive(:namespace_inheritable).with(:all_rescue_handler, rescue_handler_proc) - subject.rescue_from :all, rescue_handler_proc - end + it 'sets given proc as rescue handler' do + rescue_handler_proc = proc {} + expect(subject).to receive(:namespace_inheritable).with(:rescue_all, true) + expect(subject).to receive(:namespace_inheritable).with(:all_rescue_handler, rescue_handler_proc) + subject.rescue_from :all, rescue_handler_proc + end - it 'sets given block as rescue handler' do - rescue_handler_proc = proc {} - expect(subject).to receive(:namespace_inheritable).with(:rescue_all, true) - expect(subject).to receive(:namespace_inheritable).with(:all_rescue_handler, rescue_handler_proc) - subject.rescue_from :all, &rescue_handler_proc - end + it 'sets given block as rescue handler' do + rescue_handler_proc = proc {} + expect(subject).to receive(:namespace_inheritable).with(:rescue_all, true) + expect(subject).to receive(:namespace_inheritable).with(:all_rescue_handler, rescue_handler_proc) + subject.rescue_from :all, &rescue_handler_proc + end - it 'sets a rescue handler declared through :with option' do - with_block = -> { 'hello' } - expect(subject).to receive(:namespace_inheritable).with(:rescue_all, true) - expect(subject).to receive(:namespace_inheritable).with(:all_rescue_handler, an_instance_of(Proc)) - subject.rescue_from :all, with: with_block - end + it 'sets a rescue handler declared through :with option' do + with_block = -> { 'hello' } + expect(subject).to receive(:namespace_inheritable).with(:rescue_all, true) + expect(subject).to receive(:namespace_inheritable).with(:all_rescue_handler, an_instance_of(Proc)) + subject.rescue_from :all, with: with_block + end - it 'abort if :with option value is not Symbol, String or Proc' do - expect { subject.rescue_from :all, with: 1234 }.to raise_error(ArgumentError, "with: #{integer_class_name}, expected Symbol, String or Proc") - end + it 'abort if :with option value is not Symbol, String or Proc' do + expect { subject.rescue_from :all, with: 1234 }.to raise_error(ArgumentError, "with: #{integer_class_name}, expected Symbol, String or Proc") + end - it 'abort if both :with option and block are passed' do - expect do - subject.rescue_from :all, with: -> { 'hello' } do - error!('bye') - end - end.to raise_error(ArgumentError, 'both :with option and block cannot be passed') - end - end - - describe ':grape_exceptions' do - it 'sets rescue all to true' do - expect(subject).to receive(:namespace_inheritable).with(:rescue_all, true) - expect(subject).to receive(:namespace_inheritable).with(:rescue_grape_exceptions, true) - expect(subject).to receive(:namespace_inheritable).with(:grape_exceptions_rescue_handler, nil) - subject.rescue_from :grape_exceptions + it 'abort if both :with option and block are passed' do + expect do + subject.rescue_from :all, with: -> { 'hello' } do + error!('bye') end + end.to raise_error(ArgumentError, 'both :with option and block cannot be passed') + end + end - it 'sets given proc as rescue handler' do - rescue_handler_proc = proc {} - expect(subject).to receive(:namespace_inheritable).with(:rescue_all, true) - expect(subject).to receive(:namespace_inheritable).with(:rescue_grape_exceptions, true) - expect(subject).to receive(:namespace_inheritable).with(:grape_exceptions_rescue_handler, rescue_handler_proc) - subject.rescue_from :grape_exceptions, rescue_handler_proc - end + describe ':grape_exceptions' do + it 'sets rescue all to true' do + expect(subject).to receive(:namespace_inheritable).with(:rescue_all, true) + expect(subject).to receive(:namespace_inheritable).with(:rescue_grape_exceptions, true) + expect(subject).to receive(:namespace_inheritable).with(:grape_exceptions_rescue_handler, nil) + subject.rescue_from :grape_exceptions + end - it 'sets given block as rescue handler' do - rescue_handler_proc = proc {} - expect(subject).to receive(:namespace_inheritable).with(:rescue_all, true) - expect(subject).to receive(:namespace_inheritable).with(:rescue_grape_exceptions, true) - expect(subject).to receive(:namespace_inheritable).with(:grape_exceptions_rescue_handler, rescue_handler_proc) - subject.rescue_from :grape_exceptions, &rescue_handler_proc - end + it 'sets given proc as rescue handler' do + rescue_handler_proc = proc {} + expect(subject).to receive(:namespace_inheritable).with(:rescue_all, true) + expect(subject).to receive(:namespace_inheritable).with(:rescue_grape_exceptions, true) + expect(subject).to receive(:namespace_inheritable).with(:grape_exceptions_rescue_handler, rescue_handler_proc) + subject.rescue_from :grape_exceptions, rescue_handler_proc + end - it 'sets a rescue handler declared through :with option' do - with_block = -> { 'hello' } - expect(subject).to receive(:namespace_inheritable).with(:rescue_all, true) - expect(subject).to receive(:namespace_inheritable).with(:rescue_grape_exceptions, true) - expect(subject).to receive(:namespace_inheritable).with(:grape_exceptions_rescue_handler, an_instance_of(Proc)) - subject.rescue_from :grape_exceptions, with: with_block - end - end + it 'sets given block as rescue handler' do + rescue_handler_proc = proc {} + expect(subject).to receive(:namespace_inheritable).with(:rescue_all, true) + expect(subject).to receive(:namespace_inheritable).with(:rescue_grape_exceptions, true) + expect(subject).to receive(:namespace_inheritable).with(:grape_exceptions_rescue_handler, rescue_handler_proc) + subject.rescue_from :grape_exceptions, &rescue_handler_proc + end - describe 'list of exceptions is passed' do - it 'sets hash of exceptions as rescue handlers' do - expect(subject).to receive(:namespace_reverse_stackable).with(:rescue_handlers, { StandardError => nil }) - expect(subject).to receive(:namespace_stackable).with(:rescue_options, {}) - subject.rescue_from StandardError - end + it 'sets a rescue handler declared through :with option' do + with_block = -> { 'hello' } + expect(subject).to receive(:namespace_inheritable).with(:rescue_all, true) + expect(subject).to receive(:namespace_inheritable).with(:rescue_grape_exceptions, true) + expect(subject).to receive(:namespace_inheritable).with(:grape_exceptions_rescue_handler, an_instance_of(Proc)) + subject.rescue_from :grape_exceptions, with: with_block + end + end - it 'rescues only base handlers if rescue_subclasses: false option is passed' do - expect(subject).to receive(:namespace_reverse_stackable).with(:base_only_rescue_handlers, { StandardError => nil }) - expect(subject).to receive(:namespace_stackable).with(:rescue_options, { rescue_subclasses: false }) - subject.rescue_from StandardError, rescue_subclasses: false - end + describe 'list of exceptions is passed' do + it 'sets hash of exceptions as rescue handlers' do + expect(subject).to receive(:namespace_reverse_stackable).with(:rescue_handlers, { StandardError => nil }) + expect(subject).to receive(:namespace_stackable).with(:rescue_options, {}) + subject.rescue_from StandardError + end - it 'sets given proc as rescue handler for each key in hash' do - rescue_handler_proc = proc {} - expect(subject).to receive(:namespace_reverse_stackable).with(:rescue_handlers, { StandardError => rescue_handler_proc }) - expect(subject).to receive(:namespace_stackable).with(:rescue_options, {}) - subject.rescue_from StandardError, rescue_handler_proc - end + it 'rescues only base handlers if rescue_subclasses: false option is passed' do + expect(subject).to receive(:namespace_reverse_stackable).with(:base_only_rescue_handlers, { StandardError => nil }) + expect(subject).to receive(:namespace_stackable).with(:rescue_options, { rescue_subclasses: false }) + subject.rescue_from StandardError, rescue_subclasses: false + end - it 'sets given block as rescue handler for each key in hash' do - rescue_handler_proc = proc {} - expect(subject).to receive(:namespace_reverse_stackable).with(:rescue_handlers, { StandardError => rescue_handler_proc }) - expect(subject).to receive(:namespace_stackable).with(:rescue_options, {}) - subject.rescue_from StandardError, &rescue_handler_proc - end + it 'sets given proc as rescue handler for each key in hash' do + rescue_handler_proc = proc {} + expect(subject).to receive(:namespace_reverse_stackable).with(:rescue_handlers, { StandardError => rescue_handler_proc }) + expect(subject).to receive(:namespace_stackable).with(:rescue_options, {}) + subject.rescue_from StandardError, rescue_handler_proc + end - it 'sets a rescue handler declared through :with option for each key in hash' do - with_block = -> { 'hello' } - expect(subject).to receive(:namespace_reverse_stackable).with(:rescue_handlers, { StandardError => an_instance_of(Proc) }) - expect(subject).to receive(:namespace_stackable).with(:rescue_options, {}) - subject.rescue_from StandardError, with: with_block - end - end + it 'sets given block as rescue handler for each key in hash' do + rescue_handler_proc = proc {} + expect(subject).to receive(:namespace_reverse_stackable).with(:rescue_handlers, { StandardError => rescue_handler_proc }) + expect(subject).to receive(:namespace_stackable).with(:rescue_options, {}) + subject.rescue_from StandardError, &rescue_handler_proc end - describe '.represent' do - it 'sets a presenter for a class' do - presenter = Class.new - expect(subject).to receive(:namespace_stackable).with(:representations, ThisClass: presenter) - subject.represent :ThisClass, with: presenter - end + it 'sets a rescue handler declared through :with option for each key in hash' do + with_block = -> { 'hello' } + expect(subject).to receive(:namespace_reverse_stackable).with(:rescue_handlers, { StandardError => an_instance_of(Proc) }) + expect(subject).to receive(:namespace_stackable).with(:rescue_options, {}) + subject.rescue_from StandardError, with: with_block end end end + + describe '.represent' do + it 'sets a presenter for a class' do + presenter = Class.new + expect(subject).to receive(:namespace_stackable).with(:representations, ThisClass: presenter) + subject.represent :ThisClass, with: presenter + end + end end diff --git a/spec/grape/dsl/routing_spec.rb b/spec/grape/dsl/routing_spec.rb index e763428f6..617980bb2 100644 --- a/spec/grape/dsl/routing_spec.rb +++ b/spec/grape/dsl/routing_spec.rb @@ -1,275 +1,271 @@ # frozen_string_literal: true -module Grape - module DSL - module RoutingSpec - class Dummy - include Grape::DSL::Routing - end +describe Grape::DSL::Routing do + subject { dummy_class } + + let(:dummy_class) do + Class.new do + include Grape::DSL::Routing end + end - describe Routing do - subject { Class.new(RoutingSpec::Dummy) } + let(:proc) { -> {} } + let(:options) { { a: :b } } + let(:path) { '/dummy' } - let(:proc) { -> {} } - let(:options) { { a: :b } } - let(:path) { '/dummy' } + describe '.version' do + it 'sets a version for route' do + version = 'v1' + expect(subject).to receive(:namespace_inheritable).with(:version, [version]) + expect(subject).to receive(:namespace_inheritable).with(:version_options, { using: :path }) + expect(subject.version(version)).to eq(version) + end + end - describe '.version' do - it 'sets a version for route' do - version = 'v1' - expect(subject).to receive(:namespace_inheritable).with(:version, [version]) - expect(subject).to receive(:namespace_inheritable).with(:version_options, { using: :path }) - expect(subject.version(version)).to eq(version) - end - end + describe '.prefix' do + it 'sets a prefix for route' do + prefix = '/api' + expect(subject).to receive(:namespace_inheritable).with(:root_prefix, prefix) + subject.prefix prefix + end + end - describe '.prefix' do - it 'sets a prefix for route' do - prefix = '/api' - expect(subject).to receive(:namespace_inheritable).with(:root_prefix, prefix) - subject.prefix prefix - end - end + describe '.scope' do + it 'create a scope without affecting the URL' do + expect(subject).to receive(:within_namespace) + subject.scope {} + end + end - describe '.scope' do - it 'create a scope without affecting the URL' do - expect(subject).to receive(:within_namespace) - subject.scope {} - end - end + describe '.do_not_route_head!' do + it 'sets do not route head option' do + expect(subject).to receive(:namespace_inheritable).with(:do_not_route_head, true) + subject.do_not_route_head! + end + end - describe '.do_not_route_head!' do - it 'sets do not route head option' do - expect(subject).to receive(:namespace_inheritable).with(:do_not_route_head, true) - subject.do_not_route_head! - end - end + describe '.do_not_route_options!' do + it 'sets do not route options option' do + expect(subject).to receive(:namespace_inheritable).with(:do_not_route_options, true) + subject.do_not_route_options! + end + end - describe '.do_not_route_options!' do - it 'sets do not route options option' do - expect(subject).to receive(:namespace_inheritable).with(:do_not_route_options, true) - subject.do_not_route_options! - end + describe '.mount' do + it 'mounts on a nested path' do + subject = Class.new(Grape::API) + app1 = Class.new(Grape::API) + app2 = Class.new(Grape::API) + app2.get '/nice' do + 'play' end - describe '.mount' do - it 'mounts on a nested path' do - subject = Class.new(Grape::API) - app1 = Class.new(Grape::API) - app2 = Class.new(Grape::API) - app2.get '/nice' do - 'play' - end - - subject.mount app1 => '/app1' - app1.mount app2 => '/app2' - - expect(subject.inheritable_setting.to_hash[:namespace]).to eq({}) - expect(subject.inheritable_setting.to_hash[:namespace_inheritable]).to eq({}) - 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: ['/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 + subject.mount app1 => '/app1' + app1.mount app2 => '/app2' - describe '.route' do - before do - allow(subject).to receive(:endpoints).and_return([]) - allow(subject).to receive(:route_end) - allow(subject).to receive(:reset_validations!) - end - - it 'marks end of the route' do - expect(subject).to receive(:route_end) - subject.route(:any) - end - - it 'resets validations' do - expect(subject).to receive(:reset_validations!) - subject.route(:any) - end - - it 'defines a new endpoint' do - expect { subject.route(:any) } - .to change { subject.endpoints.count }.from(0).to(1) - end - - it 'does not duplicate identical endpoints' do - subject.route(:any) - expect { subject.route(:any) } - .not_to change(subject.endpoints, :count) - end - - it 'generates correct endpoint options' do - allow(subject).to receive(:route_setting).with(:description).and_return(fiz: 'baz') - allow(subject).to receive(:namespace_stackable_with_hash).and_return(nuz: 'naz') - - expect(Grape::Endpoint).to receive(:new) do |_inheritable_setting, endpoint_options| - expect(endpoint_options[:method]).to eq :get - expect(endpoint_options[:path]).to eq '/foo' - expect(endpoint_options[:for]).to eq subject - expect(endpoint_options[:route_options]).to eq(foo: 'bar', fiz: 'baz', params: { nuz: 'naz' }) - end.and_yield - - subject.route(:get, '/foo', { foo: 'bar' }, &proc {}) - end - end + expect(subject.inheritable_setting.to_hash[:namespace]).to eq({}) + expect(subject.inheritable_setting.to_hash[:namespace_inheritable]).to eq({}) + expect(app1.inheritable_setting.to_hash[:namespace_stackable]).to eq(mount_path: ['/app1']) - describe '.get' do - it 'delegates to .route' do - expect(subject).to receive(:route).with(Rack::GET, path, options) - subject.get path, options, &proc - end - end + expect(app2.inheritable_setting.to_hash[:namespace_stackable]).to eq(mount_path: ['/app1', '/app2']) + end - describe '.post' do - it 'delegates to .route' do - expect(subject).to receive(:route).with(Rack::POST, path, options) - subject.post path, options, &proc - end - end + 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') - describe '.put' do - it 'delegates to .route' do - expect(subject).to receive(:route).with(Rack::PUT, path, options) - subject.put path, options, &proc - end - end + 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 '.head' do - it 'delegates to .route' do - expect(subject).to receive(:route).with(Rack::HEAD, path, options) - subject.head path, options, &proc - end - end + describe '.route' do + before do + allow(subject).to receive(:endpoints).and_return([]) + allow(subject).to receive(:route_end) + allow(subject).to receive(:reset_validations!) + end - describe '.delete' do - it 'delegates to .route' do - expect(subject).to receive(:route).with(Rack::DELETE, path, options) - subject.delete path, options, &proc - end - end + it 'marks end of the route' do + expect(subject).to receive(:route_end) + subject.route(:any) + end - describe '.options' do - it 'delegates to .route' do - expect(subject).to receive(:route).with(Rack::OPTIONS, path, options) - subject.options path, options, &proc - end - end + it 'resets validations' do + expect(subject).to receive(:reset_validations!) + subject.route(:any) + end - describe '.patch' do - it 'delegates to .route' do - expect(subject).to receive(:route).with(Rack::PATCH, path, options) - subject.patch path, options, &proc - end - end + it 'defines a new endpoint' do + expect { subject.route(:any) } + .to change { subject.endpoints.count }.from(0).to(1) + end - describe '.namespace' do - let(:new_namespace) { Object.new } + it 'does not duplicate identical endpoints' do + subject.route(:any) + expect { subject.route(:any) } + .not_to change(subject.endpoints, :count) + end - it 'creates a new namespace with given name and options' do - expect(subject).to receive(:within_namespace).and_yield - expect(subject).to receive(:nest).and_yield - expect(Namespace).to receive(:new).with(:foo, foo: 'bar').and_return(new_namespace) - expect(subject).to receive(:namespace_stackable).with(:namespace, new_namespace) + it 'generates correct endpoint options' do + allow(subject).to receive(:route_setting).with(:description).and_return(fiz: 'baz') + allow(subject).to receive(:namespace_stackable_with_hash).and_return(nuz: 'naz') - subject.namespace :foo, foo: 'bar', &proc {} - end + expect(Grape::Endpoint).to receive(:new) do |_inheritable_setting, endpoint_options| + expect(endpoint_options[:method]).to eq :get + expect(endpoint_options[:path]).to eq '/foo' + expect(endpoint_options[:for]).to eq subject + expect(endpoint_options[:route_options]).to eq(foo: 'bar', fiz: 'baz', params: { nuz: 'naz' }) + end.and_yield - it 'calls #joined_space_path on Namespace' do - result_of_namspace_stackable = Object.new - allow(subject).to receive(:namespace_stackable).and_return(result_of_namspace_stackable) - expect(Namespace).to receive(:joined_space_path).with(result_of_namspace_stackable) - subject.namespace - end - end + subject.route(:get, '/foo', { foo: 'bar' }, &proc {}) + end + end - describe '.group' do - it 'is alias to #namespace' do - expect(subject.method(:group)).to eq subject.method(:namespace) - end - end + describe '.get' do + it 'delegates to .route' do + expect(subject).to receive(:route).with(Rack::GET, path, options) + subject.get path, options, &proc + end + end - describe '.resource' do - it 'is alias to #namespace' do - expect(subject.method(:resource)).to eq subject.method(:namespace) - end - end + describe '.post' do + it 'delegates to .route' do + expect(subject).to receive(:route).with(Rack::POST, path, options) + subject.post path, options, &proc + end + end - describe '.resources' do - it 'is alias to #namespace' do - expect(subject.method(:resources)).to eq subject.method(:namespace) - end - end + describe '.put' do + it 'delegates to .route' do + expect(subject).to receive(:route).with(Rack::PUT, path, options) + subject.put path, options, &proc + end + end - describe '.segment' do - it 'is alias to #namespace' do - expect(subject.method(:segment)).to eq subject.method(:namespace) - end - end + describe '.head' do + it 'delegates to .route' do + expect(subject).to receive(:route).with(Rack::HEAD, path, options) + subject.head path, options, &proc + end + end + + describe '.delete' do + it 'delegates to .route' do + expect(subject).to receive(:route).with(Rack::DELETE, path, options) + subject.delete path, options, &proc + end + end + + describe '.options' do + it 'delegates to .route' do + expect(subject).to receive(:route).with(Rack::OPTIONS, path, options) + subject.options path, options, &proc + end + end + + describe '.patch' do + it 'delegates to .route' do + expect(subject).to receive(:route).with(Rack::PATCH, path, options) + subject.patch path, options, &proc + end + end + + describe '.namespace' do + let(:new_namespace) { Object.new } + + it 'creates a new namespace with given name and options' do + expect(subject).to receive(:within_namespace).and_yield + expect(subject).to receive(:nest).and_yield + expect(Grape::Namespace).to receive(:new).with(:foo, foo: 'bar').and_return(new_namespace) + expect(subject).to receive(:namespace_stackable).with(:namespace, new_namespace) + + subject.namespace :foo, foo: 'bar', &proc {} + end + + it 'calls #joined_space_path on Namespace' do + result_of_namspace_stackable = Object.new + allow(subject).to receive(:namespace_stackable).and_return(result_of_namspace_stackable) + expect(Grape::Namespace).to receive(:joined_space_path).with(result_of_namspace_stackable) + subject.namespace + end + end + + describe '.group' do + it 'is alias to #namespace' do + expect(subject.method(:group)).to eq subject.method(:namespace) + end + end - describe '.routes' do - let(:routes) { Object.new } - - it 'returns value received from #prepare_routes' do - expect(subject).to receive(:prepare_routes).and_return(routes) - expect(subject.routes).to eq routes - end - - context 'when #routes was already called once' do - before do - allow(subject).to receive(:prepare_routes).and_return(routes) - subject.routes - end - - it 'does not call prepare_routes again' do - expect(subject).not_to receive(:prepare_routes) - expect(subject.routes).to eq routes - end - end + describe '.resource' do + it 'is alias to #namespace' do + expect(subject.method(:resource)).to eq subject.method(:namespace) + end + end + + describe '.resources' do + it 'is alias to #namespace' do + expect(subject.method(:resources)).to eq subject.method(:namespace) + end + end + + describe '.segment' do + it 'is alias to #namespace' do + expect(subject.method(:segment)).to eq subject.method(:namespace) + end + end + + describe '.routes' do + let(:routes) { Object.new } + + it 'returns value received from #prepare_routes' do + expect(subject).to receive(:prepare_routes).and_return(routes) + expect(subject.routes).to eq routes + end + + context 'when #routes was already called once' do + before do + allow(subject).to receive(:prepare_routes).and_return(routes) + subject.routes end - describe '.route_param' do - let!(:options) { { requirements: regex } } - let(:regex) { /(.*)/ } - - it 'calls #namespace with given params' do - expect(subject).to receive(:namespace).with(':foo', {}).and_yield - subject.route_param('foo', {}, &proc {}) - end - - it 'nests requirements option under param name' do - expect(subject).to receive(:namespace) do |_param, options| - expect(options[:requirements][:foo]).to eq regex - end - subject.route_param('foo', options, &proc {}) - end - - it 'does not modify options parameter' do - allow(subject).to receive(:namespace) - expect { subject.route_param('foo', options, &proc {}) } - .not_to(change { options }) - end + it 'does not call prepare_routes again' do + expect(subject).not_to receive(:prepare_routes) + expect(subject.routes).to eq routes end + end + end + + describe '.route_param' do + let!(:options) { { requirements: regex } } + let(:regex) { /(.*)/ } + + it 'calls #namespace with given params' do + expect(subject).to receive(:namespace).with(':foo', {}).and_yield + subject.route_param('foo', {}, &proc {}) + end - describe '.versions' do - it 'returns last defined version' do - subject.version 'v1' - subject.version 'v2' - expect(subject.version).to eq('v2') - end + it 'nests requirements option under param name' do + expect(subject).to receive(:namespace) do |_param, options| + expect(options[:requirements][:foo]).to eq regex end + subject.route_param('foo', options, &proc {}) + end + + it 'does not modify options parameter' do + allow(subject).to receive(:namespace) + expect { subject.route_param('foo', options, &proc {}) } + .not_to(change { options }) + end + end + + describe '.versions' do + it 'returns last defined version' do + subject.version 'v1' + subject.version 'v2' + expect(subject.version).to eq('v2') end end end diff --git a/spec/grape/dsl/settings_spec.rb b/spec/grape/dsl/settings_spec.rb index 5de6febf1..e4b20fa36 100644 --- a/spec/grape/dsl/settings_spec.rb +++ b/spec/grape/dsl/settings_spec.rb @@ -1,261 +1,257 @@ # frozen_string_literal: true -module Grape - module DSL - module SettingsSpec - class Dummy - include Grape::DSL::Settings +describe Grape::DSL::Settings do + subject { dummy_class.new } - def reset_validations!; end - end + let(:dummy_class) do + Class.new do + include Grape::DSL::Settings + + def reset_validations!; end end + end - describe Settings do - subject { SettingsSpec::Dummy.new } + describe '#unset' do + it 'deletes a key from settings' do + subject.namespace_setting :dummy, 1 + expect(subject.inheritable_setting.namespace.keys).to include(:dummy) - describe '#unset' do - it 'deletes a key from settings' do - subject.namespace_setting :dummy, 1 - expect(subject.inheritable_setting.namespace.keys).to include(:dummy) + subject.unset :namespace, :dummy + expect(subject.inheritable_setting.namespace.keys).not_to include(:dummy) + end + end - subject.unset :namespace, :dummy - expect(subject.inheritable_setting.namespace.keys).not_to include(:dummy) - end - end + describe '#get_or_set' do + it 'sets a values' do + subject.get_or_set :namespace, :dummy, 1 + expect(subject.namespace_setting(:dummy)).to eq 1 + end - describe '#get_or_set' do - it 'sets a values' do - subject.get_or_set :namespace, :dummy, 1 - expect(subject.namespace_setting(:dummy)).to eq 1 - end + it 'returns a value when nil is new value is provided' do + subject.get_or_set :namespace, :dummy, 1 + expect(subject.get_or_set(:namespace, :dummy, nil)).to eq 1 + end + end - it 'returns a value when nil is new value is provided' do - subject.get_or_set :namespace, :dummy, 1 - expect(subject.get_or_set(:namespace, :dummy, nil)).to eq 1 - end - end + describe '#global_setting' do + it 'delegates to get_or_set' do + expect(subject).to receive(:get_or_set).with(:global, :dummy, 1) + subject.global_setting(:dummy, 1) + end + end - describe '#global_setting' do - it 'delegates to get_or_set' do - expect(subject).to receive(:get_or_set).with(:global, :dummy, 1) - subject.global_setting(:dummy, 1) - end - end + describe '#unset_global_setting' do + it 'delegates to unset' do + expect(subject).to receive(:unset).with(:global, :dummy) + subject.unset_global_setting(:dummy) + end + end - describe '#unset_global_setting' do - it 'delegates to unset' do - expect(subject).to receive(:unset).with(:global, :dummy) - subject.unset_global_setting(:dummy) - end - end + describe '#route_setting' do + it 'delegates to get_or_set' do + expect(subject).to receive(:get_or_set).with(:route, :dummy, 1) + subject.route_setting(:dummy, 1) + end - describe '#route_setting' do - it 'delegates to get_or_set' do - expect(subject).to receive(:get_or_set).with(:route, :dummy, 1) - subject.route_setting(:dummy, 1) - end + it 'sets a value until the next route' do + subject.route_setting :some_thing, :foo_bar + expect(subject.route_setting(:some_thing)).to eq :foo_bar - it 'sets a value until the next route' do - subject.route_setting :some_thing, :foo_bar - expect(subject.route_setting(:some_thing)).to eq :foo_bar + subject.route_end - subject.route_end + expect(subject.route_setting(:some_thing)).to be_nil + end + end - expect(subject.route_setting(:some_thing)).to be_nil - end - end + describe '#unset_route_setting' do + it 'delegates to unset' do + expect(subject).to receive(:unset).with(:route, :dummy) + subject.unset_route_setting(:dummy) + end + end - describe '#unset_route_setting' do - it 'delegates to unset' do - expect(subject).to receive(:unset).with(:route, :dummy) - subject.unset_route_setting(:dummy) - end - end + describe '#namespace_setting' do + it 'delegates to get_or_set' do + expect(subject).to receive(:get_or_set).with(:namespace, :dummy, 1) + subject.namespace_setting(:dummy, 1) + end - describe '#namespace_setting' do - it 'delegates to get_or_set' do - expect(subject).to receive(:get_or_set).with(:namespace, :dummy, 1) - subject.namespace_setting(:dummy, 1) - end + it 'sets a value until the end of a namespace' do + subject.namespace_start - it 'sets a value until the end of a namespace' do - subject.namespace_start + subject.namespace_setting :some_thing, :foo_bar + expect(subject.namespace_setting(:some_thing)).to eq :foo_bar - subject.namespace_setting :some_thing, :foo_bar - expect(subject.namespace_setting(:some_thing)).to eq :foo_bar + subject.namespace_end - subject.namespace_end + expect(subject.namespace_setting(:some_thing)).to be_nil + end - expect(subject.namespace_setting(:some_thing)).to be_nil - end + it 'resets values after leaving nested namespaces' do + subject.namespace_start - it 'resets values after leaving nested namespaces' do - subject.namespace_start + subject.namespace_setting :some_thing, :foo_bar + expect(subject.namespace_setting(:some_thing)).to eq :foo_bar - subject.namespace_setting :some_thing, :foo_bar - expect(subject.namespace_setting(:some_thing)).to eq :foo_bar + subject.namespace_start - subject.namespace_start + expect(subject.namespace_setting(:some_thing)).to be_nil - expect(subject.namespace_setting(:some_thing)).to be_nil + subject.namespace_end + expect(subject.namespace_setting(:some_thing)).to eq :foo_bar - subject.namespace_end - expect(subject.namespace_setting(:some_thing)).to eq :foo_bar + subject.namespace_end - subject.namespace_end + expect(subject.namespace_setting(:some_thing)).to be_nil + end + end - expect(subject.namespace_setting(:some_thing)).to be_nil - end - end + describe '#unset_namespace_setting' do + it 'delegates to unset' do + expect(subject).to receive(:unset).with(:namespace, :dummy) + subject.unset_namespace_setting(:dummy) + end + end - describe '#unset_namespace_setting' do - it 'delegates to unset' do - expect(subject).to receive(:unset).with(:namespace, :dummy) - subject.unset_namespace_setting(:dummy) - end - end + describe '#namespace_inheritable' do + it 'delegates to get_or_set' do + expect(subject).to receive(:get_or_set).with(:namespace_inheritable, :dummy, 1) + subject.namespace_inheritable(:dummy, 1) + end - describe '#namespace_inheritable' do - it 'delegates to get_or_set' do - expect(subject).to receive(:get_or_set).with(:namespace_inheritable, :dummy, 1) - subject.namespace_inheritable(:dummy, 1) - end + it 'inherits values from surrounding namespace' do + subject.namespace_start - it 'inherits values from surrounding namespace' do - subject.namespace_start + subject.namespace_inheritable(:some_thing, :foo_bar) + expect(subject.namespace_inheritable(:some_thing)).to eq :foo_bar - subject.namespace_inheritable(:some_thing, :foo_bar) - expect(subject.namespace_inheritable(:some_thing)).to eq :foo_bar + subject.namespace_start - subject.namespace_start + expect(subject.namespace_inheritable(:some_thing)).to eq :foo_bar - expect(subject.namespace_inheritable(:some_thing)).to eq :foo_bar + subject.namespace_inheritable(:some_thing, :foo_bar_2) - subject.namespace_inheritable(:some_thing, :foo_bar_2) + expect(subject.namespace_inheritable(:some_thing)).to eq :foo_bar_2 - expect(subject.namespace_inheritable(:some_thing)).to eq :foo_bar_2 + subject.namespace_end + expect(subject.namespace_inheritable(:some_thing)).to eq :foo_bar + subject.namespace_end + end + end - subject.namespace_end - expect(subject.namespace_inheritable(:some_thing)).to eq :foo_bar - subject.namespace_end - end - end + describe '#unset_namespace_inheritable' do + it 'delegates to unset' do + expect(subject).to receive(:unset).with(:namespace_inheritable, :dummy) + subject.unset_namespace_inheritable(:dummy) + end + end - describe '#unset_namespace_inheritable' do - it 'delegates to unset' do - expect(subject).to receive(:unset).with(:namespace_inheritable, :dummy) - subject.unset_namespace_inheritable(:dummy) - end - end + describe '#namespace_stackable' do + it 'delegates to get_or_set' do + expect(subject).to receive(:get_or_set).with(:namespace_stackable, :dummy, 1) + subject.namespace_stackable(:dummy, 1) + end - describe '#namespace_stackable' do - it 'delegates to get_or_set' do - expect(subject).to receive(:get_or_set).with(:namespace_stackable, :dummy, 1) - subject.namespace_stackable(:dummy, 1) - end + it 'stacks values from surrounding namespace' do + subject.namespace_start - it 'stacks values from surrounding namespace' do - subject.namespace_start + subject.namespace_stackable(:some_thing, :foo_bar) + expect(subject.namespace_stackable(:some_thing)).to eq [:foo_bar] - subject.namespace_stackable(:some_thing, :foo_bar) - expect(subject.namespace_stackable(:some_thing)).to eq [:foo_bar] + subject.namespace_start - subject.namespace_start + expect(subject.namespace_stackable(:some_thing)).to eq [:foo_bar] - expect(subject.namespace_stackable(:some_thing)).to eq [:foo_bar] + subject.namespace_stackable(:some_thing, :foo_bar_2) - subject.namespace_stackable(:some_thing, :foo_bar_2) + expect(subject.namespace_stackable(:some_thing)).to eq %i[foo_bar foo_bar_2] - expect(subject.namespace_stackable(:some_thing)).to eq %i[foo_bar foo_bar_2] + subject.namespace_end + expect(subject.namespace_stackable(:some_thing)).to eq [:foo_bar] + subject.namespace_end + end + end - subject.namespace_end - expect(subject.namespace_stackable(:some_thing)).to eq [:foo_bar] - subject.namespace_end - end - end + describe '#unset_namespace_stackable' do + it 'delegates to unset' do + expect(subject).to receive(:unset).with(:namespace_stackable, :dummy) + subject.unset_namespace_stackable(:dummy) + end + end - describe '#unset_namespace_stackable' do - it 'delegates to unset' do - expect(subject).to receive(:unset).with(:namespace_stackable, :dummy) - subject.unset_namespace_stackable(:dummy) - end - end + describe '#api_class_setting' do + it 'delegates to get_or_set' do + expect(subject).to receive(:get_or_set).with(:api_class, :dummy, 1) + subject.api_class_setting(:dummy, 1) + end + end + + describe '#unset_api_class_setting' do + it 'delegates to unset' do + expect(subject).to receive(:unset).with(:api_class, :dummy) + subject.unset_api_class_setting(:dummy) + end + end + + describe '#within_namespace' do + it 'calls start and end for a namespace' do + expect(subject).to receive :namespace_start + expect(subject).to receive :namespace_end - describe '#api_class_setting' do - it 'delegates to get_or_set' do - expect(subject).to receive(:get_or_set).with(:api_class, :dummy, 1) - subject.api_class_setting(:dummy, 1) - end + subject.within_namespace do end + end - describe '#unset_api_class_setting' do - it 'delegates to unset' do - expect(subject).to receive(:unset).with(:api_class, :dummy) - subject.unset_api_class_setting(:dummy) - end + it 'returns the last result' do + result = subject.within_namespace do + 1 end - describe '#within_namespace' do - it 'calls start and end for a namespace' do - expect(subject).to receive :namespace_start - expect(subject).to receive :namespace_end + expect(result).to eq 1 + end + end - subject.within_namespace do - end - end + describe 'complex scenario' do + it 'plays well' do + obj1 = dummy_class.new + obj2 = dummy_class.new + obj3 = dummy_class.new - it 'returns the last result' do - result = subject.within_namespace do - 1 - end + obj1_copy = nil + obj2_copy = nil + obj3_copy = nil - expect(result).to eq 1 - end + obj1.within_namespace do + obj1.namespace_stackable(:some_thing, :obj1) + expect(obj1.namespace_stackable(:some_thing)).to eq [:obj1] + obj1_copy = obj1.inheritable_setting.point_in_time_copy end - describe 'complex scenario' do - it 'plays well' do - obj1 = SettingsSpec::Dummy.new - obj2 = SettingsSpec::Dummy.new - obj3 = SettingsSpec::Dummy.new - - obj1_copy = nil - obj2_copy = nil - obj3_copy = nil - - obj1.within_namespace do - obj1.namespace_stackable(:some_thing, :obj1) - expect(obj1.namespace_stackable(:some_thing)).to eq [:obj1] - obj1_copy = obj1.inheritable_setting.point_in_time_copy - end - - expect(obj1.namespace_stackable(:some_thing)).to eq [] - expect(obj1_copy.namespace_stackable[:some_thing]).to eq [:obj1] + expect(obj1.namespace_stackable(:some_thing)).to eq [] + expect(obj1_copy.namespace_stackable[:some_thing]).to eq [:obj1] - obj2.within_namespace do - obj2.namespace_stackable(:some_thing, :obj2) - expect(obj2.namespace_stackable(:some_thing)).to eq [:obj2] - obj2_copy = obj2.inheritable_setting.point_in_time_copy - end + obj2.within_namespace do + obj2.namespace_stackable(:some_thing, :obj2) + expect(obj2.namespace_stackable(:some_thing)).to eq [:obj2] + obj2_copy = obj2.inheritable_setting.point_in_time_copy + end - expect(obj2.namespace_stackable(:some_thing)).to eq [] - expect(obj2_copy.namespace_stackable[:some_thing]).to eq [:obj2] + expect(obj2.namespace_stackable(:some_thing)).to eq [] + expect(obj2_copy.namespace_stackable[:some_thing]).to eq [:obj2] - obj3.within_namespace do - obj3.namespace_stackable(:some_thing, :obj3) - expect(obj3.namespace_stackable(:some_thing)).to eq [:obj3] - obj3_copy = obj3.inheritable_setting.point_in_time_copy - end + obj3.within_namespace do + obj3.namespace_stackable(:some_thing, :obj3) + expect(obj3.namespace_stackable(:some_thing)).to eq [:obj3] + obj3_copy = obj3.inheritable_setting.point_in_time_copy + end - expect(obj3.namespace_stackable(:some_thing)).to eq [] - expect(obj3_copy.namespace_stackable[:some_thing]).to eq [:obj3] + expect(obj3.namespace_stackable(:some_thing)).to eq [] + expect(obj3_copy.namespace_stackable[:some_thing]).to eq [:obj3] - obj1.top_level_setting.inherit_from obj2_copy.point_in_time_copy - obj2.top_level_setting.inherit_from obj3_copy.point_in_time_copy + obj1.top_level_setting.inherit_from obj2_copy.point_in_time_copy + obj2.top_level_setting.inherit_from obj3_copy.point_in_time_copy - expect(obj1_copy.namespace_stackable[:some_thing]).to eq %i[obj3 obj2 obj1] - end - end + expect(obj1_copy.namespace_stackable[:some_thing]).to eq %i[obj3 obj2 obj1] end end end diff --git a/spec/grape/named_api_spec.rb b/spec/grape/named_api_spec.rb index e3aac6cb5..77e73d875 100644 --- a/spec/grape/named_api_spec.rb +++ b/spec/grape/named_api_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe 'A named API' do +describe Grape::API do subject(:api_name) { NamedAPI.endpoints.last.options[:for].to_s } let(:api) do @@ -11,9 +11,11 @@ end end - before { stub_const('NamedAPI', api) } + let(:name) { 'NamedAPI' } + + before { stub_const(name, api) } it 'can access the name of the API' do - expect(api_name).to eq 'NamedAPI' + expect(api_name).to eq name end end diff --git a/spec/grape/path_spec.rb b/spec/grape/path_spec.rb index afad438df..2a6dd2fce 100644 --- a/spec/grape/path_spec.rb +++ b/spec/grape/path_spec.rb @@ -1,243 +1,241 @@ # frozen_string_literal: true -module Grape - describe Path do - describe '#initialize' do - it 'remembers the path' do - path = described_class.new('/:id', anything, anything) - expect(path.raw_path).to eql('/:id') - end +describe Grape::Path do + describe '#initialize' do + it 'remembers the path' do + path = described_class.new('/:id', anything, anything) + expect(path.raw_path).to eql('/:id') + end - it 'remembers the namespace' do - path = described_class.new(anything, '/users', anything) - expect(path.namespace).to eql('/users') - end + it 'remembers the namespace' do + path = described_class.new(anything, '/users', anything) + expect(path.namespace).to eql('/users') + end - it 'remebers the settings' do - path = described_class.new(anything, anything, foo: 'bar') - expect(path.settings).to eql(foo: 'bar') - end + it 'remebers the settings' do + path = described_class.new(anything, anything, foo: 'bar') + expect(path.settings).to eql(foo: 'bar') end + end - describe '#mount_path' do - it 'is nil when no mount path setting exists' do - path = described_class.new(anything, anything, {}) - expect(path.mount_path).to be_nil - end + describe '#mount_path' do + it 'is nil when no mount path setting exists' do + path = described_class.new(anything, anything, {}) + expect(path.mount_path).to be_nil + end - it 'is nil when the mount path is nil' do - path = described_class.new(anything, anything, mount_path: nil) - expect(path.mount_path).to be_nil - end + it 'is nil when the mount path is nil' do + path = described_class.new(anything, anything, mount_path: nil) + expect(path.mount_path).to be_nil + end - it 'splits the mount path' do - path = described_class.new(anything, anything, mount_path: %w[foo bar]) - expect(path.mount_path).to eql(%w[foo bar]) - end + it 'splits the mount path' do + path = described_class.new(anything, anything, mount_path: %w[foo bar]) + expect(path.mount_path).to eql(%w[foo bar]) end + end - describe '#root_prefix' do - it 'is nil when no root prefix setting exists' do - path = described_class.new(anything, anything, {}) - expect(path.root_prefix).to be_nil - end + describe '#root_prefix' do + it 'is nil when no root prefix setting exists' do + path = described_class.new(anything, anything, {}) + expect(path.root_prefix).to be_nil + end - it 'is nil when the mount path is nil' do - path = described_class.new(anything, anything, root_prefix: nil) - expect(path.root_prefix).to be_nil - end + it 'is nil when the mount path is nil' do + path = described_class.new(anything, anything, root_prefix: nil) + expect(path.root_prefix).to be_nil + end - it 'splits the mount path' do - path = described_class.new(anything, anything, root_prefix: 'hello/world') - expect(path.root_prefix).to eql('hello/world') - end + it 'splits the mount path' do + path = described_class.new(anything, anything, root_prefix: 'hello/world') + expect(path.root_prefix).to eql('hello/world') end + end - describe '#uses_path_versioning?' do - it 'is false when the version setting is nil' do - path = described_class.new(anything, anything, version: nil) - expect(path.uses_path_versioning?).to be false - end + describe '#uses_path_versioning?' do + it 'is false when the version setting is nil' do + path = described_class.new(anything, anything, version: nil) + expect(path.uses_path_versioning?).to be false + end - it 'is false when the version option is header' do - path = described_class.new( - anything, - anything, - version: 'v1', - version_options: { using: :header } - ) + it 'is false when the version option is header' do + path = described_class.new( + anything, + anything, + version: 'v1', + version_options: { using: :header } + ) - expect(path.uses_path_versioning?).to be false - end + expect(path.uses_path_versioning?).to be false + end - it 'is true when the version option is path' do - path = described_class.new( - anything, - anything, - version: 'v1', - version_options: { using: :path } - ) + it 'is true when the version option is path' do + path = described_class.new( + anything, + anything, + version: 'v1', + version_options: { using: :path } + ) - expect(path.uses_path_versioning?).to be true - end + expect(path.uses_path_versioning?).to be true end + end - describe '#namespace?' do - it 'is false when the namespace is nil' do - path = described_class.new(anything, nil, anything) - expect(path).not_to be_namespace - end + describe '#namespace?' do + it 'is false when the namespace is nil' do + path = described_class.new(anything, nil, anything) + expect(path).not_to be_namespace + end - it 'is false when the namespace starts with whitespace' do - path = described_class.new(anything, ' /foo', anything) - expect(path).not_to be_namespace - end + it 'is false when the namespace starts with whitespace' do + path = described_class.new(anything, ' /foo', anything) + expect(path).not_to be_namespace + end - it 'is false when the namespace is the root path' do - path = described_class.new(anything, '/', anything) - expect(path.namespace?).to be false - end + it 'is false when the namespace is the root path' do + path = described_class.new(anything, '/', anything) + expect(path.namespace?).to be false + end - it 'is true otherwise' do - path = described_class.new(anything, '/world', anything) - expect(path.namespace?).to be true - end + it 'is true otherwise' do + path = described_class.new(anything, '/world', anything) + expect(path.namespace?).to be true end + end - describe '#path?' do - it 'is false when the path is nil' do - path = described_class.new(nil, anything, anything) - expect(path).not_to be_path - end + describe '#path?' do + it 'is false when the path is nil' do + path = described_class.new(nil, anything, anything) + expect(path).not_to be_path + end - it 'is false when the path starts with whitespace' do - path = described_class.new(' /foo', anything, anything) - expect(path).not_to be_path - end + it 'is false when the path starts with whitespace' do + path = described_class.new(' /foo', anything, anything) + expect(path).not_to be_path + end - it 'is false when the path is the root path' do - path = described_class.new('/', anything, anything) - expect(path.path?).to be false - end + it 'is false when the path is the root path' do + path = described_class.new('/', anything, anything) + expect(path.path?).to be false + end - it 'is true otherwise' do - path = described_class.new('/hello', anything, anything) - expect(path.path?).to be true - end + it 'is true otherwise' do + path = described_class.new('/hello', anything, anything) + expect(path.path?).to be true end + end - describe '#path' do - context 'mount_path' do - it 'is not included when it is nil' do - path = described_class.new(nil, nil, mount_path: '/foo/bar') - expect(path.path).to eql '/foo/bar' - end + describe '#path' do + context 'mount_path' do + it 'is not included when it is nil' do + path = described_class.new(nil, nil, mount_path: '/foo/bar') + expect(path.path).to eql '/foo/bar' + end - it 'is included when it is not nil' do - path = described_class.new(nil, nil, {}) - expect(path.path).to eql('/') - end + it 'is included when it is not nil' do + path = described_class.new(nil, nil, {}) + expect(path.path).to eql('/') end + end - context 'root_prefix' do - it 'is not included when it is nil' do - path = described_class.new(nil, nil, {}) - expect(path.path).to eql('/') - end - - it 'is included after the mount path' do - path = described_class.new( - nil, - nil, - mount_path: '/foo', - root_prefix: '/hello' - ) - - expect(path.path).to eql('/foo/hello') - end + context 'root_prefix' do + it 'is not included when it is nil' do + path = described_class.new(nil, nil, {}) + expect(path.path).to eql('/') end - it 'uses the namespace after the mount path and root prefix' do + it 'is included after the mount path' do path = described_class.new( nil, - 'namespace', + nil, mount_path: '/foo', root_prefix: '/hello' ) - expect(path.path).to eql('/foo/hello/namespace') + expect(path.path).to eql('/foo/hello') end + end - it 'uses the raw path after the namespace' do - path = described_class.new( - 'raw_path', - 'namespace', - mount_path: '/foo', - root_prefix: '/hello' - ) + it 'uses the namespace after the mount path and root prefix' do + path = described_class.new( + nil, + 'namespace', + mount_path: '/foo', + root_prefix: '/hello' + ) - expect(path.path).to eql('/foo/hello/namespace/raw_path') - end + expect(path.path).to eql('/foo/hello/namespace') end - describe '#suffix' do - context 'when using a specific format' do - it 'accepts specified format' do - path = described_class.new(nil, nil, {}) - allow(path).to receive_messages(uses_specific_format?: true, settings: { format: :json }) + it 'uses the raw path after the namespace' do + path = described_class.new( + 'raw_path', + 'namespace', + mount_path: '/foo', + root_prefix: '/hello' + ) - expect(path.suffix).to eql('(.json)') - end - end + expect(path.path).to eql('/foo/hello/namespace/raw_path') + end + end - context 'when path versioning is used' do - it "includes a '/'" do - path = described_class.new(nil, nil, {}) - allow(path).to receive_messages(uses_specific_format?: false, uses_path_versioning?: true) + describe '#suffix' do + context 'when using a specific format' do + it 'accepts specified format' do + path = described_class.new(nil, nil, {}) + allow(path).to receive_messages(uses_specific_format?: true, settings: { format: :json }) - expect(path.suffix).to eql('(/.:format)') - end + expect(path.suffix).to eql('(.json)') end + end - context 'when path versioning is not used' do - it "does not include a '/' when the path has a namespace" do - path = described_class.new(nil, 'namespace', {}) - allow(path).to receive_messages(uses_specific_format?: false, uses_path_versioning?: true) + context 'when path versioning is used' do + it "includes a '/'" do + path = described_class.new(nil, nil, {}) + allow(path).to receive_messages(uses_specific_format?: false, uses_path_versioning?: true) - expect(path.suffix).to eql('(.:format)') - end + expect(path.suffix).to eql('(/.:format)') + end + end - it "does not include a '/' when the path has a path" do - path = described_class.new('/path', nil, {}) - allow(path).to receive_messages(uses_specific_format?: false, uses_path_versioning?: true) + context 'when path versioning is not used' do + it "does not include a '/' when the path has a namespace" do + path = described_class.new(nil, 'namespace', {}) + allow(path).to receive_messages(uses_specific_format?: false, uses_path_versioning?: true) - expect(path.suffix).to eql('(.:format)') - end + expect(path.suffix).to eql('(.:format)') + end - it "includes a '/' otherwise" do - path = described_class.new(nil, nil, {}) - allow(path).to receive_messages(uses_specific_format?: false, uses_path_versioning?: true) + it "does not include a '/' when the path has a path" do + path = described_class.new('/path', nil, {}) + allow(path).to receive_messages(uses_specific_format?: false, uses_path_versioning?: true) - expect(path.suffix).to eql('(/.:format)') - end + expect(path.suffix).to eql('(.:format)') end - end - describe '#path_with_suffix' do - it 'combines the path and suffix' do + it "includes a '/' otherwise" do path = described_class.new(nil, nil, {}) - allow(path).to receive_messages(path: '/the/path', suffix: 'suffix') + allow(path).to receive_messages(uses_specific_format?: false, uses_path_versioning?: true) - expect(path.path_with_suffix).to eql('/the/pathsuffix') + expect(path.suffix).to eql('(/.:format)') end + end + end - context 'when using a specific format' do - it 'might have a suffix with specified format' do - path = described_class.new(nil, nil, {}) - allow(path).to receive_messages(path: '/the/path', uses_specific_format?: true, settings: { format: :json }) + describe '#path_with_suffix' do + it 'combines the path and suffix' do + path = described_class.new(nil, nil, {}) + allow(path).to receive_messages(path: '/the/path', suffix: 'suffix') + + expect(path.path_with_suffix).to eql('/the/pathsuffix') + end + + context 'when using a specific format' do + it 'might have a suffix with specified format' do + path = described_class.new(nil, nil, {}) + allow(path).to receive_messages(path: '/the/path', uses_specific_format?: true, settings: { format: :json }) - expect(path.path_with_suffix).to eql('/the/path(.json)') - end + expect(path.path_with_suffix).to eql('/the/path(.json)') end end end diff --git a/spec/grape/presenters/presenter_spec.rb b/spec/grape/presenters/presenter_spec.rb index da155190b..f673b8fa0 100644 --- a/spec/grape/presenters/presenter_spec.rb +++ b/spec/grape/presenters/presenter_spec.rb @@ -1,69 +1,65 @@ # frozen_string_literal: true -module Grape - module Presenters - module PresenterSpec - class Dummy - include Grape::DSL::InsideRoute +describe Grape::Presenters::Presenter do + subject { dummy_class.new } - attr_reader :env, :request, :new_settings + let(:dummy_class) do + Class.new do + include Grape::DSL::InsideRoute - def initialize - @env = {} - @header = {} - @new_settings = { namespace_inheritable: {}, namespace_stackable: {} } - end + attr_reader :env, :request, :new_settings + + def initialize + @env = {} + @header = {} + @new_settings = { namespace_inheritable: {}, namespace_stackable: {} } end end + end - describe Presenter do - subject { PresenterSpec::Dummy.new } - - describe 'represent' do - let(:object_mock) do - Object.new - end + describe 'represent' do + let(:object_mock) do + Object.new + end - it 'represent object' do - expect(described_class.represent(object_mock)).to eq object_mock - end - end + it 'represent object' do + expect(described_class.represent(object_mock)).to eq object_mock + end + end - describe 'present' do - let(:hash_mock) do - { key: :value } - end + describe 'present' do + let(:hash_mock) do + { key: :value } + end - describe 'instance' do - before do - subject.present hash_mock, with: described_class - end + describe 'instance' do + before do + subject.present hash_mock, with: described_class + end - it 'presents dummy hash' do - expect(subject.body).to eq hash_mock - end - end + it 'presents dummy hash' do + expect(subject.body).to eq hash_mock + end + end - describe 'multiple presenter' do - let(:hash_mock1) do - { key1: :value1 } - end + describe 'multiple presenter' do + let(:hash_mock1) do + { key1: :value1 } + end - let(:hash_mock2) do - { key2: :value2 } - end + let(:hash_mock2) do + { key2: :value2 } + end - describe 'instance' do - before do - subject.present hash_mock1, with: described_class - subject.present hash_mock2, with: described_class - end + describe 'instance' do + before do + subject.present hash_mock1, with: described_class + subject.present hash_mock2, with: described_class + end - it 'presents both dummy presenter' do - expect(subject.body[:key1]).to eq hash_mock1[:key1] - expect(subject.body[:key2]).to eq hash_mock2[:key2] - end - end + it 'presents both dummy presenter' do + expect(subject.body[:key1]).to eq hash_mock1[:key1] + expect(subject.body[:key2]).to eq hash_mock2[:key2] end end end diff --git a/spec/grape/util/inheritable_setting_spec.rb b/spec/grape/util/inheritable_setting_spec.rb index c9ad93bd9..03a79368c 100644 --- a/spec/grape/util/inheritable_setting_spec.rb +++ b/spec/grape/util/inheritable_setting_spec.rb @@ -1,239 +1,236 @@ # frozen_string_literal: true -module Grape - module Util - describe InheritableSetting do - before do - described_class.reset_global! - subject.inherit_from parent - end - - let(:parent) do - described_class.new.tap do |settings| - settings.global[:global_thing] = :global_foo_bar - settings.namespace[:namespace_thing] = :namespace_foo_bar - settings.namespace_inheritable[:namespace_inheritable_thing] = :namespace_inheritable_foo_bar - settings.namespace_stackable[:namespace_stackable_thing] = :namespace_stackable_foo_bar - settings.namespace_reverse_stackable[:namespace_reverse_stackable_thing] = :namespace_reverse_stackable_foo_bar - settings.route[:route_thing] = :route_foo_bar - end - end - - let(:other_parent) do - described_class.new.tap do |settings| - settings.namespace[:namespace_thing] = :namespace_foo_bar_other - settings.namespace_inheritable[:namespace_inheritable_thing] = :namespace_inheritable_foo_bar_other - settings.namespace_stackable[:namespace_stackable_thing] = :namespace_stackable_foo_bar_other - settings.namespace_reverse_stackable[:namespace_reverse_stackable_thing] = :namespace_reverse_stackable_foo_bar_other - settings.route[:route_thing] = :route_foo_bar_other - end - end - - describe '#global' do - it 'sets a global value' do - subject.global[:some_thing] = :foo_bar - expect(subject.global[:some_thing]).to eq :foo_bar - subject.global[:some_thing] = :foo_bar_next - expect(subject.global[:some_thing]).to eq :foo_bar_next - end - - it 'sets the global inherited values' do - expect(subject.global[:global_thing]).to eq :global_foo_bar - end - - it 'overrides global values' do - subject.global[:global_thing] = :global_new_foo_bar - expect(parent.global[:global_thing]).to eq :global_new_foo_bar - end - - it 'handles different parents' do - subject.global[:global_thing] = :global_new_foo_bar - - subject.inherit_from other_parent - - expect(parent.global[:global_thing]).to eq :global_new_foo_bar - expect(other_parent.global[:global_thing]).to eq :global_new_foo_bar - end - end - - describe '#api_class' do - it 'is specific to the class' do - subject.api_class[:some_thing] = :foo_bar - parent.api_class[:some_thing] = :some_thing - - expect(subject.api_class[:some_thing]).to eq :foo_bar - expect(parent.api_class[:some_thing]).to eq :some_thing - end - end - - describe '#namespace' do - it 'sets a value until the end of a namespace' do - subject.namespace[:some_thing] = :foo_bar - expect(subject.namespace[:some_thing]).to eq :foo_bar - end - - it 'uses new values when a new namespace starts' do - subject.namespace[:namespace_thing] = :new_namespace_foo_bar - expect(subject.namespace[:namespace_thing]).to eq :new_namespace_foo_bar - - expect(parent.namespace[:namespace_thing]).to eq :namespace_foo_bar - end - end - - describe '#namespace_inheritable' do - it 'works with inheritable values' do - expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :namespace_inheritable_foo_bar - end - - it 'handles different parents' do - expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :namespace_inheritable_foo_bar - - subject.inherit_from other_parent +describe Grape::Util::InheritableSetting do + before do + described_class.reset_global! + subject.inherit_from parent + end + + let(:parent) do + described_class.new.tap do |settings| + settings.global[:global_thing] = :global_foo_bar + settings.namespace[:namespace_thing] = :namespace_foo_bar + settings.namespace_inheritable[:namespace_inheritable_thing] = :namespace_inheritable_foo_bar + settings.namespace_stackable[:namespace_stackable_thing] = :namespace_stackable_foo_bar + settings.namespace_reverse_stackable[:namespace_reverse_stackable_thing] = :namespace_reverse_stackable_foo_bar + settings.route[:route_thing] = :route_foo_bar + end + end + + let(:other_parent) do + described_class.new.tap do |settings| + settings.namespace[:namespace_thing] = :namespace_foo_bar_other + settings.namespace_inheritable[:namespace_inheritable_thing] = :namespace_inheritable_foo_bar_other + settings.namespace_stackable[:namespace_stackable_thing] = :namespace_stackable_foo_bar_other + settings.namespace_reverse_stackable[:namespace_reverse_stackable_thing] = :namespace_reverse_stackable_foo_bar_other + settings.route[:route_thing] = :route_foo_bar_other + end + end + + describe '#global' do + it 'sets a global value' do + subject.global[:some_thing] = :foo_bar + expect(subject.global[:some_thing]).to eq :foo_bar + subject.global[:some_thing] = :foo_bar_next + expect(subject.global[:some_thing]).to eq :foo_bar_next + end + + it 'sets the global inherited values' do + expect(subject.global[:global_thing]).to eq :global_foo_bar + end + + it 'overrides global values' do + subject.global[:global_thing] = :global_new_foo_bar + expect(parent.global[:global_thing]).to eq :global_new_foo_bar + end + + it 'handles different parents' do + subject.global[:global_thing] = :global_new_foo_bar + + subject.inherit_from other_parent + + expect(parent.global[:global_thing]).to eq :global_new_foo_bar + expect(other_parent.global[:global_thing]).to eq :global_new_foo_bar + end + end + + describe '#api_class' do + it 'is specific to the class' do + subject.api_class[:some_thing] = :foo_bar + parent.api_class[:some_thing] = :some_thing + + expect(subject.api_class[:some_thing]).to eq :foo_bar + expect(parent.api_class[:some_thing]).to eq :some_thing + end + end + + describe '#namespace' do + it 'sets a value until the end of a namespace' do + subject.namespace[:some_thing] = :foo_bar + expect(subject.namespace[:some_thing]).to eq :foo_bar + end + + it 'uses new values when a new namespace starts' do + subject.namespace[:namespace_thing] = :new_namespace_foo_bar + expect(subject.namespace[:namespace_thing]).to eq :new_namespace_foo_bar + + expect(parent.namespace[:namespace_thing]).to eq :namespace_foo_bar + end + end + + describe '#namespace_inheritable' do + it 'works with inheritable values' do + expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :namespace_inheritable_foo_bar + end - expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :namespace_inheritable_foo_bar_other - - subject.inherit_from parent + it 'handles different parents' do + expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :namespace_inheritable_foo_bar - expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :namespace_inheritable_foo_bar - - subject.inherit_from other_parent + subject.inherit_from other_parent - subject.namespace_inheritable[:namespace_inheritable_thing] = :my_thing - - expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :my_thing + expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :namespace_inheritable_foo_bar_other - subject.inherit_from parent + subject.inherit_from parent - expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :my_thing - end - end + expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :namespace_inheritable_foo_bar - describe '#namespace_stackable' do - it 'works with stackable values' do - expect(subject.namespace_stackable[:namespace_stackable_thing]).to eq [:namespace_stackable_foo_bar] + subject.inherit_from other_parent - subject.inherit_from other_parent + subject.namespace_inheritable[:namespace_inheritable_thing] = :my_thing - expect(subject.namespace_stackable[:namespace_stackable_thing]).to eq [:namespace_stackable_foo_bar_other] - end - end + expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :my_thing - describe '#namespace_reverse_stackable' do - it 'works with reverse stackable values' do - expect(subject.namespace_reverse_stackable[:namespace_reverse_stackable_thing]).to eq [:namespace_reverse_stackable_foo_bar] + subject.inherit_from parent - subject.inherit_from other_parent + expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :my_thing + end + end + + describe '#namespace_stackable' do + it 'works with stackable values' do + expect(subject.namespace_stackable[:namespace_stackable_thing]).to eq [:namespace_stackable_foo_bar] + + subject.inherit_from other_parent + + expect(subject.namespace_stackable[:namespace_stackable_thing]).to eq [:namespace_stackable_foo_bar_other] + end + end + + describe '#namespace_reverse_stackable' do + it 'works with reverse stackable values' do + expect(subject.namespace_reverse_stackable[:namespace_reverse_stackable_thing]).to eq [:namespace_reverse_stackable_foo_bar] + + subject.inherit_from other_parent + + expect(subject.namespace_reverse_stackable[:namespace_reverse_stackable_thing]).to eq [:namespace_reverse_stackable_foo_bar_other] + end + end + + describe '#route' do + it 'sets a value until the next route' do + subject.route[:some_thing] = :foo_bar + expect(subject.route[:some_thing]).to eq :foo_bar + + subject.route_end + + expect(subject.route[:some_thing]).to be_nil + end - expect(subject.namespace_reverse_stackable[:namespace_reverse_stackable_thing]).to eq [:namespace_reverse_stackable_foo_bar_other] - end - end + it 'works with route values' do + expect(subject.route[:route_thing]).to eq :route_foo_bar + end + end - describe '#route' do - it 'sets a value until the next route' do - subject.route[:some_thing] = :foo_bar - expect(subject.route[:some_thing]).to eq :foo_bar + describe '#api_class' do + it 'is specific to the class' do + subject.api_class[:some_thing] = :foo_bar + expect(subject.api_class[:some_thing]).to eq :foo_bar + end + end - subject.route_end + describe '#inherit_from' do + it 'notifies clones' do + new_settings = subject.point_in_time_copy + expect(new_settings).to receive(:inherit_from).with(other_parent) - expect(subject.route[:some_thing]).to be_nil - end + subject.inherit_from other_parent + end + end - it 'works with route values' do - expect(subject.route[:route_thing]).to eq :route_foo_bar - end - end + describe '#point_in_time_copy' do + let!(:cloned_obj) { subject.point_in_time_copy } - describe '#api_class' do - it 'is specific to the class' do - subject.api_class[:some_thing] = :foo_bar - expect(subject.api_class[:some_thing]).to eq :foo_bar - end - end + it 'resets point_in_time_copies' do + expect(cloned_obj.point_in_time_copies).to be_empty + end - describe '#inherit_from' do - it 'notifies clones' do - new_settings = subject.point_in_time_copy - expect(new_settings).to receive(:inherit_from).with(other_parent) + it 'decouples namespace values' do + subject.namespace[:namespace_thing] = :namespace_foo_bar - subject.inherit_from other_parent - end - end + cloned_obj.namespace[:namespace_thing] = :new_namespace_foo_bar + expect(subject.namespace[:namespace_thing]).to eq :namespace_foo_bar + end - describe '#point_in_time_copy' do - let!(:cloned_obj) { subject.point_in_time_copy } + it 'decouples namespace inheritable values' do + expect(cloned_obj.namespace_inheritable[:namespace_inheritable_thing]).to eq :namespace_inheritable_foo_bar - it 'resets point_in_time_copies' do - expect(cloned_obj.point_in_time_copies).to be_empty - end + subject.namespace_inheritable[:namespace_inheritable_thing] = :my_thing + expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :my_thing - it 'decouples namespace values' do - subject.namespace[:namespace_thing] = :namespace_foo_bar + expect(cloned_obj.namespace_inheritable[:namespace_inheritable_thing]).to eq :namespace_inheritable_foo_bar - cloned_obj.namespace[:namespace_thing] = :new_namespace_foo_bar - expect(subject.namespace[:namespace_thing]).to eq :namespace_foo_bar - end + cloned_obj.namespace_inheritable[:namespace_inheritable_thing] = :my_cloned_thing + expect(cloned_obj.namespace_inheritable[:namespace_inheritable_thing]).to eq :my_cloned_thing + expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :my_thing + end - it 'decouples namespace inheritable values' do - expect(cloned_obj.namespace_inheritable[:namespace_inheritable_thing]).to eq :namespace_inheritable_foo_bar + it 'decouples namespace stackable values' do + expect(cloned_obj.namespace_stackable[:namespace_stackable_thing]).to eq [:namespace_stackable_foo_bar] - subject.namespace_inheritable[:namespace_inheritable_thing] = :my_thing - expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :my_thing + subject.namespace_stackable[:namespace_stackable_thing] = :other_thing + expect(subject.namespace_stackable[:namespace_stackable_thing]).to eq %i[namespace_stackable_foo_bar other_thing] + expect(cloned_obj.namespace_stackable[:namespace_stackable_thing]).to eq [:namespace_stackable_foo_bar] + end - expect(cloned_obj.namespace_inheritable[:namespace_inheritable_thing]).to eq :namespace_inheritable_foo_bar + it 'decouples namespace reverse stackable values' do + expect(cloned_obj.namespace_reverse_stackable[:namespace_reverse_stackable_thing]).to eq [:namespace_reverse_stackable_foo_bar] - cloned_obj.namespace_inheritable[:namespace_inheritable_thing] = :my_cloned_thing - expect(cloned_obj.namespace_inheritable[:namespace_inheritable_thing]).to eq :my_cloned_thing - expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :my_thing - end + subject.namespace_reverse_stackable[:namespace_reverse_stackable_thing] = :other_thing + expect(subject.namespace_reverse_stackable[:namespace_reverse_stackable_thing]).to eq %i[other_thing namespace_reverse_stackable_foo_bar] + expect(cloned_obj.namespace_reverse_stackable[:namespace_reverse_stackable_thing]).to eq [:namespace_reverse_stackable_foo_bar] + end - it 'decouples namespace stackable values' do - expect(cloned_obj.namespace_stackable[:namespace_stackable_thing]).to eq [:namespace_stackable_foo_bar] + it 'decouples route values' do + expect(cloned_obj.route[:route_thing]).to eq :route_foo_bar - subject.namespace_stackable[:namespace_stackable_thing] = :other_thing - expect(subject.namespace_stackable[:namespace_stackable_thing]).to eq %i[namespace_stackable_foo_bar other_thing] - expect(cloned_obj.namespace_stackable[:namespace_stackable_thing]).to eq [:namespace_stackable_foo_bar] - end + subject.route[:route_thing] = :new_route_foo_bar + expect(cloned_obj.route[:route_thing]).to eq :route_foo_bar + end - it 'decouples namespace reverse stackable values' do - expect(cloned_obj.namespace_reverse_stackable[:namespace_reverse_stackable_thing]).to eq [:namespace_reverse_stackable_foo_bar] + it 'adds itself to original as clone' do + expect(subject.point_in_time_copies).to include(cloned_obj) + end + end - subject.namespace_reverse_stackable[:namespace_reverse_stackable_thing] = :other_thing - expect(subject.namespace_reverse_stackable[:namespace_reverse_stackable_thing]).to eq %i[other_thing namespace_reverse_stackable_foo_bar] - expect(cloned_obj.namespace_reverse_stackable[:namespace_reverse_stackable_thing]).to eq [:namespace_reverse_stackable_foo_bar] - end - - it 'decouples route values' do - expect(cloned_obj.route[:route_thing]).to eq :route_foo_bar - - subject.route[:route_thing] = :new_route_foo_bar - expect(cloned_obj.route[:route_thing]).to eq :route_foo_bar - end - - it 'adds itself to original as clone' do - expect(subject.point_in_time_copies).to include(cloned_obj) - end - end - - describe '#to_hash' do - it 'return all settings as a hash' do - subject.global[:global_thing] = :global_foo_bar - subject.namespace[:namespace_thing] = :namespace_foo_bar - subject.namespace_inheritable[:namespace_inheritable_thing] = :namespace_inheritable_foo_bar - subject.namespace_stackable[:namespace_stackable_thing] = [:namespace_stackable_foo_bar] - subject.namespace_reverse_stackable[:namespace_reverse_stackable_thing] = [:namespace_reverse_stackable_foo_bar] - subject.route[:route_thing] = :route_foo_bar - - expect(subject.to_hash).to include(global: { global_thing: :global_foo_bar }) - expect(subject.to_hash).to include(namespace: { namespace_thing: :namespace_foo_bar }) - expect(subject.to_hash).to include(namespace_inheritable: { - namespace_inheritable_thing: :namespace_inheritable_foo_bar - }) - expect(subject.to_hash).to include(namespace_stackable: { namespace_stackable_thing: [:namespace_stackable_foo_bar, [:namespace_stackable_foo_bar]] }) - expect(subject.to_hash).to include(namespace_reverse_stackable: - { namespace_reverse_stackable_thing: [[:namespace_reverse_stackable_foo_bar], :namespace_reverse_stackable_foo_bar] }) - expect(subject.to_hash).to include(route: { route_thing: :route_foo_bar }) - end - end + describe '#to_hash' do + it 'return all settings as a hash' do + subject.global[:global_thing] = :global_foo_bar + subject.namespace[:namespace_thing] = :namespace_foo_bar + subject.namespace_inheritable[:namespace_inheritable_thing] = :namespace_inheritable_foo_bar + subject.namespace_stackable[:namespace_stackable_thing] = [:namespace_stackable_foo_bar] + subject.namespace_reverse_stackable[:namespace_reverse_stackable_thing] = [:namespace_reverse_stackable_foo_bar] + subject.route[:route_thing] = :route_foo_bar + expect(subject.to_hash).to match( + global: { global_thing: :global_foo_bar }, + namespace: { namespace_thing: :namespace_foo_bar }, + namespace_inheritable: { + namespace_inheritable_thing: :namespace_inheritable_foo_bar + }, + namespace_stackable: { namespace_stackable_thing: [:namespace_stackable_foo_bar, [:namespace_stackable_foo_bar]] }, + namespace_reverse_stackable: + { namespace_reverse_stackable_thing: [[:namespace_reverse_stackable_foo_bar], :namespace_reverse_stackable_foo_bar] }, + route: { route_thing: :route_foo_bar } + ) end end end diff --git a/spec/grape/util/inheritable_values_spec.rb b/spec/grape/util/inheritable_values_spec.rb index fed4a62c2..1f424e59f 100644 --- a/spec/grape/util/inheritable_values_spec.rb +++ b/spec/grape/util/inheritable_values_spec.rb @@ -1,78 +1,74 @@ # frozen_string_literal: true -module Grape - module Util - describe InheritableValues do - subject { described_class.new(parent) } +describe Grape::Util::InheritableValues do + subject { described_class.new(parent) } - let(:parent) { described_class.new } + let(:parent) { described_class.new } - describe '#delete' do - it 'deletes a key' do - subject[:some_thing] = :new_foo_bar - subject.delete :some_thing - expect(subject[:some_thing]).to be_nil - end + describe '#delete' do + it 'deletes a key' do + subject[:some_thing] = :new_foo_bar + subject.delete :some_thing + expect(subject[:some_thing]).to be_nil + end - it 'does not delete parent values' do - parent[:some_thing] = :foo - subject[:some_thing] = :new_foo_bar - subject.delete :some_thing - expect(subject[:some_thing]).to eq :foo - end - end + it 'does not delete parent values' do + parent[:some_thing] = :foo + subject[:some_thing] = :new_foo_bar + subject.delete :some_thing + expect(subject[:some_thing]).to eq :foo + end + end - describe '#[]' do - it 'returns a value' do - subject[:some_thing] = :foo - expect(subject[:some_thing]).to eq :foo - end + describe '#[]' do + it 'returns a value' do + subject[:some_thing] = :foo + expect(subject[:some_thing]).to eq :foo + end - it 'returns parent value when no value is set' do - parent[:some_thing] = :foo - expect(subject[:some_thing]).to eq :foo - end + it 'returns parent value when no value is set' do + parent[:some_thing] = :foo + expect(subject[:some_thing]).to eq :foo + end - it 'overwrites parent value with the current one' do - parent[:some_thing] = :foo - subject[:some_thing] = :foo_bar - expect(subject[:some_thing]).to eq :foo_bar - end + it 'overwrites parent value with the current one' do + parent[:some_thing] = :foo + subject[:some_thing] = :foo_bar + expect(subject[:some_thing]).to eq :foo_bar + end - it 'parent values are not changed' do - parent[:some_thing] = :foo - subject[:some_thing] = :foo_bar - expect(parent[:some_thing]).to eq :foo - end - end + it 'parent values are not changed' do + parent[:some_thing] = :foo + subject[:some_thing] = :foo_bar + expect(parent[:some_thing]).to eq :foo + end + end - describe '#[]=' do - it 'sets a value' do - subject[:some_thing] = :foo - expect(subject[:some_thing]).to eq :foo - end - end + describe '#[]=' do + it 'sets a value' do + subject[:some_thing] = :foo + expect(subject[:some_thing]).to eq :foo + end + end - describe '#to_hash' do - it 'returns a Hash representation' do - parent[:some_thing] = :foo - subject[:some_thing_more] = :foo_bar - expect(subject.to_hash).to eq(some_thing: :foo, some_thing_more: :foo_bar) - end - end + describe '#to_hash' do + it 'returns a Hash representation' do + parent[:some_thing] = :foo + subject[:some_thing_more] = :foo_bar + expect(subject.to_hash).to eq(some_thing: :foo, some_thing_more: :foo_bar) + end + end - describe '#clone' do - let(:obj_cloned) { subject.clone } + describe '#clone' do + let(:obj_cloned) { subject.clone } - context 'complex (i.e. not primitive) data types (ex. entity classes, please see bug #891)' do - let(:description) { { entity: double } } + context 'complex (i.e. not primitive) data types (ex. entity classes, please see bug #891)' do + let(:description) { { entity: double } } - before { subject[:description] = description } + before { subject[:description] = description } - it 'copies values; does not duplicate them' do - expect(obj_cloned[:description]).to eq description - end - end + it 'copies values; does not duplicate them' do + expect(obj_cloned[:description]).to eq description end end end diff --git a/spec/grape/util/reverse_stackable_values_spec.rb b/spec/grape/util/reverse_stackable_values_spec.rb index e2f5b282f..4037f5778 100644 --- a/spec/grape/util/reverse_stackable_values_spec.rb +++ b/spec/grape/util/reverse_stackable_values_spec.rb @@ -1,133 +1,129 @@ # frozen_string_literal: true -module Grape - module Util - describe ReverseStackableValues do - subject { described_class.new(parent) } +describe Grape::Util::ReverseStackableValues do + subject { described_class.new(parent) } - let(:parent) { described_class.new } + let(:parent) { described_class.new } - describe '#keys' do - it 'returns all keys' do - subject[:some_thing] = :foo_bar - subject[:some_thing_else] = :foo_bar - expect(subject.keys).to eq %i[some_thing some_thing_else].sort - end + describe '#keys' do + it 'returns all keys' do + subject[:some_thing] = :foo_bar + subject[:some_thing_else] = :foo_bar + expect(subject.keys).to eq %i[some_thing some_thing_else].sort + end - it 'returns merged keys with parent' do - parent[:some_thing] = :foo - parent[:some_thing_else] = :foo + it 'returns merged keys with parent' do + parent[:some_thing] = :foo + parent[:some_thing_else] = :foo - subject[:some_thing] = :foo_bar - subject[:some_thing_more] = :foo_bar + subject[:some_thing] = :foo_bar + subject[:some_thing_more] = :foo_bar - expect(subject.keys).to eq %i[some_thing some_thing_else some_thing_more].sort - end - end + expect(subject.keys).to eq %i[some_thing some_thing_else some_thing_more].sort + end + end - describe '#delete' do - it 'deletes a key' do - subject[:some_thing] = :new_foo_bar - subject.delete :some_thing - expect(subject[:some_thing]).to eq [] - end - - it 'does not delete parent values' do - parent[:some_thing] = :foo - subject[:some_thing] = :new_foo_bar - subject.delete :some_thing - expect(subject[:some_thing]).to eq [:foo] - end - end + describe '#delete' do + it 'deletes a key' do + subject[:some_thing] = :new_foo_bar + subject.delete :some_thing + expect(subject[:some_thing]).to eq [] + end - describe '#[]' do - it 'returns an array of values' do - subject[:some_thing] = :foo - expect(subject[:some_thing]).to eq [:foo] - end - - it 'returns parent value when no value is set' do - parent[:some_thing] = :foo - expect(subject[:some_thing]).to eq [:foo] - end - - it 'combines parent and actual values (actual first)' do - parent[:some_thing] = :foo - subject[:some_thing] = :foo_bar - expect(subject[:some_thing]).to eq %i[foo_bar foo] - end - - it 'parent values are not changed' do - parent[:some_thing] = :foo - subject[:some_thing] = :foo_bar - expect(parent[:some_thing]).to eq [:foo] - end - end + it 'does not delete parent values' do + parent[:some_thing] = :foo + subject[:some_thing] = :new_foo_bar + subject.delete :some_thing + expect(subject[:some_thing]).to eq [:foo] + end + end - describe '#[]=' do - it 'sets a value' do - subject[:some_thing] = :foo - expect(subject[:some_thing]).to eq [:foo] - end + describe '#[]' do + it 'returns an array of values' do + subject[:some_thing] = :foo + expect(subject[:some_thing]).to eq [:foo] + end - it 'pushes further values' do - subject[:some_thing] = :foo - subject[:some_thing] = :bar - expect(subject[:some_thing]).to eq %i[foo bar] - end + it 'returns parent value when no value is set' do + parent[:some_thing] = :foo + expect(subject[:some_thing]).to eq [:foo] + end - it 'can handle array values' do - subject[:some_thing] = :foo - subject[:some_thing] = %i[bar more] - expect(subject[:some_thing]).to eq [:foo, %i[bar more]] + it 'combines parent and actual values (actual first)' do + parent[:some_thing] = :foo + subject[:some_thing] = :foo_bar + expect(subject[:some_thing]).to eq %i[foo_bar foo] + end - parent[:some_thing_else] = %i[foo bar] - subject[:some_thing_else] = %i[some bar foo] + it 'parent values are not changed' do + parent[:some_thing] = :foo + subject[:some_thing] = :foo_bar + expect(parent[:some_thing]).to eq [:foo] + end + end - expect(subject[:some_thing_else]).to eq [%i[some bar foo], %i[foo bar]] - end - end + describe '#[]=' do + it 'sets a value' do + subject[:some_thing] = :foo + expect(subject[:some_thing]).to eq [:foo] + end - describe '#to_hash' do - it 'returns a Hash representation' do - parent[:some_thing] = :foo - subject[:some_thing] = %i[bar more] - subject[:some_thing_more] = :foo_bar - expect(subject.to_hash).to eq( - some_thing: [%i[bar more], :foo], - some_thing_more: [:foo_bar] - ) - end - end + it 'pushes further values' do + subject[:some_thing] = :foo + subject[:some_thing] = :bar + expect(subject[:some_thing]).to eq %i[foo bar] + end - describe '#clone' do - let(:obj_cloned) { subject.clone } + it 'can handle array values' do + subject[:some_thing] = :foo + subject[:some_thing] = %i[bar more] + expect(subject[:some_thing]).to eq [:foo, %i[bar more]] - it 'copies all values' do - parent = described_class.new - child = described_class.new parent - grandchild = described_class.new child + parent[:some_thing_else] = %i[foo bar] + subject[:some_thing_else] = %i[some bar foo] - parent[:some_thing] = :foo - child[:some_thing] = %i[bar more] - grandchild[:some_thing] = :grand_foo_bar - grandchild[:some_thing_more] = :foo_bar + expect(subject[:some_thing_else]).to eq [%i[some bar foo], %i[foo bar]] + end + end - expect(grandchild.clone.to_hash).to eq( - some_thing: [:grand_foo_bar, %i[bar more], :foo], - some_thing_more: [:foo_bar] - ) - end + describe '#to_hash' do + it 'returns a Hash representation' do + parent[:some_thing] = :foo + subject[:some_thing] = %i[bar more] + subject[:some_thing_more] = :foo_bar + expect(subject.to_hash).to eq( + some_thing: [%i[bar more], :foo], + some_thing_more: [:foo_bar] + ) + end + end + + describe '#clone' do + let(:obj_cloned) { subject.clone } + + it 'copies all values' do + parent = described_class.new + child = described_class.new parent + grandchild = described_class.new child + + parent[:some_thing] = :foo + child[:some_thing] = %i[bar more] + grandchild[:some_thing] = :grand_foo_bar + grandchild[:some_thing_more] = :foo_bar + + expect(grandchild.clone.to_hash).to eq( + some_thing: [:grand_foo_bar, %i[bar more], :foo], + some_thing_more: [:foo_bar] + ) + end - context 'complex (i.e. not primitive) data types (ex. middleware, please see bug #930)' do - let(:middleware) { double } + context 'complex (i.e. not primitive) data types (ex. middleware, please see bug #930)' do + let(:middleware) { double } - before { subject[:middleware] = middleware } + before { subject[:middleware] = middleware } - it 'copies values; does not duplicate them' do - expect(obj_cloned[:middleware]).to eq [middleware] - end - end + it 'copies values; does not duplicate them' do + expect(obj_cloned[:middleware]).to eq [middleware] end end end diff --git a/spec/grape/util/stackable_values_spec.rb b/spec/grape/util/stackable_values_spec.rb index e857c7f90..0bcd9c3d9 100644 --- a/spec/grape/util/stackable_values_spec.rb +++ b/spec/grape/util/stackable_values_spec.rb @@ -1,127 +1,123 @@ # frozen_string_literal: true -module Grape - module Util - describe StackableValues do - subject { described_class.new(parent) } +describe Grape::Util::StackableValues do + subject { described_class.new(parent) } - let(:parent) { described_class.new } + let(:parent) { described_class.new } - describe '#keys' do - it 'returns all keys' do - subject[:some_thing] = :foo_bar - subject[:some_thing_else] = :foo_bar - expect(subject.keys).to eq %i[some_thing some_thing_else].sort - end + describe '#keys' do + it 'returns all keys' do + subject[:some_thing] = :foo_bar + subject[:some_thing_else] = :foo_bar + expect(subject.keys).to eq %i[some_thing some_thing_else].sort + end - it 'returns merged keys with parent' do - parent[:some_thing] = :foo - parent[:some_thing_else] = :foo + it 'returns merged keys with parent' do + parent[:some_thing] = :foo + parent[:some_thing_else] = :foo - subject[:some_thing] = :foo_bar - subject[:some_thing_more] = :foo_bar + subject[:some_thing] = :foo_bar + subject[:some_thing_more] = :foo_bar - expect(subject.keys).to eq %i[some_thing some_thing_else some_thing_more].sort - end - end + expect(subject.keys).to eq %i[some_thing some_thing_else some_thing_more].sort + end + end - describe '#delete' do - it 'deletes a key' do - subject[:some_thing] = :new_foo_bar - subject.delete :some_thing - expect(subject[:some_thing]).to eq [] - end - - it 'does not delete parent values' do - parent[:some_thing] = :foo - subject[:some_thing] = :new_foo_bar - subject.delete :some_thing - expect(subject[:some_thing]).to eq [:foo] - end - end + describe '#delete' do + it 'deletes a key' do + subject[:some_thing] = :new_foo_bar + subject.delete :some_thing + expect(subject[:some_thing]).to eq [] + end - describe '#[]' do - it 'returns an array of values' do - subject[:some_thing] = :foo - expect(subject[:some_thing]).to eq [:foo] - end - - it 'returns parent value when no value is set' do - parent[:some_thing] = :foo - expect(subject[:some_thing]).to eq [:foo] - end - - it 'combines parent and actual values' do - parent[:some_thing] = :foo - subject[:some_thing] = :foo_bar - expect(subject[:some_thing]).to eq %i[foo foo_bar] - end - - it 'parent values are not changed' do - parent[:some_thing] = :foo - subject[:some_thing] = :foo_bar - expect(parent[:some_thing]).to eq [:foo] - end - end + it 'does not delete parent values' do + parent[:some_thing] = :foo + subject[:some_thing] = :new_foo_bar + subject.delete :some_thing + expect(subject[:some_thing]).to eq [:foo] + end + end - describe '#[]=' do - it 'sets a value' do - subject[:some_thing] = :foo - expect(subject[:some_thing]).to eq [:foo] - end + describe '#[]' do + it 'returns an array of values' do + subject[:some_thing] = :foo + expect(subject[:some_thing]).to eq [:foo] + end - it 'pushes further values' do - subject[:some_thing] = :foo - subject[:some_thing] = :bar - expect(subject[:some_thing]).to eq %i[foo bar] - end + it 'returns parent value when no value is set' do + parent[:some_thing] = :foo + expect(subject[:some_thing]).to eq [:foo] + end - it 'can handle array values' do - subject[:some_thing] = :foo - subject[:some_thing] = %i[bar more] - expect(subject[:some_thing]).to eq [:foo, %i[bar more]] + it 'combines parent and actual values' do + parent[:some_thing] = :foo + subject[:some_thing] = :foo_bar + expect(subject[:some_thing]).to eq %i[foo foo_bar] + end - parent[:some_thing_else] = %i[foo bar] - subject[:some_thing_else] = %i[some bar foo] + it 'parent values are not changed' do + parent[:some_thing] = :foo + subject[:some_thing] = :foo_bar + expect(parent[:some_thing]).to eq [:foo] + end + end - expect(subject[:some_thing_else]).to eq [%i[foo bar], %i[some bar foo]] - end - end + describe '#[]=' do + it 'sets a value' do + subject[:some_thing] = :foo + expect(subject[:some_thing]).to eq [:foo] + end - describe '#to_hash' do - it 'returns a Hash representation' do - parent[:some_thing] = :foo - subject[:some_thing] = %i[bar more] - subject[:some_thing_more] = :foo_bar - expect(subject.to_hash).to eq(some_thing: [:foo, %i[bar more]], some_thing_more: [:foo_bar]) - end - end + it 'pushes further values' do + subject[:some_thing] = :foo + subject[:some_thing] = :bar + expect(subject[:some_thing]).to eq %i[foo bar] + end - describe '#clone' do - let(:obj_cloned) { subject.clone } + it 'can handle array values' do + subject[:some_thing] = :foo + subject[:some_thing] = %i[bar more] + expect(subject[:some_thing]).to eq [:foo, %i[bar more]] - it 'copies all values' do - parent = described_class.new - child = described_class.new parent - grandchild = described_class.new child + parent[:some_thing_else] = %i[foo bar] + subject[:some_thing_else] = %i[some bar foo] - parent[:some_thing] = :foo - child[:some_thing] = %i[bar more] - grandchild[:some_thing] = :grand_foo_bar - grandchild[:some_thing_more] = :foo_bar + expect(subject[:some_thing_else]).to eq [%i[foo bar], %i[some bar foo]] + end + end - expect(grandchild.clone.to_hash).to eq(some_thing: [:foo, %i[bar more], :grand_foo_bar], some_thing_more: [:foo_bar]) - end + describe '#to_hash' do + it 'returns a Hash representation' do + parent[:some_thing] = :foo + subject[:some_thing] = %i[bar more] + subject[:some_thing_more] = :foo_bar + expect(subject.to_hash).to eq(some_thing: [:foo, %i[bar more]], some_thing_more: [:foo_bar]) + end + end + + describe '#clone' do + let(:obj_cloned) { subject.clone } + + it 'copies all values' do + parent = described_class.new + child = described_class.new parent + grandchild = described_class.new child + + parent[:some_thing] = :foo + child[:some_thing] = %i[bar more] + grandchild[:some_thing] = :grand_foo_bar + grandchild[:some_thing_more] = :foo_bar + + expect(grandchild.clone.to_hash).to eq(some_thing: [:foo, %i[bar more], :grand_foo_bar], some_thing_more: [:foo_bar]) + end - context 'complex (i.e. not primitive) data types (ex. middleware, please see bug #930)' do - let(:middleware) { double } + context 'complex (i.e. not primitive) data types (ex. middleware, please see bug #930)' do + let(:middleware) { double } - before { subject[:middleware] = middleware } + before { subject[:middleware] = middleware } - it 'copies values; does not duplicate them' do - expect(obj_cloned[:middleware]).to eq [middleware] - end - end + it 'copies values; does not duplicate them' do + expect(obj_cloned[:middleware]).to eq [middleware] end end end diff --git a/spec/grape/util/strict_hash_configuration_spec.rb b/spec/grape/util/strict_hash_configuration_spec.rb index 7f059eee1..300986860 100644 --- a/spec/grape/util/strict_hash_configuration_spec.rb +++ b/spec/grape/util/strict_hash_configuration_spec.rb @@ -1,38 +1,34 @@ # frozen_string_literal: true -module Grape - module Util - describe 'StrictHashConfiguration' do - subject do - Class.new do - include Grape::Util::StrictHashConfiguration.module(:config1, :config2, config3: [:config4], config5: [config6: %i[config7 config8]]) - end - end +describe Grape::Util::StrictHashConfiguration do + subject do + Class.new do + include Grape::Util::StrictHashConfiguration.module(:config1, :config2, config3: [:config4], config5: [config6: %i[config7 config8]]) + end + end - it 'set nested configs' do - subject.configure do - config1 'alpha' - config2 'beta' + it 'set nested configs' do + subject.configure do + config1 'alpha' + config2 'beta' - config3 do - config4 'gamma' - end + config3 do + config4 'gamma' + end - local_var = 8 + local_var = 8 - config5 do - config6 do - config7 7 - config8 local_var - end - end + config5 do + config6 do + config7 7 + config8 local_var end - - expect(subject.settings).to eq(config1: 'alpha', - config2: 'beta', - config3: { config4: 'gamma' }, - config5: { config6: { config7: 7, config8: 8 } }) end end + + expect(subject.settings).to eq(config1: 'alpha', + config2: 'beta', + config3: { config4: 'gamma' }, + config5: { config6: { config7: 7, config8: 8 } }) end end diff --git a/spec/grape/validations/validators/all_or_none_spec.rb b/spec/grape/validations/validators/all_or_none_validator_spec.rb similarity index 100% rename from spec/grape/validations/validators/all_or_none_spec.rb rename to spec/grape/validations/validators/all_or_none_validator_spec.rb diff --git a/spec/grape/validations/validators/allow_blank_spec.rb b/spec/grape/validations/validators/allow_blank_validator_spec.rb similarity index 100% rename from spec/grape/validations/validators/allow_blank_spec.rb rename to spec/grape/validations/validators/allow_blank_validator_spec.rb diff --git a/spec/grape/validations/validators/at_least_one_of_spec.rb b/spec/grape/validations/validators/at_least_one_of_validator_spec.rb similarity index 100% rename from spec/grape/validations/validators/at_least_one_of_spec.rb rename to spec/grape/validations/validators/at_least_one_of_validator_spec.rb diff --git a/spec/grape/validations/validators/coerce_spec.rb b/spec/grape/validations/validators/coerce_validator_spec.rb similarity index 100% rename from spec/grape/validations/validators/coerce_spec.rb rename to spec/grape/validations/validators/coerce_validator_spec.rb diff --git a/spec/grape/validations/validators/default_spec.rb b/spec/grape/validations/validators/default_validator_spec.rb similarity index 100% rename from spec/grape/validations/validators/default_spec.rb rename to spec/grape/validations/validators/default_validator_spec.rb diff --git a/spec/grape/validations/validators/exactly_one_of_spec.rb b/spec/grape/validations/validators/exactly_one_of_validator_spec.rb similarity index 100% rename from spec/grape/validations/validators/exactly_one_of_spec.rb rename to spec/grape/validations/validators/exactly_one_of_validator_spec.rb diff --git a/spec/grape/validations/validators/except_values_spec.rb b/spec/grape/validations/validators/except_values_spec.rb deleted file mode 100644 index 63c62dd1d..000000000 --- a/spec/grape/validations/validators/except_values_spec.rb +++ /dev/null @@ -1,192 +0,0 @@ -# frozen_string_literal: true - -describe Grape::Validations::Validators::ExceptValuesValidator do - module ValidationsSpec - class ExceptValuesModel - DEFAULT_EXCEPTS = %w[invalid-type1 invalid-type2 invalid-type3].freeze - class << self - attr_accessor :excepts - - def excepts - @excepts ||= [] - [DEFAULT_EXCEPTS + @excepts].flatten.uniq - end - end - end - - TEST_CASES = { - req_except: { - requires: { except_values: ExceptValuesModel.excepts }, - tests: [ - { value: 'invalid-type1', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, - { value: 'invalid-type3', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, - { value: 'valid-type', rc: 200, body: { type: 'valid-type' }.to_json } - ] - }, - req_except_hash: { - requires: { except_values: { value: ExceptValuesModel.excepts } }, - tests: [ - { value: 'invalid-type1', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, - { value: 'invalid-type3', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, - { value: 'valid-type', rc: 200, body: { type: 'valid-type' }.to_json } - ] - }, - req_except_custom_message: { - requires: { except_values: { value: ExceptValuesModel.excepts, message: 'is not allowed' } }, - tests: [ - { value: 'invalid-type1', rc: 400, body: { error: 'type is not allowed' }.to_json }, - { value: 'invalid-type3', rc: 400, body: { error: 'type is not allowed' }.to_json }, - { value: 'valid-type', rc: 200, body: { type: 'valid-type' }.to_json } - ] - }, - req_except_no_value: { - requires: { except_values: { message: 'is not allowed' } }, - tests: [ - { value: 'invalid-type1', rc: 200, body: { type: 'invalid-type1' }.to_json } - ] - }, - req_except_empty: { - requires: { except_values: [] }, - tests: [ - { value: 'invalid-type1', rc: 200, body: { type: 'invalid-type1' }.to_json } - ] - }, - req_except_lambda: { - requires: { except_values: -> { ExceptValuesModel.excepts } }, - add_excepts: ['invalid-type4'], - tests: [ - { value: 'invalid-type1', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, - { value: 'invalid-type4', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, - { value: 'valid-type', rc: 200, body: { type: 'valid-type' }.to_json } - ] - }, - req_except_lambda_custom_message: { - requires: { except_values: { value: -> { ExceptValuesModel.excepts }, message: 'is not allowed' } }, - add_excepts: ['invalid-type4'], - tests: [ - { value: 'invalid-type1', rc: 400, body: { error: 'type is not allowed' }.to_json }, - { value: 'invalid-type4', rc: 400, body: { error: 'type is not allowed' }.to_json }, - { value: 'valid-type', rc: 200, body: { type: 'valid-type' }.to_json } - ] - }, - opt_except_default: { - optional: { except_values: ExceptValuesModel.excepts, default: 'valid-type2' }, - tests: [ - { value: 'invalid-type1', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, - { value: 'invalid-type3', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, - { value: 'valid-type', rc: 200, body: { type: 'valid-type' }.to_json }, - { rc: 200, body: { type: 'valid-type2' }.to_json } - ] - }, - opt_except_lambda_default: { - optional: { except_values: -> { ExceptValuesModel.excepts }, default: 'valid-type2' }, - tests: [ - { value: 'invalid-type1', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, - { value: 'invalid-type3', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, - { value: 'valid-type', rc: 200, body: { type: 'valid-type' }.to_json }, - { rc: 200, body: { type: 'valid-type2' }.to_json } - ] - }, - req_except_type_coerce: { - requires: { type: Integer, except_values: [10, 11] }, - tests: [ - { value: 'invalid-type1', rc: 400, body: { error: 'type is invalid' }.to_json }, - { value: 11, rc: 400, body: { error: 'type has a value not allowed' }.to_json }, - { value: '11', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, - { value: '3', rc: 200, body: { type: 3 }.to_json }, - { value: 3, rc: 200, body: { type: 3 }.to_json } - ] - }, - opt_except_type_coerce_default: { - optional: { type: Integer, except_values: [10, 11], default: 12 }, - tests: [ - { value: 'invalid-type1', rc: 400, body: { error: 'type is invalid' }.to_json }, - { value: 10, rc: 400, body: { error: 'type has a value not allowed' }.to_json }, - { value: '3', rc: 200, body: { type: 3 }.to_json }, - { value: 3, rc: 200, body: { type: 3 }.to_json }, - { rc: 200, body: { type: 12 }.to_json } - ] - }, - opt_except_array_type_coerce_default: { - optional: { type: Array[Integer], except_values: [10, 11], default: 12 }, - tests: [ - { value: 'invalid-type1', rc: 400, body: { error: 'type is invalid' }.to_json }, - { value: 10, rc: 400, body: { error: 'type is invalid' }.to_json }, - { value: [10], rc: 400, body: { error: 'type has a value not allowed' }.to_json }, - { value: ['3'], rc: 200, body: { type: [3] }.to_json }, - { value: [3], rc: 200, body: { type: [3] }.to_json }, - { rc: 200, body: { type: 12 }.to_json } - ] - }, - req_except_range: { - optional: { type: Integer, except_values: 10..12 }, - tests: [ - { value: 11, rc: 400, body: { error: 'type has a value not allowed' }.to_json }, - { value: 13, rc: 200, body: { type: 13 }.to_json } - ] - } - }.freeze - - module ExceptValidatorSpec - class API < Grape::API - default_format :json - - TEST_CASES.each_with_index do |(k, v), _i| - params do - requires :type, v[:requires] if v.key? :requires - optional :type, v[:optional] if v.key? :optional - end - get k do - { type: params[:type] } - end - end - end - end - end - - it 'raises IncompatibleOptionValues on a default value in exclude' do - subject = Class.new(Grape::API) - expect do - subject.params do - optional :type, except_values: ValidationsSpec::ExceptValuesModel.excepts, - default: ValidationsSpec::ExceptValuesModel.excepts.sample - end - end.to raise_error Grape::Exceptions::IncompatibleOptionValues - end - - it 'raises IncompatibleOptionValues when a default array has excluded values' do - subject = Class.new(Grape::API) - expect do - subject.params do - optional :type, type: Array[Integer], - except_values: 10..12, - default: [8, 9, 10] - end - end.to raise_error Grape::Exceptions::IncompatibleOptionValues - end - - it 'raises IncompatibleOptionValues when type is incompatible with values array' do - subject = Class.new(Grape::API) - expect do - subject.params { optional :type, except_values: %w[valid-type1 valid-type2 valid-type3], type: Symbol } - end.to raise_error Grape::Exceptions::IncompatibleOptionValues - end - - def app - ValidationsSpec::ExceptValidatorSpec::API - end - - ValidationsSpec::TEST_CASES.each_with_index do |(k, v), i| - v[:tests].each do |t| - it "#{i}: #{k} - #{t[:value]}" do - ValidationsSpec::ExceptValuesModel.excepts = v[:add_excepts] if v.key? :add_excepts - body = {} - body[:type] = t[:value] if t.key? :value - get k.to_s, **body - expect(last_response.status).to eq t[:rc] - expect(last_response.body).to eq t[:body] - ValidationsSpec::ExceptValuesModel.excepts = nil - end - end - end -end diff --git a/spec/grape/validations/validators/except_values_validator_spec.rb b/spec/grape/validations/validators/except_values_validator_spec.rb new file mode 100644 index 000000000..d72d0092d --- /dev/null +++ b/spec/grape/validations/validators/except_values_validator_spec.rb @@ -0,0 +1,194 @@ +# frozen_string_literal: true + +describe Grape::Validations::Validators::ExceptValuesValidator do + describe 'IncompatibleOptionValues' do + subject { api } + + context 'when a default value is set' do + let(:api) do + ev = except_values + dv = default_value + Class.new(Grape::API) do + params do + optional :type, except_values: ev, default: dv + end + end + end + + context 'when default value is in exclude' do + let(:except_values) { 1..10 } + let(:default_value) { except_values.to_a.sample } + + it 'raises IncompatibleOptionValues' do + expect { subject }.to raise_error Grape::Exceptions::IncompatibleOptionValues + end + end + + context 'when default array has excluded values' do + let(:except_values) { 1..10 } + let(:default_value) { [8, 9, 10] } + + it 'raises IncompatibleOptionValues' do + expect { subject }.to raise_error Grape::Exceptions::IncompatibleOptionValues + end + end + end + + context 'when type is incompatible' do + let(:api) do + Class.new(Grape::API) do + params do + optional :type, except_values: 1..10, type: Symbol + end + end + end + + it 'raises IncompatibleOptionValues' do + expect { subject }.to raise_error Grape::Exceptions::IncompatibleOptionValues + end + end + end + + { + req_except: { + requires: { except_values: %w[invalid-type1 invalid-type2 invalid-type3] }, + tests: [ + { value: 'invalid-type1', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, + { value: 'invalid-type3', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, + { value: 'valid-type', rc: 200, body: { type: 'valid-type' }.to_json } + ] + }, + req_except_hash: { + requires: { except_values: { value: %w[invalid-type1 invalid-type2 invalid-type3] } }, + tests: [ + { value: 'invalid-type1', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, + { value: 'invalid-type3', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, + { value: 'valid-type', rc: 200, body: { type: 'valid-type' }.to_json } + ] + }, + req_except_custom_message: { + requires: { except_values: { value: %w[invalid-type1 invalid-type2 invalid-type3], message: 'is not allowed' } }, + tests: [ + { value: 'invalid-type1', rc: 400, body: { error: 'type is not allowed' }.to_json }, + { value: 'invalid-type3', rc: 400, body: { error: 'type is not allowed' }.to_json }, + { value: 'valid-type', rc: 200, body: { type: 'valid-type' }.to_json } + ] + }, + req_except_no_value: { + requires: { except_values: { message: 'is not allowed' } }, + tests: [ + { value: 'invalid-type1', rc: 200, body: { type: 'invalid-type1' }.to_json } + ] + }, + req_except_empty: { + requires: { except_values: [] }, + tests: [ + { value: 'invalid-type1', rc: 200, body: { type: 'invalid-type1' }.to_json } + ] + }, + req_except_lambda: { + requires: { except_values: -> { %w[invalid-type1 invalid-type2 invalid-type3 invalid-type4] } }, + tests: [ + { value: 'invalid-type1', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, + { value: 'invalid-type4', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, + { value: 'valid-type', rc: 200, body: { type: 'valid-type' }.to_json } + ] + }, + req_except_lambda_custom_message: { + requires: { except_values: { value: -> { %w[invalid-type1 invalid-type2 invalid-type3 invalid-type4] }, message: 'is not allowed' } }, + tests: [ + { value: 'invalid-type1', rc: 400, body: { error: 'type is not allowed' }.to_json }, + { value: 'invalid-type4', rc: 400, body: { error: 'type is not allowed' }.to_json }, + { value: 'valid-type', rc: 200, body: { type: 'valid-type' }.to_json } + ] + }, + opt_except_default: { + optional: { except_values: %w[invalid-type1 invalid-type2 invalid-type3], default: 'valid-type2' }, + tests: [ + { value: 'invalid-type1', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, + { value: 'invalid-type3', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, + { value: 'valid-type', rc: 200, body: { type: 'valid-type' }.to_json }, + { rc: 200, body: { type: 'valid-type2' }.to_json } + ] + }, + opt_except_lambda_default: { + optional: { except_values: -> { %w[invalid-type1 invalid-type2 invalid-type3] }, default: 'valid-type2' }, + tests: [ + { value: 'invalid-type1', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, + { value: 'invalid-type3', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, + { value: 'valid-type', rc: 200, body: { type: 'valid-type' }.to_json }, + { rc: 200, body: { type: 'valid-type2' }.to_json } + ] + }, + req_except_type_coerce: { + requires: { type: Integer, except_values: [10, 11] }, + tests: [ + { value: 'invalid-type1', rc: 400, body: { error: 'type is invalid' }.to_json }, + { value: 11, rc: 400, body: { error: 'type has a value not allowed' }.to_json }, + { value: '11', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, + { value: '3', rc: 200, body: { type: 3 }.to_json }, + { value: 3, rc: 200, body: { type: 3 }.to_json } + ] + }, + opt_except_type_coerce_default: { + optional: { type: Integer, except_values: [10, 11], default: 12 }, + tests: [ + { value: 'invalid-type1', rc: 400, body: { error: 'type is invalid' }.to_json }, + { value: 10, rc: 400, body: { error: 'type has a value not allowed' }.to_json }, + { value: '3', rc: 200, body: { type: 3 }.to_json }, + { value: 3, rc: 200, body: { type: 3 }.to_json }, + { rc: 200, body: { type: 12 }.to_json } + ] + }, + opt_except_array_type_coerce_default: { + optional: { type: Array[Integer], except_values: [10, 11], default: 12 }, + tests: [ + { value: 'invalid-type1', rc: 400, body: { error: 'type is invalid' }.to_json }, + { value: 10, rc: 400, body: { error: 'type is invalid' }.to_json }, + { value: [10], rc: 400, body: { error: 'type has a value not allowed' }.to_json }, + { value: ['3'], rc: 200, body: { type: [3] }.to_json }, + { value: [3], rc: 200, body: { type: [3] }.to_json }, + { rc: 200, body: { type: 12 }.to_json } + ] + }, + req_except_range: { + optional: { type: Integer, except_values: 10..12 }, + tests: [ + { value: 11, rc: 400, body: { error: 'type has a value not allowed' }.to_json }, + { value: 13, rc: 200, body: { type: 13 }.to_json } + ] + } + }.each do |path, param_def| + param_def[:tests].each do |t| + describe "when #{path}" do + let(:app) do + Class.new(Grape::API) do + default_format :json + params do + requires :type, param_def[:requires] if param_def.key? :requires + optional :type, param_def[:optional] if param_def.key? :optional + end + get path do + { type: params[:type] } + end + end + end + + let(:body) do + {}.tap do |body| + body[:type] = t[:value] if t.key? :value + end + end + + before do + get path.to_s, **body + end + + it "returns body #{t[:body]} with status #{t[:rc]}" do + expect(last_response.status).to eq t[:rc] + expect(last_response.body).to eq t[:body] + end + end + end + end +end diff --git a/spec/grape/validations/validators/length_spec.rb b/spec/grape/validations/validators/length_validator_spec.rb similarity index 100% rename from spec/grape/validations/validators/length_spec.rb rename to spec/grape/validations/validators/length_validator_spec.rb diff --git a/spec/grape/validations/validators/mutual_exclusion_spec.rb b/spec/grape/validations/validators/mutual_exclusion_validator_spec.rb similarity index 100% rename from spec/grape/validations/validators/mutual_exclusion_spec.rb rename to spec/grape/validations/validators/mutual_exclusion_validator_spec.rb diff --git a/spec/grape/validations/validators/presence_spec.rb b/spec/grape/validations/validators/presence_validator_spec.rb similarity index 100% rename from spec/grape/validations/validators/presence_spec.rb rename to spec/grape/validations/validators/presence_validator_spec.rb diff --git a/spec/grape/validations/validators/regexp_spec.rb b/spec/grape/validations/validators/regexp_validator_spec.rb similarity index 100% rename from spec/grape/validations/validators/regexp_spec.rb rename to spec/grape/validations/validators/regexp_validator_spec.rb diff --git a/spec/grape/validations/validators/same_as_spec.rb b/spec/grape/validations/validators/same_as_validator_spec.rb similarity index 100% rename from spec/grape/validations/validators/same_as_spec.rb rename to spec/grape/validations/validators/same_as_validator_spec.rb diff --git a/spec/grape/validations/validators/values_spec.rb b/spec/grape/validations/validators/values_validator_spec.rb similarity index 100% rename from spec/grape/validations/validators/values_spec.rb rename to spec/grape/validations/validators/values_validator_spec.rb From f4e2af5ed38c92206fabe1038bccc8afd3013f81 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sun, 20 Oct 2024 16:10:47 +0200 Subject: [PATCH 271/304] Fix fetch_formatter api_format (#2506) * Add `api_format` function accessible from inside_route fetch_formatter will look api.format first Use fetch {} instead of [] || Update spec Add HTTP_VERSION in Grape::Http::Headers Update README * Add CHANGELOG and UPGRADING.md * Change `api_format` to ':txt' * Fix upgrading --- CHANGELOG.md | 1 + README.md | 4 ++-- UPGRADING.md | 6 ++++++ lib/grape/dsl/inside_route.rb | 6 +++++- lib/grape/http/headers.rb | 1 + lib/grape/middleware/formatter.rb | 2 +- spec/grape/api_spec.rb | 4 ++-- 7 files changed, 18 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5d85dfec..7aea93408 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ #### Fixes * [#2504](https://github.com/ruby-grape/grape/pull/2504): Fix leaky modules in specs - [@ericproulx](https://github.com/ericproulx). +* [#2506](https://github.com/ruby-grape/grape/pull/2506): Fix fetch_formatter api_format - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 2.2.0 (2024-09-14) diff --git a/README.md b/README.md index 9d46af651..0c6c56aff 100644 --- a/README.md +++ b/README.md @@ -3066,7 +3066,7 @@ end * `GET /hello.xls` with an `Accept: application/xml` header has an unrecognized extension, but the `Accept` header corresponds to a recognized format, so it will respond with XML. * `GET /hello.xls` with an `Accept: text/plain` header has an unrecognized extension *and* an unrecognized `Accept` header, so it will respond with JSON (the default format). -You can override this process explicitly by specifying `env['api.format']` in the API itself. +You can override this process explicitly by calling `api_format` in the API itself. For example, the following API will let you upload arbitrary files and return their contents as an attachment with the correct MIME type. ```ruby @@ -3074,7 +3074,7 @@ class Twitter::API < Grape::API post 'attachment' do filename = params[:file][:filename] content_type MIME::Types.type_for(filename)[0].to_s - env['api.format'] = :binary # there's no formatter for :binary, data will be returned "as is" + api_format :binary # there's no formatter for :binary, data will be returned "as is" header 'Content-Disposition', "attachment; filename*=UTF-8''#{CGI.escape(filename)}" params[:file][:tempfile].read end diff --git a/UPGRADING.md b/UPGRADING.md index ab80fe4cb..0c6c54f6e 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -3,6 +3,12 @@ Upgrading Grape ### Upgrading to >= 2.3.0 +### `content_type` vs `api.format` inside API + +Before 2.3.0, `content_type` had priority over `env['api.format']` when set in an API, which was incorrect. The priority has been flipped and `env['api.format']` will be checked first. +In addition, the function `api_format` has been added. Instead of setting `env['api.format']` directly, you can call `api_format`. +See [#2506](https://github.com/ruby-grape/grape/pull/2506) for more information. + #### Remove Deprecated Methods and Options - Deprecated `file` method has been removed. Use `send_file` or `stream`. diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index 72b365bc4..a7efcd490 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -452,7 +452,11 @@ def entity_representation_for(entity_class, object, options) end def http_version - env['HTTP_VERSION'] || env[Rack::SERVER_PROTOCOL] + env.fetch(Grape::Http::Headers::HTTP_VERSION) { env[Rack::SERVER_PROTOCOL] } + end + + def api_format(format) + env[Grape::Env::API_FORMAT] = format end def context diff --git a/lib/grape/http/headers.rb b/lib/grape/http/headers.rb index ab8770ab4..eb4d38915 100644 --- a/lib/grape/http/headers.rb +++ b/lib/grape/http/headers.rb @@ -6,6 +6,7 @@ module Headers HTTP_ACCEPT_VERSION = 'HTTP_ACCEPT_VERSION' HTTP_ACCEPT = 'HTTP_ACCEPT' HTTP_TRANSFER_ENCODING = 'HTTP_TRANSFER_ENCODING' + HTTP_VERSION = 'HTTP_VERSION' ALLOW = 'Allow' LOCATION = 'Location' diff --git a/lib/grape/middleware/formatter.rb b/lib/grape/middleware/formatter.rb index 4de1af02e..5cb761463 100644 --- a/lib/grape/middleware/formatter.rb +++ b/lib/grape/middleware/formatter.rb @@ -53,7 +53,7 @@ def build_formatted_response(status, headers, bodies) end def fetch_formatter(headers, options) - api_format = mime_types[headers[Rack::CONTENT_TYPE]] || env[Grape::Env::API_FORMAT] + api_format = env.fetch(Grape::Env::API_FORMAT) { mime_types[headers[Rack::CONTENT_TYPE]] } Grape::Formatter.formatter_for(api_format, options[:formatters]) end diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index 14e2c9257..bd6398780 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -4092,9 +4092,9 @@ def my_method expect(last_response.body).to eq({ meaning_of_life: 42 }.to_json) end - it 'can be overwritten with an explicit content type' do + it 'can be overwritten with an explicit api_format' do subject.get '/meaning_of_life_with_content_type' do - content_type 'text/plain' + api_format :txt { meaning_of_life: 42 }.to_s end get '/meaning_of_life_with_content_type' From de0a43176d85f30a8fa50f12f2b12c131275f5b4 Mon Sep 17 00:00:00 2001 From: Nikolai B Date: Wed, 23 Oct 2024 12:58:19 +0100 Subject: [PATCH 272/304] Fix type: Set with values --- CHANGELOG.md | 1 + lib/grape/validations/params_scope.rb | 2 +- spec/grape/validations/params_scope_spec.rb | 18 ++++++++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7aea93408..a66dacc32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ * [#2504](https://github.com/ruby-grape/grape/pull/2504): Fix leaky modules in specs - [@ericproulx](https://github.com/ericproulx). * [#2506](https://github.com/ruby-grape/grape/pull/2506): Fix fetch_formatter api_format - [@ericproulx](https://github.com/ericproulx). +* [#2507](https://github.com/ruby-grape/grape/pull/2507): Fix type: Set with values - [@nikolai-b](https://github.com/nikolai-b). * Your contribution here. ### 2.2.0 (2024-09-14) diff --git a/lib/grape/validations/params_scope.rb b/lib/grape/validations/params_scope.rb index 1d3384883..b3c4268f6 100644 --- a/lib/grape/validations/params_scope.rb +++ b/lib/grape/validations/params_scope.rb @@ -490,7 +490,7 @@ def validate(type, options, attrs, doc, opts) def validate_value_coercion(coerce_type, *values_list) return unless coerce_type - coerce_type = coerce_type.first if coerce_type.is_a?(Array) + coerce_type = coerce_type.first if coerce_type.is_a?(Enumerable) values_list.each do |values| next if !values || values.is_a?(Proc) diff --git a/spec/grape/validations/params_scope_spec.rb b/spec/grape/validations/params_scope_spec.rb index 454fdf6c0..8e26f6fa6 100644 --- a/spec/grape/validations/params_scope_spec.rb +++ b/spec/grape/validations/params_scope_spec.rb @@ -189,6 +189,24 @@ def initialize(value) end end + context 'a Set with coerce type explicitly given' do + context 'and the values are allowed' do + it 'does not raise an exception' do + expect do + subject.params { optional :numbers, type: Set[Integer], values: 0..2, default: 0..2 } + end.not_to raise_error + end + end + + context 'and the values are not allowed' do + it 'raises exception' do + expect do + subject.params { optional :numbers, type: Set[Integer], values: %w[a b] } + end.to raise_error Grape::Exceptions::IncompatibleOptionValues + end + end + end + context 'with range values' do context "when left range endpoint isn't #kind_of? the type" do it 'raises exception' do From 0477baf66feb02833c0462f5f0dd4da4036703aa Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Mon, 28 Oct 2024 18:57:54 +0100 Subject: [PATCH 273/304] Fix ContractScope validator + small tweaks (#2510) * ContractScope's validator inherits from Grape::Validations::Validator::Base initialize method has been updated accordingly opts adds fail_fast Use << when concatenating string Use map instead of each [] * Add CHANGELOG.md Add inherits spec * Fix cop * Fix spec --- CHANGELOG.md | 1 + lib/grape/validations/contract_scope.rb | 29 +++++++++---------- .../dry_validation/dry_validation_spec.rb | 8 +++++ 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a66dacc32..37ff1642b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ * [#2504](https://github.com/ruby-grape/grape/pull/2504): Fix leaky modules in specs - [@ericproulx](https://github.com/ericproulx). * [#2506](https://github.com/ruby-grape/grape/pull/2506): Fix fetch_formatter api_format - [@ericproulx](https://github.com/ericproulx). * [#2507](https://github.com/ruby-grape/grape/pull/2507): Fix type: Set with values - [@nikolai-b](https://github.com/nikolai-b). +* [#2510](https://github.com/ruby-grape/grape/pull/2510): Fix ContractScope's validator inheritance - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 2.2.0 (2024-09-14) diff --git a/lib/grape/validations/contract_scope.rb b/lib/grape/validations/contract_scope.rb index 0255051b4..3b66df572 100644 --- a/lib/grape/validations/contract_scope.rb +++ b/lib/grape/validations/contract_scope.rb @@ -24,17 +24,18 @@ def initialize(api, contract = nil, &block) validator_options = { validator_class: Validator, - opts: { schema: contract } + opts: { schema: contract, fail_fast: false } } api.namespace_stackable(:validations, validator_options) end - class Validator + class Validator < Grape::Validations::Validators::Base attr_reader :schema - def initialize(*_args, schema:) - @schema = schema + def initialize(_attrs, _options, _required, _scope, opts) + super + @schema = opts.fetch(:schema) end # Validates a given request. @@ -49,21 +50,17 @@ def validate(request) return end - errors = [] - - res.errors.messages.each do |message| - full_name = message.path.first.to_s + raise Grape::Exceptions::ValidationArrayErrors.new(build_errors_from_messages(res.errors.messages)) + end - full_name += "[#{message.path[1..].join('][')}]" if message.path.size > 1 + private - errors << Grape::Exceptions::Validation.new(params: [full_name], message: message.text) + def build_errors_from_messages(messages) + messages.map do |message| + full_name = message.path.first.to_s + full_name << "[#{message.path[1..].join('][')}]" if message.path.size > 1 + Grape::Exceptions::Validation.new(params: [full_name], message: message.text) end - - raise Grape::Exceptions::ValidationArrayErrors.new(errors) - end - - def fail_fast? - false end end end diff --git a/spec/integration/dry_validation/dry_validation_spec.rb b/spec/integration/dry_validation/dry_validation_spec.rb index d7b2f8efa..6333bdacd 100644 --- a/spec/integration/dry_validation/dry_validation_spec.rb +++ b/spec/integration/dry_validation/dry_validation_spec.rb @@ -236,4 +236,12 @@ end end end + + describe Grape::Validations::ContractScope::Validator do + describe '.inherits' do + subject { described_class } + + it { is_expected.to be < Grape::Validations::Validators::Base } + end + end end From 86648fa9e3d7aff34aa0847440bc02d340da4e09 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Tue, 12 Nov 2024 20:49:30 +0100 Subject: [PATCH 274/304] Optimize hash alloc (#2512) * Remove double splat operators when not needed including specs Grape::Validations::Validators::Base last arguments is now just a simple param Grape::Exceptions::Validation and Grape::Exceptions::ValidationErrors does not end with **args. Grape::Request has a named param build_params_with: nil instead of **options Remove @namespace_description in routing.rb * Revert compile_many_routes.rb * Optimize Head Route * Use dup instead of new for Head Route * Use opts = {} for validator instead of just opts rubocop * Optimize add_head_not_allowed_methods_and_options_methods Renamed to add_head_and_options_methods * Remove requirements and path from greedy route (not used) allow header is joined * Remove {} for opts since its internal. * Fix rubocop * Remove frozen allow_header. Now Dynamic * Update CHANGELOG.md --- CHANGELOG.md | 1 + lib/grape/api.rb | 2 +- lib/grape/api/instance.rb | 80 +++------- lib/grape/dsl/inside_route.rb | 15 +- lib/grape/dsl/parameters.rb | 4 +- lib/grape/dsl/routing.rb | 17 +-- lib/grape/endpoint.rb | 143 +++++++++--------- lib/grape/exceptions/base.rb | 48 +++--- lib/grape/exceptions/validation.rb | 9 +- lib/grape/exceptions/validation_errors.rb | 4 +- lib/grape/middleware/versioner/header.rb | 10 +- lib/grape/namespace.rb | 2 +- lib/grape/request.rb | 4 +- lib/grape/router.rb | 10 +- lib/grape/router/base_route.rb | 4 +- lib/grape/router/greedy_route.rb | 4 +- lib/grape/router/pattern.rb | 16 +- lib/grape/router/route.rb | 10 +- lib/grape/validations/params_scope.rb | 14 +- lib/grape/validations/validator_factory.rb | 4 +- lib/grape/validations/validators/base.rb | 9 +- .../validators/coerce_validator.rb | 2 +- .../validators/default_validator.rb | 2 +- .../validators/except_values_validator.rb | 2 +- .../validators/length_validator.rb | 2 +- .../validators/values_validator.rb | 2 +- spec/grape/dsl/parameters_spec.rb | 2 +- spec/grape/dsl/routing_spec.rb | 2 +- spec/grape/middleware/error_spec.rb | 2 +- .../versioner/accept_version_header_spec.rb | 2 +- .../grape/middleware/versioner/header_spec.rb | 2 +- spec/grape/middleware/versioner/param_spec.rb | 2 +- spec/grape/middleware/versioner/path_spec.rb | 2 +- spec/grape/router/greedy_route_spec.rb | 2 +- spec/integration/grape_entity/entity_spec.rb | 2 +- spec/integration/hashie/hashie_spec.rb | 4 +- spec/shared/versioning_examples.rb | 40 ++--- spec/support/versioned_helpers.rb | 10 +- 38 files changed, 214 insertions(+), 278 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37ff1642b..0a15293fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * [#2500](https://github.com/ruby-grape/grape/pull/2500): Remove deprecated `file` method - [@ericproulx](https://github.com/ericproulx). * [#2501](https://github.com/ruby-grape/grape/pull/2501): Remove deprecated `except` and `proc` options in values validator - [@ericproulx](https://github.com/ericproulx). * [#2502](https://github.com/ruby-grape/grape/pull/2502): Remove deprecation `options` in `desc` - [@ericproulx](https://github.com/ericproulx). +* [#2512](https://github.com/ruby-grape/grape/pull/2512): Optimize hash alloc - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/lib/grape/api.rb b/lib/grape/api.rb index eaa351c9b..2a3435aa3 100644 --- a/lib/grape/api.rb +++ b/lib/grape/api.rb @@ -91,7 +91,7 @@ def const_missing(*args) # 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) + def mount_instance(opts = {}) instance = Class.new(@base_parent) instance.configuration = Grape::Util::EndpointConfiguration.new(opts[:configuration] || {}) instance.base = self diff --git a/lib/grape/api/instance.rb b/lib/grape/api/instance.rb index ce290df7c..c6a608713 100644 --- a/lib/grape/api/instance.rb +++ b/lib/grape/api/instance.rb @@ -194,88 +194,52 @@ def cascade? # 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. + all_routes = self.class.endpoints.map(&:routes).flatten + # 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| - next if config[:options][:matching_wildchar] - - allowed_methods = config[:methods].dup - - allowed_methods |= [Rack::HEAD] if !self.class.namespace_inheritable(:do_not_route_head) && allowed_methods.include?(Rack::GET) - - allow_header = (self.class.namespace_inheritable(:do_not_route_options) ? allowed_methods : [Rack::OPTIONS] | allowed_methods) - - config[:endpoint].options[:options_route_enabled] = true unless self.class.namespace_inheritable(:do_not_route_options) || allowed_methods.include?(Rack::OPTIONS) - - attributes = config.merge(allowed_methods: allowed_methods, allow_header: allow_header) - generate_not_allowed_method(config[:pattern], **attributes) - end - end - end + without_root_prefix_and_versioning { collect_route_config_per_pattern(all_routes) } end - def collect_route_config_per_pattern - all_routes = self.class.endpoints.map(&:routes).flatten + def collect_route_config_per_pattern(all_routes) routes_by_regexp = all_routes.group_by(&:pattern_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: { matching_wildchar: matching_wildchar }, - 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 + routes_by_regexp.each_value do |routes| + last_route = routes.last # Most of the configuration is taken from the last endpoint + next if routes.any? { |route| route.request_method == '*' } - # 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 - @router.associate_routes(pattern, not_allowed_methods: not_allowed_methods, **attributes) + allowed_methods = routes.map(&:request_method) + allowed_methods |= [Rack::HEAD] if !self.class.namespace_inheritable(:do_not_route_head) && allowed_methods.include?(Rack::GET) + + allow_header = self.class.namespace_inheritable(:do_not_route_options) ? allowed_methods : [Rack::OPTIONS] | allowed_methods + last_route.app.options[:options_route_enabled] = true unless self.class.namespace_inheritable(:do_not_route_options) || allowed_methods.include?(Rack::OPTIONS) + + @router.associate_routes(last_route.pattern, { + endpoint: last_route.app, + allow_header: allow_header + }) + end end # Allows definition of endpoints that ignore the versioning configuration # used by the rest of your API. - def without_versioning(&_block) + def without_root_prefix_and_versioning old_version = self.class.namespace_inheritable(:version) old_version_options = self.class.namespace_inheritable(:version_options) + old_root_prefix = self.class.namespace_inheritable(:root_prefix) self.class.namespace_inheritable_to_nil(:version) self.class.namespace_inheritable_to_nil(:version_options) + self.class.namespace_inheritable_to_nil(:root_prefix) 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) + self.class.namespace_inheritable(:root_prefix, old_root_prefix) end end end diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index a7efcd490..c88f7867a 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -26,8 +26,8 @@ def self.post_filter_methods(type) # has completed module PostBeforeFilter def declared(passed_params, options = {}, declared_params = nil, params_nested_path = []) - options = options.reverse_merge(include_missing: true, include_parent_namespaces: true, evaluate_given: false) - declared_params ||= optioned_declared_params(**options) + options.reverse_merge!(include_missing: true, include_parent_namespaces: true, evaluate_given: false) + declared_params ||= optioned_declared_params(options[:include_parent_namespaces]) res = if passed_params.is_a?(Array) declared_array(passed_params, options, declared_params, params_nested_path) @@ -120,8 +120,8 @@ def optioned_param_key(declared_param, options) options[:stringify] ? declared_param.to_s : declared_param.to_sym end - def optioned_declared_params(**options) - declared_params = if options[:include_parent_namespaces] + def optioned_declared_params(include_parent_namespaces) + declared_params = if include_parent_namespaces # Declared params including parent namespaces route_setting(:declared_params) else @@ -199,10 +199,9 @@ def rack_response(message, status = 200, headers = { Rack::CONTENT_TYPE => conte # Redirect to a new url. # # @param url [String] The url to be redirect. - # @param options [Hash] The options used when redirect. - # :permanent, default false. - # :body, default a short message including the URL. - def redirect(url, permanent: false, body: nil, **_options) + # @param permanent [Boolean] default false. + # @param body default a short message including the URL. + def redirect(url, permanent: false, body: nil) body_message = body if permanent status 301 diff --git a/lib/grape/dsl/parameters.rb b/lib/grape/dsl/parameters.rb index 821da5d79..48f53bcb9 100644 --- a/lib/grape/dsl/parameters.rb +++ b/lib/grape/dsl/parameters.rb @@ -136,7 +136,7 @@ def requires(*attrs, &block) require_required_and_optional_fields(attrs.first, opts) else validate_attributes(attrs, opts, &block) - block ? new_scope(orig_attrs, &block) : push_declared_params(attrs, **opts.slice(:as)) + block ? new_scope(orig_attrs, &block) : push_declared_params(attrs, opts.slice(:as)) end end @@ -162,7 +162,7 @@ def optional(*attrs, &block) else validate_attributes(attrs, opts, &block) - block ? new_scope(orig_attrs, true, &block) : push_declared_params(attrs, **opts.slice(:as)) + block ? new_scope(orig_attrs, true, &block) : push_declared_params(attrs, opts.slice(:as)) end end diff --git a/lib/grape/dsl/routing.rb b/lib/grape/dsl/routing.rb index 812ddd1d0..8145726ad 100644 --- a/lib/grape/dsl/routing.rb +++ b/lib/grape/dsl/routing.rb @@ -175,19 +175,12 @@ 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 - within_namespace do - previous_namespace_description = @namespace_description - @namespace_description = (@namespace_description || {}).deep_merge(namespace_setting(:description) || {}) - nest(block) do - namespace_stackable(:namespace, Namespace.new(space, **options)) if space - end - @namespace_description = previous_namespace_description + return Namespace.joined_space_path(namespace_stackable(:namespace)) unless space || block + + within_namespace do + nest(block) do + namespace_stackable(:namespace, Namespace.new(space, options)) if space end - else - Namespace.joined_space_path(namespace_stackable(:namespace)) end end diff --git a/lib/grape/endpoint.rb b/lib/grape/endpoint.rb index 5fdb03567..6bd72c23c 100644 --- a/lib/grape/endpoint.rb +++ b/lib/grape/endpoint.rb @@ -145,17 +145,16 @@ def reset_routes! end def mount_in(router) - if endpoints - endpoints.each { |e| e.mount_in(router) } - else - reset_routes! - routes.each do |route| - methods = [route.request_method] - methods << Rack::HEAD if !namespace_inheritable(:do_not_route_head) && route.request_method == Rack::GET - methods.each do |method| - route = Grape::Router::Route.new(method, route.origin, **route.attributes.to_h) unless route.request_method == method - router.append(route.apply(self)) - end + return endpoints.each { |e| e.mount_in(router) } if endpoints + + reset_routes! + routes.each do |route| + router.append(route.apply(self)) + next unless !namespace_inheritable(:do_not_route_head) && route.request_method == Rack::GET + + route.dup.then do |head_route| + head_route.convert_to_head_request! + router.append(head_route.apply(self)) end end end @@ -164,8 +163,9 @@ 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) + route_options[:suffix] = path.suffix + params = options[:route_options].merge(route_options) + route = Grape::Router::Route.new(method, path.path, params) route.apply(self) end.flatten end @@ -196,10 +196,6 @@ def prepare_version version.length == 1 ? version.first : version end - def merge_route_options(**default) - options[:route_options].clone.merge!(**default) - end - def map_routes options[:method].map { |method| options[:path].map { |path| yield method, path } } end @@ -259,9 +255,10 @@ def run 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? + allow_header_value = allowed_methods.join(', ') + raise Grape::Exceptions::MethodNotAllowed.new(header.merge('Allow' => allow_header_value)) unless options? - header Grape::Http::Headers::ALLOW, allowed_methods + header Grape::Http::Headers::ALLOW, allow_header_value response_object = '' status 204 else @@ -287,59 +284,6 @@ def run end end - def build_stack(helpers) - stack = Grape::Middleware::Stack.new - - content_types = namespace_stackable_with_hash(:content_types) - format = namespace_inheritable(:format) - - stack.use Rack::Head - stack.use Class.new(Grape::Middleware::Error), - helpers: helpers, - format: format, - content_types: content_types, - default_status: namespace_inheritable(:default_error_status), - rescue_all: namespace_inheritable(:rescue_all), - rescue_grape_exceptions: namespace_inheritable(:rescue_grape_exceptions), - default_error_formatter: namespace_inheritable(:default_error_formatter), - error_formatters: namespace_stackable_with_hash(:error_formatters), - rescue_options: namespace_stackable_with_hash(:rescue_options), - rescue_handlers: namespace_reverse_stackable_with_hash(:rescue_handlers), - base_only_rescue_handlers: namespace_stackable_with_hash(:base_only_rescue_handlers), - all_rescue_handler: namespace_inheritable(:all_rescue_handler), - grape_exceptions_rescue_handler: namespace_inheritable(:grape_exceptions_rescue_handler) - - stack.concat namespace_stackable(:middleware) - - if namespace_inheritable(:version).present? - stack.use Grape::Middleware::Versioner.using(namespace_inheritable(:version_options)[:using]), - versions: namespace_inheritable(:version).flatten, - version_options: namespace_inheritable(:version_options), - prefix: namespace_inheritable(:root_prefix), - mount_path: namespace_stackable(:mount_path).first - end - - stack.use Grape::Middleware::Formatter, - format: format, - default_format: namespace_inheritable(:default_format) || :txt, - content_types: content_types, - formatters: namespace_stackable_with_hash(:formatters), - parsers: namespace_stackable_with_hash(:parsers) - - builder = stack.build - builder.run ->(env) { env[Grape::Env::API_ENDPOINT].run } - builder.to_app - end - - def build_helpers - helpers = namespace_stackable(:helpers) - return if helpers.empty? - - Module.new { helpers.each { |mod_to_include| include mod_to_include } } - end - - private :build_stack, :build_helpers - def execute @block&.call(self) end @@ -411,7 +355,7 @@ def validations return enum_for(:validations) unless block_given? route_setting(:saved_validations)&.each do |saved_validation| - yield Grape::Validations::ValidatorFactory.create_validator(**saved_validation) + yield Grape::Validations::ValidatorFactory.create_validator(saved_validation) end end @@ -419,5 +363,58 @@ def options? options[:options_route_enabled] && env[Rack::REQUEST_METHOD] == Rack::OPTIONS end + + private + + def build_stack(helpers) + stack = Grape::Middleware::Stack.new + + content_types = namespace_stackable_with_hash(:content_types) + format = namespace_inheritable(:format) + + stack.use Rack::Head + stack.use Class.new(Grape::Middleware::Error), + helpers: helpers, + format: format, + content_types: content_types, + default_status: namespace_inheritable(:default_error_status), + rescue_all: namespace_inheritable(:rescue_all), + rescue_grape_exceptions: namespace_inheritable(:rescue_grape_exceptions), + default_error_formatter: namespace_inheritable(:default_error_formatter), + error_formatters: namespace_stackable_with_hash(:error_formatters), + rescue_options: namespace_stackable_with_hash(:rescue_options), + rescue_handlers: namespace_reverse_stackable_with_hash(:rescue_handlers), + base_only_rescue_handlers: namespace_stackable_with_hash(:base_only_rescue_handlers), + all_rescue_handler: namespace_inheritable(:all_rescue_handler), + grape_exceptions_rescue_handler: namespace_inheritable(:grape_exceptions_rescue_handler) + + stack.concat namespace_stackable(:middleware) + + if namespace_inheritable(:version).present? + stack.use Grape::Middleware::Versioner.using(namespace_inheritable(:version_options)[:using]), + versions: namespace_inheritable(:version).flatten, + version_options: namespace_inheritable(:version_options), + prefix: namespace_inheritable(:root_prefix), + mount_path: namespace_stackable(:mount_path).first + end + + stack.use Grape::Middleware::Formatter, + format: format, + default_format: namespace_inheritable(:default_format) || :txt, + content_types: content_types, + formatters: namespace_stackable_with_hash(:formatters), + parsers: namespace_stackable_with_hash(:parsers) + + builder = stack.build + builder.run ->(env) { env[Grape::Env::API_ENDPOINT].run } + builder.to_app + end + + def build_helpers + helpers = namespace_stackable(:helpers) + return if helpers.empty? + + Module.new { helpers.each { |mod_to_include| include mod_to_include } } + end end end diff --git a/lib/grape/exceptions/base.rb b/lib/grape/exceptions/base.rb index e262646c9..27ef78b45 100644 --- a/lib/grape/exceptions/base.rb +++ b/lib/grape/exceptions/base.rb @@ -9,7 +9,7 @@ class Base < StandardError attr_reader :status, :headers - def initialize(status: nil, message: nil, headers: nil, **_options) + def initialize(status: nil, message: nil, headers: nil) super(message) @status = status @@ -26,42 +26,32 @@ def [](index) # if BASE_ATTRIBUTES_KEY.key respond to a string message, then short_message is returned # if BASE_ATTRIBUTES_KEY.key respond to a Hash, means it may have problem , summary and resolution def compose_message(key, **attributes) - short_message = translate_message(key, **attributes) - if short_message.is_a? Hash - @problem = problem(key, **attributes) - @summary = summary(key, **attributes) - @resolution = resolution(key, **attributes) - [['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 - else - short_message - end - end + short_message = translate_message(key, attributes) + return short_message unless short_message.is_a?(Hash) - def problem(key, **attributes) - translate_message(:"#{key}.problem", **attributes) + each_steps(key, attributes).with_object(+'') do |detail_array, message| + message << "\n#{detail_array[0]}:\n #{detail_array[1]}" unless detail_array[1].blank? + end end - def summary(key, **attributes) - translate_message(:"#{key}.summary", **attributes) - end + def each_steps(key, attributes) + return enum_for(:each_steps, key, attributes) unless block_given? - def resolution(key, **attributes) - translate_message(:"#{key}.resolution", **attributes) + yield 'Problem', translate_message(:"#{key}.problem", attributes) + yield 'Summary', translate_message(:"#{key}.summary", attributes) + yield 'Resolution', translate_message(:"#{key}.resolution", attributes) end - def translate_attributes(keys, **options) + def translate_attributes(keys, options = {}) keys.map do |key| - translate("#{BASE_ATTRIBUTES_KEY}.#{key}", default: key, **options) + translate("#{BASE_ATTRIBUTES_KEY}.#{key}", options.merge(default: key.to_s)) end.join(', ') end - def translate_message(key, **options) + def translate_message(key, options = {}) case key when Symbol - translate("#{BASE_MESSAGES_KEY}.#{key}", default: '', **options) + translate("#{BASE_MESSAGES_KEY}.#{key}", options.merge(default: '')) when Proc key.call else @@ -69,14 +59,12 @@ def translate_message(key, **options) end end - def translate(key, **options) - options = options.dup - options[:default] &&= options[:default].to_s + def translate(key, options) message = ::I18n.translate(key, **options) - message.presence || fallback_message(key, **options) + message.presence || fallback_message(key, options) end - def fallback_message(key, **options) + def fallback_message(key, options) if ::I18n.enforce_available_locales && ::I18n.available_locales.exclude?(FALLBACK_LOCALE) key else diff --git a/lib/grape/exceptions/validation.rb b/lib/grape/exceptions/validation.rb index 8d368d277..0a9e9c5dd 100644 --- a/lib/grape/exceptions/validation.rb +++ b/lib/grape/exceptions/validation.rb @@ -2,16 +2,17 @@ module Grape module Exceptions - class Validation < Grape::Exceptions::Base + class Validation < Base attr_accessor :params, :message_key - def initialize(params:, message: nil, **args) + def initialize(params:, message: nil, status: nil, headers: nil) @params = params if message @message_key = message if message.is_a?(Symbol) - args[:message] = translate_message(message) + message = translate_message(message) end - super(**args) + + super(status: status, message: message, headers: headers) end # Remove all the unnecessary stuff from Grape::Exceptions::Base like status diff --git a/lib/grape/exceptions/validation_errors.rb b/lib/grape/exceptions/validation_errors.rb index b8a843b1a..8859d579b 100644 --- a/lib/grape/exceptions/validation_errors.rb +++ b/lib/grape/exceptions/validation_errors.rb @@ -2,7 +2,7 @@ module Grape module Exceptions - class ValidationErrors < Grape::Exceptions::Base + class ValidationErrors < Base ERRORS_FORMAT_KEY = 'grape.errors.format' DEFAULT_ERRORS_FORMAT = '%s %s' @@ -10,7 +10,7 @@ class ValidationErrors < Grape::Exceptions::Base attr_reader :errors - def initialize(errors: [], headers: {}, **_options) + def initialize(errors: [], headers: {}) @errors = errors.group_by(&:params) super(message: full_messages.join(', '), status: 400, headers: headers) end diff --git a/lib/grape/middleware/versioner/header.rb b/lib/grape/middleware/versioner/header.rb index 619549304..11cbfc7bc 100644 --- a/lib/grape/middleware/versioner/header.rb +++ b/lib/grape/middleware/versioner/header.rb @@ -46,14 +46,10 @@ def match_best_quality_media_type! if media_type yield media_type else - fail!(allowed_methods) + fail! end end - def allowed_methods - env[Grape::Env::GRAPE_ALLOWED_METHODS] - end - def accept_header env[Grape::Http::Headers::HTTP_ACCEPT] end @@ -93,8 +89,8 @@ def invalid_version_header!(message) raise Grape::Exceptions::InvalidVersionHeader.new(message, error_headers) end - def fail!(grape_allowed_methods) - return grape_allowed_methods if grape_allowed_methods.present? + def fail! + return if env[Grape::Env::GRAPE_ALLOWED_METHODS].present? media_types = q_values_mime_types.map { |mime_type| Grape::Util::MediaType.parse(mime_type) } vendor_not_found!(media_types) || version_not_found!(media_types) diff --git a/lib/grape/namespace.rb b/lib/grape/namespace.rb index 537e7ff66..bdaa3a53f 100644 --- a/lib/grape/namespace.rb +++ b/lib/grape/namespace.rb @@ -12,7 +12,7 @@ class Namespace # @option options :requirements [Hash] param-regex pairs, all of which must # be met by a request's params for all endpoints in this namespace, or # validation will fail and return a 422. - def initialize(space, **options) + def initialize(space, options) @space = space.to_s @options = options end diff --git a/lib/grape/request.rb b/lib/grape/request.rb index 7093ab95d..b2a62053e 100644 --- a/lib/grape/request.rb +++ b/lib/grape/request.rb @@ -6,8 +6,8 @@ class Request < Rack::Request alias rack_params params - def initialize(env, **options) - extend options[:build_params_with] || Grape.config.param_builder + def initialize(env, build_params_with: nil) + extend build_params_with || Grape.config.param_builder super(env) end diff --git a/lib/grape/router.rb b/lib/grape/router.rb index 61722011a..6889b4213 100644 --- a/lib/grape/router.rb +++ b/lib/grape/router.rb @@ -38,8 +38,8 @@ def append(route) map[route.request_method] << route end - def associate_routes(pattern, **options) - Grape::Router::GreedyRoute.new(pattern: pattern, **options).then do |greedy_route| + def associate_routes(pattern, options) + Grape::Router::GreedyRoute.new(pattern, options).then do |greedy_route| @neutral_regexes << greedy_route.to_regexp(@neutral_map.length) @neutral_map << greedy_route end @@ -107,7 +107,7 @@ def transaction(env) route = match?(input, '*') - return last_neighbor_route.endpoint.call(env) if last_neighbor_route && last_response_cascade && route + return last_neighbor_route.options[:endpoint].call(env) if last_neighbor_route && last_response_cascade && route last_response_cascade = cascade_or_return_response.call(process_route(route, env)) if route @@ -152,8 +152,8 @@ def greedy_match?(input) 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) + env[Grape::Env::GRAPE_ALLOWED_METHODS] = route.options[:allow_header] + route.options[:endpoint].call(env) end def prepare_env_from_route(env, route) diff --git a/lib/grape/router/base_route.rb b/lib/grape/router/base_route.rb index 9d19c8720..86439e908 100644 --- a/lib/grape/router/base_route.rb +++ b/lib/grape/router/base_route.rb @@ -7,8 +7,8 @@ class BaseRoute attr_reader :index, :pattern, :options - def initialize(**options) - @options = ActiveSupport::OrderedOptions.new.update(options) + def initialize(options) + @options = options.is_a?(ActiveSupport::OrderedOptions) ? options : ActiveSupport::OrderedOptions.new.update(options) end alias attributes options diff --git a/lib/grape/router/greedy_route.rb b/lib/grape/router/greedy_route.rb index a999c1b90..c2fbcf8e8 100644 --- a/lib/grape/router/greedy_route.rb +++ b/lib/grape/router/greedy_route.rb @@ -6,9 +6,9 @@ module Grape class Router class GreedyRoute < BaseRoute - def initialize(pattern:, **options) + def initialize(pattern, options) @pattern = pattern - super(**options) + super(options) end # Grape::Router:Route defines params as a function diff --git a/lib/grape/router/pattern.rb b/lib/grape/router/pattern.rb index 7b5b5276f..5761b1ea1 100644 --- a/lib/grape/router/pattern.rb +++ b/lib/grape/router/pattern.rb @@ -13,9 +13,9 @@ class Pattern def_delegators :to_regexp, :=== alias match? === - def initialize(pattern, **options) + def initialize(pattern, options) @origin = pattern - @path = build_path(pattern, anchor: options[:anchor], suffix: options[:suffix]) + @path = build_path(pattern, options) @pattern = build_pattern(@path, options) @to_regexp = @pattern.to_regexp end @@ -33,15 +33,15 @@ def build_pattern(path, options) path, uri_decode: true, params: options[:params], - capture: extract_capture(**options) + capture: extract_capture(options) ) end - def build_path(pattern, anchor: false, suffix: nil) - PatternCache[[build_path_from_pattern(pattern, anchor: anchor), suffix]] + def build_path(pattern, options) + PatternCache[[build_path_from_pattern(pattern, options), options[:suffix]]] end - def extract_capture(**options) + def extract_capture(options) sliced_options = options .slice(:format, :version) .delete_if { |_k, v| v.blank? } @@ -51,10 +51,10 @@ def extract_capture(**options) options[:requirements].merge(sliced_options) end - def build_path_from_pattern(pattern, anchor: false) + def build_path_from_pattern(pattern, options) if pattern.end_with?('*path') pattern.dup.insert(pattern.rindex('/') + 1, '?') - elsif anchor + elsif options[:anchor] pattern elsif pattern.end_with?('/') "#{pattern}?*path" diff --git a/lib/grape/router/route.rb b/lib/grape/router/route.rb index 335ecb712..244b0a407 100644 --- a/lib/grape/router/route.rb +++ b/lib/grape/router/route.rb @@ -9,10 +9,14 @@ class Route < BaseRoute def_delegators :pattern, :path, :origin - def initialize(method, pattern, **options) + def initialize(method, pattern, options) @request_method = upcase_method(method) - @pattern = Grape::Router::Pattern.new(pattern, **options) - super(**options) + @pattern = Grape::Router::Pattern.new(pattern, options) + super(options) + end + + def convert_to_head_request! + @request_method = Rack::HEAD end def exec(env) diff --git a/lib/grape/validations/params_scope.rb b/lib/grape/validations/params_scope.rb index b3c4268f6..36c6ed4d3 100644 --- a/lib/grape/validations/params_scope.rb +++ b/lib/grape/validations/params_scope.rb @@ -174,16 +174,12 @@ def reset_index # Adds a parameter declaration to our list of validations. # @param attrs [Array] (see Grape::DSL::Parameters#requires) - def push_declared_params(attrs, **opts) - opts = opts.merge(declared_params_scope: self) unless opts.key?(:declared_params_scope) - if lateral? - @parent.push_declared_params(attrs, **opts) - else - push_renamed_param(full_path + [attrs.first], opts[:as]) \ - if opts && opts[:as] + def push_declared_params(attrs, opts = {}) + opts[:declared_params_scope] = self unless opts.key?(:declared_params_scope) + return @parent.push_declared_params(attrs, opts) if lateral? - @declared_params.concat(attrs.map { |attr| ::Grape::Validations::ParamsScope::Attr.new(attr, opts[:declared_params_scope]) }) - end + push_renamed_param(full_path + [attrs.first], opts[:as]) if opts[:as] + @declared_params.concat(attrs.map { |attr| ::Grape::Validations::ParamsScope::Attr.new(attr, opts[:declared_params_scope]) }) end # Get the full path of the parameter scope in the hierarchy. diff --git a/lib/grape/validations/validator_factory.rb b/lib/grape/validations/validator_factory.rb index 444fa0421..0e2022d3a 100644 --- a/lib/grape/validations/validator_factory.rb +++ b/lib/grape/validations/validator_factory.rb @@ -3,12 +3,12 @@ module Grape module Validations class ValidatorFactory - def self.create_validator(**options) + def self.create_validator(options) options[:validator_class].new(options[:attributes], options[:options], options[:required], options[:params_scope], - **options[:opts]) + options[:opts]) end end end diff --git a/lib/grape/validations/validators/base.rb b/lib/grape/validations/validators/base.rb index 3dd49fd79..ee2dc4a87 100644 --- a/lib/grape/validations/validators/base.rb +++ b/lib/grape/validations/validators/base.rb @@ -13,15 +13,14 @@ 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 [Array] additional validation options - def initialize(attrs, options, required, scope, *opts) + # @param opts [Hash] additional validation options + def initialize(attrs, options, required, scope, opts) @attrs = Array(attrs) @option = options @required = required @scope = scope - opts = opts.any? ? opts.shift : {} - @fail_fast = opts.fetch(:fail_fast, false) - @allow_blank = opts.fetch(:allow_blank, false) + @fail_fast = opts[:fail_fast] + @allow_blank = opts[:allow_blank] end # Validates a given request. diff --git a/lib/grape/validations/validators/coerce_validator.rb b/lib/grape/validations/validators/coerce_validator.rb index 979ad47c6..eaf7c4069 100644 --- a/lib/grape/validations/validators/coerce_validator.rb +++ b/lib/grape/validations/validators/coerce_validator.rb @@ -4,7 +4,7 @@ module Grape module Validations module Validators class CoerceValidator < Base - def initialize(attrs, options, required, scope, **opts) + def initialize(attrs, options, required, scope, opts) super @converter = if type.is_a?(Grape::Validations::Types::VariantCollectionCoercer) diff --git a/lib/grape/validations/validators/default_validator.rb b/lib/grape/validations/validators/default_validator.rb index 9a59e5da1..eba8c7730 100644 --- a/lib/grape/validations/validators/default_validator.rb +++ b/lib/grape/validations/validators/default_validator.rb @@ -4,7 +4,7 @@ module Grape module Validations module Validators class DefaultValidator < Base - def initialize(attrs, options, required, scope, **opts) + def initialize(attrs, options, required, scope, opts = {}) @default = options super end diff --git a/lib/grape/validations/validators/except_values_validator.rb b/lib/grape/validations/validators/except_values_validator.rb index b125c12cf..298eb0ab9 100644 --- a/lib/grape/validations/validators/except_values_validator.rb +++ b/lib/grape/validations/validators/except_values_validator.rb @@ -4,7 +4,7 @@ module Grape module Validations module Validators 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 diff --git a/lib/grape/validations/validators/length_validator.rb b/lib/grape/validations/validators/length_validator.rb index f844f047e..c84b4c096 100644 --- a/lib/grape/validations/validators/length_validator.rb +++ b/lib/grape/validations/validators/length_validator.rb @@ -4,7 +4,7 @@ module Grape module Validations module Validators class LengthValidator < Base - def initialize(attrs, options, required, scope, **opts) + def initialize(attrs, options, required, scope, opts) @min = options[:min] @max = options[:max] @is = options[:is] diff --git a/lib/grape/validations/validators/values_validator.rb b/lib/grape/validations/validators/values_validator.rb index cc2a64172..30cdeee7b 100644 --- a/lib/grape/validations/validators/values_validator.rb +++ b/lib/grape/validations/validators/values_validator.rb @@ -4,7 +4,7 @@ module Grape module Validations module Validators class ValuesValidator < Base - def initialize(attrs, options, required, scope, **opts) + def initialize(attrs, options, required, scope, opts) @values = options.is_a?(Hash) ? options[:value] : options super end diff --git a/spec/grape/dsl/parameters_spec.rb b/spec/grape/dsl/parameters_spec.rb index 28ff1cce0..8c7c1e96c 100644 --- a/spec/grape/dsl/parameters_spec.rb +++ b/spec/grape/dsl/parameters_spec.rb @@ -20,7 +20,7 @@ def validate_attributes_reader @validate_attributes end - def push_declared_params(args, **_opts) + def push_declared_params(args, _opts) @push_declared_params = args end diff --git a/spec/grape/dsl/routing_spec.rb b/spec/grape/dsl/routing_spec.rb index 617980bb2..1b54ba913 100644 --- a/spec/grape/dsl/routing_spec.rb +++ b/spec/grape/dsl/routing_spec.rb @@ -179,7 +179,7 @@ it 'creates a new namespace with given name and options' do expect(subject).to receive(:within_namespace).and_yield expect(subject).to receive(:nest).and_yield - expect(Grape::Namespace).to receive(:new).with(:foo, foo: 'bar').and_return(new_namespace) + expect(Grape::Namespace).to receive(:new).with(:foo, { foo: 'bar' }).and_return(new_namespace) expect(subject).to receive(:namespace_stackable).with(:namespace, new_namespace) subject.namespace :foo, foo: 'bar', &proc {} diff --git a/spec/grape/middleware/error_spec.rb b/spec/grape/middleware/error_spec.rb index 88eb88725..cc8a735d4 100644 --- a/spec/grape/middleware/error_spec.rb +++ b/spec/grape/middleware/error_spec.rb @@ -29,7 +29,7 @@ def call(_env) context = self Rack::Builder.app do use Spec::Support::EndpointFaker - use Grape::Middleware::Error, **opts # rubocop:disable RSpec/DescribedClass + use Grape::Middleware::Error, opts # rubocop:disable RSpec/DescribedClass run context.err_app end end diff --git a/spec/grape/middleware/versioner/accept_version_header_spec.rb b/spec/grape/middleware/versioner/accept_version_header_spec.rb index 4f4438498..6bcf7b14a 100644 --- a/spec/grape/middleware/versioner/accept_version_header_spec.rb +++ b/spec/grape/middleware/versioner/accept_version_header_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true describe Grape::Middleware::Versioner::AcceptVersionHeader do - subject { described_class.new(app, **(@options || {})) } + subject { described_class.new(app, @options) } let(:app) { ->(env) { [200, env, env] } } diff --git a/spec/grape/middleware/versioner/header_spec.rb b/spec/grape/middleware/versioner/header_spec.rb index 1d686aae3..12fd837e0 100644 --- a/spec/grape/middleware/versioner/header_spec.rb +++ b/spec/grape/middleware/versioner/header_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true describe Grape::Middleware::Versioner::Header do - subject { described_class.new(app, **(@options || {})) } + subject { described_class.new(app, @options) } let(:app) { ->(env) { [200, env, env] } } diff --git a/spec/grape/middleware/versioner/param_spec.rb b/spec/grape/middleware/versioner/param_spec.rb index 00099dfc9..4e6bb4aa7 100644 --- a/spec/grape/middleware/versioner/param_spec.rb +++ b/spec/grape/middleware/versioner/param_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true describe Grape::Middleware::Versioner::Param do - subject { described_class.new(app, **options) } + subject { described_class.new(app, options) } let(:app) { ->(env) { [200, env, env[Grape::Env::API_VERSION]] } } let(:options) { {} } diff --git a/spec/grape/middleware/versioner/path_spec.rb b/spec/grape/middleware/versioner/path_spec.rb index 79aff5376..4593eca03 100644 --- a/spec/grape/middleware/versioner/path_spec.rb +++ b/spec/grape/middleware/versioner/path_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true describe Grape::Middleware::Versioner::Path do - subject { described_class.new(app, **options) } + subject { described_class.new(app, options) } let(:app) { ->(env) { [200, env, env[Grape::Env::API_VERSION]] } } let(:options) { {} } diff --git a/spec/grape/router/greedy_route_spec.rb b/spec/grape/router/greedy_route_spec.rb index 29e966280..f93c013b4 100644 --- a/spec/grape/router/greedy_route_spec.rb +++ b/spec/grape/router/greedy_route_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.describe Grape::Router::GreedyRoute do - let(:instance) { described_class.new(pattern: pattern, **options) } + let(:instance) { described_class.new(pattern, options) } let(:index) { 0 } let(:pattern) { :pattern } let(:params) do diff --git a/spec/integration/grape_entity/entity_spec.rb b/spec/integration/grape_entity/entity_spec.rb index cdac039a6..eaea82382 100644 --- a/spec/integration/grape_entity/entity_spec.rb +++ b/spec/integration/grape_entity/entity_spec.rb @@ -348,7 +348,7 @@ def call(_env) opts = options Rack::Builder.app do use Spec::Support::EndpointFaker - use Grape::Middleware::Error, **opts + use Grape::Middleware::Error, opts run ErrApp end end diff --git a/spec/integration/hashie/hashie_spec.rb b/spec/integration/hashie/hashie_spec.rb index 73c97ce1e..d90b77126 100644 --- a/spec/integration/hashie/hashie_spec.rb +++ b/spec/integration/hashie/hashie_spec.rb @@ -164,11 +164,9 @@ end describe 'when the build_params_with is set to Hashie' do - subject(:request_params) { Grape::Request.new(env, **opts).params } + subject(:request_params) { Grape::Request.new(env, build_params_with: Grape::Extensions::Hashie::Mash::ParamBuilder).params } context 'when the API includes a specific param builder' do - let(:opts) { { build_params_with: Grape::Extensions::Hashie::Mash::ParamBuilder } } - it { is_expected.to be_a(Hashie::Mash) } end end diff --git a/spec/shared/versioning_examples.rb b/spec/shared/versioning_examples.rb index 0215a7c44..9f42eeda1 100644 --- a/spec/shared/versioning_examples.rb +++ b/spec/shared/versioning_examples.rb @@ -7,7 +7,7 @@ subject.get :hello do "Version: #{request.env[Grape::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 @@ -18,7 +18,7 @@ subject.get :hello do "Version: #{request.env[Grape::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 @@ -34,14 +34,14 @@ end end - versioned_get '/awesome', 'v1', **macro_options + versioned_get '/awesome', 'v1', macro_options expect(last_response.status).to be 404 - versioned_get '/awesome', 'v2', **macro_options + versioned_get '/awesome', 'v2', macro_options expect(last_response.status).to be 200 - versioned_get '/legacy', 'v1', **macro_options + versioned_get '/legacy', 'v1', macro_options expect(last_response.status).to be 200 - versioned_get '/legacy', 'v2', **macro_options + versioned_get '/legacy', 'v2', macro_options expect(last_response.status).to be 404 end @@ -51,11 +51,11 @@ 'I exist' end - versioned_get '/awesome', 'v1', **macro_options + versioned_get '/awesome', 'v1', macro_options expect(last_response.status).to be 200 - versioned_get '/awesome', 'v2', **macro_options + versioned_get '/awesome', 'v2', macro_options expect(last_response.status).to be 200 - versioned_get '/awesome', 'v3', **macro_options + versioned_get '/awesome', 'v3', macro_options expect(last_response.status).to be 404 end @@ -74,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 @@ -98,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 @@ -133,11 +133,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 @@ -150,7 +150,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 @@ -186,13 +186,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 @@ -200,13 +200,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/support/versioned_helpers.rb b/spec/support/versioned_helpers.rb index 75e56e7d1..c4e841249 100644 --- a/spec/support/versioned_helpers.rb +++ b/spec/support/versioned_helpers.rb @@ -6,7 +6,7 @@ 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]) @@ -17,7 +17,7 @@ def versioned_path(**options) end end - def versioned_headers(**options) + def versioned_headers(options) case options[:using] when :path, :param {} @@ -37,9 +37,9 @@ 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 = {} params = { version_options[:parameter] => version_name } if version_options[:using] == :param get path, params, headers From 92573ea4f025dbafadf4dbb0304cf07828197208 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sat, 30 Nov 2024 15:52:38 +0100 Subject: [PATCH 275/304] Optimize Grape::Path -> Reduce Hash Allocation (#2513) * Grape::Path is now exposing only path and suffix Last parameter is now double splatted * Remove double splat operator * Optimize default path settings Use const static function for route match? * Revert compile_many_routes.rb * Add CHANGELOG.md --- CHANGELOG.md | 1 + lib/grape/endpoint.rb | 18 ++-- lib/grape/path.rb | 95 ++++++++------------ lib/grape/router/pattern.rb | 43 +++++---- lib/grape/router/route.rb | 12 ++- spec/grape/path_spec.rb | 174 +++--------------------------------- 6 files changed, 92 insertions(+), 251 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a15293fb..d758f3a77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * [#2501](https://github.com/ruby-grape/grape/pull/2501): Remove deprecated `except` and `proc` options in values validator - [@ericproulx](https://github.com/ericproulx). * [#2502](https://github.com/ruby-grape/grape/pull/2502): Remove deprecation `options` in `desc` - [@ericproulx](https://github.com/ericproulx). * [#2512](https://github.com/ruby-grape/grape/pull/2512): Optimize hash alloc - [@ericproulx](https://github.com/ericproulx). +* [#2513](https://github.com/ruby-grape/grape/pull/2513): Optimize Grape::Path - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/lib/grape/endpoint.rb b/lib/grape/endpoint.rb index 6bd72c23c..4fb0594d9 100644 --- a/lib/grape/endpoint.rb +++ b/lib/grape/endpoint.rb @@ -160,12 +160,13 @@ def mount_in(router) end def to_routes - route_options = prepare_default_route_attributes - map_routes do |method, path| - path = prepare_path(path) - route_options[:suffix] = path.suffix - params = options[:route_options].merge(route_options) - route = Grape::Router::Route.new(method, path.path, params) + default_route_options = prepare_default_route_attributes + default_path_settings = prepare_default_path_settings + + map_routes do |method, raw_path| + prepared_path = Path.new(raw_path, namespace, default_path_settings) + params = options[:route_options].present? ? options[:route_options].merge(default_route_options) : default_route_options + route = Grape::Router::Route.new(method, prepared_path.origin, prepared_path.suffix, params) route.apply(self) end.flatten end @@ -200,11 +201,10 @@ def map_routes options[:method].map { |method| options[:path].map { |path| yield method, path } } end - def prepare_path(path) + def prepare_default_path_settings namespace_stackable_hash = inheritable_setting.namespace_stackable.to_hash namespace_inheritable_hash = inheritable_setting.namespace_inheritable.to_hash - path_settings = namespace_stackable_hash.merge!(namespace_inheritable_hash) - Path.new(path, namespace, path_settings) + namespace_stackable_hash.merge!(namespace_inheritable_hash) end def namespace diff --git a/lib/grape/path.rb b/lib/grape/path.rb index fd0577893..b4e2570de 100644 --- a/lib/grape/path.rb +++ b/lib/grape/path.rb @@ -3,65 +3,66 @@ module Grape # Represents a path to an endpoint. class Path - attr_reader :raw_path, :namespace, :settings + DEFAULT_FORMAT_SEGMENT = '(/.:format)' + NO_VERSIONING_WITH_VALID_PATH_FORMAT_SEGMENT = '(.:format)' + VERSION_SEGMENT = ':version' - def initialize(raw_path, namespace, settings) - @raw_path = raw_path - @namespace = namespace - @settings = settings - end + attr_reader :origin, :suffix - def mount_path - settings[:mount_path] + def initialize(raw_path, raw_namespace, settings) + @origin = PartsCache[build_parts(raw_path, raw_namespace, settings)] + @suffix = build_suffix(raw_path, raw_namespace, settings) end - def root_prefix - settings[:root_prefix] + def to_s + "#{origin}#{suffix}" end - def uses_specific_format? - return false unless settings.key?(:format) && settings.key?(:content_types) + private - settings[:format] && Array(settings[:content_types]).size == 1 + def build_suffix(raw_path, raw_namespace, settings) + if uses_specific_format?(settings) + "(.#{settings[:format]})" + elsif !uses_path_versioning?(settings) || (valid_part?(raw_namespace) || valid_part?(raw_path)) + NO_VERSIONING_WITH_VALID_PATH_FORMAT_SEGMENT + else + DEFAULT_FORMAT_SEGMENT + end end - def uses_path_versioning? - return false unless settings.key?(:version) && settings[:version_options]&.key?(:using) - - settings[:version] && settings[:version_options][:using] == :path + def build_parts(raw_path, raw_namespace, settings) + [].tap do |parts| + add_part(parts, settings[:mount_path]) + add_part(parts, settings[:root_prefix]) + parts << VERSION_SEGMENT if uses_path_versioning?(settings) + add_part(parts, raw_namespace) + add_part(parts, raw_path) + end end - def namespace? - namespace&.match?(/^\S/) && not_slash?(namespace) + def add_part(parts, value) + parts << value if value && not_slash?(value) end - def path? - raw_path&.match?(/^\S/) && not_slash?(raw_path) + def not_slash?(value) + value != '/' end - def suffix - if uses_specific_format? - "(.#{settings[:format]})" - elsif !uses_path_versioning? || (namespace? || path?) - '(.:format)' - else - '(/.:format)' - end - end + def uses_specific_format?(settings) + return false unless settings.key?(:format) && settings.key?(:content_types) - def path - PartsCache[parts] + settings[:format] && Array(settings[:content_types]).size == 1 end - def path_with_suffix - "#{path}#{suffix}" - end + def uses_path_versioning?(settings) + return false unless settings.key?(:version) && settings[:version_options]&.key?(:using) - def to_s - path_with_suffix + settings[:version] && settings[:version_options][:using] == :path end - private + def valid_part?(part) + part&.match?(/^\S/) && not_slash?(part) + end class PartsCache < Grape::Util::Cache def initialize @@ -71,23 +72,5 @@ def initialize end end end - - def parts - [].tap do |parts| - add_part(parts, mount_path) - add_part(parts, root_prefix) - parts << ':version' if uses_path_versioning? - add_part(parts, namespace) - add_part(parts, raw_path) - end - end - - def add_part(parts, value) - parts << value if value && not_slash?(value) - end - - def not_slash?(value) - value != '/' - end end end diff --git a/lib/grape/router/pattern.rb b/lib/grape/router/pattern.rb index 5761b1ea1..4529a9271 100644 --- a/lib/grape/router/pattern.rb +++ b/lib/grape/router/pattern.rb @@ -9,14 +9,14 @@ class Pattern attr_reader :origin, :path, :pattern, :to_regexp - def_delegators :pattern, :named_captures, :params + def_delegators :pattern, :params def_delegators :to_regexp, :=== alias match? === - def initialize(pattern, options) - @origin = pattern - @path = build_path(pattern, options) - @pattern = build_pattern(@path, options) + def initialize(origin, suffix, options) + @origin = origin + @path = build_path(origin, options[:anchor], suffix) + @pattern = build_pattern(@path, options[:params], options[:format], options[:version], options[:requirements]) @to_regexp = @pattern.to_regexp end @@ -28,33 +28,34 @@ def captures_default private - def build_pattern(path, options) + def build_pattern(path, params, format, version, requirements) Mustermann::Grape.new( path, uri_decode: true, - params: options[:params], - capture: extract_capture(options) + params: params, + capture: extract_capture(format, version, requirements) ) end - def build_path(pattern, options) - PatternCache[[build_path_from_pattern(pattern, options), options[:suffix]]] + def build_path(pattern, anchor, suffix) + PatternCache[[build_path_from_pattern(pattern, anchor), suffix]] end - def extract_capture(options) - sliced_options = options - .slice(:format, :version) - .delete_if { |_k, v| v.blank? } - .transform_values { |v| Array.wrap(v).map(&:to_s) } - return sliced_options if options[:requirements].blank? + def extract_capture(format, version, requirements) + capture = {}.tap do |h| + h[:format] = map_str(format) if format.present? + h[:version] = map_str(version) if version.present? + end + + return capture if requirements.blank? - options[:requirements].merge(sliced_options) + requirements.merge(capture) end - def build_path_from_pattern(pattern, options) + def build_path_from_pattern(pattern, anchor) if pattern.end_with?('*path') pattern.dup.insert(pattern.rindex('/') + 1, '?') - elsif options[:anchor] + elsif anchor pattern elsif pattern.end_with?('/') "#{pattern}?*path" @@ -63,6 +64,10 @@ def build_path_from_pattern(pattern, options) end end + def map_str(value) + Array.wrap(value).map(&:to_s) + end + class PatternCache < Grape::Util::Cache def initialize super diff --git a/lib/grape/router/route.rb b/lib/grape/router/route.rb index 244b0a407..48599610c 100644 --- a/lib/grape/router/route.rb +++ b/lib/grape/router/route.rb @@ -5,13 +5,17 @@ class Router class Route < BaseRoute extend Forwardable + FORWARD_MATCH_METHOD = ->(input, pattern) { input.start_with?(pattern.origin) } + NON_FORWARD_MATCH_METHOD = ->(input, pattern) { pattern.match?(input) } + attr_reader :app, :request_method def_delegators :pattern, :path, :origin - def initialize(method, pattern, options) + def initialize(method, origin, path, options) @request_method = upcase_method(method) - @pattern = Grape::Router::Pattern.new(pattern, options) + @pattern = Grape::Router::Pattern.new(origin, path, options) + @match_function = options[:forward_match] ? FORWARD_MATCH_METHOD : NON_FORWARD_MATCH_METHOD super(options) end @@ -31,7 +35,7 @@ def apply(app) def match?(input) return false if input.blank? - options[:forward_match] ? input.start_with?(pattern.origin) : pattern.match?(input) + @match_function.call(input, pattern) end def params(input = nil) @@ -46,7 +50,7 @@ def params(input = nil) private def params_without_input - pattern.captures_default.merge(attributes.params) + @params_without_input ||= pattern.captures_default.merge(attributes.params) end def upcase_method(method) diff --git a/spec/grape/path_spec.rb b/spec/grape/path_spec.rb index 2a6dd2fce..9b9b2ed26 100644 --- a/spec/grape/path_spec.rb +++ b/spec/grape/path_spec.rb @@ -1,147 +1,23 @@ # frozen_string_literal: true describe Grape::Path do - describe '#initialize' do - it 'remembers the path' do - path = described_class.new('/:id', anything, anything) - expect(path.raw_path).to eql('/:id') - end - - it 'remembers the namespace' do - path = described_class.new(anything, '/users', anything) - expect(path.namespace).to eql('/users') - end - - it 'remebers the settings' do - path = described_class.new(anything, anything, foo: 'bar') - expect(path.settings).to eql(foo: 'bar') - end - end - - describe '#mount_path' do - it 'is nil when no mount path setting exists' do - path = described_class.new(anything, anything, {}) - expect(path.mount_path).to be_nil - end - - it 'is nil when the mount path is nil' do - path = described_class.new(anything, anything, mount_path: nil) - expect(path.mount_path).to be_nil - end - - it 'splits the mount path' do - path = described_class.new(anything, anything, mount_path: %w[foo bar]) - expect(path.mount_path).to eql(%w[foo bar]) - end - end - - describe '#root_prefix' do - it 'is nil when no root prefix setting exists' do - path = described_class.new(anything, anything, {}) - expect(path.root_prefix).to be_nil - end - - it 'is nil when the mount path is nil' do - path = described_class.new(anything, anything, root_prefix: nil) - expect(path.root_prefix).to be_nil - end - - it 'splits the mount path' do - path = described_class.new(anything, anything, root_prefix: 'hello/world') - expect(path.root_prefix).to eql('hello/world') - end - end - - describe '#uses_path_versioning?' do - it 'is false when the version setting is nil' do - path = described_class.new(anything, anything, version: nil) - expect(path.uses_path_versioning?).to be false - end - - it 'is false when the version option is header' do - path = described_class.new( - anything, - anything, - version: 'v1', - version_options: { using: :header } - ) - - expect(path.uses_path_versioning?).to be false - end - - it 'is true when the version option is path' do - path = described_class.new( - anything, - anything, - version: 'v1', - version_options: { using: :path } - ) - - expect(path.uses_path_versioning?).to be true - end - end - - describe '#namespace?' do - it 'is false when the namespace is nil' do - path = described_class.new(anything, nil, anything) - expect(path).not_to be_namespace - end - - it 'is false when the namespace starts with whitespace' do - path = described_class.new(anything, ' /foo', anything) - expect(path).not_to be_namespace - end - - it 'is false when the namespace is the root path' do - path = described_class.new(anything, '/', anything) - expect(path.namespace?).to be false - end - - it 'is true otherwise' do - path = described_class.new(anything, '/world', anything) - expect(path.namespace?).to be true - end - end - - describe '#path?' do - it 'is false when the path is nil' do - path = described_class.new(nil, anything, anything) - expect(path).not_to be_path - end - - it 'is false when the path starts with whitespace' do - path = described_class.new(' /foo', anything, anything) - expect(path).not_to be_path - end - - it 'is false when the path is the root path' do - path = described_class.new('/', anything, anything) - expect(path.path?).to be false - end - - it 'is true otherwise' do - path = described_class.new('/hello', anything, anything) - expect(path.path?).to be true - end - end - - describe '#path' do + describe '#origin' do context 'mount_path' do it 'is not included when it is nil' do path = described_class.new(nil, nil, mount_path: '/foo/bar') - expect(path.path).to eql '/foo/bar' + expect(path.origin).to eql '/foo/bar' end it 'is included when it is not nil' do path = described_class.new(nil, nil, {}) - expect(path.path).to eql('/') + expect(path.origin).to eql('/') end end context 'root_prefix' do it 'is not included when it is nil' do path = described_class.new(nil, nil, {}) - expect(path.path).to eql('/') + expect(path.origin).to eql('/') end it 'is included after the mount path' do @@ -152,7 +28,7 @@ root_prefix: '/hello' ) - expect(path.path).to eql('/foo/hello') + expect(path.origin).to eql('/foo/hello') end end @@ -164,7 +40,7 @@ root_prefix: '/hello' ) - expect(path.path).to eql('/foo/hello/namespace') + expect(path.origin).to eql('/foo/hello/namespace') end it 'uses the raw path after the namespace' do @@ -175,25 +51,21 @@ root_prefix: '/hello' ) - expect(path.path).to eql('/foo/hello/namespace/raw_path') + expect(path.origin).to eql('/foo/hello/namespace/raw_path') end end describe '#suffix' do context 'when using a specific format' do it 'accepts specified format' do - path = described_class.new(nil, nil, {}) - allow(path).to receive_messages(uses_specific_format?: true, settings: { format: :json }) - + path = described_class.new(nil, nil, format: 'json', content_types: 'application/json') expect(path.suffix).to eql('(.json)') end end context 'when path versioning is used' do it "includes a '/'" do - path = described_class.new(nil, nil, {}) - allow(path).to receive_messages(uses_specific_format?: false, uses_path_versioning?: true) - + path = described_class.new(nil, nil, version: :v1, version_options: { using: :path }) expect(path.suffix).to eql('(/.:format)') end end @@ -201,42 +73,18 @@ context 'when path versioning is not used' do it "does not include a '/' when the path has a namespace" do path = described_class.new(nil, 'namespace', {}) - allow(path).to receive_messages(uses_specific_format?: false, uses_path_versioning?: true) - expect(path.suffix).to eql('(.:format)') end it "does not include a '/' when the path has a path" do - path = described_class.new('/path', nil, {}) - allow(path).to receive_messages(uses_specific_format?: false, uses_path_versioning?: true) - + path = described_class.new('/path', nil, version: :v1, version_options: { using: :path }) expect(path.suffix).to eql('(.:format)') end it "includes a '/' otherwise" do - path = described_class.new(nil, nil, {}) - allow(path).to receive_messages(uses_specific_format?: false, uses_path_versioning?: true) - + path = described_class.new(nil, nil, version: :v1, version_options: { using: :path }) expect(path.suffix).to eql('(/.:format)') end end end - - describe '#path_with_suffix' do - it 'combines the path and suffix' do - path = described_class.new(nil, nil, {}) - allow(path).to receive_messages(path: '/the/path', suffix: 'suffix') - - expect(path.path_with_suffix).to eql('/the/pathsuffix') - end - - context 'when using a specific format' do - it 'might have a suffix with specified format' do - path = described_class.new(nil, nil, {}) - allow(path).to receive_messages(path: '/the/path', uses_specific_format?: true, settings: { format: :json }) - - expect(path.path_with_suffix).to eql('/the/path(.json)') - end - end - end end From 30b3a4377172814ca0bf0c72473f1df1bf7abcfb Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sat, 30 Nov 2024 21:42:21 +0100 Subject: [PATCH 276/304] Add Rails 8.0 gemfile (#2514) * Add Rails 8.0 gemfile Add Rails 8.0 test matrix * Fix Rails 8.0 test * Removes rails_6_0.gemfile from CI * Add CHANGELOG.md --- .github/workflows/test.yml | 11 ++++++++++- CHANGELOG.md | 1 + gemfiles/{rails_6_0.gemfile => rails_8_0.gemfile} | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) rename gemfiles/{rails_6_0.gemfile => rails_8_0.gemfile} (79%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6951c9432..a7710f2aa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,7 +24,7 @@ jobs: fail-fast: false matrix: ruby: ['2.7', '3.0', '3.1', '3.2', '3.3'] - gemfile: [Gemfile, gemfiles/rack_2_0.gemfile, gemfiles/rack_3_0.gemfile, gemfiles/rack_3_1.gemfile, gemfiles/rails_6_0.gemfile, gemfiles/rails_6_1.gemfile, gemfiles/rails_7_0.gemfile, gemfiles/rails_7_1.gemfile, gemfiles/rails_7_2.gemfile] + gemfile: [Gemfile, gemfiles/rack_2_0.gemfile, gemfiles/rack_3_0.gemfile, gemfiles/rack_3_1.gemfile, gemfiles/rails_6_1.gemfile, gemfiles/rails_7_0.gemfile, gemfiles/rails_7_1.gemfile, gemfiles/rails_7_2.gemfile, gemfiles/rails_8_0.gemfile] specs: ['spec --exclude-pattern=spec/integration/**/*_spec.rb'] include: - ruby: '2.7' @@ -57,11 +57,20 @@ jobs: - ruby: '3.3' gemfile: gemfiles/rails_7_2.gemfile specs: 'spec/integration/rails' + - ruby: '3.3' + gemfile: gemfiles/rails_8_0.gemfile + specs: 'spec/integration/rails' exclude: - ruby: '2.7' gemfile: gemfiles/rails_7_2.gemfile - ruby: '3.0' gemfile: gemfiles/rails_7_2.gemfile + - ruby: '2.7' + gemfile: gemfiles/rails_8_0.gemfile + - ruby: '3.0' + gemfile: gemfiles/rails_8_0.gemfile + - ruby: '3.1' + gemfile: gemfiles/rails_8_0.gemfile runs-on: ubuntu-latest env: BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }} diff --git a/CHANGELOG.md b/CHANGELOG.md index d758f3a77..d274b058e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * [#2502](https://github.com/ruby-grape/grape/pull/2502): Remove deprecation `options` in `desc` - [@ericproulx](https://github.com/ericproulx). * [#2512](https://github.com/ruby-grape/grape/pull/2512): Optimize hash alloc - [@ericproulx](https://github.com/ericproulx). * [#2513](https://github.com/ruby-grape/grape/pull/2513): Optimize Grape::Path - [@ericproulx](https://github.com/ericproulx). +* [#2514](https://github.com/ruby-grape/grape/pull/2514): Add rails 8.0 to CI - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/gemfiles/rails_6_0.gemfile b/gemfiles/rails_8_0.gemfile similarity index 79% rename from gemfiles/rails_6_0.gemfile rename to gemfiles/rails_8_0.gemfile index 0e775d785..715b61502 100644 --- a/gemfiles/rails_6_0.gemfile +++ b/gemfiles/rails_8_0.gemfile @@ -2,5 +2,5 @@ eval_gemfile '../Gemfile' -gem 'rails', '~> 6.0.0' +gem 'rails', '~> 8.0' gem 'tzinfo-data', require: false From 7ec3e6d20d97de9e3101504c0877eead51d30ae0 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Fri, 27 Dec 2024 13:02:59 +0100 Subject: [PATCH 277/304] Dynamic registration (#2516) * use `to_enum` instead of including `Enumerable` to attributes_iterator.rb and validation_errors.rb * Remove unused build_coercer.rb * Remove const_missing in api.rb * Add Grape::Util::Registry Add deregister module in spec * Add Grape::Parser::Base and use Grape::Util::Registry * Add Grape::Formatter::Base and use Grape::Util::Registry * Add Grape::Middleware::Versioner::Base and use Grape::Util::Registry * Add Grape::ErrorFormatter::Base and use Grape::Util::Registry * Add Grape::Util::Registry to Grape::Validations ContractScope validator has been moved to validations/validators and renamed properly * Add `deregister in `before(:all)`` * Add `deregister` to Grape::Validations only * Use `prepend` * Fix Ruby 2.7 Fix rubocop * Refactor collection_coercer_for Refactor Grape::Validations::Types cache_key * Add CHANGELOG.md * Revert coercer_cache changes. Will do it another time * Revert enumerable change * Refactor registry * Update CHANGELOG.md Co-authored-by: Daniel (dB.) Doubrovkine --------- Co-authored-by: Daniel (dB.) Doubrovkine --- CHANGELOG.md | 1 + lib/grape/api.rb | 9 -- lib/grape/error_formatter.rb | 16 +--- lib/grape/error_formatter/base.rb | 72 ++++++++++----- lib/grape/error_formatter/json.rb | 31 ++----- lib/grape/error_formatter/jsonapi.rb | 7 ++ .../error_formatter/serializable_hash.rb | 7 ++ lib/grape/error_formatter/txt.rb | 33 +++---- lib/grape/error_formatter/xml.rb | 16 +--- lib/grape/formatter.rb | 16 +--- lib/grape/formatter/base.rb | 16 ++++ lib/grape/formatter/json.rb | 10 +- lib/grape/formatter/serializable_hash.rb | 2 +- lib/grape/formatter/txt.rb | 8 +- lib/grape/formatter/xml.rb | 10 +- lib/grape/middleware/versioner.rb | 8 +- .../versioner/accept_version_header.rb | 2 - lib/grape/middleware/versioner/base.rb | 82 +++++++++++++++++ lib/grape/middleware/versioner/header.rb | 2 - lib/grape/middleware/versioner/param.rb | 2 - lib/grape/middleware/versioner/path.rb | 2 - lib/grape/middleware/versioner_helpers.rb | 75 --------------- lib/grape/parser.rb | 12 +-- lib/grape/parser/base.rb | 16 ++++ lib/grape/parser/json.rb | 14 ++- lib/grape/parser/jsonapi.rb | 7 ++ lib/grape/parser/xml.rb | 14 ++- lib/grape/util/registry.rb | 27 ++++++ lib/grape/validations.rb | 25 ++--- lib/grape/validations/contract_scope.rb | 36 +------- lib/grape/validations/params_scope.rb | 2 +- lib/grape/validations/types/build_coercer.rb | 92 ------------------- .../validations/types/dry_type_coercer.rb | 16 ++-- lib/grape/validations/validators/base.rb | 5 +- .../validators/contract_scope_validator.rb | 41 +++++++++ spec/grape/api/custom_validations_spec.rb | 45 ++++++++- spec/grape/parser_spec.rb | 14 --- .../contract_scope_validator_spec.rb | 9 ++ spec/grape/validations_spec.rb | 24 ++++- .../dry_validation/dry_validation_spec.rb | 8 -- spec/spec_helper.rb | 4 + spec/support/deregister.rb | 7 ++ 42 files changed, 421 insertions(+), 424 deletions(-) create mode 100644 lib/grape/error_formatter/jsonapi.rb create mode 100644 lib/grape/error_formatter/serializable_hash.rb create mode 100644 lib/grape/formatter/base.rb create mode 100644 lib/grape/middleware/versioner/base.rb delete mode 100644 lib/grape/middleware/versioner_helpers.rb create mode 100644 lib/grape/parser/base.rb create mode 100644 lib/grape/parser/jsonapi.rb create mode 100644 lib/grape/util/registry.rb delete mode 100644 lib/grape/validations/types/build_coercer.rb create mode 100644 lib/grape/validations/validators/contract_scope_validator.rb create mode 100644 spec/grape/validations/validators/contract_scope_validator_spec.rb create mode 100644 spec/support/deregister.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index d274b058e..6ec8b6d64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * [#2512](https://github.com/ruby-grape/grape/pull/2512): Optimize hash alloc - [@ericproulx](https://github.com/ericproulx). * [#2513](https://github.com/ruby-grape/grape/pull/2513): Optimize Grape::Path - [@ericproulx](https://github.com/ericproulx). * [#2514](https://github.com/ruby-grape/grape/pull/2514): Add rails 8.0 to CI - [@ericproulx](https://github.com/ericproulx). +* [#2516](https://github.com/ruby-grape/grape/pull/2516): Dynamic registration for parsers, formatters, versioners - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/lib/grape/api.rb b/lib/grape/api.rb index 2a3435aa3..8ce789c25 100644 --- a/lib/grape/api.rb +++ b/lib/grape/api.rb @@ -78,15 +78,6 @@ def call(...) instance_for_rack.call(...) end - # 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 - super - end - end - # The remountable class can have a configuration hash to provide some dynamic class-level variables. # For instance, a description could be done using: `desc configuration[:description]` if it may vary # depending on where the endpoint is mounted. Use with care, if you find yourself using configuration diff --git a/lib/grape/error_formatter.rb b/lib/grape/error_formatter.rb index 7784055b6..7d9ace8a8 100644 --- a/lib/grape/error_formatter.rb +++ b/lib/grape/error_formatter.rb @@ -2,22 +2,14 @@ module Grape module ErrorFormatter - module_function + extend Grape::Util::Registry - DEFAULTS = { - serializable_hash: Grape::ErrorFormatter::Json, - json: Grape::ErrorFormatter::Json, - jsonapi: Grape::ErrorFormatter::Json, - txt: Grape::ErrorFormatter::Txt, - xml: Grape::ErrorFormatter::Xml - }.freeze + module_function def formatter_for(format, error_formatters = nil, default_error_formatter = nil) - select_formatter(error_formatters, format) || default_error_formatter || DEFAULTS[:txt] - end + return error_formatters[format] if error_formatters&.key?(format) - def select_formatter(error_formatters, format) - error_formatters&.key?(format) ? error_formatters[format] : DEFAULTS[format] + registry[format] || default_error_formatter || Grape::ErrorFormatter::Txt end end end diff --git a/lib/grape/error_formatter/base.rb b/lib/grape/error_formatter/base.rb index 2a1e758eb..f2cf223c6 100644 --- a/lib/grape/error_formatter/base.rb +++ b/lib/grape/error_formatter/base.rb @@ -2,36 +2,66 @@ module Grape module ErrorFormatter - module Base - def present(message, env) - present_options = {} - presented_message = message - if presented_message.is_a?(Hash) - presented_message = presented_message.dup - present_options[:with] = presented_message.delete(:with) + class Base + class << self + def call(message, backtrace, options = {}, env = nil, original_exception = nil) + merge_backtrace = backtrace.present? && options.dig(:rescue_options, :backtrace) + merge_original_exception = original_exception && options.dig(:rescue_options, :original_exception) + + wrapped_message = wrap_message(present(message, env)) + if wrapped_message.is_a?(Hash) + wrapped_message[:backtrace] = backtrace if merge_backtrace + wrapped_message[:original_exception] = original_exception.inspect if merge_original_exception + end + + format_structured_message(wrapped_message) end - presenter = env[Grape::Env::API_ENDPOINT].entity_class_for_obj(presented_message, present_options) + def present(message, env) + present_options = {} + presented_message = message + if presented_message.is_a?(Hash) + presented_message = presented_message.dup + present_options[:with] = presented_message.delete(:with) + end + + presenter = env[Grape::Env::API_ENDPOINT].entity_class_for_obj(presented_message, present_options) + + unless presenter || env[Grape::Env::GRAPE_ROUTING_ARGS].nil? + # env['api.endpoint'].route does not work when the error occurs within a middleware + # the Endpoint does not have a valid env at this moment + http_codes = env[Grape::Env::GRAPE_ROUTING_ARGS][:route_info].http_codes || [] + + found_code = http_codes.find do |http_code| + (http_code[0].to_i == env[Grape::Env::API_ENDPOINT].status) && http_code[2].respond_to?(:represent) + end if env[Grape::Env::API_ENDPOINT].request - unless presenter || env[Grape::Env::GRAPE_ROUTING_ARGS].nil? - # env['api.endpoint'].route does not work when the error occurs within a middleware - # the Endpoint does not have a valid env at this moment - http_codes = env[Grape::Env::GRAPE_ROUTING_ARGS][:route_info].http_codes || [] + presenter = found_code[2] if found_code + end - found_code = http_codes.find do |http_code| - (http_code[0].to_i == env[Grape::Env::API_ENDPOINT].status) && http_code[2].respond_to?(:represent) - end if env[Grape::Env::API_ENDPOINT].request + if presenter + embeds = { env: env } + embeds[:version] = env[Grape::Env::API_VERSION] if env.key?(Grape::Env::API_VERSION) + presented_message = presenter.represent(presented_message, embeds).serializable_hash + end - presenter = found_code[2] if found_code + presented_message end - if presenter - embeds = { env: env } - embeds[:version] = env[Grape::Env::API_VERSION] if env.key?(Grape::Env::API_VERSION) - presented_message = presenter.represent(presented_message, embeds).serializable_hash + def wrap_message(message) + return message if message.is_a?(Hash) + + { message: message } + end + + def format_structured_message(_structured_message) + raise NotImplementedError end - presented_message + def inherited(klass) + super + ErrorFormatter.register(klass) + end end end end diff --git a/lib/grape/error_formatter/json.rb b/lib/grape/error_formatter/json.rb index f4df46e84..bed5bd39d 100644 --- a/lib/grape/error_formatter/json.rb +++ b/lib/grape/error_formatter/json.rb @@ -2,28 +2,19 @@ module Grape module ErrorFormatter - module Json - extend Base - + class Json < Base class << self - def call(message, backtrace, options = {}, env = nil, original_exception = nil) - result = wrap_message(present(message, env)) - - result = merge_rescue_options(result, backtrace, options, original_exception) if result.is_a?(Hash) - - ::Grape::Json.dump(result) + def format_structured_message(structured_message) + ::Grape::Json.dump(structured_message) end private def wrap_message(message) - if message.is_a?(Hash) - message - elsif message.is_a?(Exceptions::ValidationErrors) - message.as_json - else - { error: ensure_utf8(message) } - end + return message if message.is_a?(Hash) + return message.as_json if message.is_a?(Exceptions::ValidationErrors) + + { error: ensure_utf8(message) } end def ensure_utf8(message) @@ -31,14 +22,6 @@ def ensure_utf8(message) message.encode('UTF-8', invalid: :replace, undef: :replace) end - - def merge_rescue_options(result, backtrace, options, original_exception) - rescue_options = options[:rescue_options] || {} - result = result.merge(backtrace: backtrace) if rescue_options[:backtrace] && backtrace && !backtrace.empty? - result = result.merge(original_exception: original_exception.inspect) if rescue_options[:original_exception] && original_exception - - result - end end end end diff --git a/lib/grape/error_formatter/jsonapi.rb b/lib/grape/error_formatter/jsonapi.rb new file mode 100644 index 000000000..ed1d2d30f --- /dev/null +++ b/lib/grape/error_formatter/jsonapi.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Grape + module ErrorFormatter + class Jsonapi < Json; end + end +end diff --git a/lib/grape/error_formatter/serializable_hash.rb b/lib/grape/error_formatter/serializable_hash.rb new file mode 100644 index 000000000..14b9ed597 --- /dev/null +++ b/lib/grape/error_formatter/serializable_hash.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Grape + module ErrorFormatter + class SerializableHash < Json; end + end +end diff --git a/lib/grape/error_formatter/txt.rb b/lib/grape/error_formatter/txt.rb index 22fc5c538..7c0c75918 100644 --- a/lib/grape/error_formatter/txt.rb +++ b/lib/grape/error_formatter/txt.rb @@ -2,26 +2,19 @@ module Grape module ErrorFormatter - module Txt - extend Base - - class << self - def call(message, backtrace, options = {}, env = nil, original_exception = nil) - message = present(message, env) - - result = message.is_a?(Hash) ? ::Grape::Json.dump(message) : message - Array.wrap(result).tap do |final_result| - rescue_options = options[:rescue_options] || {} - if rescue_options[:backtrace] && backtrace.present? - final_result << 'backtrace:' - final_result.concat(backtrace) - end - if rescue_options[:original_exception] && original_exception - final_result << 'original exception:' - final_result << original_exception.inspect - end - end.join("\r\n ") - end + class Txt < Base + def self.format_structured_message(structured_message) + message = structured_message[:message] || Grape::Json.dump(structured_message) + Array.wrap(message).tap do |final_message| + if structured_message.key?(:backtrace) + final_message << 'backtrace:' + final_message.concat(structured_message[:backtrace]) + end + if structured_message.key?(:original_exception) + final_message << 'original exception:' + final_message << structured_message[:original_exception] + end + end.join("\r\n ") end end end diff --git a/lib/grape/error_formatter/xml.rb b/lib/grape/error_formatter/xml.rb index e423c2fd9..c78f6d650 100644 --- a/lib/grape/error_formatter/xml.rb +++ b/lib/grape/error_formatter/xml.rb @@ -2,19 +2,9 @@ module Grape module ErrorFormatter - module Xml - extend Base - - class << self - def call(message, backtrace, options = {}, env = nil, original_exception = nil) - message = present(message, env) - - result = message.is_a?(Hash) ? message : { message: message } - rescue_options = options[:rescue_options] || {} - result = result.merge(backtrace: backtrace) if rescue_options[:backtrace] && backtrace && !backtrace.empty? - result = result.merge(original_exception: original_exception.inspect) if rescue_options[:original_exception] && original_exception - result.respond_to?(:to_xml) ? result.to_xml(root: :error) : result.to_s - end + class Xml < Base + def self.format_structured_message(structured_message) + structured_message.respond_to?(:to_xml) ? structured_message.to_xml(root: :error) : structured_message.to_s end end end diff --git a/lib/grape/formatter.rb b/lib/grape/formatter.rb index d586b0bd6..6d5affb34 100644 --- a/lib/grape/formatter.rb +++ b/lib/grape/formatter.rb @@ -2,24 +2,16 @@ module Grape module Formatter - module_function + extend Grape::Util::Registry - DEFAULTS = { - json: Grape::Formatter::Json, - jsonapi: Grape::Formatter::Json, - serializable_hash: Grape::Formatter::SerializableHash, - txt: Grape::Formatter::Txt, - xml: Grape::Formatter::Xml - }.freeze + module_function DEFAULT_LAMBDA_FORMATTER = ->(obj, _env) { obj } def formatter_for(api_format, formatters) - select_formatter(formatters, api_format) || DEFAULT_LAMBDA_FORMATTER - end + return formatters[api_format] if formatters&.key?(api_format) - def select_formatter(formatters, api_format) - formatters&.key?(api_format) ? formatters[api_format] : DEFAULTS[api_format] + registry[api_format] || DEFAULT_LAMBDA_FORMATTER end end end diff --git a/lib/grape/formatter/base.rb b/lib/grape/formatter/base.rb new file mode 100644 index 000000000..dfd56d65d --- /dev/null +++ b/lib/grape/formatter/base.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Grape + module Formatter + class Base + def self.call(_object, _env) + raise NotImplementedError + end + + def self.inherited(klass) + super + Formatter.register(klass) + end + end + end +end diff --git a/lib/grape/formatter/json.rb b/lib/grape/formatter/json.rb index 0f0152f6c..bfdd2ca28 100644 --- a/lib/grape/formatter/json.rb +++ b/lib/grape/formatter/json.rb @@ -2,13 +2,11 @@ module Grape module Formatter - module Json - class << self - def call(object, _env) - return object.to_json if object.respond_to?(:to_json) + class Json < Base + def self.call(object, _env) + return object.to_json if object.respond_to?(:to_json) - ::Grape::Json.dump(object) - end + ::Grape::Json.dump(object) end end end diff --git a/lib/grape/formatter/serializable_hash.rb b/lib/grape/formatter/serializable_hash.rb index cb9bfb7c1..5b29ece15 100644 --- a/lib/grape/formatter/serializable_hash.rb +++ b/lib/grape/formatter/serializable_hash.rb @@ -2,7 +2,7 @@ module Grape module Formatter - module SerializableHash + class SerializableHash < Base class << self def call(object, _env) return object if object.is_a?(String) diff --git a/lib/grape/formatter/txt.rb b/lib/grape/formatter/txt.rb index 1f1f9ef2f..cb77e4f07 100644 --- a/lib/grape/formatter/txt.rb +++ b/lib/grape/formatter/txt.rb @@ -2,11 +2,9 @@ module Grape module Formatter - module Txt - class << self - def call(object, _env) - object.respond_to?(:to_txt) ? object.to_txt : object.to_s - end + class Txt < Base + def self.call(object, _env) + object.respond_to?(:to_txt) ? object.to_txt : object.to_s end end end diff --git a/lib/grape/formatter/xml.rb b/lib/grape/formatter/xml.rb index c170d1843..de0531758 100644 --- a/lib/grape/formatter/xml.rb +++ b/lib/grape/formatter/xml.rb @@ -2,13 +2,11 @@ module Grape module Formatter - module Xml - class << self - def call(object, _env) - return object.to_xml if object.respond_to?(:to_xml) + class Xml < Base + def self.call(object, _env) + return object.to_xml if object.respond_to?(:to_xml) - raise Grape::Exceptions::InvalidFormatter.new(object.class, 'xml') - end + raise Grape::Exceptions::InvalidFormatter.new(object.class, 'xml') end end end diff --git a/lib/grape/middleware/versioner.rb b/lib/grape/middleware/versioner.rb index 1589f9b76..bcca9fe59 100644 --- a/lib/grape/middleware/versioner.rb +++ b/lib/grape/middleware/versioner.rb @@ -11,14 +11,16 @@ module Grape module Middleware module Versioner + extend Grape::Util::Registry + module_function # @param strategy [Symbol] :path, :header, :accept_version_header or :param # @return a middleware class based on strategy def using(strategy) - Grape::Middleware::Versioner.const_get(:"#{strategy.to_s.camelize}") - rescue NameError - raise Grape::Exceptions::InvalidVersionerOption, strategy + raise Grape::Exceptions::InvalidVersionerOption, strategy unless registry.key?(strategy) + + registry[strategy] end end end diff --git a/lib/grape/middleware/versioner/accept_version_header.rb b/lib/grape/middleware/versioner/accept_version_header.rb index 1cf2bb674..ff76a7372 100644 --- a/lib/grape/middleware/versioner/accept_version_header.rb +++ b/lib/grape/middleware/versioner/accept_version_header.rb @@ -17,8 +17,6 @@ module Versioner # X-Cascade header to alert Grape::Router to attempt the next matched # route. class AcceptVersionHeader < Base - include VersionerHelpers - def before potential_version = env[Grape::Http::Headers::HTTP_ACCEPT_VERSION]&.strip not_acceptable!('Accept-Version header must be set.') if strict? && potential_version.blank? diff --git a/lib/grape/middleware/versioner/base.rb b/lib/grape/middleware/versioner/base.rb new file mode 100644 index 000000000..68604f14e --- /dev/null +++ b/lib/grape/middleware/versioner/base.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +module Grape + module Middleware + module Versioner + class Base < Grape::Middleware::Base + DEFAULT_PATTERN = /.*/i.freeze + DEFAULT_PARAMETER = 'apiver' + + def self.inherited(klass) + super + Versioner.register(klass) + end + + def default_options + { + versions: nil, + prefix: nil, + mount_path: nil, + pattern: DEFAULT_PATTERN, + version_options: { + strict: false, + cascade: true, + parameter: DEFAULT_PARAMETER + } + } + end + + def versions + options[:versions] + end + + def prefix + options[:prefix] + end + + def mount_path + options[:mount_path] + end + + def pattern + options[:pattern] + end + + def version_options + options[:version_options] + end + + def strict? + version_options[:strict] + end + + # By default those errors contain an `X-Cascade` header set to `pass`, which allows nesting and stacking + # of routes (see Grape::Router) for more information). To prevent + # this behavior, and not add the `X-Cascade` header, one can set the `:cascade` option to `false`. + def cascade? + version_options[:cascade] + end + + def parameter_key + version_options[:parameter] + end + + def vendor + version_options[:vendor] + end + + def error_headers + cascade? ? { Grape::Http::Headers::X_CASCADE => 'pass' } : {} + end + + def potential_version_match?(potential_version) + versions.blank? || versions.any? { |v| v.to_s == potential_version } + end + + def version_not_found! + throw :error, status: 404, message: '404 API Version Not Found', headers: { Grape::Http::Headers::X_CASCADE => 'pass' } + end + end + end + end +end diff --git a/lib/grape/middleware/versioner/header.rb b/lib/grape/middleware/versioner/header.rb index 11cbfc7bc..a34a80fc7 100644 --- a/lib/grape/middleware/versioner/header.rb +++ b/lib/grape/middleware/versioner/header.rb @@ -22,8 +22,6 @@ module Versioner # X-Cascade header to alert Grape::Router to attempt the next matched # route. class Header < Base - include VersionerHelpers - def before match_best_quality_media_type! do |media_type| env.update( diff --git a/lib/grape/middleware/versioner/param.rb b/lib/grape/middleware/versioner/param.rb index 0c8f88a47..771faf616 100644 --- a/lib/grape/middleware/versioner/param.rb +++ b/lib/grape/middleware/versioner/param.rb @@ -19,8 +19,6 @@ module Versioner # # env['api.version'] => 'v1' class Param < Base - include VersionerHelpers - def before potential_version = Rack::Utils.parse_nested_query(env[Rack::QUERY_STRING])[parameter_key] return if potential_version.blank? diff --git a/lib/grape/middleware/versioner/path.rb b/lib/grape/middleware/versioner/path.rb index c824f2df8..dd4379767 100644 --- a/lib/grape/middleware/versioner/path.rb +++ b/lib/grape/middleware/versioner/path.rb @@ -17,8 +17,6 @@ module Versioner # env['api.version'] => 'v1' # class Path < Base - include VersionerHelpers - def before path_info = Grape::Router.normalize_path(env[Rack::PATH_INFO]) return if path_info == '/' diff --git a/lib/grape/middleware/versioner_helpers.rb b/lib/grape/middleware/versioner_helpers.rb deleted file mode 100644 index 0cce2055c..000000000 --- a/lib/grape/middleware/versioner_helpers.rb +++ /dev/null @@ -1,75 +0,0 @@ -# frozen_string_literal: true - -module Grape - module Middleware - module VersionerHelpers - DEFAULT_PATTERN = /.*/i.freeze - DEFAULT_PARAMETER = 'apiver' - - def default_options - { - versions: nil, - prefix: nil, - mount_path: nil, - pattern: DEFAULT_PATTERN, - version_options: { - strict: false, - cascade: true, - parameter: DEFAULT_PARAMETER - } - } - end - - def versions - options[:versions] - end - - def prefix - options[:prefix] - end - - def mount_path - options[:mount_path] - end - - def pattern - options[:pattern] - end - - def version_options - options[:version_options] - end - - def strict? - version_options[:strict] - end - - # By default those errors contain an `X-Cascade` header set to `pass`, which allows nesting and stacking - # of routes (see Grape::Router) for more information). To prevent - # this behavior, and not add the `X-Cascade` header, one can set the `:cascade` option to `false`. - def cascade? - version_options[:cascade] - end - - def parameter_key - version_options[:parameter] - end - - def vendor - version_options[:vendor] - end - - def error_headers - cascade? ? { Grape::Http::Headers::X_CASCADE => 'pass' } : {} - end - - def potential_version_match?(potential_version) - versions.blank? || versions.any? { |v| v.to_s == potential_version } - end - - def version_not_found! - throw :error, status: 404, message: '404 API Version Not Found', headers: { Grape::Http::Headers::X_CASCADE => 'pass' } - end - end - end -end diff --git a/lib/grape/parser.rb b/lib/grape/parser.rb index a446b4da2..9dcb81ef3 100644 --- a/lib/grape/parser.rb +++ b/lib/grape/parser.rb @@ -2,16 +2,14 @@ module Grape module Parser - module_function + extend Grape::Util::Registry - DEFAULTS = { - json: Grape::Parser::Json, - jsonapi: Grape::Parser::Json, - xml: Grape::Parser::Xml - }.freeze + module_function def parser_for(format, parsers = nil) - parsers&.key?(format) ? parsers[format] : DEFAULTS[format] + return parsers[format] if parsers&.key?(format) + + registry[format] end end end diff --git a/lib/grape/parser/base.rb b/lib/grape/parser/base.rb new file mode 100644 index 000000000..56640d2e5 --- /dev/null +++ b/lib/grape/parser/base.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Grape + module Parser + class Base + def self.call(_object, _env) + raise NotImplementedError + end + + def self.inherited(klass) + super + Parser.register(klass) + end + end + end +end diff --git a/lib/grape/parser/json.rb b/lib/grape/parser/json.rb index 4e665a1ec..a808999cc 100644 --- a/lib/grape/parser/json.rb +++ b/lib/grape/parser/json.rb @@ -2,14 +2,12 @@ module Grape module Parser - module Json - class << self - 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.new('application/json') - end + class Json < Base + def self.call(object, _env) + ::Grape::Json.load(object) + rescue ::Grape::Json::ParseError + # handle JSON parsing errors via the rescue handlers or provide error message + raise Grape::Exceptions::InvalidMessageBody.new('application/json') end end end diff --git a/lib/grape/parser/jsonapi.rb b/lib/grape/parser/jsonapi.rb new file mode 100644 index 000000000..58e16571b --- /dev/null +++ b/lib/grape/parser/jsonapi.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Grape + module Parser + class Jsonapi < Json; end + end +end diff --git a/lib/grape/parser/xml.rb b/lib/grape/parser/xml.rb index 930c57f13..bdb2a485f 100644 --- a/lib/grape/parser/xml.rb +++ b/lib/grape/parser/xml.rb @@ -2,14 +2,12 @@ module Grape module Parser - module Xml - class << self - 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.new('application/xml') - end + class Xml < Base + def self.call(object, _env) + ::Grape::Xml.parse(object) + rescue ::Grape::Xml::ParseError + # handle XML parsing errors via the rescue handlers or provide error message + raise Grape::Exceptions::InvalidMessageBody.new('application/xml') end end end diff --git a/lib/grape/util/registry.rb b/lib/grape/util/registry.rb new file mode 100644 index 000000000..6980445e6 --- /dev/null +++ b/lib/grape/util/registry.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Grape + module Util + module Registry + def register(klass) + short_name = build_short_name(klass) + return if short_name.nil? + + warn "#{short_name} is already registered with class #{klass}" if registry.key?(short_name) + registry[short_name] = klass + end + + private + + def build_short_name(klass) + return if klass.name.blank? + + klass.name.demodulize.underscore + end + + def registry + @registry ||= {}.with_indifferent_access + end + end + end +end diff --git a/lib/grape/validations.rb b/lib/grape/validations.rb index 9ae22ae6a..fd33071d0 100644 --- a/lib/grape/validations.rb +++ b/lib/grape/validations.rb @@ -2,29 +2,20 @@ module Grape module Validations + extend Grape::Util::Registry + module_function - def validators - @validators ||= {} - end + def require_validator(short_name) + raise Grape::Exceptions::UnknownValidator, short_name unless registry.key?(short_name) - # Register a new validator, so it can be used to validate parameters. - # @param short_name [String] all lower-case, no spaces - # @param klass [Class] the validator class. Should inherit from - # Grape::Validations::Validators::Base. - def register_validator(short_name, klass) - validators[short_name] = klass + registry[short_name] end - def deregister_validator(short_name) - validators.delete(short_name) - end + def build_short_name(klass) + return if klass.name.blank? - def require_validator(short_name) - str_name = short_name.to_s - validators.fetch(str_name) { Grape::Validations::Validators.const_get(:"#{str_name.camelize}Validator") } - rescue NameError - raise Grape::Exceptions::UnknownValidator, short_name + klass.name.demodulize.underscore.delete_suffix('_validator') end end end diff --git a/lib/grape/validations/contract_scope.rb b/lib/grape/validations/contract_scope.rb index 3b66df572..218f47eec 100644 --- a/lib/grape/validations/contract_scope.rb +++ b/lib/grape/validations/contract_scope.rb @@ -23,46 +23,12 @@ def initialize(api, contract = nil, &block) api.namespace_stackable(:contract_key_map, key_map) validator_options = { - validator_class: Validator, + validator_class: Grape::Validations.require_validator(:contract_scope), opts: { schema: contract, fail_fast: false } } api.namespace_stackable(:validations, validator_options) end - - class Validator < Grape::Validations::Validators::Base - attr_reader :schema - - def initialize(_attrs, _options, _required, _scope, opts) - super - @schema = opts.fetch(:schema) - end - - # Validates a given request. - # @param request [Grape::Request] the request currently being handled - # @raise [Grape::Exceptions::ValidationArrayErrors] if validation failed - # @return [void] - def validate(request) - res = schema.call(request.params) - - if res.success? - request.params.deep_merge!(res.to_h) - return - end - - raise Grape::Exceptions::ValidationArrayErrors.new(build_errors_from_messages(res.errors.messages)) - end - - private - - def build_errors_from_messages(messages) - messages.map do |message| - full_name = message.path.first.to_s - full_name << "[#{message.path[1..].join('][')}]" if message.path.size > 1 - Grape::Exceptions::Validation.new(params: [full_name], message: message.text) - end - end - end end end end diff --git a/lib/grape/validations/params_scope.rb b/lib/grape/validations/params_scope.rb index 36c6ed4d3..cb9b3f43e 100644 --- a/lib/grape/validations/params_scope.rb +++ b/lib/grape/validations/params_scope.rb @@ -525,7 +525,7 @@ def derive_validator_options(validations) def validates_presence(validations, attrs, doc, opts) return unless validations.key?(:presence) && validations[:presence] - validate(:presence, validations.delete(:presence), attrs, doc, opts) + validate('presence', validations.delete(:presence), attrs, doc, opts) validations.delete(:message) if validations.key?(:message) end end diff --git a/lib/grape/validations/types/build_coercer.rb b/lib/grape/validations/types/build_coercer.rb deleted file mode 100644 index 5f15a1a0c..000000000 --- a/lib/grape/validations/types/build_coercer.rb +++ /dev/null @@ -1,92 +0,0 @@ -# frozen_string_literal: true - -module Grape - module Validations - module Types - module BuildCoercer - # Chooses the best coercer for the given type. For example, if the type - # is Integer, it will return a coercer which will be able to coerce a value - # to the integer. - # - # There are a few very special coercers which might be returned. - # - # +Grape::Types::MultipleTypeCoercer+ is a coercer which is returned when - # the given type implies values in an array with different types. - # For example, +[Integer, String]+ allows integer and string values in - # an array. - # - # +Grape::Types::CustomTypeCoercer+ is a coercer which is returned when - # a method is specified by a user with +coerce_with+ option or the user - # specifies a custom type which implements requirments of - # +Grape::Types::CustomTypeCoercer+. - # - # +Grape::Types::CustomTypeCollectionCoercer+ is a very similar to the - # previous one, but it expects an array or set of values having a custom - # type implemented by the user. - # - # There is also a group of custom types implemented by Grape, check - # +Grape::Validations::Types::SPECIAL+ to get the full list. - # - # @param type [Class] the type to which input strings - # should be coerced - # @param method [Class,#call] the coercion method to use - # @return [Object] object to be used - # for coercion and type validation - def 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, strict) - # Maps a custom type provided by Grape, it doesn't map types wrapped by collections!!! - type = Types.map_special(type) - - # Use a special coercer for multiply-typed parameters. - if Types.multiple?(type) - MultipleTypeCoercer.new(type, method) - - # Use a special coercer for custom types and coercion methods. - elsif method || Types.custom?(type) - CustomTypeCoercer.new(type, method) - - # Special coercer for collections of types that implement a parse method. - # CustomTypeCoercer (above) already handles such types when an explicit coercion - # method is supplied. - elsif Types.collection_of_custom?(type) - Types::CustomTypeCollectionCoercer.new( - Types.map_special(type.first), type.is_a?(Set) - ) - else - DryTypeCoercer.coercer_instance_for(type, strict) - end - end - - def self.cache_instance(type, method, strict, &_block) - key = cache_key(type, method, strict) - - return @__cache[key] if @__cache.key?(key) - - instance = yield - - @__cache_write_lock.synchronize do - @__cache[key] = instance - end - - instance - end - - 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, {}) - instance_variable_set(:@__cache_write_lock, Mutex.new) - end - end - end -end diff --git a/lib/grape/validations/types/dry_type_coercer.rb b/lib/grape/validations/types/dry_type_coercer.rb index 1067eaf3a..f9672198e 100644 --- a/lib/grape/validations/types/dry_type_coercer.rb +++ b/lib/grape/validations/types/dry_type_coercer.rb @@ -22,16 +22,20 @@ class << self # collection_coercer_for(Array) # #=> Grape::Validations::Types::ArrayCoercer def collection_coercer_for(type) - Grape::Validations::Types.const_get(:"#{type.name.camelize}Coercer") + case type + when Array + ArrayCoercer + when Set + SetCoercer + else + raise ArgumentError, "Unknown type: #{type}" + end end # Returns an instance of a coercer for a given type def coercer_instance_for(type, strict = false) - return PrimitiveCoercer.new(type, strict) if type.instance_of?(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) + klass = type.instance_of?(Class) ? PrimitiveCoercer : collection_coercer_for(type) + klass.new(type, strict) end end diff --git a/lib/grape/validations/validators/base.rb b/lib/grape/validations/validators/base.rb index ee2dc4a87..890963d9b 100644 --- a/lib/grape/validations/validators/base.rb +++ b/lib/grape/validations/validators/base.rb @@ -59,10 +59,7 @@ def validate!(params) def self.inherited(klass) super - return if klass.name.blank? - - short_validator_name = klass.name.demodulize.underscore.delete_suffix('_validator') - Validations.register_validator(short_validator_name, klass) + Validations.register(klass) end def message(default_key = nil) diff --git a/lib/grape/validations/validators/contract_scope_validator.rb b/lib/grape/validations/validators/contract_scope_validator.rb new file mode 100644 index 000000000..b8a3365c1 --- /dev/null +++ b/lib/grape/validations/validators/contract_scope_validator.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Grape + module Validations + module Validators + class ContractScopeValidator < Base + attr_reader :schema + + def initialize(_attrs, _options, _required, _scope, opts) + super + @schema = opts.fetch(:schema) + end + + # Validates a given request. + # @param request [Grape::Request] the request currently being handled + # @raise [Grape::Exceptions::ValidationArrayErrors] if validation failed + # @return [void] + def validate(request) + res = schema.call(request.params) + + if res.success? + request.params.deep_merge!(res.to_h) + return + end + + raise Grape::Exceptions::ValidationArrayErrors.new(build_errors_from_messages(res.errors.messages)) + end + + private + + def build_errors_from_messages(messages) + messages.map do |message| + full_name = message.path.first.to_s + full_name << "[#{message.path[1..].join('][')}]" if message.path.size > 1 + Grape::Exceptions::Validation.new(params: [full_name], message: message.text) + end + end + end + end + end +end diff --git a/spec/grape/api/custom_validations_spec.rb b/spec/grape/api/custom_validations_spec.rb index d16c307fe..6ed10527a 100644 --- a/spec/grape/api/custom_validations_spec.rb +++ b/spec/grape/api/custom_validations_spec.rb @@ -35,7 +35,14 @@ def validate_param!(attr_name, params) end let(:app) { subject } - before { stub_const('Grape::Validations::Validators::DefaultLengthValidator', default_length_validator) } + before do + stub_const('DefaultLengthValidator', default_length_validator) + described_class.register(DefaultLengthValidator) + end + + after do + described_class.deregister(:default_length) + end it 'under 140 characters' do get '/', text: 'abc' @@ -77,7 +84,14 @@ def validate(request) end let(:app) { subject } - before { stub_const('Grape::Validations::Validators::InBodyValidator', in_body_validator) } + before do + stub_const('InBodyValidator', in_body_validator) + described_class.register(InBodyValidator) + end + + after do + described_class.deregister(:in_body) + end it 'allows field in body' do get '/', text: 'abc' @@ -113,7 +127,14 @@ def validate_param!(attr_name, _params) end let(:app) { subject } - before { stub_const('Grape::Validations::Validators::WithMessageKeyValidator', message_key_validator) } + before do + stub_const('WithMessageKeyValidator', message_key_validator) + described_class.register(WithMessageKeyValidator) + end + + after do + described_class.deregister(:with_message_key) + end it 'fails with message' do get '/', text: 'foobar' @@ -159,7 +180,14 @@ def access_header let(:app) { subject } let(:x_access_token_header) { 'x-access-token' } - before { stub_const('Grape::Validations::Validators::AdminValidator', admin_validator) } + before do + stub_const('AdminValidator', admin_validator) + described_class.register(AdminValidator) + end + + after do + described_class.deregister(:admin) + end it 'fail when non-admin user sets an admin field' do get '/', admin_field: 'tester', non_admin_field: 'toaster' @@ -218,7 +246,14 @@ def validate_param!(_attr_name, _params) end end - before { stub_const('Grape::Validations::Validators::InstanceValidatorValidator', validator_type) } + before do + stub_const('InstanceValidatorValidator', validator_type) + described_class.register(InstanceValidatorValidator) + end + + after do + described_class.deregister(:instance_validator) + end it 'passes validation every time' do expect(validator_type).to receive(:new).twice.and_call_original diff --git a/spec/grape/parser_spec.rb b/spec/grape/parser_spec.rb index ecc5fdfa4..a349d61e7 100644 --- a/spec/grape/parser_spec.rb +++ b/spec/grape/parser_spec.rb @@ -3,20 +3,6 @@ describe Grape::Parser do subject { described_class } - describe 'DEFAULTS' do - subject { described_class::DEFAULTS } - - let(:expected_defaults) do - { - json: Grape::Parser::Json, - jsonapi: Grape::Parser::Json, - xml: Grape::Parser::Xml - } - end - - it { is_expected.to eq(expected_defaults) } - end - describe '.parser_for' do let(:options) { {} } diff --git a/spec/grape/validations/validators/contract_scope_validator_spec.rb b/spec/grape/validations/validators/contract_scope_validator_spec.rb new file mode 100644 index 000000000..b8d462c31 --- /dev/null +++ b/spec/grape/validations/validators/contract_scope_validator_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +describe Grape::Validations::Validators::ContractScopeValidator do + describe '.inherits' do + subject { described_class } + + it { is_expected.to be < Grape::Validations::Validators::Base } + end +end diff --git a/spec/grape/validations_spec.rb b/spec/grape/validations_spec.rb index 19dd130f0..5f983d2aa 100644 --- a/spec/grape/validations_spec.rb +++ b/spec/grape/validations_spec.rb @@ -494,7 +494,8 @@ def validate_param!(attr_name, params) end before do - stub_const('Grape::Validations::Validators::DateRangeValidator', date_range_validator) + stub_const('DateRangeValidator', date_range_validator) + described_class.register(DateRangeValidator) subject.params do optional :date_range, date_range: true, type: Hash do requires :from, type: Integer @@ -515,6 +516,10 @@ def validate_param!(attr_name, params) end end + after do + described_class.deregister(:date_range) + end + context 'which is optional' do it "doesn't throw an error if the validation passes" do get '/optional', date_range: { from: 1, to: 2 } @@ -1186,7 +1191,14 @@ def validate_param!(attr_name, params) end end - before { stub_const('Grape::Validations::Validators::CustomvalidatorValidator', custom_validator) } + before do + stub_const('CustomvalidatorValidator', custom_validator) + described_class.register(CustomvalidatorValidator) + end + + after do + described_class.deregister(:customvalidator) + end context 'when using optional with a custom validator' do before do @@ -1338,8 +1350,8 @@ def validate_param!(attr_name, params) end before do - stub_const('Grape::Validations::Validators::CustomvalidatorWithOptionsValidator', custom_validator_with_options) - + stub_const('CustomvalidatorWithOptionsValidator', custom_validator_with_options) + described_class.register(CustomvalidatorWithOptionsValidator) subject.params do optional :custom, customvalidator_with_options: { text: 'im custom with options', message: 'is not custom with options!' } end @@ -1348,6 +1360,10 @@ def validate_param!(attr_name, params) end end + after do + described_class.deregister(:customvalidator_with_options) + end + it 'validates param with custom validator with options' do get '/optional_custom', custom: 'im custom with options' expect(last_response.status).to eq(200) diff --git a/spec/integration/dry_validation/dry_validation_spec.rb b/spec/integration/dry_validation/dry_validation_spec.rb index 6333bdacd..d7b2f8efa 100644 --- a/spec/integration/dry_validation/dry_validation_spec.rb +++ b/spec/integration/dry_validation/dry_validation_spec.rb @@ -236,12 +236,4 @@ end end end - - describe Grape::Validations::ContractScope::Validator do - describe '.inherits' do - subject { described_class } - - it { is_expected.to be < Grape::Validations::Validators::Base } - end - end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 1589a0881..06cb282a0 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -13,6 +13,10 @@ end end +Grape::Util::Registry.include(Deregister) +# issue with ruby 2.7 with ^. We need to extend it again +Grape::Validations.extend(Grape::Util::Registry) if Gem::Version.new(RUBY_VERSION).release < Gem::Version.new('3.0') + # 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 diff --git a/spec/support/deregister.rb b/spec/support/deregister.rb new file mode 100644 index 000000000..f5dc7e5b2 --- /dev/null +++ b/spec/support/deregister.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Deregister + def deregister(key) + registry.delete(key) + end +end From 3559681682a2d8ddecbdd0e82ef92d13a3e51d7b Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sun, 29 Dec 2024 14:03:19 +0100 Subject: [PATCH 278/304] Add ruby 3.4 to CI (#2518) * Add ruby 3.4 to CI Fix specs * CHANGELOG.md * Add mutex_m for rails 6.1 and 7.0 gemfiles --- .github/workflows/danger.yml | 2 +- .github/workflows/edge.yml | 2 +- .github/workflows/test.yml | 4 ++-- CHANGELOG.md | 1 + gemfiles/rails_6_1.gemfile | 1 + gemfiles/rails_7_0.gemfile | 1 + spec/grape/api_spec.rb | 5 +++-- spec/grape/endpoint_spec.rb | 13 ++++++++----- spec/grape/middleware/exception_spec.rb | 3 ++- 9 files changed, 20 insertions(+), 12 deletions(-) diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml index 5e99cbf53..12372470c 100644 --- a/.github/workflows/danger.yml +++ b/.github/workflows/danger.yml @@ -12,7 +12,7 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: 2.7 + ruby-version: 3.4 bundler-cache: true - name: Run Danger run: | diff --git a/.github/workflows/edge.yml b/.github/workflows/edge.yml index c9d26044d..c73a44ad3 100644 --- a/.github/workflows/edge.yml +++ b/.github/workflows/edge.yml @@ -6,7 +6,7 @@ jobs: strategy: fail-fast: false matrix: - ruby: ['2.7', '3.0', '3.1', '3.2', '3.3', ruby-head, truffleruby-head, jruby-head] + ruby: ['2.7', '3.0', '3.1', '3.2', '3.3', '3.4', ruby-head, truffleruby-head, jruby-head] gemfile: [rails_edge, rack_edge] exclude: - ruby: '2.7' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a7710f2aa..14a663818 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: 3.3 + ruby-version: 3.4 bundler-cache: true rubygems: latest @@ -23,7 +23,7 @@ jobs: strategy: fail-fast: false matrix: - ruby: ['2.7', '3.0', '3.1', '3.2', '3.3'] + ruby: ['2.7', '3.0', '3.1', '3.2', '3.3', '3.4'] gemfile: [Gemfile, gemfiles/rack_2_0.gemfile, gemfiles/rack_3_0.gemfile, gemfiles/rack_3_1.gemfile, gemfiles/rails_6_1.gemfile, gemfiles/rails_7_0.gemfile, gemfiles/rails_7_1.gemfile, gemfiles/rails_7_2.gemfile, gemfiles/rails_8_0.gemfile] specs: ['spec --exclude-pattern=spec/integration/**/*_spec.rb'] include: diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ec8b6d64..13b1b0913 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ * [#2513](https://github.com/ruby-grape/grape/pull/2513): Optimize Grape::Path - [@ericproulx](https://github.com/ericproulx). * [#2514](https://github.com/ruby-grape/grape/pull/2514): Add rails 8.0 to CI - [@ericproulx](https://github.com/ericproulx). * [#2516](https://github.com/ruby-grape/grape/pull/2516): Dynamic registration for parsers, formatters, versioners - [@ericproulx](https://github.com/ericproulx). +* [#2518](https://github.com/ruby-grape/grape/pull/2518): Add ruby 3.4 to CI - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/gemfiles/rails_6_1.gemfile b/gemfiles/rails_6_1.gemfile index f6ae64477..edb73448c 100644 --- a/gemfiles/rails_6_1.gemfile +++ b/gemfiles/rails_6_1.gemfile @@ -2,5 +2,6 @@ eval_gemfile '../Gemfile' +gem 'mutex_m' gem 'rails', '~> 6.1' gem 'tzinfo-data', require: false diff --git a/gemfiles/rails_7_0.gemfile b/gemfiles/rails_7_0.gemfile index e9c87639d..b458a7d6f 100644 --- a/gemfiles/rails_7_0.gemfile +++ b/gemfiles/rails_7_0.gemfile @@ -2,5 +2,6 @@ eval_gemfile '../Gemfile' +gem 'mutex_m' gem 'rails', '~> 7.0.0' gem 'tzinfo-data', require: false diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index bd6398780..20193ccb3 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -2970,9 +2970,10 @@ def self.call(object, _env) subject.put :yaml do params[:tag] end - put '/yaml', 'a123', 'CONTENT_TYPE' => 'application/xml' + body = 'a123' + put '/yaml', body, 'CONTENT_TYPE' => 'application/xml' expect(last_response).to be_successful - expect(last_response.body).to eql '{"type"=>"symbol", "__content__"=>"a123"}' + expect(last_response.body).to eq(Grape::Xml.parse(body)['tag'].to_s) end end end diff --git a/spec/grape/endpoint_spec.rb b/spec/grape/endpoint_spec.rb index f51e58213..0674731fe 100644 --- a/spec/grape/endpoint_spec.rb +++ b/spec/grape/endpoint_spec.rb @@ -391,14 +391,16 @@ def app expect(last_response.body).to eq('Bobby T.') end else + let(:body) { 'Bobby T.' } + it 'converts XML bodies to params' do - post '/request_body', 'Bobby T.', 'CONTENT_TYPE' => 'application/xml' - expect(last_response.body).to eq('{"__content__"=>"Bobby T."}') + post '/request_body', body, 'CONTENT_TYPE' => 'application/xml' + expect(last_response.body).to eq(Grape::Xml.parse(body)['user'].to_s) end it 'converts XML bodies to params' do - put '/request_body', 'Bobby T.', 'CONTENT_TYPE' => 'application/xml' - expect(last_response.body).to eq('{"__content__"=>"Bobby T."}') + put '/request_body', body, 'CONTENT_TYPE' => 'application/xml' + expect(last_response.body).to eq(Grape::Xml.parse(body)['user'].to_s) end end @@ -685,7 +687,8 @@ def app if Gem::Version.new(RUBY_VERSION).release <= Gem::Version.new('3.2') %r{undefined local variable or method `undefined_helper' for # in '/hey' endpoint} else - /undefined local variable or method `undefined_helper' for/ + opening_quote = Gem::Version.new(RUBY_VERSION).release >= Gem::Version.new('3.4') ? "'" : '`' + /undefined local variable or method #{opening_quote}undefined_helper' for/ end end diff --git a/spec/grape/middleware/exception_spec.rb b/spec/grape/middleware/exception_spec.rb index b3fe18144..b209b726f 100644 --- a/spec/grape/middleware/exception_spec.rb +++ b/spec/grape/middleware/exception_spec.rb @@ -220,7 +220,8 @@ def call(_env) it 'is possible to specify a custom formatter' do get '/' - expect(last_response.body).to eq('{:custom_formatter=>"rain!"}') + response = Rack::Utils.escape_html({ custom_formatter: 'rain!' }.inspect) + expect(last_response.body).to eq(response) end end From 976ae1f306737a6326d5f91e49bfe6badd01cc25 Mon Sep 17 00:00:00 2001 From: Tran Dang Duc Dat <76593070+datpmt@users.noreply.github.com> Date: Thu, 9 Jan 2025 22:48:50 +0700 Subject: [PATCH 279/304] fixed typos in README.md (#2521) --- CHANGELOG.md | 1 + README.md | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13b1b0913..578e8e985 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ * [#2506](https://github.com/ruby-grape/grape/pull/2506): Fix fetch_formatter api_format - [@ericproulx](https://github.com/ericproulx). * [#2507](https://github.com/ruby-grape/grape/pull/2507): Fix type: Set with values - [@nikolai-b](https://github.com/nikolai-b). * [#2510](https://github.com/ruby-grape/grape/pull/2510): Fix ContractScope's validator inheritance - [@ericproulx](https://github.com/ericproulx). +* [#2521](https://github.com/ruby-grape/grape/pull/2521): Fixed typo in README - [@datpmt](https://github.com/datpmt). * Your contribution here. ### 2.2.0 (2024-09-14) diff --git a/README.md b/README.md index 0c6c56aff..c6fa674d8 100644 --- a/README.md +++ b/README.md @@ -2046,7 +2046,7 @@ end ```ruby params do requires :code, type: String, length: { is: 2, message: 'code is expected to be exactly 2 characters long' } - requires :str, type: String, length: { min: 5, message: 'str is expected to be atleast 5 characters long' } + requires :str, type: String, length: { min: 5, message: 'str is expected to be at least 5 characters long' } requires :list, type: [Integer], length: { min: 2, max: 3, message: 'list is expected to have between 2 and 3 elements' } end ``` @@ -3536,8 +3536,8 @@ Please use `Route#xyz` instead. Note that difference of `Route#options` and `Route#settings`. -The `options` can be referred from your route, it should be set by specifing key and value on verb methods such as `get`, `post` and `put`. -The `settings` can also be referred from your route, but it should be set by specifing key and value on `route_setting`. +The `options` can be referred from your route, it should be set by specifying key and value on verb methods such as `get`, `post` and `put`. +The `settings` can also be referred from your route, but it should be set by specifying key and value on `route_setting`. ## Current Route and Endpoint From 926c28dd53e3fa94a3dfd705ec689249c9818225 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Mon, 20 Jan 2025 02:37:23 +0100 Subject: [PATCH 280/304] Require logger for concurrent-ruby 1.3.5 (#2525) * require logger for concurrent-ruby 1.3.5 * Remove duplicate require 'logger' * Add CHANGELOG.md entry --- CHANGELOG.md | 1 + lib/grape.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 578e8e985..219102174 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ * [#2507](https://github.com/ruby-grape/grape/pull/2507): Fix type: Set with values - [@nikolai-b](https://github.com/nikolai-b). * [#2510](https://github.com/ruby-grape/grape/pull/2510): Fix ContractScope's validator inheritance - [@ericproulx](https://github.com/ericproulx). * [#2521](https://github.com/ruby-grape/grape/pull/2521): Fixed typo in README - [@datpmt](https://github.com/datpmt). +* [#2525](https://github.com/ruby-grape/grape/pull/2525): Require logger before active_support - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 2.2.0 (2024-09-14) diff --git a/lib/grape.rb b/lib/grape.rb index 963e37364..ed6f9058a 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require 'logger' require 'active_support' require 'active_support/concern' require 'active_support/configurable' @@ -33,7 +34,6 @@ require 'dry-types' require 'forwardable' require 'json' -require 'logger' require 'mustermann/grape' require 'pathname' require 'rack' From 5ce44def9ce7026ad140a31a146820d081ab6798 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Mon, 20 Jan 2025 14:28:38 +0100 Subject: [PATCH 281/304] Validators bad encoding (#2524) * Use scrub when possible Add specs for bad encoding. * Fix rubocop Add CHANGELOG.md entry --- CHANGELOG.md | 1 + .../versioner/accept_version_header.rb | 4 +++- .../validators/allow_blank_validator.rb | 2 +- .../validators/regexp_validator.rb | 2 +- .../validators/values_validator.rb | 2 ++ .../versioner/accept_version_header_spec.rb | 12 ++++++++++++ .../validators/allow_blank_validator_spec.rb | 19 +++++++++++++++++++ .../validators/regexp_validator_spec.rb | 19 +++++++++++++++++++ .../validators/values_validator_spec.rb | 19 +++++++++++++++++++ 9 files changed, 77 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 219102174..c88b65f80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ * [#2510](https://github.com/ruby-grape/grape/pull/2510): Fix ContractScope's validator inheritance - [@ericproulx](https://github.com/ericproulx). * [#2521](https://github.com/ruby-grape/grape/pull/2521): Fixed typo in README - [@datpmt](https://github.com/datpmt). * [#2525](https://github.com/ruby-grape/grape/pull/2525): Require logger before active_support - [@ericproulx](https://github.com/ericproulx). +* [#2524](https://github.com/ruby-grape/grape/pull/2524): Fix validators bad encoding - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 2.2.0 (2024-09-14) diff --git a/lib/grape/middleware/versioner/accept_version_header.rb b/lib/grape/middleware/versioner/accept_version_header.rb index ff76a7372..aa7a8b353 100644 --- a/lib/grape/middleware/versioner/accept_version_header.rb +++ b/lib/grape/middleware/versioner/accept_version_header.rb @@ -18,7 +18,9 @@ module Versioner # route. class AcceptVersionHeader < Base def before - potential_version = env[Grape::Http::Headers::HTTP_ACCEPT_VERSION]&.strip + potential_version = env[Grape::Http::Headers::HTTP_ACCEPT_VERSION] + potential_version = potential_version.scrub unless potential_version.nil? + not_acceptable!('Accept-Version header must be set.') if strict? && potential_version.blank? return if potential_version.blank? diff --git a/lib/grape/validations/validators/allow_blank_validator.rb b/lib/grape/validations/validators/allow_blank_validator.rb index c35753ed3..b9954c1d8 100644 --- a/lib/grape/validations/validators/allow_blank_validator.rb +++ b/lib/grape/validations/validators/allow_blank_validator.rb @@ -8,7 +8,7 @@ def validate_param!(attr_name, params) return if (options_key?(:value) ? @option[:value] : @option) || !params.is_a?(Hash) value = params[attr_name] - value = value.strip if value.respond_to?(:strip) + value = value.scrub if value.respond_to?(:scrub) return if value == false || value.present? diff --git a/lib/grape/validations/validators/regexp_validator.rb b/lib/grape/validations/validators/regexp_validator.rb index ce7af87b6..86d3bbe0c 100644 --- a/lib/grape/validations/validators/regexp_validator.rb +++ b/lib/grape/validations/validators/regexp_validator.rb @@ -6,7 +6,7 @@ module Validators 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.match?((options_key?(:value) ? @option[:value] : @option)) } + return if Array.wrap(params[attr_name]).all? { |param| param.nil? || param.to_s.scrub.match?((options_key?(:value) ? @option[:value] : @option)) } raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:regexp)) end diff --git a/lib/grape/validations/validators/values_validator.rb b/lib/grape/validations/validators/values_validator.rb index 30cdeee7b..11f314a57 100644 --- a/lib/grape/validations/validators/values_validator.rb +++ b/lib/grape/validations/validators/values_validator.rb @@ -16,6 +16,8 @@ def validate_param!(attr_name, params) return if val.nil? && !required_for_root_scope? + val = val.scrub if val.respond_to?(:scrub) + # don't forget that +false.blank?+ is true return if val != false && val.blank? && @allow_blank diff --git a/spec/grape/middleware/versioner/accept_version_header_spec.rb b/spec/grape/middleware/versioner/accept_version_header_spec.rb index 6bcf7b14a..491abe6f9 100644 --- a/spec/grape/middleware/versioner/accept_version_header_spec.rb +++ b/spec/grape/middleware/versioner/accept_version_header_spec.rb @@ -13,6 +13,18 @@ } end + describe '#bad encoding' do + before do + @options[:versions] = %w[v1] + end + + it 'does not raise an error' do + expect do + subject.call(Grape::Http::Headers::HTTP_ACCEPT_VERSION => "\x80") + end.to throw_symbol(:error, status: 406, headers: { Grape::Http::Headers::X_CASCADE => 'pass' }, message: 'The requested version is not supported.') + end + end + context 'api.version' do before do @options[:versions] = ['v1'] diff --git a/spec/grape/validations/validators/allow_blank_validator_spec.rb b/spec/grape/validations/validators/allow_blank_validator_spec.rb index a699bdff9..88c527508 100644 --- a/spec/grape/validations/validators/allow_blank_validator_spec.rb +++ b/spec/grape/validations/validators/allow_blank_validator_spec.rb @@ -244,6 +244,25 @@ end end + describe 'bad encoding' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + requires :name, type: String, allow_blank: false + end + get '/bad_encoding' + end + end + + context 'when value has bad encoding' do + it 'does not raise an error' do + expect { get('/bad_encoding', { name: "Hello \x80" }) }.not_to raise_error + end + end + end + context 'invalid input' do it 'refuses empty string' do get '/disallow_blank', name: '' diff --git a/spec/grape/validations/validators/regexp_validator_spec.rb b/spec/grape/validations/validators/regexp_validator_spec.rb index 98073b86e..c12e8a60a 100644 --- a/spec/grape/validations/validators/regexp_validator_spec.rb +++ b/spec/grape/validations/validators/regexp_validator_spec.rb @@ -41,6 +41,25 @@ end end + describe '#bad encoding' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + requires :name, regexp: { value: /^[a-z]+$/ } + end + get '/bad_encoding' + end + end + + context 'when value as bad encoding' do + it 'does not raise an error' do + expect { get '/bad_encoding', name: "Hello \x80" }.not_to raise_error + end + end + end + context 'custom validation message' do context 'with invalid input' do it 'refuses inapppopriate' do diff --git a/spec/grape/validations/validators/values_validator_spec.rb b/spec/grape/validations/validators/values_validator_spec.rb index f78f63348..7ef60a58e 100644 --- a/spec/grape/validations/validators/values_validator_spec.rb +++ b/spec/grape/validations/validators/values_validator_spec.rb @@ -277,6 +277,25 @@ def default_excepts stub_const('ValuesModel', values_model) end + describe '#bad encoding' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + requires :type, type: String, values: %w[a b] + end + get '/bad_encoding' + end + end + + context 'when value as bad encoding' do + it 'does not raise an error' do + expect { get '/bad_encoding', type: "Hello \x80" }.not_to raise_error + end + end + end + context 'with a custom validation message' do it 'allows a valid value for a parameter' do get('/custom_message', type: 'valid-type1') From 2e0d33b54467c17ef7eba16fff43512afb7a76cc Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sat, 1 Feb 2025 23:02:06 +0100 Subject: [PATCH 282/304] Force endpoint status in error_response and error! (#2530) * Force endpoint status in error_response and error! Add specs * Add CHANGELOG.md --- CHANGELOG.md | 1 + lib/grape/middleware/error.rb | 2 ++ spec/grape/endpoint_spec.rb | 52 +++++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c88b65f80..40f3361e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ * [#2521](https://github.com/ruby-grape/grape/pull/2521): Fixed typo in README - [@datpmt](https://github.com/datpmt). * [#2525](https://github.com/ruby-grape/grape/pull/2525): Require logger before active_support - [@ericproulx](https://github.com/ericproulx). * [#2524](https://github.com/ruby-grape/grape/pull/2524): Fix validators bad encoding - [@ericproulx](https://github.com/ericproulx). +* [#2530](https://github.com/ruby-grape/grape/pull/2530): Fix endpoint's status when rescue_from without a block - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 2.2.0 (2024-09-14) diff --git a/lib/grape/middleware/error.rb b/lib/grape/middleware/error.rb index f201ee8ef..2dd39bc16 100644 --- a/lib/grape/middleware/error.rb +++ b/lib/grape/middleware/error.rb @@ -65,6 +65,7 @@ def find_handler(klass) def error_response(error = {}) status = error[:status] || options[:default_status] + env[Grape::Env::API_ENDPOINT].status(status) # error! may not have been called message = error[:message] || options[:default_message] headers = { Rack::CONTENT_TYPE => content_type }.tap do |h| h.merge!(error[:headers]) if error[:headers].is_a?(Hash) @@ -130,6 +131,7 @@ def run_rescue_handler(handler, error, endpoint) end def error!(message, status = options[:default_status], headers = {}, backtrace = [], original_exception = nil) + env[Grape::Env::API_ENDPOINT].status(status) # not error! inside route rack_response( status, headers.reverse_merge(Rack::CONTENT_TYPE => content_type), format_message(message, backtrace, original_exception) diff --git a/spec/grape/endpoint_spec.rb b/spec/grape/endpoint_spec.rb index 0674731fe..bcdf5f62e 100644 --- a/spec/grape/endpoint_spec.rb +++ b/spec/grape/endpoint_spec.rb @@ -115,6 +115,58 @@ def app expect(memoized_status).to eq(201) expect(last_response.body).to eq('Hello') end + + context 'when rescue_from' do + subject { last_request.env[Grape::Env::API_ENDPOINT].status } + + before do + post '/' + end + + context 'when :all blockless' do + context 'when default_error_status is not set' do + let(:app) do + Class.new(Grape::API) do + rescue_from :all + + post { raise StandardError } + end + end + + it { is_expected.to eq(last_response.status) } + end + + context 'when default_error_status is set' do + let(:app) do + Class.new(Grape::API) do + default_error_status 418 + rescue_from :all + + post { raise StandardError } + end + end + + it { is_expected.to eq(last_response.status) } + end + end + + context 'when :with' do + let(:app) do + Class.new(Grape::API) do + helpers do + def handle_argument_error + error!("I'm a teapot!", 418) + end + end + rescue_from ArgumentError, with: :handle_argument_error + + post { raise ArgumentError } + end + end + + it { is_expected.to eq(last_response.status) } + end + end end describe '#header' do From af1a6a2384a4d7403323b77a4424d89379c8b59e Mon Sep 17 00:00:00 2001 From: Kuntz Thomas Date: Sun, 2 Feb 2025 21:49:28 +0100 Subject: [PATCH 283/304] Change Grape::API's `@setup` var to an Array (from a Set) (#2529) --- CHANGELOG.md | 1 + lib/grape/api.rb | 2 +- spec/grape/api_remount_spec.rb | 60 ++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40f3361e0..f7b2472c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ * [#2525](https://github.com/ruby-grape/grape/pull/2525): Require logger before active_support - [@ericproulx](https://github.com/ericproulx). * [#2524](https://github.com/ruby-grape/grape/pull/2524): Fix validators bad encoding - [@ericproulx](https://github.com/ericproulx). * [#2530](https://github.com/ruby-grape/grape/pull/2530): Fix endpoint's status when rescue_from without a block - [@ericproulx](https://github.com/ericproulx). +* [#2529](https://github.com/ruby-grape/grape/pull/2529): Fix missing settings on mounted routes (when settings are identical) - [@Haerezis](https://github.com/Haerezis). * Your contribution here. ### 2.2.0 (2024-09-14) diff --git a/lib/grape/api.rb b/lib/grape/api.rb index 8ce789c25..54a4ca894 100644 --- a/lib/grape/api.rb +++ b/lib/grape/api.rb @@ -40,7 +40,7 @@ def inherited(api) # 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 + @setup = [] @base_parent = base_instance_parent @base_instance = mount_instance end diff --git a/spec/grape/api_remount_spec.rb b/spec/grape/api_remount_spec.rb index 793d49d9f..ed5326b82 100644 --- a/spec/grape/api_remount_spec.rb +++ b/spec/grape/api_remount_spec.rb @@ -505,5 +505,65 @@ def printed_response end end end + + context 'with route settings' do + before do + a_remounted_api.desc 'Identical description' + a_remounted_api.route_setting :custom, key: 'value' + a_remounted_api.route_setting :custom_diff, key: 'foo' + a_remounted_api.get '/api1' do + status 200 + end + + a_remounted_api.desc 'Identical description' + a_remounted_api.route_setting :custom, key: 'value' + a_remounted_api.route_setting :custom_diff, key: 'bar' + a_remounted_api.get '/api2' do + status 200 + end + end + + it 'has all the settings for both routes' do + expect(a_remounted_api.routes.count).to be(2) + expect(a_remounted_api.routes[0].settings).to include( + { + description: { description: 'Identical description' }, + custom: { key: 'value' }, + custom_diff: { key: 'foo' } + } + ) + expect(a_remounted_api.routes[1].settings).to include( + { + description: { description: 'Identical description' }, + custom: { key: 'value' }, + custom_diff: { key: 'bar' } + } + ) + end + + context 'when mounting it' do + before do + root_api.mount a_remounted_api + end + + it 'still has all the settings for both routes' do + expect(root_api.routes.count).to be(2) + expect(root_api.routes[0].settings).to include( + { + description: { description: 'Identical description' }, + custom: { key: 'value' }, + custom_diff: { key: 'foo' } + } + ) + expect(root_api.routes[1].settings).to include( + { + description: { description: 'Identical description' }, + custom: { key: 'value' }, + custom_diff: { key: 'bar' } + } + ) + end + end + end end end From 68d65c92f3ecdeda9fd28ef515188902be764a29 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sat, 8 Feb 2025 14:26:21 +0100 Subject: [PATCH 284/304] Preparing for release, 2.3.0 --- CHANGELOG.md | 4 +--- README.md | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7b2472c0..22e85d516 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -### 2.3.0 (Next) +### 2.3.0 (2025-02-08) #### Features @@ -11,7 +11,6 @@ * [#2514](https://github.com/ruby-grape/grape/pull/2514): Add rails 8.0 to CI - [@ericproulx](https://github.com/ericproulx). * [#2516](https://github.com/ruby-grape/grape/pull/2516): Dynamic registration for parsers, formatters, versioners - [@ericproulx](https://github.com/ericproulx). * [#2518](https://github.com/ruby-grape/grape/pull/2518): Add ruby 3.4 to CI - [@ericproulx](https://github.com/ericproulx). -* Your contribution here. #### Fixes @@ -24,7 +23,6 @@ * [#2524](https://github.com/ruby-grape/grape/pull/2524): Fix validators bad encoding - [@ericproulx](https://github.com/ericproulx). * [#2530](https://github.com/ruby-grape/grape/pull/2530): Fix endpoint's status when rescue_from without a block - [@ericproulx](https://github.com/ericproulx). * [#2529](https://github.com/ruby-grape/grape/pull/2529): Fix missing settings on mounted routes (when settings are identical) - [@Haerezis](https://github.com/Haerezis). -* Your contribution here. ### 2.2.0 (2024-09-14) diff --git a/README.md b/README.md index c6fa674d8..76be92807 100644 --- a/README.md +++ b/README.md @@ -157,8 +157,8 @@ Grape is a REST-like API framework for Ruby. It's designed to run on Rack or com ## Stable Release -You're reading the documentation for the next release of Grape, which should be 2.3.0. -The current stable release is [2.2.0](https://github.com/ruby-grape/grape/blob/v2.2.0/README.md). +You're reading the documentation for the next release of Grape, 2.3.0. +Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. ## Project Resources From b4abb95ea36c29851a4beeb9a451ffcc0a981e4c Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sat, 8 Feb 2025 14:37:02 +0100 Subject: [PATCH 285/304] Preparing for next development iteration, 2.4.0. --- CHANGELOG.md | 10 ++++++++++ README.md | 4 ++-- lib/grape/version.rb | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22e85d516..0dd38098d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +### 2.4.0 (Next) + +#### Features + +* Your contribution here. + +#### Fixes + +* Your contribution here. + ### 2.3.0 (2025-02-08) #### Features diff --git a/README.md b/README.md index 76be92807..3597a0206 100644 --- a/README.md +++ b/README.md @@ -157,8 +157,8 @@ Grape is a REST-like API framework for Ruby. It's designed to run on Rack or com ## Stable Release -You're reading the documentation for the next release of Grape, 2.3.0. -Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. +You're reading the documentation for the next release of Grape, which should be 2.4.0. +The current stable release is [2.3.0](https://github.com/ruby-grape/grape/blob/v2.3.0/README.md). ## Project Resources diff --git a/lib/grape/version.rb b/lib/grape/version.rb index 246308b4a..72bbbe335 100644 --- a/lib/grape/version.rb +++ b/lib/grape/version.rb @@ -2,5 +2,5 @@ module Grape # The current version of Grape. - VERSION = '2.3.0' + VERSION = '2.4.0' end From 5ebbdd36513aa0c88a81a7bf5c73db27242ec02d Mon Sep 17 00:00:00 2001 From: Stuart Chinery Date: Tue, 11 Feb 2025 18:04:49 +0000 Subject: [PATCH 286/304] Added Upgrading to >= 2.4.0 section with note about Custom Validators --- UPGRADING.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/UPGRADING.md b/UPGRADING.md index 0c6c54f6e..708e29252 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,6 +1,14 @@ Upgrading Grape =============== +### Upgrading to >= 2.4.0 + +#### Custom Validators + +If you now receive an error of `'Grape::Validations.require_validator': unknown validator: your_custom_validation (Grape::Exceptions::UnknownValidator)` after upgrading to 2.4.0 then you will need to ensure that you require the `your_custom_validation` file before your Grape API code is loaded. + +See [2533](https://github.com/ruby-grape/grape/issues/2533) for more information. + ### Upgrading to >= 2.3.0 ### `content_type` vs `api.format` inside API From 430a45d50c7f4b83fe2b5ee45c333e1566546383 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Thu, 13 Feb 2025 00:55:37 +0100 Subject: [PATCH 287/304] Update rubocop and its dependencies (#2532) * Update rubocop and its dependencies Apply fixes * Add CHANGELOG.md --- .rubocop_todo.yml | 4 ++-- CHANGELOG.md | 1 + Gemfile | 6 +++--- lib/grape/cookies.rb | 2 -- lib/grape/dsl/helpers.rb | 2 +- lib/grape/dsl/inside_route.rb | 4 ++-- 6 files changed, 9 insertions(+), 10 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 11eb9f51e..b779d24c4 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2024-10-06 16:00:59 UTC using RuboCop version 1.66.1. +# on 2025-02-08 13:42:40 UTC using RuboCop version 1.71.2. # 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 @@ -60,7 +60,7 @@ RSpec/IndexedLet: - 'spec/grape/presenters/presenter_spec.rb' - 'spec/shared/versioning_examples.rb' -# Offense count: 38 +# Offense count: 39 # Configuration parameters: AssignmentOnly. RSpec/InstanceVariable: Exclude: diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dd38098d..d5614ce06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ #### Features +* [#2532](https://github.com/ruby-grape/grape/pull/2532): Update RuboCop 1.71.2 - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/Gemfile b/Gemfile index 17972e469..ae3753cd9 100644 --- a/Gemfile +++ b/Gemfile @@ -10,9 +10,9 @@ group :development, :test do gem 'builder', require: false gem 'bundler' gem 'rake' - gem 'rubocop', '1.66.1', require: false - gem 'rubocop-performance', '1.21.1', require: false - gem 'rubocop-rspec', '3.0.5', require: false + gem 'rubocop', '1.71.2', require: false + gem 'rubocop-performance', '1.23.1', require: false + gem 'rubocop-rspec', '3.4.0', require: false end group :development do diff --git a/lib/grape/cookies.rb b/lib/grape/cookies.rb index 7afdb67c2..102b09047 100644 --- a/lib/grape/cookies.rb +++ b/lib/grape/cookies.rb @@ -34,11 +34,9 @@ def each(&block) end # see https://github.com/rack/rack/blob/main/lib/rack/utils.rb#L338-L340 - # rubocop:disable Layout/SpaceBeforeBrackets def delete(name, **opts) options = opts.merge(max_age: '0', value: '', expires: Time.at(0)) self.[]=(name, options) end - # rubocop:enable Layout/SpaceBeforeBrackets end end diff --git a/lib/grape/dsl/helpers.rb b/lib/grape/dsl/helpers.rb index 180712282..875fee511 100644 --- a/lib/grape/dsl/helpers.rb +++ b/lib/grape/dsl/helpers.rb @@ -98,7 +98,7 @@ def api_changed(new_api) protected def process_named_params - return unless instance_variable_defined?(:@named_params) && @named_params && @named_params.any? + return if @named_params.blank? api.namespace_stackable(:named_params, @named_params) end diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index c88f7867a..b772654be 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -107,7 +107,7 @@ def handle_passed_param(params_nested_path, has_passed_children = false, &_block if type == 'Hash' && !has_children {} - elsif type == 'Array' || (type&.start_with?('[') && type&.exclude?(',')) + elsif type == 'Array' || (type&.start_with?('[') && type.exclude?(',')) [] elsif type == 'Set' || type&.start_with?('# Date: Tue, 18 Feb 2025 03:02:09 +0100 Subject: [PATCH 288/304] Delegates calls to inner object is some classes by extends Forwardable (#2535) * Add forwardable to api.rb and delegate functions to base_instance and instance_for_rack Use `delegate_missing_to` instead of method_missing Add forwardable in endpoint.rb. Delegate `params` and `headers` to `request` Remove LazyObject for build_headers since its not forced in endpoint.rb anymore. Small refactor to request.rb Use `each_header` from Rack::Request instead of `env.each_pair` Remove `.to_s` since Ruby 2.7 added `start_with?` to symbols Add forwardable in stack.rb. * Add CHANGELOG.md --- CHANGELOG.md | 1 + lib/grape/api.rb | 44 ++++++++--------------------------- lib/grape/endpoint.rb | 7 +++--- lib/grape/middleware/stack.rb | 29 +++++++---------------- lib/grape/request.rb | 15 ++++-------- 5 files changed, 28 insertions(+), 68 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5614ce06..8490e9cb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ #### Features * [#2532](https://github.com/ruby-grape/grape/pull/2532): Update RuboCop 1.71.2 - [@ericproulx](https://github.com/ericproulx). +* [#2535](https://github.com/ruby-grape/grape/pull/2535): Delegates calls to inner objects - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/lib/grape/api.rb b/lib/grape/api.rb index 54a4ca894..4f7e97336 100644 --- a/lib/grape/api.rb +++ b/lib/grape/api.rb @@ -20,12 +20,18 @@ class Instance end class << self + extend Forwardable attr_accessor :base_instance, :instances - # Rather than initializing an object of type Grape::API, create an object of type Instance - def new(...) - base_instance.new(...) - end + delegate_missing_to :base_instance + def_delegators :base_instance, :new, :configuration + + # 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. + # NOTE: This will only be called on an API directly mounted on RACK + def_delegators :instance_for_rack, :call, :compile! # 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 @@ -69,15 +75,6 @@ def configure 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. - # NOTE: This will only be called on an API directly mounted on RACK - def call(...) - instance_for_rack.call(...) - end - # The remountable class can have a configuration hash to provide some dynamic class-level variables. # For instance, a description could be done using: `desc configuration[:description]` if it may vary # depending on where the endpoint is mounted. Use with care, if you find yourself using configuration @@ -98,27 +95,6 @@ def replay_setup_on(instance) end end - def respond_to?(method, include_private = false) - super || base_instance.respond_to?(method, include_private) - end - - def respond_to_missing?(method, include_private = false) - base_instance.respond_to?(method, include_private) - end - - 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 - super - end - end - - def compile! - instance_for_rack.compile! # See API::Instance.compile! - end - private def instance_for_rack diff --git a/lib/grape/endpoint.rb b/lib/grape/endpoint.rb index 4fb0594d9..23079b226 100644 --- a/lib/grape/endpoint.rb +++ b/lib/grape/endpoint.rb @@ -6,11 +6,14 @@ module Grape # on the instance level of this class may be called # from inside a `get`, `post`, etc. class Endpoint + extend Forwardable include Grape::DSL::Settings include Grape::DSL::InsideRoute attr_accessor :block, :source, :options - attr_reader :env, :request, :headers, :params + attr_reader :env, :request + + def_delegators :request, :params, :headers class << self def new(...) @@ -247,8 +250,6 @@ def run ActiveSupport::Notifications.instrument('endpoint_run.grape', endpoint: self, env: env) do @header = Grape::Util::Header.new @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) diff --git a/lib/grape/middleware/stack.rb b/lib/grape/middleware/stack.rb index 8e25af385..63dc035f6 100644 --- a/lib/grape/middleware/stack.rb +++ b/lib/grape/middleware/stack.rb @@ -5,19 +5,20 @@ module Middleware # Class to handle the stack of middlewares based on ActionDispatch::MiddlewareStack # It allows to insert and insert after class Stack + extend Forwardable class Middleware + extend Forwardable + attr_reader :args, :block, :klass + def_delegators :klass, :name + def initialize(klass, *args, &block) @klass = klass @args = args @block = block end - def name - klass.name - end - def ==(other) case other when Middleware @@ -32,7 +33,7 @@ def inspect end def use_in(builder) - builder.use(@klass, *@args, &@block) + builder.use(klass, *args, &block) end end @@ -40,27 +41,13 @@ def use_in(builder) attr_accessor :middlewares, :others + def_delegators :middlewares, :each, :size, :last, :[] + def initialize @middlewares = [] @others = [] end - def each(&block) - @middlewares.each(&block) - end - - def size - middlewares.size - end - - def last - middlewares.last - end - - def [](index) - middlewares[index] - end - def insert(index, *args, &block) index = assert_index(index, :before) middleware = self.class::Middleware.new(*args, &block) diff --git a/lib/grape/request.rb b/lib/grape/request.rb index b2a62053e..9bbf13a53 100644 --- a/lib/grape/request.rb +++ b/lib/grape/request.rb @@ -26,21 +26,16 @@ def headers private def grape_routing_args - args = env[Grape::Env::GRAPE_ROUTING_ARGS].dup # preserve version from query string parameters - args.delete(:version) - args.delete(:route_info) - args + env[Grape::Env::GRAPE_ROUTING_ARGS].except(:version, :route_info) end def build_headers - Grape::Util::Lazy::Object.new do - env.each_pair.with_object(Grape::Util::Header.new) do |(k, v), headers| - next unless k.to_s.start_with? HTTP_PREFIX + each_header.with_object(Grape::Util::Header.new) do |(k, v), headers| + next unless k.start_with? HTTP_PREFIX - transformed_header = Grape::Http::Headers::HTTP_HEADERS[k] || transform_header(k) - headers[transformed_header] = v - end + transformed_header = Grape::Http::Headers::HTTP_HEADERS[k] || transform_header(k) + headers[transformed_header] = v end end From 48ebc255c3f105aa2d2d77dce82bc2567d951f39 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Tue, 18 Feb 2025 03:08:50 +0100 Subject: [PATCH 289/304] Use ActiveSupport `try` pattern (#2537) * Replace several `if .. respond_to?` by simple `try` * Add CHANGELOG.md --------- Co-authored-by: Daniel (dB.) Doubrovkine --- CHANGELOG.md | 1 + lib/grape.rb | 1 + lib/grape/api.rb | 4 ++-- lib/grape/api/instance.rb | 2 +- lib/grape/endpoint.rb | 6 +++--- lib/grape/middleware/formatter.rb | 8 ++------ lib/grape/router.rb | 2 +- lib/grape/validations/validators/base.rb | 4 ++-- .../validations/validators/except_values_validator.rb | 2 +- lib/grape/validations/validators/presence_validator.rb | 2 +- lib/grape/validations/validators/regexp_validator.rb | 2 +- spec/support/chunked_response.rb | 2 +- 12 files changed, 17 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8490e9cb3..ce9cd11fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * [#2532](https://github.com/ruby-grape/grape/pull/2532): Update RuboCop 1.71.2 - [@ericproulx](https://github.com/ericproulx). * [#2535](https://github.com/ruby-grape/grape/pull/2535): Delegates calls to inner objects - [@ericproulx](https://github.com/ericproulx). +* [#2537](https://github.com/ruby-grape/grape/pull/2537): Use activesupport `try` pattern - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/lib/grape.rb b/lib/grape.rb index ed6f9058a..58b9701be 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -20,6 +20,7 @@ require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/deep_dup' +require 'active_support/core_ext/object/try' require 'active_support/core_ext/object/duplicable' require 'active_support/core_ext/string/output_safety' require 'active_support/core_ext/string/exclude' diff --git a/lib/grape/api.rb b/lib/grape/api.rb index 4f7e97336..24d401745 100644 --- a/lib/grape/api.rb +++ b/lib/grape/api.rb @@ -148,12 +148,12 @@ def skip_immediate_run?(instance, args) end def any_lazy?(args) - args.any? { |argument| argument.respond_to?(:lazy?) && argument.lazy? } + args.any? { |argument| argument.try(:lazy?) } end def evaluate_arguments(configuration, *args) args.map do |argument| - if argument.respond_to?(:lazy?) && argument.lazy? + if argument.try(:lazy?) argument.evaluate_from(configuration) elsif argument.is_a?(Hash) argument.transform_values { |value| evaluate_arguments(configuration, value).first } diff --git a/lib/grape/api/instance.rb b/lib/grape/api/instance.rb index c6a608713..facd0f9f9 100644 --- a/lib/grape/api/instance.rb +++ b/lib/grape/api/instance.rb @@ -112,7 +112,7 @@ def nest(*blocks, &block) def evaluate_as_instance_with_configuration(block, lazy: false) lazy_block = Grape::Util::Lazy::Block.new do |configuration| value_for_configuration = configuration - self.configuration = value_for_configuration.evaluate if value_for_configuration.respond_to?(:lazy?) && value_for_configuration.lazy? + self.configuration = value_for_configuration.evaluate if value_for_configuration.try(:lazy?) response = instance_eval(&block) self.configuration = value_for_configuration response diff --git a/lib/grape/endpoint.rb b/lib/grape/endpoint.rb index 23079b226..d7f60e9fe 100644 --- a/lib/grape/endpoint.rb +++ b/lib/grape/endpoint.rb @@ -33,7 +33,7 @@ def before_each(new_setup = false, &block) def run_before_each(endpoint) superclass.run_before_each(endpoint) unless self == Endpoint - before_each.each { |blk| blk.call(endpoint) if blk.respond_to?(:call) } + before_each.each { |blk| blk.try(:call, endpoint) } end # @api private @@ -138,7 +138,7 @@ def method_name end def routes - @routes ||= endpoints ? endpoints.collect(&:routes).flatten : to_routes + @routes ||= endpoints&.collect(&:routes)&.flatten || to_routes end def reset_routes! @@ -228,7 +228,7 @@ def call!(env) # Return the collection of endpoints within this endpoint. # This is the case when an Grape::API mounts another Grape::API. def endpoints - options[:app].endpoints if options[:app].respond_to?(:endpoints) + @endpoints ||= options[:app].try(:endpoints) end def equals?(endpoint) diff --git a/lib/grape/middleware/formatter.rb b/lib/grape/middleware/formatter.rb index 5cb761463..88a52afa5 100644 --- a/lib/grape/middleware/formatter.rb +++ b/lib/grape/middleware/formatter.rb @@ -83,12 +83,12 @@ def read_body_input return unless (input = env[Rack::RACK_INPUT]) - rewind_input input + input.try(:rewind) body = env[Grape::Env::API_REQUEST_INPUT] = input.read begin read_rack_input(body) if body && !body.empty? ensure - rewind_input input + input.try(:rewind) end end @@ -173,10 +173,6 @@ def mime_array .sort_by { |_, quality_preference| -(quality_preference ? quality_preference.to_f : 1.0) } .flat_map { |mime, _| [mime, mime.sub(vendor_prefix_pattern, '')] } end - - def rewind_input(input) - input.rewind if input.respond_to?(:rewind) - end end end end diff --git a/lib/grape/router.rb b/lib/grape/router.rb index 6889b4213..1b79324ae 100644 --- a/lib/grape/router.rb +++ b/lib/grape/router.rb @@ -93,7 +93,7 @@ def transaction(env) return response unless cascade # we need to close the body if possible before dismissing - response[2].close if response[2].respond_to?(:close) + response[2].try(:close) end end end diff --git a/lib/grape/validations/validators/base.rb b/lib/grape/validations/validators/base.rb index 890963d9b..beaba3502 100644 --- a/lib/grape/validations/validators/base.rb +++ b/lib/grape/validations/validators/base.rb @@ -49,7 +49,7 @@ def validate!(params) next if !@scope.required? && empty_val next unless @scope.meets_dependency?(val, params) - validate_param!(attr_name, val) if @required || (val.respond_to?(:key?) && val.key?(attr_name)) + validate_param!(attr_name, val) if @required || val.try(:key?, attr_name) rescue Grape::Exceptions::Validation => e array_errors << e end @@ -69,7 +69,7 @@ def message(default_key = nil) def options_key?(key, options = nil) options = instance_variable_get(:@option) if options.nil? - options.respond_to?(:key?) && options.key?(key) && !options[key].nil? + options.try(:key?, key) && !options[key].nil? end def fail_fast? diff --git a/lib/grape/validations/validators/except_values_validator.rb b/lib/grape/validations/validators/except_values_validator.rb index 298eb0ab9..980226c1d 100644 --- a/lib/grape/validations/validators/except_values_validator.rb +++ b/lib/grape/validations/validators/except_values_validator.rb @@ -10,7 +10,7 @@ def initialize(attrs, options, required, scope, opts) end def validate_param!(attr_name, params) - return unless params.respond_to?(:key?) && params.key?(attr_name) + return unless params.try(:key?, attr_name) excepts = @except.is_a?(Proc) ? @except.call : @except return if excepts.nil? diff --git a/lib/grape/validations/validators/presence_validator.rb b/lib/grape/validations/validators/presence_validator.rb index ae31dc3fb..5961aa172 100644 --- a/lib/grape/validations/validators/presence_validator.rb +++ b/lib/grape/validations/validators/presence_validator.rb @@ -5,7 +5,7 @@ module Validations module Validators class PresenceValidator < Base def validate_param!(attr_name, params) - return if params.respond_to?(:key?) && params.key?(attr_name) + return if params.try(:key?, attr_name) raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:presence)) end diff --git a/lib/grape/validations/validators/regexp_validator.rb b/lib/grape/validations/validators/regexp_validator.rb index 86d3bbe0c..7b9b2864f 100644 --- a/lib/grape/validations/validators/regexp_validator.rb +++ b/lib/grape/validations/validators/regexp_validator.rb @@ -5,7 +5,7 @@ module Validations module Validators class RegexpValidator < Base def validate_param!(attr_name, params) - return unless params.respond_to?(:key?) && params.key?(attr_name) + return unless params.try(:key?, attr_name) return if Array.wrap(params[attr_name]).all? { |param| param.nil? || param.to_s.scrub.match?((options_key?(:value) ? @option[:value] : @option)) } raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:regexp)) diff --git a/spec/support/chunked_response.rb b/spec/support/chunked_response.rb index 4e118d1dc..c74f18f09 100644 --- a/spec/support/chunked_response.rb +++ b/spec/support/chunked_response.rb @@ -29,7 +29,7 @@ def each(&block) # Close the response body if the response body supports it. def close - @body.close if @body.respond_to?(:close) + @body.try(:close) end private From 6e6958f35e6f502cd50e8590aeb501f4b78ae1fd Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Wed, 19 Feb 2025 15:23:35 +0100 Subject: [PATCH 290/304] Update normalize_path like Rails (#2536) * Update normalize_path like rails * Fix cop * Add CHANGELOG.md * Add specs and comment --- CHANGELOG.md | 1 + lib/grape/router.rb | 24 ++++++++++++++--- spec/grape/router_spec.rb | 57 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 spec/grape/router_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index ce9cd11fa..7cbf377b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * [#2532](https://github.com/ruby-grape/grape/pull/2532): Update RuboCop 1.71.2 - [@ericproulx](https://github.com/ericproulx). * [#2535](https://github.com/ruby-grape/grape/pull/2535): Delegates calls to inner objects - [@ericproulx](https://github.com/ericproulx). * [#2537](https://github.com/ruby-grape/grape/pull/2537): Use activesupport `try` pattern - [@ericproulx](https://github.com/ericproulx). +* [#2536](https://github.com/ruby-grape/grape/pull/2536): Update normalize_path like Rails - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/lib/grape/router.rb b/lib/grape/router.rb index 1b79324ae..99065b6e4 100644 --- a/lib/grape/router.rb +++ b/lib/grape/router.rb @@ -4,12 +4,30 @@ module Grape class Router attr_reader :map, :compiled + # Taken from Rails + # normalize_path("/foo") # => "/foo" + # normalize_path("/foo/") # => "/foo" + # normalize_path("foo") # => "/foo" + # normalize_path("") # => "/" + # normalize_path("/%ab") # => "/%AB" + # https://github.com/rails/rails/blob/00cc4ff0259c0185fe08baadaa40e63ea2534f6e/actionpack/lib/action_dispatch/journey/router/utils.rb#L19 def self.normalize_path(path) + return +'/' unless path + + # Fast path for the overwhelming majority of paths that don't need to be normalized + return path.dup if path == '/' || (path.start_with?('/') && !(path.end_with?('/') || path.match?(%r{%|//}))) + + # Slow path + encoding = path.encoding path = +"/#{path}" path.squeeze!('/') - path.sub!(%r{/+\Z}, '') - path = '/' if path == '' - path + + unless path == '/' + path.delete_suffix!('/') + path.gsub!(/(%[a-f0-9]{2})/) { ::Regexp.last_match(1).upcase } + end + + path.force_encoding(encoding) end def initialize diff --git a/spec/grape/router_spec.rb b/spec/grape/router_spec.rb new file mode 100644 index 000000000..afa05d461 --- /dev/null +++ b/spec/grape/router_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +describe Grape::Router do + describe '.normalize_path' do + subject { described_class.normalize_path(path) } + + context 'when no leading slash' do + let(:path) { 'foo%20bar%20baz' } + + it { is_expected.to eq '/foo%20bar%20baz' } + end + + context 'when path ends with slash' do + let(:path) { '/foo%20bar%20baz/' } + + it { is_expected.to eq '/foo%20bar%20baz' } + end + + context 'when path has recurring slashes' do + let(:path) { '////foo%20bar%20baz' } + + it { is_expected.to eq '/foo%20bar%20baz' } + end + + context 'when not greedy' do + let(:path) { '/foo%20bar%20baz' } + + it { is_expected.to eq '/foo%20bar%20baz' } + end + + context 'when encoded string in lowercase' do + let(:path) { '/foo%aabar%aabaz' } + + it { is_expected.to eq '/foo%AAbar%AAbaz' } + end + + context 'when nil' do + let(:path) { nil } + + it { is_expected.to eq '/' } + end + + context 'when empty string' do + let(:path) { '' } + + it { is_expected.to eq '/' } + end + + context 'when encoding is different' do + subject { described_class.normalize_path(path).encoding } + + let(:path) { '/foo%AAbar%AAbaz'.b } + + it { is_expected.to eq(Encoding::BINARY) } + end + end +end From 0f57e01dc79df9903949c3a1cf42ffc73dabb670 Mon Sep 17 00:00:00 2001 From: Mohammed Nasser <135416851+mohammednasser-32@users.noreply.github.com> Date: Tue, 25 Feb 2025 14:17:47 +0200 Subject: [PATCH 291/304] Handle json array (#2538) * Handle json array * Fix rubocop offenses * fix index * fix json array with nested json array * add change to changelog --- .rubocop_todo.yml | 2 +- CHANGELOG.md | 2 +- lib/grape/dsl/parameters.rb | 2 +- lib/grape/validations/params_scope.rb | 11 +- spec/grape/dsl/parameters_spec.rb | 2 +- spec/grape/validations/params_scope_spec.rb | 171 ++++++++++++++++++++ 6 files changed, 183 insertions(+), 7 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index b779d24c4..f93343852 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2025-02-08 13:42:40 UTC using RuboCop version 1.71.2. +# on 2025-02-23 19:41:28 UTC using RuboCop version 1.71.2. # 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cbf377b5..dddace6b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ #### Fixes -* Your contribution here. +* [#2538](https://github.com/ruby-grape/grape/pull/2538): Fix validating nested json array params - [@mohammednasser-32](https://github.com/mohammednasser-32). ### 2.3.0 (2025-02-08) diff --git a/lib/grape/dsl/parameters.rb b/lib/grape/dsl/parameters.rb index 48f53bcb9..41e6ac6fe 100644 --- a/lib/grape/dsl/parameters.rb +++ b/lib/grape/dsl/parameters.rb @@ -251,7 +251,7 @@ def map_params(params, element, is_array = false) # @return hash of parameters relevant for the current scope # @api private def params(params) - params = @parent.params(params) if instance_variable_defined?(:@parent) && @parent + params = @parent.params_meeting_dependency.presence || @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/validations/params_scope.rb b/lib/grape/validations/params_scope.rb index cb9b3f43e..b80664e6f 100644 --- a/lib/grape/validations/params_scope.rb +++ b/lib/grape/validations/params_scope.rb @@ -4,7 +4,7 @@ module Grape module Validations class ParamsScope attr_accessor :element, :parent, :index - attr_reader :type + attr_reader :type, :params_meeting_dependency include Grape::DSL::Parameters @@ -67,6 +67,7 @@ def initialize(opts, &block) @type = opts[:type] @group = opts[:group] @dependent_on = opts[:dependent_on] + @params_meeting_dependency = [] @declared_params = [] @index = nil @@ -94,7 +95,11 @@ def should_validate?(parameters) def meets_dependency?(params, request_params) return true unless @dependent_on return false if @parent.present? && !@parent.meets_dependency?(@parent.params(request_params), request_params) - return params.any? { |param| meets_dependency?(param, request_params) } if params.is_a?(Array) + + if params.is_a?(Array) + @params_meeting_dependency = params.flatten.filter { |param| meets_dependency?(param, request_params) } + return @params_meeting_dependency.present? + end meets_hash_dependency?(params) end @@ -127,7 +132,7 @@ def meets_hash_dependency?(params) def full_name(name, index: nil) if nested? # Find our containing element's name, and append ours. - "#{@parent.full_name(@element)}#{brackets(@index || index)}#{brackets(name)}" + "#{@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. diff --git a/spec/grape/dsl/parameters_spec.rb b/spec/grape/dsl/parameters_spec.rb index 8c7c1e96c..fbd6308ce 100644 --- a/spec/grape/dsl/parameters_spec.rb +++ b/spec/grape/dsl/parameters_spec.rb @@ -258,7 +258,7 @@ def extract_message_option(attrs) it 'inherits params from parent' do parent_params = { foo: 'bar' } subject.parent = Object.new - allow(subject.parent).to receive(:params).and_return(parent_params) + allow(subject.parent).to receive_messages(params: parent_params, params_meeting_dependency: nil) expect(subject.params({})).to eq parent_params end diff --git a/spec/grape/validations/params_scope_spec.rb b/spec/grape/validations/params_scope_spec.rb index 8e26f6fa6..3caec4dd7 100644 --- a/spec/grape/validations/params_scope_spec.rb +++ b/spec/grape/validations/params_scope_spec.rb @@ -630,6 +630,177 @@ def initialize(value) expect(last_response.body).to eq 'inner3[0][baz][0][baz_category] is missing' end + context 'detect when json array' do + before do + subject.params do + requires :array, type: Array do + requires :a, type: String + given a: ->(val) { val == 'a' } do + requires :json, type: Hash do + requires :b, type: String + end + end + end + end + + subject.post '/nested_array' do + 'nested array works!' + end + end + + it 'succeeds' do + params = { + array: [ + { + a: 'a', + json: { b: 'b' } + }, + { + a: 'b' + } + ] + } + post '/nested_array', params.to_json, 'CONTENT_TYPE' => 'application/json' + + expect(last_response.status).to eq(201) + end + + it 'fails' do + params = { + array: [ + { + a: 'a', + json: { b: 'b' } + }, + { + a: 'a' + } + ] + } + post '/nested_array', params.to_json, 'CONTENT_TYPE' => 'application/json' + + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('array[1][json] is missing, array[1][json][b] is missing') + end + end + + context 'array without given' do + before do + subject.params do + requires :array, type: Array do + requires :a, type: Integer + requires :b, type: Integer + end + end + + subject.post '/array_without_given' + end + + it 'fails' do + params = { + array: [ + { + a: 1, + b: 2 + }, + { + a: 3 + }, + { + a: 5 + } + ] + } + post '/array_without_given', params.to_json, 'CONTENT_TYPE' => 'application/json' + expect(last_response.body).to eq('array[1][b] is missing, array[2][b] is missing') + expect(last_response.status).to eq(400) + end + end + + context 'array with given' do + before do + subject.params do + requires :array, type: Array do + requires :a, type: Integer + given a: lambda(&:odd?) do + requires :b, type: Integer + end + end + end + + subject.post '/array_with_given' + end + + it 'fails' do + params = { + array: [ + { + a: 1, + b: 2 + }, + { + a: 3 + }, + { + a: 5 + } + ] + } + post '/array_with_given', params.to_json, 'CONTENT_TYPE' => 'application/json' + expect(last_response.body).to eq('array[1][b] is missing, array[2][b] is missing') + expect(last_response.status).to eq(400) + end + end + + context 'nested json array with given' do + before do + subject.params do + requires :workflow_nodes, type: Array do + requires :steps, type: Array do + requires :id, type: String + optional :type, type: String, values: %w[send_messsge assign_user assign_team tag_conversation snooze close add_commit] + given type: ->(val) { val == 'send_messsge' } do + requires :message, type: Hash do + requires :content, type: String + end + end + end + end + end + + subject.post '/nested_json_array_with_given' + end + + it 'passes' do + params = { + workflow_nodes: [ + { + id: 'eqibmvEzPo8hQOSt', + title: 'Node 1', + is_start: true, + steps: [ + { + id: 'DvdSZaIm1hEd5XO5', + type: 'send_messsge', + message: { + content: '打击好', + menus: [] + } + }, + { + id: 'VY6MIwycBw0b51Ib', + type: 'add_commit', + comment_content: '初来乍到' + } + ] + } + ] + } + post '/nested_json_array_with_given', params.to_json, 'CONTENT_TYPE' => 'application/json' + expect(last_response.status).to eq(201) + end + end + it 'includes the parameter within #declared(params)' do get '/test', a: true, b: true From 45abe0dd9f18fcc0e1993b1d5a2988135cb216e1 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sat, 1 Mar 2025 14:41:03 +0100 Subject: [PATCH 292/304] Deprecates Grape's Extensions for ParamsBuilder in favor of build_with (#2540) * Deprecates Grape's Extensions for ParamsBuilder in favor of build_with Adds params_builder.rb registration process Update README.md Update UPGRADING.md Add exceptions query_parsing.rb and unknown_params_builder.rb * Fix hashie specs * Fix cops * Remove query_parsing part * Fix has has * Revert Grape.config.param_builder * Add CHANGELOG.md * Fixes --- CHANGELOG.md | 1 + README.md | 14 +- UPGRADING.md | 5 + lib/grape.rb | 2 +- lib/grape/dsl/parameters.rb | 4 +- lib/grape/dsl/routing.rb | 4 + .../exceptions/unknown_params_builder.rb | 11 ++ .../hash_with_indifferent_access.rb | 7 +- lib/grape/extensions/hash.rb | 3 +- lib/grape/extensions/hashie/mash.rb | 8 +- lib/grape/locale/en.yml | 2 + lib/grape/params_builder.rb | 32 +++++ lib/grape/params_builder/base.rb | 18 +++ lib/grape/params_builder/hash.rb | 11 ++ .../hash_with_indifferent_access.rb | 11 ++ lib/grape/params_builder/hashie_mash.rb | 11 ++ lib/grape/request.rb | 24 ++-- spec/grape/api_spec.rb | 21 +++ spec/grape/endpoint/declared_spec.rb | 4 +- .../extensions/param_builders/hash_spec.rb | 112 +++------------ .../hash_with_indifferent_access_spec.rb | 136 +++--------------- spec/grape/grape_spec.rb | 9 -- spec/grape/params_builder/hash_spec.rb | 112 +++++++++++++++ .../hash_with_indifferent_access_spec.rb | 134 +++++++++++++++++ spec/grape/request_spec.rb | 2 +- spec/integration/hashie/hashie_spec.rb | 58 ++++++-- 26 files changed, 496 insertions(+), 260 deletions(-) create mode 100644 lib/grape/exceptions/unknown_params_builder.rb create mode 100644 lib/grape/params_builder.rb create mode 100644 lib/grape/params_builder/base.rb create mode 100644 lib/grape/params_builder/hash.rb create mode 100644 lib/grape/params_builder/hash_with_indifferent_access.rb create mode 100644 lib/grape/params_builder/hashie_mash.rb delete mode 100644 spec/grape/grape_spec.rb create mode 100644 spec/grape/params_builder/hash_spec.rb create mode 100644 spec/grape/params_builder/hash_with_indifferent_access_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index dddace6b0..0b0c75194 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * [#2535](https://github.com/ruby-grape/grape/pull/2535): Delegates calls to inner objects - [@ericproulx](https://github.com/ericproulx). * [#2537](https://github.com/ruby-grape/grape/pull/2537): Use activesupport `try` pattern - [@ericproulx](https://github.com/ericproulx). * [#2536](https://github.com/ruby-grape/grape/pull/2536): Update normalize_path like Rails - [@ericproulx](https://github.com/ericproulx). +* [#2540](https://github.com/ruby-grape/grape/pull/2540): Introduce Params builder with symbolized short name - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/README.md b/README.md index 3597a0206..0add92cd9 100644 --- a/README.md +++ b/README.md @@ -719,10 +719,13 @@ For example, for the `param_builder`, the following code could run in an initial ```ruby Grape.configure do |config| - config.param_builder = Grape::Extensions::Hashie::Mash::ParamBuilder + config.param_builder = :hashie_mash end ``` +Available parameter builders are `:hash`, `:hash_with_indifferent_access`, and `:hashie_mash`. +See [params_builder](lib/grape/params_builder). + You can also configure a single API: ```ruby @@ -789,7 +792,7 @@ By default parameters are available as `ActiveSupport::HashWithIndifferentAccess ```ruby class API < Grape::API - include Grape::Extensions::Hashie::Mash::ParamBuilder + build_with :hashie_mash params do optional :color, type: String @@ -803,16 +806,15 @@ The class can also be overridden on individual parameter blocks using `build_wit ```ruby params do - build_with Grape::Extensions::Hash::ParamBuilder + build_with :hash optional :color, type: String 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`. +Available parameter builders are `:hash`, `:hash_with_indifferent_access`, and `:hashie_mash`. +See [params_builder](lib/grape/params_builder). ### Declared diff --git a/UPGRADING.md b/UPGRADING.md index 708e29252..a09d36ad6 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,6 +1,11 @@ Upgrading Grape =============== +#### Params Builder + +- Passing a class to `build_with` or `Grape.config.param_builder` has been deprecated in favor of a symbolized short_name. See `SHORTNAME_LOOKUP` in [params_builder](lib/grape/params_builder.rb). +- Including Grape's extensions like `Grape::Extensions::Hashie::Mash::ParamBuilder` has been deprecated in favor of using `build_with` at the route level. + ### Upgrading to >= 2.4.0 #### Custom Validators diff --git a/lib/grape.rb b/lib/grape.rb index 58b9701be..91d3f3f8a 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -64,7 +64,7 @@ def self.deprecator end configure do |config| - config.param_builder = Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder + config.param_builder = :hash_with_indifferent_access config.compile_methods! end end diff --git a/lib/grape/dsl/parameters.rb b/lib/grape/dsl/parameters.rb index 41e6ac6fe..6002aff66 100644 --- a/lib/grape/dsl/parameters.rb +++ b/lib/grape/dsl/parameters.rb @@ -23,14 +23,14 @@ module Parameters # class API < Grape::API # desc "Get collection" # params do - # build_with Grape::Extensions::Hashie::Mash::ParamBuilder + # build_with :hashie_mash # requires :user_id, type: Integer # end # get do # params['user_id'] # end # end - def build_with(build_with = nil) + def build_with(build_with) @api.namespace_inheritable(:build_params_with, build_with) end diff --git a/lib/grape/dsl/routing.rb b/lib/grape/dsl/routing.rb index 8145726ad..c2738f933 100644 --- a/lib/grape/dsl/routing.rb +++ b/lib/grape/dsl/routing.rb @@ -67,6 +67,10 @@ def scope(_name = nil, &block) end end + def build_with(build_with) + namespace_inheritable(:build_params_with, build_with) + end + # Do not route HEAD requests to GET requests automatically. def do_not_route_head! namespace_inheritable(:do_not_route_head, true) diff --git a/lib/grape/exceptions/unknown_params_builder.rb b/lib/grape/exceptions/unknown_params_builder.rb new file mode 100644 index 000000000..449860998 --- /dev/null +++ b/lib/grape/exceptions/unknown_params_builder.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Grape + module Exceptions + class UnknownParamsBuilder < Base + def initialize(params_builder_type) + super(message: compose_message(:unknown_params_builder, params_builder_type: params_builder_type)) + end + end + end +end 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 2129e1c80..c90b734b7 100644 --- a/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +++ b/lib/grape/extensions/active_support/hash_with_indifferent_access.rb @@ -8,11 +8,8 @@ module ParamBuilder extend ::ActiveSupport::Concern included do - namespace_inheritable(:build_params_with, Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder) - end - - def params_builder - Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder + Grape.deprecator.warn 'This concern has been deprecated. Use `build_with` with one of the following short_name (:hash, :hash_with_indifferent_access, :hashie_mash) instead.' + namespace_inheritable(:build_params_with, :hash_with_indifferent_access) end def build_params diff --git a/lib/grape/extensions/hash.rb b/lib/grape/extensions/hash.rb index cb8bd4b06..6d5ef6e65 100644 --- a/lib/grape/extensions/hash.rb +++ b/lib/grape/extensions/hash.rb @@ -7,7 +7,8 @@ module ParamBuilder extend ::ActiveSupport::Concern included do - namespace_inheritable(:build_params_with, Grape::Extensions::Hash::ParamBuilder) + Grape.deprecator.warn 'This concern has been deprecated. Use `build_with` with one of the following short_name (:hash, :hash_with_indifferent_access, :hashie_mash) instead.' + namespace_inheritable(:build_params_with, :hash) end def build_params diff --git a/lib/grape/extensions/hashie/mash.rb b/lib/grape/extensions/hashie/mash.rb index b6ae5061b..c144b4fd2 100644 --- a/lib/grape/extensions/hashie/mash.rb +++ b/lib/grape/extensions/hashie/mash.rb @@ -6,12 +6,10 @@ module Hashie module Mash module ParamBuilder extend ::ActiveSupport::Concern - included do - namespace_inheritable(:build_params_with, Grape::Extensions::Hashie::Mash::ParamBuilder) - end - def params_builder - Grape::Extensions::Hashie::Mash::ParamBuilder + included do + Grape.deprecator.warn 'This concern has been deprecated. Use `build_with` with one of the following short_name (:hash, :hash_with_indifferent_access, :hashie_mash) instead.' + namespace_inheritable(:build_params_with, :hashie_mash) end def build_params diff --git a/lib/grape/locale/en.yml b/lib/grape/locale/en.yml index 6b1b6ae06..35cdeef4e 100644 --- a/lib/grape/locale/en.yml +++ b/lib/grape/locale/en.yml @@ -33,6 +33,7 @@ en: problem: 'unknown :using for versioner: %{strategy}' resolution: 'available strategy for :using is :path, :header, :accept_version_header, :param' unknown_validator: 'unknown validator: %{validator_type}' + unknown_params_builder: 'unknown params_builder: %{params_builder_type}' unknown_options: 'unknown options: %{options}' unknown_parameter: 'unknown parameter: %{param}' incompatible_option_values: '%{option1}: %{value1} is incompatible with %{option2}: %{value2}' @@ -57,3 +58,4 @@ en: problem: 'invalid version header' resolution: '%{message}' invalid_response: 'Invalid response' + query_parsing: 'query params are not parsable' diff --git a/lib/grape/params_builder.rb b/lib/grape/params_builder.rb new file mode 100644 index 000000000..03cd9d1e3 --- /dev/null +++ b/lib/grape/params_builder.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Grape + module ParamsBuilder + extend Grape::Util::Registry + + SHORT_NAME_LOOKUP = { + 'Grape::Extensions::Hash::ParamBuilder' => :hash, + 'Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder' => :hash_with_indifferent_access, + 'Grape::Extensions::Hashie::Mash::ParamBuilder' => :hashie_mash + }.freeze + + module_function + + def params_builder_for(short_name) + verified_short_name = verify_short_name!(short_name) + + raise Grape::Exceptions::UnknownParamsBuilder, verified_short_name unless registry.key?(verified_short_name) + + registry[verified_short_name] + end + + def verify_short_name!(short_name) + return short_name if short_name.is_a?(Symbol) + + class_name = short_name.name + SHORT_NAME_LOOKUP[class_name].tap do |real_short_name| + Grape.deprecator.warn "#{class_name} has been deprecated. Use short name :#{real_short_name} instead." + end + end + end +end diff --git a/lib/grape/params_builder/base.rb b/lib/grape/params_builder/base.rb new file mode 100644 index 000000000..49f583d74 --- /dev/null +++ b/lib/grape/params_builder/base.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Grape + module ParamsBuilder + class Base + class << self + def call(_params) + raise NotImplementedError + end + + def inherited(klass) + super + ParamsBuilder.register(klass) + end + end + end + end +end diff --git a/lib/grape/params_builder/hash.rb b/lib/grape/params_builder/hash.rb new file mode 100644 index 000000000..25d777c7e --- /dev/null +++ b/lib/grape/params_builder/hash.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Grape + module ParamsBuilder + class Hash < Base + def self.call(params) + params.deep_symbolize_keys + end + end + end +end diff --git a/lib/grape/params_builder/hash_with_indifferent_access.rb b/lib/grape/params_builder/hash_with_indifferent_access.rb new file mode 100644 index 000000000..cce22fc21 --- /dev/null +++ b/lib/grape/params_builder/hash_with_indifferent_access.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Grape + module ParamsBuilder + class HashWithIndifferentAccess < Base + def self.call(params) + params.with_indifferent_access + end + end + end +end diff --git a/lib/grape/params_builder/hashie_mash.rb b/lib/grape/params_builder/hashie_mash.rb new file mode 100644 index 000000000..f20ff6dc1 --- /dev/null +++ b/lib/grape/params_builder/hashie_mash.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Grape + module ParamsBuilder + class HashieMash < Base + def self.call(params) + ::Hashie::Mash.new(params) + end + end + end +end diff --git a/lib/grape/request.rb b/lib/grape/request.rb index 9bbf13a53..62bf1c8db 100644 --- a/lib/grape/request.rb +++ b/lib/grape/request.rb @@ -2,32 +2,38 @@ module Grape class Request < Rack::Request + DEFAULT_PARAMS_BUILDER = :hash_with_indifferent_access HTTP_PREFIX = 'HTTP_' alias rack_params params def initialize(env, build_params_with: nil) - extend build_params_with || Grape.config.param_builder super(env) + @params_builder = Grape::ParamsBuilder.params_builder_for(build_params_with || Grape.config.param_builder) end def params - @params ||= build_params - rescue EOFError - raise Grape::Exceptions::EmptyMessageBody.new(content_type) - rescue Rack::Multipart::MultipartPartLimitError - raise Grape::Exceptions::TooManyMultipartFiles.new(Rack::Utils.multipart_part_limit) + @params ||= make_params end def headers @headers ||= build_headers end - private - + # needs to be public until extensions param_builder are removed def grape_routing_args # preserve version from query string parameters - env[Grape::Env::GRAPE_ROUTING_ARGS].except(:version, :route_info) + env[Grape::Env::GRAPE_ROUTING_ARGS]&.except(:version, :route_info) || {} + end + + private + + def make_params + @params_builder.call(rack_params).deep_merge!(grape_routing_args) + rescue EOFError + raise Grape::Exceptions::EmptyMessageBody.new(content_type) + rescue Rack::Multipart::MultipartPartLimitError + raise Grape::Exceptions::TooManyMultipartFiles.new(Rack::Utils.multipart_part_limit) end def build_headers diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index 20193ccb3..71cf9ac38 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -4694,4 +4694,25 @@ def uniqe_id_route it { is_expected.to be_bad_request } end + + describe '.build_with' do + let(:app) do + Class.new(described_class) do + build_with :unknown + params do + requires :a_param, type: Integer + end + get + end + end + + before do + get '/' + end + + it 'raises an UnknownParamsBuilder error' do + expect(last_response).to be_server_error + expect(last_response.body).to eq('unknown params_builder: unknown') + end + end end diff --git a/spec/grape/endpoint/declared_spec.rb b/spec/grape/endpoint/declared_spec.rb index 9b561d886..cf8b0c73f 100644 --- a/spec/grape/endpoint/declared_spec.rb +++ b/spec/grape/endpoint/declared_spec.rb @@ -46,7 +46,7 @@ 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 + build_with :hash_with_indifferent_access end subject.get '/declared' do d = declared(params, include_missing: true) @@ -59,7 +59,7 @@ it 'returns an object that corresponds with the params class - hash' do subject.params do - build_with Grape::Extensions::Hash::ParamBuilder + build_with :hash end subject.get '/declared' do d = declared(params, include_missing: true) diff --git a/spec/grape/extensions/param_builders/hash_spec.rb b/spec/grape/extensions/param_builders/hash_spec.rb index fdce95226..d4bc7b784 100644 --- a/spec/grape/extensions/param_builders/hash_spec.rb +++ b/spec/grape/extensions/param_builders/hash_spec.rb @@ -1,112 +1,38 @@ # frozen_string_literal: true describe Grape::Extensions::Hash::ParamBuilder do - subject { Class.new(Grape::API) } - - def app - subject - end - - describe 'in an endpoint' do - describe '#params' do - before do - subject.params do - build_with Grape::Extensions::Hash::ParamBuilder # rubocop:disable RSpec/DescribedClass - end - - subject.get do - params.class + describe 'deprecation' do + context 'when included' do + subject do + Class.new(Grape::API) do + include Grape::Extensions::Hash::ParamBuilder end end - it 'is of type Hash' do - get '/' - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('Hash') - end - end - end - - describe 'in an api' do - before do - subject.include Grape::Extensions::Hash::ParamBuilder # rubocop:disable RSpec/DescribedClass - end - - describe '#params' do - before do - subject.get do - params.class - end + let(:message) do + 'This concern has been deprecated. Use `build_with` with one of the following short_name (:hash, :hash_with_indifferent_access, :hashie_mash) instead.' end - it 'is Hash' do - get '/' - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('Hash') + it 'raises a deprecation' do + expect(Grape.deprecator).to receive(:warn).with(message).and_raise(ActiveSupport::DeprecationException, :deprecated) + expect { subject }.to raise_error(ActiveSupport::DeprecationException, 'deprecated') end end - it 'symbolizes params keys' do - subject.params do - optional :a, type: Hash do - optional :b, type: Hash do - optional :c, type: String + context 'when using class name' do + let(:app) do + Class.new(Grape::API) do + params do + build_with Grape::Extensions::Hash::ParamBuilder end - optional :d, type: Array + get end end - subject.get '/foo' do - [params[:a][:b][:c], params[:a][:d]] - end - - get '/foo', 'a' => { b: { c: 'bar' }, 'd' => ['foo'] } - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('["bar", ["foo"]]') - end - - it 'symbolizes the params' do - subject.params do - build_with Grape::Extensions::Hash::ParamBuilder # rubocop:disable RSpec/DescribedClass - requires :a, type: String + it 'raises a deprecation' do + expect(Grape.deprecator).to receive(:warn).with("#{described_class} has been deprecated. Use short name :hash instead.").and_raise(ActiveSupport::DeprecationException, :deprecated) + expect { get '/' }.to raise_error(ActiveSupport::DeprecationException, 'deprecated') end - - subject.get '/foo' do - [params[:a], params['a']] - end - - get '/foo', a: 'bar' - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('["bar", nil]') - end - - it 'does not overwrite route_param with a regular param if they have same name' do - subject.namespace :route_param do - route_param :foo do - get { params.to_json } - end - end - - get '/route_param/bar', foo: 'baz' - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('{"foo":"bar"}') - end - - it 'does not overwrite route_param with a defined regular param if they have same name' do - subject.namespace :route_param do - params do - requires :foo, type: String - end - route_param :foo do - get do - params[:foo] - end - end - end - - get '/route_param/bar', foo: 'baz' - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('bar') end end end 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 97b4e56cd..86ae1c84a 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,134 +1,38 @@ # frozen_string_literal: true describe Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder do - subject { Class.new(Grape::API) } - - def app - subject - end - - describe 'in an endpoint' do - describe '#params' do - before do - subject.params do - build_with Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder # rubocop:disable RSpec/DescribedClass - end - - subject.get do - params.class - end - end - - it 'is of type Hash' do - get '/' - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('ActiveSupport::HashWithIndifferentAccess') - end - end - end - - describe 'in an api' do - before do - subject.include Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder # rubocop:disable RSpec/DescribedClass - end - - describe '#params' do - before do - subject.get do - params.class + describe 'deprecation' do + context 'when included' do + subject do + Class.new(Grape::API) do + include Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder end end - it 'is a Hash' do - get '/' - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('ActiveSupport::HashWithIndifferentAccess') + let(:message) do + 'This concern has been deprecated. Use `build_with` with one of the following short_name (:hash, :hash_with_indifferent_access, :hashie_mash) instead.' end - it 'parses sub hash params' do - subject.params do - build_with Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder # rubocop:disable RSpec/DescribedClass - - optional :a, type: Hash do - optional :b, type: Hash do - optional :c, type: String - end - optional :d, type: Array - end - end - - subject.get '/foo' do - [params[:a]['b'][:c], params['a'][:d]] - end - - get '/foo', a: { b: { c: 'bar' }, d: ['foo'] } - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('["bar", ["foo"]]') - end - - it 'params are indifferent to symbol or string keys' do - subject.params do - build_with Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder # rubocop:disable RSpec/DescribedClass - optional :a, type: Hash do - optional :b, type: Hash do - optional :c, type: String - end - optional :d, type: Array - end - end - - subject.get '/foo' do - [params[:a]['b'][:c], params['a'][:d]] - end - - get '/foo', 'a' => { b: { c: 'bar' }, 'd' => ['foo'] } - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('["bar", ["foo"]]') - end - - it 'responds to string keys' do - subject.params do - build_with Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder # rubocop:disable RSpec/DescribedClass - requires :a, type: String - end - - subject.get '/foo' do - [params[:a], params['a']] - end - - get '/foo', a: 'bar' - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('["bar", "bar"]') + it 'raises a deprecation' do + expect(Grape.deprecator).to receive(:warn).with(message).and_raise(ActiveSupport::DeprecationException, :deprecated) + expect { subject }.to raise_error(ActiveSupport::DeprecationException, 'deprecated') end end + end - it 'does not overwrite route_param with a regular param if they have same name' do - subject.namespace :route_param do - route_param :foo do - get { params.to_json } - end - end - - get '/route_param/bar', foo: 'baz' - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('{"foo":"bar"}') - end - - it 'does not overwrite route_param with a defined regular param if they have same name' do - subject.namespace :route_param do + context 'when using class name' do + let(:app) do + Class.new(Grape::API) do params do - requires :foo, type: String - end - route_param :foo do - get do - [params[:foo], params['foo']] - end + build_with Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder end + get end + end - get '/route_param/bar', foo: 'baz' - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('["bar", "bar"]') + it 'raises a deprecation' do + expect(Grape.deprecator).to receive(:warn).with("#{described_class} has been deprecated. Use short name :hash_with_indifferent_access instead.").and_raise(ActiveSupport::DeprecationException, :deprecated) + expect { get '/' }.to raise_error(ActiveSupport::DeprecationException, 'deprecated') end end end diff --git a/spec/grape/grape_spec.rb b/spec/grape/grape_spec.rb deleted file mode 100644 index 021aa861d..000000000 --- a/spec/grape/grape_spec.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Grape do - describe '.config' do - subject { described_class.config } - - it { is_expected.to eq(param_builder: Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder) } - end -end diff --git a/spec/grape/params_builder/hash_spec.rb b/spec/grape/params_builder/hash_spec.rb new file mode 100644 index 000000000..7b185f34b --- /dev/null +++ b/spec/grape/params_builder/hash_spec.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +describe Grape::ParamsBuilder::Hash do + subject { app } + + let(:app) do + Class.new(Grape::API) + end + + describe 'in an endpoint' do + describe '#params' do + before do + subject.params do + build_with :hash + end + + subject.get do + params.class + end + end + + it 'is of type Hash' do + get '/' + expect(last_response.status).to eq(200) + expect(last_response.body).to eq('Hash') + end + end + end + + describe 'in an api' do + before do + subject.build_with :hash + end + + describe '#params' do + before do + subject.get do + params.class + end + end + + it 'is Hash' do + get '/' + expect(last_response.status).to eq(200) + expect(last_response.body).to eq('Hash') + end + end + + it 'symbolizes params keys' do + subject.params do + optional :a, type: Hash do + optional :b, type: Hash do + optional :c, type: String + end + optional :d, type: Array + end + end + + subject.get '/foo' do + [params[:a][:b][:c], params[:a][:d]] + end + + get '/foo', 'a' => { b: { c: 'bar' }, 'd' => ['foo'] } + expect(last_response.status).to eq(200) + expect(last_response.body).to eq('["bar", ["foo"]]') + end + + it 'symbolizes the params' do + subject.params do + build_with :hash + requires :a, type: String + end + + subject.get '/foo' do + [params[:a], params['a']] + end + + get '/foo', a: 'bar' + expect(last_response.status).to eq(200) + expect(last_response.body).to eq('["bar", nil]') + end + + it 'does not overwrite route_param with a regular param if they have same name' do + subject.namespace :route_param do + route_param :foo do + get { params.to_json } + end + end + + get '/route_param/bar', foo: 'baz' + expect(last_response.status).to eq(200) + expect(last_response.body).to eq('{"foo":"bar"}') + end + + it 'does not overwrite route_param with a defined regular param if they have same name' do + subject.namespace :route_param do + params do + requires :foo, type: String + end + route_param :foo do + get do + params[:foo] + end + end + end + + get '/route_param/bar', foo: 'baz' + expect(last_response.status).to eq(200) + expect(last_response.body).to eq('bar') + end + end +end diff --git a/spec/grape/params_builder/hash_with_indifferent_access_spec.rb b/spec/grape/params_builder/hash_with_indifferent_access_spec.rb new file mode 100644 index 000000000..ee0fd1ecc --- /dev/null +++ b/spec/grape/params_builder/hash_with_indifferent_access_spec.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +describe Grape::ParamsBuilder::HashWithIndifferentAccess do + subject { app } + + let(:app) do + Class.new(Grape::API) + end + + describe 'in an endpoint' do + describe '#params' do + before do + subject.params do + build_with :hash_with_indifferent_access + end + + subject.get do + params.class + end + end + + it 'is of type Hash' do + get '/' + expect(last_response.status).to eq(200) + expect(last_response.body).to eq('ActiveSupport::HashWithIndifferentAccess') + end + end + end + + describe 'in an api' do + before do + subject.build_with :hash_with_indifferent_access + end + + describe '#params' do + before do + subject.get do + params.class + end + end + + it 'is a Hash' do + get '/' + expect(last_response.status).to eq(200) + expect(last_response.body).to eq('ActiveSupport::HashWithIndifferentAccess') + end + + it 'parses sub hash params' do + subject.params do + build_with :hash_with_indifferent_access + + optional :a, type: Hash do + optional :b, type: Hash do + optional :c, type: String + end + optional :d, type: Array + end + end + + subject.get '/foo' do + [params[:a]['b'][:c], params['a'][:d]] + end + + get '/foo', a: { b: { c: 'bar' }, d: ['foo'] } + expect(last_response.status).to eq(200) + expect(last_response.body).to eq('["bar", ["foo"]]') + end + + it 'params are indifferent to symbol or string keys' do + subject.params do + build_with :hash_with_indifferent_access + optional :a, type: Hash do + optional :b, type: Hash do + optional :c, type: String + end + optional :d, type: Array + end + end + + subject.get '/foo' do + [params[:a]['b'][:c], params['a'][:d]] + end + + get '/foo', 'a' => { b: { c: 'bar' }, 'd' => ['foo'] } + expect(last_response.status).to eq(200) + expect(last_response.body).to eq('["bar", ["foo"]]') + end + + it 'responds to string keys' do + subject.params do + build_with :hash_with_indifferent_access + requires :a, type: String + end + + subject.get '/foo' do + [params[:a], params['a']] + end + + get '/foo', a: 'bar' + expect(last_response.status).to eq(200) + expect(last_response.body).to eq('["bar", "bar"]') + end + end + + it 'does not overwrite route_param with a regular param if they have same name' do + subject.namespace :route_param do + route_param :foo do + get { params.to_json } + end + end + + get '/route_param/bar', foo: 'baz' + expect(last_response.status).to eq(200) + expect(last_response.body).to eq('{"foo":"bar"}') + end + + it 'does not overwrite route_param with a defined regular param if they have same name' do + subject.namespace :route_param do + params do + requires :foo, type: String + end + route_param :foo do + get do + [params[:foo], params['foo']] + end + end + end + + get '/route_param/bar', foo: 'baz' + expect(last_response.status).to eq(200) + expect(last_response.body).to eq('["bar", "bar"]') + end + end +end diff --git a/spec/grape/request_spec.rb b/spec/grape/request_spec.rb index 298c70513..3ed8d8f03 100644 --- a/spec/grape/request_spec.rb +++ b/spec/grape/request_spec.rb @@ -35,7 +35,7 @@ context 'when build_params_with: Grape::Extensions::Hash::ParamBuilder is specified' do let(:request) do - described_class.new(env, build_params_with: Grape::Extensions::Hash::ParamBuilder) + described_class.new(env, build_params_with: :hash) end it 'returns symbolized params' do diff --git a/spec/integration/hashie/hashie_spec.rb b/spec/integration/hashie/hashie_spec.rb index d90b77126..8d9021c6d 100644 --- a/spec/integration/hashie/hashie_spec.rb +++ b/spec/integration/hashie/hashie_spec.rb @@ -1,16 +1,54 @@ # frozen_string_literal: true describe 'Hashie', if: defined?(Hashie) do - subject { Class.new(Grape::API) } + subject { app } - let(:app) { subject } + let(:app) { Class.new(Grape::API) } describe 'Grape::Extensions::Hashie::Mash::ParamBuilder' do + describe 'deprecation' do + context 'when included' do + subject do + Class.new(Grape::API) do + include Grape::Extensions::Hashie::Mash::ParamBuilder + end + end + + let(:message) do + 'This concern has been deprecated. Use `build_with` with one of the following short_name (:hash, :hash_with_indifferent_access, :hashie_mash) instead.' + end + + it 'raises a deprecation' do + expect(Grape.deprecator).to receive(:warn).with(message).and_raise(ActiveSupport::DeprecationException, :deprecated) + expect { subject }.to raise_error(ActiveSupport::DeprecationException, 'deprecated') + end + end + + context 'when using class name' do + let(:app) do + Class.new(Grape::API) do + params do + build_with Grape::Extensions::Hashie::Mash::ParamBuilder + end + get + end + end + + it 'raises a deprecation' do + expect(Grape.deprecator).to receive(:warn).with('Grape::Extensions::Hashie::Mash::ParamBuilder has been deprecated. Use short name :hashie_mash instead.').and_raise(ActiveSupport::DeprecationException, + :deprecated) + expect { get '/' }.to raise_error(ActiveSupport::DeprecationException, 'deprecated') + end + end + end + end + + describe 'Grape::ParamsBuilder::HashieMash' do describe 'in an endpoint' do describe '#params' do before do subject.params do - build_with Grape::Extensions::Hashie::Mash::ParamBuilder + build_with :hashie_mash end subject.get do @@ -28,7 +66,7 @@ describe 'in an api' do before do - subject.include Grape::Extensions::Hashie::Mash::ParamBuilder + subject.build_with :hashie_mash end describe '#params' do @@ -63,7 +101,7 @@ it 'is indifferent to key or symbol access' do subject.params do - build_with Grape::Extensions::Hashie::Mash::ParamBuilder + build_with :hashie_mash requires :a, type: String end subject.get '/foo' do @@ -90,7 +128,7 @@ it 'does not overwrite route_param with a defined regular param if they have same name' do subject.namespace :route_param do params do - build_with Grape::Extensions::Hashie::Mash::ParamBuilder + build_with :hashie_mash requires :foo, type: String end route_param :foo do @@ -138,7 +176,7 @@ end context 'when build_params_with: Grape::Extensions::Hash::ParamBuilder is specified' do - let(:request) { Grape::Request.new(env, build_params_with: Grape::Extensions::Hash::ParamBuilder) } + let(:request) { Grape::Request.new(env, build_params_with: :hash) } it 'returns symbolized params' do expect(request.params).to eq(a: '123', b: 'xyz') @@ -164,7 +202,7 @@ end describe 'when the build_params_with is set to Hashie' do - subject(:request_params) { Grape::Request.new(env, build_params_with: Grape::Extensions::Hashie::Mash::ParamBuilder).params } + subject(:request_params) { Grape::Request.new(env, build_params_with: :hashie_mash).params } context 'when the API includes a specific param builder' do it { is_expected.to be_a(Hashie::Mash) } @@ -177,7 +215,7 @@ context 'for primitive collections' do before do subject.params do - build_with Grape::Extensions::Hashie::Mash::ParamBuilder + build_with :hashie_mash optional :a, types: [String, Array[String]] optional :b, types: [Array[Integer], Array[String]] optional :c, type: Array[Integer, String] @@ -287,7 +325,7 @@ context 'when params are not built with default class' do it 'returns an object that corresponds with the params class - hashie mash' do subject.params do - build_with Grape::Extensions::Hashie::Mash::ParamBuilder + build_with :hashie_mash end subject.get '/declared' do d = declared(params, include_missing: true) From d956e44739e260150a3bf8a23163abcb0d070122 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sun, 2 Mar 2025 17:20:52 +0100 Subject: [PATCH 293/304] Reduce array alloc on mount (#2543) * Use << instead of += Some refactor Remove wrongful spec * Fix refresh_mount_step * Add CHANGELOG.md + missing `your contribution here` --- CHANGELOG.md | 2 ++ lib/grape/api.rb | 59 +++++++++++++++++++++--------------------- spec/grape/api_spec.rb | 5 ---- 3 files changed, 32 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b0c75194..39207aba5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ #### Fixes * [#2538](https://github.com/ruby-grape/grape/pull/2538): Fix validating nested json array params - [@mohammednasser-32](https://github.com/mohammednasser-32). +* [#2543](https://github.com/ruby-grape/grape/pull/2543): Fix array allocation on mount - [@ericproulx](https://github.com/ericproulx). +* Your contribution here. ### 2.3.0 (2025-02-08) diff --git a/lib/grape/api.rb b/lib/grape/api.rb index 24d401745..8ddc207ef 100644 --- a/lib/grape/api.rb +++ b/lib/grape/api.rb @@ -55,7 +55,7 @@ def initial_setup(base_instance_parent) def override_all_methods! (base_instance.methods - Class.methods - NON_OVERRIDABLE).each do |method_override| define_singleton_method(method_override) do |*args, &block| - add_setup(method_override, *args, &block) + add_setup(method: method_override, args: args, block: block) end end end @@ -79,24 +79,24 @@ def configure # 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 + def mount_instance(configuration: nil) + Class.new(@base_parent).tap do |instance| + instance.configuration = Grape::Util::EndpointConfiguration.new(configuration || {}) + instance.base = self + replay_setup_on(instance) + end end + private + # 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) + replay_step_on(instance, **setup_step) end end - private - def instance_for_rack if never_mounted? base_instance @@ -106,34 +106,35 @@ def instance_for_rack end # 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] + def add_setup(step) + @setup << step last_response = nil @instances.each do |instance| - last_response = replay_step_on(instance, setup_step) + last_response = replay_step_on(instance, **step) end - # Updating all previously mounted classes in the case that new methods have been executed. - if method != :mount && @setup.any? - previous_mount_steps = @setup.select { |step| step[:method] == :mount } - previous_mount_steps.each do |mount_step| - refresh_mount_step = mount_step.merge(method: :refresh_mounted_api) - @setup += [refresh_mount_step] - @instances.each do |instance| - replay_step_on(instance, refresh_mount_step) - end + refresh_mount_step if step[:method] != :mount + last_response + end + + # Updating all previously mounted classes in the case that new methods have been executed. + def refresh_mount_step + @setup.each do |setup_step| + next if setup_step[:method] != :mount + + refresh_mount_step = setup_step.merge(method: :refresh_mounted_api) + @setup << refresh_mount_step + @instances.each do |instance| + replay_step_on(instance, **refresh_mount_step) end end - - last_response end - def replay_step_on(instance, setup_step) - return if skip_immediate_run?(instance, setup_step[:args]) + def replay_step_on(instance, method:, args:, block:) + return if skip_immediate_run?(instance, args) - args = evaluate_arguments(instance.configuration, *setup_step[:args]) - response = instance.send(setup_step[:method], *args, &setup_step[:block]) + eval_args = evaluate_arguments(instance.configuration, *args) + response = instance.send(method, *eval_args, &block) if skip_immediate_run?(instance, [response]) response else diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index 71cf9ac38..50e68fe70 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -1699,11 +1699,6 @@ def self.io expect(subject.io.string).to include(message) end end - - it 'does not unnecessarily retain duplicate setup blocks' do - subject.logger - expect { subject.logger }.not_to change(subject.instance_variable_get(:@setup), :size) - end end describe '.helpers' do From c23d376a9df83fe9908ca6e7cdbfdc3af60494a5 Mon Sep 17 00:00:00 2001 From: "Daniel (dB.) Doubrovkine" Date: Thu, 6 Mar 2025 15:37:17 -0500 Subject: [PATCH 294/304] Fix badges, update links. --- CONTRIBUTING.md | 2 +- README.md | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 084f91228..18500e912 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,7 @@ Contributing to Grape ===================== -Grape is work of [hundreds of contributors](https://github.com/ruby-grape/grape/graphs/contributors). You're encouraged to submit [pull requests](https://github.com/ruby-grape/grape/pulls), [propose features and discuss issues](https://github.com/ruby-grape/grape/issues). When in doubt, ask a question in the [Grape Google Group](http://groups.google.com/group/ruby-grape). +Grape is work of [hundreds of contributors](https://github.com/ruby-grape/grape/graphs/contributors). You're encouraged to submit [pull requests](https://github.com/ruby-grape/grape/pulls), [propose features and discuss issues](https://github.com/ruby-grape/grape/issues). #### Fork the Project diff --git a/README.md b/README.md index 0add92cd9..b6d404a39 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,8 @@ ![grape logo](grape.png) [![Gem Version](https://badge.fury.io/rb/grape.svg)](http://badge.fury.io/rb/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) +[![test](https://github.com/ruby-grape/grape/actions/workflows/test.yml/badge.svg)](https://github.com/ruby-grape/grape/actions/workflows/test.yml) [![Coverage Status](https://coveralls.io/repos/github/ruby-grape/grape/badge.svg?branch=master)](https://coveralls.io/github/ruby-grape/grape?branch=master) -[![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 @@ -164,7 +161,7 @@ The current stable release is [2.3.0](https://github.com/ruby-grape/grape/blob/v * [Grape Website](http://www.ruby-grape.org) * [Documentation](http://www.rubydoc.info/gems/grape) -* Need help? Try [Grape Google Group](http://groups.google.com/group/ruby-grape) or [Gitter](https://gitter.im/ruby-grape/grape) +* Need help? [Open an Issue](https://github.com/ruby-grape/grape/issues) * [Follow us on Twitter](https://twitter.com/grapeframework) ## Grape for Enterprise From 97f2413b49e76f8d7a001c9cce9ecf9408c48c4a Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sun, 9 Mar 2025 13:02:50 +0100 Subject: [PATCH 295/304] Fix middleware with keywords (#2546) * Refactor Stack and Middleware closer to ActionDispatch::MiddlewareStack Flag hash arg with `ruby2_keywords_hash` Add spec * Add CHANGELOG.md * Change 1 liner --- CHANGELOG.md | 1 + lib/grape/middleware/stack.rb | 49 +++++++++++++++++++---------------- spec/grape/api_spec.rb | 28 ++++++++++++++++++++ 3 files changed, 55 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39207aba5..773aa4dde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ * [#2538](https://github.com/ruby-grape/grape/pull/2538): Fix validating nested json array params - [@mohammednasser-32](https://github.com/mohammednasser-32). * [#2543](https://github.com/ruby-grape/grape/pull/2543): Fix array allocation on mount - [@ericproulx](https://github.com/ericproulx). +* [#2546](https://github.com/ruby-grape/grape/pull/2546): Fix middleware with keywords - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 2.3.0 (2025-02-08) diff --git a/lib/grape/middleware/stack.rb b/lib/grape/middleware/stack.rb index 63dc035f6..1ba65080e 100644 --- a/lib/grape/middleware/stack.rb +++ b/lib/grape/middleware/stack.rb @@ -7,18 +7,18 @@ module Middleware class Stack extend Forwardable class Middleware - extend Forwardable - attr_reader :args, :block, :klass - def_delegators :klass, :name - - def initialize(klass, *args, &block) + def initialize(klass, args, block) @klass = klass @args = args @block = block end + def name + klass.name + end + def ==(other) case other when Middleware @@ -32,7 +32,11 @@ def inspect klass.to_s end - def use_in(builder) + def build(builder) + # we need to force the ruby2_keywords_hash for middlewares that initialize contains keywords + # like ActionDispatch::RequestId since middleware arguments are serialized + # https://rubyapi.org/3.4/o/hash#method-c-ruby2_keywords_hash + args[-1] = Hash.ruby2_keywords_hash(args[-1]) if args.last.is_a?(Hash) && Hash.respond_to?(:ruby2_keywords_hash) builder.use(klass, *args, &block) end end @@ -48,12 +52,10 @@ def initialize @others = [] end - def insert(index, *args, &block) + def insert(index, klass, *args, &block) index = assert_index(index, :before) - middleware = self.class::Middleware.new(*args, &block) - middlewares.insert(index, middleware) + middlewares.insert(index, self.class::Middleware.new(klass, args, block)) end - ruby2_keywords :insert if respond_to?(:ruby2_keywords, true) alias insert_before insert @@ -61,38 +63,39 @@ 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(...) - middleware = self.class::Middleware.new(...) + def use(klass, *args, &block) + middleware = self.class::Middleware.new(klass, args, block) middlewares.push(middleware) end def merge_with(middleware_specs) - middleware_specs.each do |operation, *args| + middleware_specs.each do |operation, klass, *args| if args.last.is_a?(Proc) last_proc = args.pop - public_send(operation, *args, &last_proc) + public_send(operation, klass, *args, &last_proc) else - public_send(operation, *args) + public_send(operation, klass, *args) end end end # @return [Rack::Builder] the builder object with our middlewares applied - def build(builder = Rack::Builder.new) - others.shift(others.size).each { |m| merge_with(m) } - middlewares.each do |m| - m.use_in(builder) + def build + Rack::Builder.new.tap do |builder| + others.shift(others.size).each { |m| merge_with(m) } + middlewares.each do |m| + m.build(builder) + end end - builder end # @description Add middlewares with :use operation to the stack. Store others with :insert_* operation for later # @param [Array] other_specs An array of middleware specifications (e.g. [[:use, klass], [:insert_before, *args]]) def concat(other_specs) - @others << Array(other_specs).reject { |o| o.first == :use } - merge_with(Array(other_specs).select { |o| o.first == :use }) + use, not_use = other_specs.partition { |o| o.first == :use } + others << not_use + merge_with(use) end protected diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index 50e68fe70..013e0741b 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -1525,6 +1525,34 @@ def before expect(last_response).to be_bad_request expect(last_response.body).to eq('Caught in the Net') end + + context 'when middleware initialize as keywords' do + let(:middleware_with_keywords) do + Class.new do + def initialize(app, keyword:) + @app = app + @keyword = keyword + end + + def call(env) + env['middleware_with_keywords'] = @keyword + @app.call(env) + end + end + end + + before do + subject.use middleware_with_keywords, keyword: 'hello' + subject.get '/' do + env['middleware_with_keywords'] + end + get '/' + end + + it 'returns the middleware value' do + expect(last_response.body).to eq('hello') + end + end end describe '.insert_before' do From 4f6725a9fefb0b6ec72fb3cf9b576ce042a38bd4 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sat, 15 Mar 2025 17:04:35 +0100 Subject: [PATCH 296/304] Remove jsonapi related code (#2547) * Remove specs related to jsonapi Remove jsonapi parser and error_formatter * Add CHANGELOG.md --- CHANGELOG.md | 1 + lib/grape/error_formatter/jsonapi.rb | 7 ------- lib/grape/parser/jsonapi.rb | 7 ------- spec/grape/middleware/exception_spec.rb | 21 --------------------- spec/grape/middleware/formatter_spec.rb | 17 ----------------- 5 files changed, 1 insertion(+), 52 deletions(-) delete mode 100644 lib/grape/error_formatter/jsonapi.rb delete mode 100644 lib/grape/parser/jsonapi.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 773aa4dde..ce18d325b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ * [#2538](https://github.com/ruby-grape/grape/pull/2538): Fix validating nested json array params - [@mohammednasser-32](https://github.com/mohammednasser-32). * [#2543](https://github.com/ruby-grape/grape/pull/2543): Fix array allocation on mount - [@ericproulx](https://github.com/ericproulx). * [#2546](https://github.com/ruby-grape/grape/pull/2546): Fix middleware with keywords - [@ericproulx](https://github.com/ericproulx). +* [#2547](https://github.com/ruby-grape/grape/pull/2547): Remove jsonapi related code - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 2.3.0 (2025-02-08) diff --git a/lib/grape/error_formatter/jsonapi.rb b/lib/grape/error_formatter/jsonapi.rb deleted file mode 100644 index ed1d2d30f..000000000 --- a/lib/grape/error_formatter/jsonapi.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -module Grape - module ErrorFormatter - class Jsonapi < Json; end - end -end diff --git a/lib/grape/parser/jsonapi.rb b/lib/grape/parser/jsonapi.rb deleted file mode 100644 index 58e16571b..000000000 --- a/lib/grape/parser/jsonapi.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -module Grape - module Parser - class Jsonapi < Json; end - end -end diff --git a/spec/grape/middleware/exception_spec.rb b/spec/grape/middleware/exception_spec.rb index b209b726f..ccd48b7b7 100644 --- a/spec/grape/middleware/exception_spec.rb +++ b/spec/grape/middleware/exception_spec.rb @@ -162,27 +162,6 @@ def call(_env) end end - context do - let(:running_app) { exception_app } - let(:options) { { rescue_all: true, format: :jsonapi } } - - it 'is possible to return errors in jsonapi format' do - get '/' - expect(last_response.body).to eq('{"error":"rain!"}') - end - end - - context do - let(:running_app) { error_hash_app } - let(:options) { { rescue_all: true, format: :jsonapi } } - - it 'is possible to return hash errors in jsonapi format' do - get '/' - expect(['{"error":"rain!","detail":"missing widget"}', - '{"detail":"missing widget","error":"rain!"}']).to include(last_response.body) - end - end - context do let(:running_app) { exception_app } let(:options) { { rescue_all: true, format: :xml } } diff --git a/spec/grape/middleware/formatter_spec.rb b/spec/grape/middleware/formatter_spec.rb index 28ec6c9aa..037e0915c 100644 --- a/spec/grape/middleware/formatter_spec.rb +++ b/spec/grape/middleware/formatter_spec.rb @@ -36,23 +36,6 @@ def to_json(*_args) end end - context 'jsonapi' do - let(:body) { { 'foos' => [{ 'bar' => 'baz' }] } } - let(:env) do - { Rack::PATH_INFO => '/somewhere', Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.api+json' } - end - - it 'calls #to_json if the content type is jsonapi' do - body.instance_eval do - def to_json(*_args) - '{"foos":[{"bar":"baz"}] }' - end - end - r = Rack::MockResponse[*subject.call(env)] - expect(r.body).to eq(Grape::Json.dump(body)) - end - end - context 'xml' do let(:body) { +'string' } let(:env) do From 5bb4c58e59b11d81df6ec36d0dd9a06167ef2e26 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Fri, 28 Mar 2025 23:36:50 +0100 Subject: [PATCH 297/304] Drop active support 6.0 (#2550) * Gemspec activesupport >= 6.1 * Add CHANGELOG.md --- CHANGELOG.md | 1 + grape.gemspec | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce18d325b..71813183b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * [#2537](https://github.com/ruby-grape/grape/pull/2537): Use activesupport `try` pattern - [@ericproulx](https://github.com/ericproulx). * [#2536](https://github.com/ruby-grape/grape/pull/2536): Update normalize_path like Rails - [@ericproulx](https://github.com/ericproulx). * [#2540](https://github.com/ruby-grape/grape/pull/2540): Introduce Params builder with symbolized short name - [@ericproulx](https://github.com/ericproulx). +* [#2550](https://github.com/ruby-grape/grape/pull/2550): Drop ActiveSupport 6.0 - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/grape.gemspec b/grape.gemspec index a5d7c57a0..5aca62670 100644 --- a/grape.gemspec +++ b/grape.gemspec @@ -21,7 +21,7 @@ Gem::Specification.new do |s| 'rubygems_mfa_required' => 'true' } - s.add_dependency 'activesupport', '>= 6' + s.add_dependency 'activesupport', '>= 6.1' s.add_dependency 'dry-types', '>= 1.1' s.add_dependency 'mustermann-grape', '~> 1.1.0' s.add_dependency 'rack', '>= 2' From 809add468f7c4c7262a54741318a055b19864400 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Fri, 28 Mar 2025 23:43:32 +0100 Subject: [PATCH 298/304] Delegate cookies management to Grape::Request (#2549) --- CHANGELOG.md | 1 + lib/grape/cookies.rb | 54 +++++++++++++++++------------ lib/grape/dsl/inside_route.rb | 12 ------- lib/grape/endpoint.rb | 47 ++++++++++--------------- lib/grape/request.rb | 5 +++ spec/grape/dsl/inside_route_spec.rb | 6 ---- 6 files changed, 56 insertions(+), 69 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71813183b..92a838ba7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * [#2536](https://github.com/ruby-grape/grape/pull/2536): Update normalize_path like Rails - [@ericproulx](https://github.com/ericproulx). * [#2540](https://github.com/ruby-grape/grape/pull/2540): Introduce Params builder with symbolized short name - [@ericproulx](https://github.com/ericproulx). * [#2550](https://github.com/ruby-grape/grape/pull/2550): Drop ActiveSupport 6.0 - [@ericproulx](https://github.com/ericproulx). +* [#2549](https://github.com/ruby-grape/grape/pull/2549): Delegate cookies management to `Grape::Request` - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/lib/grape/cookies.rb b/lib/grape/cookies.rb index 102b09047..039bf4c4f 100644 --- a/lib/grape/cookies.rb +++ b/lib/grape/cookies.rb @@ -2,41 +2,49 @@ module Grape class Cookies - def initialize - @cookies = {} - @send_cookies = {} - end + extend Forwardable - def read(request) - request.cookies.each do |name, value| - @cookies[name.to_s] = value - end + DELETED_COOKIES_ATTRS = { + max_age: '0', + value: '', + expires: Time.at(0) + }.freeze + + def_delegators :cookies, :[], :each + + def initialize(rack_cookies) + @cookies = rack_cookies + @send_cookies = nil end - def write(header) - @cookies.select { |key, _value| @send_cookies[key] == true }.each do |name, value| - cookie_value = value.is_a?(Hash) ? value : { value: value } - Rack::Utils.set_cookie_header! header, name, cookie_value + def response_cookies + return unless @send_cookies + + send_cookies.each do |name| + yield name, cookies[name] end end - def [](name) - @cookies[name.to_s] + def []=(name, value) + cookies[name] = value + send_cookies << name end - def []=(name, value) - @cookies[name.to_s] = value - @send_cookies[name.to_s] = true + # see https://github.com/rack/rack/blob/main/lib/rack/utils.rb#L338-L340 + def delete(name, **opts) + self.[]=(name, opts.merge(DELETED_COOKIES_ATTRS)) end - def each(&block) - @cookies.each(&block) + private + + def cookies + return @cookies unless @cookies.is_a?(Proc) + + @cookies = @cookies.call.with_indifferent_access end - # see https://github.com/rack/rack/blob/main/lib/rack/utils.rb#L338-L340 - def delete(name, **opts) - options = opts.merge(max_age: '0', value: '', expires: Time.at(0)) - self.[]=(name, options) + def send_cookies + @send_cookies ||= Set.new end end end diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index b772654be..61acd7343 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -257,18 +257,6 @@ def content_type(val = nil) end end - # Set or get a cookie - # - # @example - # cookies[:mycookie] = 'mycookie val' - # cookies['mycookie-string'] = 'mycookie string val' - # cookies[:more] = { value: '123', expires: Time.at(0) } - # cookies.delete :more - # - def cookies - @cookies ||= Cookies.new - end - # Allows you to define the response body as something other than the # return value. # diff --git a/lib/grape/endpoint.rb b/lib/grape/endpoint.rb index d7f60e9fe..a97181fe3 100644 --- a/lib/grape/endpoint.rb +++ b/lib/grape/endpoint.rb @@ -13,7 +13,8 @@ class Endpoint attr_accessor :block, :source, :options attr_reader :env, :request - def_delegators :request, :params, :headers + def_delegators :request, :params, :headers, :cookies + def_delegator :cookies, :response_cookies class << self def new(...) @@ -164,10 +165,9 @@ def mount_in(router) def to_routes default_route_options = prepare_default_route_attributes - default_path_settings = prepare_default_path_settings map_routes do |method, raw_path| - prepared_path = Path.new(raw_path, namespace, default_path_settings) + prepared_path = Path.new(raw_path, namespace, prepare_default_path_settings) params = options[:route_options].present? ? options[:route_options].merge(default_route_options) : default_route_options route = Grape::Router::Route.new(method, prepared_path.origin, prepared_path.suffix, params) route.apply(self) @@ -248,18 +248,16 @@ def inspect def run ActiveSupport::Notifications.instrument('endpoint_run.grape', endpoint: self, env: env) do - @header = Grape::Util::Header.new @request = Grape::Request.new(env, build_params_with: namespace_inheritable(:build_params_with)) begin - cookies.read(@request) self.class.run_before_each(self) run_filters befores, :before - if (allowed_methods = env[Grape::Env::GRAPE_ALLOWED_METHODS]) - allow_header_value = allowed_methods.join(', ') - raise Grape::Exceptions::MethodNotAllowed.new(header.merge('Allow' => allow_header_value)) unless options? + if env.key?(Grape::Env::GRAPE_ALLOWED_METHODS) + header['Allow'] = env[Grape::Env::GRAPE_ALLOWED_METHODS].join(', ') + raise Grape::Exceptions::MethodNotAllowed.new(header) unless options? - header Grape::Http::Headers::ALLOW, allow_header_value + header Grape::Http::Headers::ALLOW, header['Allow'] response_object = '' status 204 else @@ -270,7 +268,7 @@ def run end run_filters afters, :after - cookies.write(header) + build_response_cookies # status verifies body presence when DELETE @body ||= response_object @@ -332,24 +330,10 @@ def run_filters(filters, type = :other) extend post_extension if post_extension end - def befores - namespace_stackable(:befores) - end - - def before_validations - namespace_stackable(:before_validations) - end - - def after_validations - namespace_stackable(:after_validations) - end - - def afters - namespace_stackable(:afters) - end - - def finallies - namespace_stackable(:finallies) + %i[befores before_validations after_validations afters finallies].each do |method| + define_method method do + namespace_stackable(method) + end end def validations @@ -417,5 +401,12 @@ def build_helpers Module.new { helpers.each { |mod_to_include| include mod_to_include } } end + + def build_response_cookies + response_cookies do |name, value| + cookie_value = value.is_a?(Hash) ? value : { value: value } + Rack::Utils.set_cookie_header! header, name, cookie_value + end + end end end diff --git a/lib/grape/request.rb b/lib/grape/request.rb index 62bf1c8db..1b9469c8a 100644 --- a/lib/grape/request.rb +++ b/lib/grape/request.rb @@ -6,6 +6,7 @@ class Request < Rack::Request HTTP_PREFIX = 'HTTP_' alias rack_params params + alias rack_cookies cookies def initialize(env, build_params_with: nil) super(env) @@ -20,6 +21,10 @@ def headers @headers ||= build_headers end + def cookies + @cookies ||= Grape::Cookies.new(-> { rack_cookies }) + end + # needs to be public until extensions param_builder are removed def grape_routing_args # preserve version from query string parameters diff --git a/spec/grape/dsl/inside_route_spec.rb b/spec/grape/dsl/inside_route_spec.rb index ea8d6d987..9e72e8087 100644 --- a/spec/grape/dsl/inside_route_spec.rb +++ b/spec/grape/dsl/inside_route_spec.rb @@ -165,12 +165,6 @@ def initialize end end - describe '#cookies' do - it 'returns an instance of Cookies' do - expect(subject.cookies).to be_a Grape::Cookies - end - end - describe '#body' do describe 'set' do before do From 2b35fede4088885efc741acfc04a1b7acf338b68 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sat, 29 Mar 2025 22:41:19 +0100 Subject: [PATCH 299/304] Formatting from header acts like Versioning from header (#2548) --- CHANGELOG.md | 1 + UPGRADING.md | 19 ++++++++ lib/grape/middleware/formatter.rb | 47 +++++--------------- spec/grape/middleware/formatter_spec.rb | 58 ++++++++++++------------- 4 files changed, 58 insertions(+), 67 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92a838ba7..526c14d3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ * [#2543](https://github.com/ruby-grape/grape/pull/2543): Fix array allocation on mount - [@ericproulx](https://github.com/ericproulx). * [#2546](https://github.com/ruby-grape/grape/pull/2546): Fix middleware with keywords - [@ericproulx](https://github.com/ericproulx). * [#2547](https://github.com/ruby-grape/grape/pull/2547): Remove jsonapi related code - [@ericproulx](https://github.com/ericproulx). +* [#2548](https://github.com/ruby-grape/grape/pull/2548): Formatting from header acts like versioning from header - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 2.3.0 (2025-02-08) diff --git a/UPGRADING.md b/UPGRADING.md index a09d36ad6..a1d82bf80 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -6,6 +6,25 @@ Upgrading Grape - Passing a class to `build_with` or `Grape.config.param_builder` has been deprecated in favor of a symbolized short_name. See `SHORTNAME_LOOKUP` in [params_builder](lib/grape/params_builder.rb). - Including Grape's extensions like `Grape::Extensions::Hashie::Mash::ParamBuilder` has been deprecated in favor of using `build_with` at the route level. +#### Accept Header Negotiation Harmonized + +[Accept](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept) header is now fully interpreted through `Rack::Utils.best_q_match` which is following [RFC2616 14.1](https://datatracker.ietf.org/doc/html/rfc2616#section-14.1). Since [Grape 2.1.0](https://github.com/ruby-grape/grape/blob/master/CHANGELOG.md#210-20240615), the [header versioning strategy](https://github.com/ruby-grape/grape?tab=readme-ov-file#header) was adhering to it, but `Grape::Middleware::Formatter` never did. + +Your API might act differently since it will strictly follow the [RFC2616 14.1](https://datatracker.ietf.org/doc/html/rfc2616#section-14.1) when interpreting the `Accept` header. Here are the differences: + +###### Invalid or missing quality ranking +The following used to yield `application/xml` and now will yield `application/json` as the preferred media type: +- `application/json;q=invalid,application/xml;q=0.5` +- `application/json,application/xml;q=1.0` + +For the invalid case, the value `invalid` was automatically `to_f` and `invalid.to_f` equals `0.0`. Now, since it doesn't match [Rack's regex](https://github.com/rack/rack/blob/3-1-stable/lib/rack/utils.rb#L138), its interpreted as non provided and its quality ranking equals 1.0. + +For the non provided case, 1.0 was automatically assigned and in a case of multiple best matches, the first was returned based on Ruby's sort_by `quality`. Now, 1.0 is still assigned and the last is returned in case of multiple best matches. See [Rack's implementation](https://github.com/rack/rack/blob/e8f47608668d507e0f231a932fa37c9ca551c0a5/lib/rack/utils.rb#L167) of the RFC. + +###### Considering the closest generic when vendor tree +Excluding the [header versioning strategy](https://github.com/ruby-grape/grape?tab=readme-ov-file#header), whenever a media type with the [vendor tree](https://datatracker.ietf.org/doc/html/rfc6838#section-3.2) leading facet `vnd.` like `application/vnd.api+json` was provided, Grape would also consider its closest generic when negotiating. In that case, `application/json` was added to the negotiation. Now, it will just consider the provided media types without considering any closest generics, and you'll need to [register](https://github.com/ruby-grape/grape?tab=readme-ov-file#api-formats) it. +You can find the official vendor tree registrations on [IANA](https://www.iana.org/assignments/media-types/media-types.xhtml) + ### Upgrading to >= 2.4.0 #### Custom Validators diff --git a/lib/grape/middleware/formatter.rb b/lib/grape/middleware/formatter.rb index 88a52afa5..b35500c9e 100644 --- a/lib/grape/middleware/formatter.rb +++ b/lib/grape/middleware/formatter.rb @@ -122,56 +122,31 @@ def read_rack_input(body) def negotiate_content_type fmt = format_from_extension || format_from_params || options[:format] || format_from_header || options[:default_format] if content_type_for(fmt) - env[Grape::Env::API_FORMAT] = fmt + env[Grape::Env::API_FORMAT] = fmt.to_sym else throw :error, status: 406, message: "The requested format '#{fmt}' is not supported." end end def format_from_extension - parts = request.path.split('.') + request_path = request.path.try(:scrub) + dot_pos = request_path.rindex('.') + return unless dot_pos - if parts.size > 1 - extension = parts.last - # avoid symbol memory leak on an unknown format - return extension.to_sym if content_type_for(extension) - end - nil + extension = request_path[dot_pos + 1..] + extension if content_type_for(extension) end def format_from_params - fmt = Rack::Utils.parse_nested_query(env[Rack::QUERY_STRING])[FORMAT] - # avoid symbol memory leak on an unknown format - return fmt.to_sym if content_type_for(fmt) - - fmt + Rack::Utils.parse_nested_query(env[Rack::QUERY_STRING])[FORMAT] end def format_from_header - mime_array.each do |t| - return mime_types[t] if mime_types.key?(t) - end - nil - end - - def mime_array - accept = env[Grape::Http::Headers::HTTP_ACCEPT] - return [] unless accept - - accept_into_mime_and_quality = %r{ - ( - \w+/[\w+.-]+) # eg application/vnd.example.myformat+xml - (?: - (?:;[^,]*?)? # optionally multiple formats in a row - ;\s*q=([\w.]+) # optional "quality" preference (eg q=0.5) - )? - }x - - vendor_prefix_pattern = /vnd\.[^+]+\+/ + accept_header = env[Grape::Http::Headers::HTTP_ACCEPT].try(:scrub) + return if accept_header.blank? - accept.scan(accept_into_mime_and_quality) - .sort_by { |_, quality_preference| -(quality_preference ? quality_preference.to_f : 1.0) } - .flat_map { |mime, _| [mime, mime.sub(vendor_prefix_pattern, '')] } + media_type = Rack::Utils.best_q_match(accept_header, mime_types.keys) + mime_types[media_type] if media_type end end end diff --git a/spec/grape/middleware/formatter_spec.rb b/spec/grape/middleware/formatter_spec.rb index 037e0915c..af72f029d 100644 --- a/spec/grape/middleware/formatter_spec.rb +++ b/spec/grape/middleware/formatter_spec.rb @@ -82,6 +82,12 @@ def to_xml end context 'detection' do + context 'when path contains invalid byte sequence' do + it 'does not raise an exception' do + expect { subject.call(Rack::PATH_INFO => "/info.\x80") }.not_to raise_error + end + end + it 'uses the xml extension if one is provided' do subject.call(Rack::PATH_INFO => '/info.xml') expect(subject.env[Grape::Env::API_FORMAT]).to eq(:xml) @@ -95,8 +101,6 @@ def to_xml it 'uses the format parameter if one is provided' do subject.call(Rack::PATH_INFO => '/info', Rack::QUERY_STRING => 'format=json') expect(subject.env[Grape::Env::API_FORMAT]).to eq(:json) - subject.call(Rack::PATH_INFO => '/info', Rack::QUERY_STRING => 'format=xml') - expect(subject.env[Grape::Env::API_FORMAT]).to eq(:xml) end it 'uses the default format if none is provided' do @@ -116,6 +120,12 @@ def to_xml end context 'accept header detection' do + context 'when header contains invalid byte sequence' do + it 'does not raise an exception' do + expect { subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => "Hello \x80") }.not_to raise_error + end + end + it 'detects from the Accept header' do subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/xml') expect(subject.env[Grape::Env::API_FORMAT]).to eq(:xml) @@ -131,10 +141,10 @@ def to_xml it 'handles quality rankings mixed with nothing' do subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/json,application/xml; q=1.0') - expect(subject.env[Grape::Env::API_FORMAT]).to eq(:json) + expect(subject.env[Grape::Env::API_FORMAT]).to eq(:xml) subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/xml; q=1.0,application/json') - expect(subject.env[Grape::Env::API_FORMAT]).to eq(:xml) + expect(subject.env[Grape::Env::API_FORMAT]).to eq(:json) end it 'handles quality rankings that have a default 1.0 value' do @@ -156,30 +166,21 @@ def to_xml expect(subject.env[Grape::Env::API_FORMAT]).to eq(:xml) end - it 'ignores invalid quality rankings' do - subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/json;q=invalid,application/xml;q=0.5') - expect(subject.env[Grape::Env::API_FORMAT]).to eq(:xml) - subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/xml;q=0.5,application/json;q=invalid') - expect(subject.env[Grape::Env::API_FORMAT]).to eq(:xml) - - subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/json;q=,application/xml;q=0.5') - expect(subject.env[Grape::Env::API_FORMAT]).to eq(:json) - - subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/json;q=nil,application/xml;q=0.5') - expect(subject.env[Grape::Env::API_FORMAT]).to eq(:xml) - end - - it 'parses headers with vendor and api version' do - subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.test-v1+xml') - expect(subject.env[Grape::Env::API_FORMAT]).to eq(:xml) - end - context 'with custom vendored content types' do - subject { described_class.new(app, content_types: { custom: 'application/vnd.test+json' }) } + context 'when registered' do + subject { described_class.new(app, content_types: { custom: 'application/vnd.test+json' }) } - it 'uses the custom type' do - subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.test+json') - expect(subject.env[Grape::Env::API_FORMAT]).to eq(:custom) + it 'uses the custom type' do + subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.test+json') + expect(subject.env[Grape::Env::API_FORMAT]).to eq(:custom) + end + end + + context 'when unregistered' do + it 'returns the default content type text/plain' do + r = Rack::MockResponse[*subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.test+json')] + expect(r.headers[Rack::CONTENT_TYPE]).to eq('text/plain') + end end end @@ -216,11 +217,6 @@ def to_xml _, headers, = s.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.test+json') expect(headers[Rack::CONTENT_TYPE]).to eq('application/vnd.test+json') end - - it 'is set to closest generic for custom vendored/versioned without registered type' do - _, headers, = subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.test+json') - expect(headers[Rack::CONTENT_TYPE]).to eq('application/json') - end end context 'format' do From 67ea1524074b010b649d96dfbd94378640afadcc Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sat, 5 Apr 2025 21:07:42 +0200 Subject: [PATCH 300/304] Fix declared_hash_attr when passed_params is not an Hash (#2552) * Fix declared_hash_attr where passed_params could not respond to `try` * Add CHANGELOG.md Fix rubocop --- CHANGELOG.md | 1 + lib/grape/dsl/inside_route.rb | 2 +- spec/grape/endpoint/declared_spec.rb | 22 ++++++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 526c14d3d..5f7a46533 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ * [#2546](https://github.com/ruby-grape/grape/pull/2546): Fix middleware with keywords - [@ericproulx](https://github.com/ericproulx). * [#2547](https://github.com/ruby-grape/grape/pull/2547): Remove jsonapi related code - [@ericproulx](https://github.com/ericproulx). * [#2548](https://github.com/ruby-grape/grape/pull/2548): Formatting from header acts like versioning from header - [@ericproulx](https://github.com/ericproulx). +* [#2552](https://github.com/ruby-grape/grape/pull/2552): Fix declared params optional array - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 2.3.0 (2025-02-08) diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index 61acd7343..c5861cc92 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -79,7 +79,7 @@ def declared_hash_attr(passed_params, options, declared_param, params_nested_pat else # If it is not a Hash then it does not have children. # Find its value or set it to nil. - return unless options[:include_missing] || passed_params.key?(declared_param) + return unless options[:include_missing] || passed_params.try(:key?, declared_param) rename_path = params_nested_path + [declared_param.to_s] renamed_param_name = renamed_params[rename_path] diff --git a/spec/grape/endpoint/declared_spec.rb b/spec/grape/endpoint/declared_spec.rb index cf8b0c73f..538f8a302 100644 --- a/spec/grape/endpoint/declared_spec.rb +++ b/spec/grape/endpoint/declared_spec.rb @@ -856,4 +856,26 @@ end end end + + describe 'optional_array' do + subject { last_response } + + let(:app) do + Class.new(Grape::API) do + params do + requires :z, type: Array do + optional :a, type: Integer + end + end + + post do + declared(params, include_missing: false) + end + end + end + + before { post '/', { z: [] } } + + it { is_expected.to be_successful } + end end From 933e0ece06e462ec76fc3fb6d871c100db9cc67d Mon Sep 17 00:00:00 2001 From: Mark Delk Date: Mon, 7 Apr 2025 09:15:43 -0500 Subject: [PATCH 301/304] fix a typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b6d404a39..7fe411835 100644 --- a/README.md +++ b/README.md @@ -269,7 +269,7 @@ Grape's [deprecator](https://api.rubyonrails.org/v7.1.0/classes/ActiveSupport/De ### All -By default Grape will compile the routes on the first route, it is possible to pre-load routes using the `compile!` method. +By default Grape will compile the routes on the first route, but it is possible to pre-load routes using the `compile!` method. ```ruby Twitter::API.compile! From c42b66fbef822c12a20fc5733b5a9bc809eca95e Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Wed, 9 Apr 2025 04:20:45 +0200 Subject: [PATCH 302/304] Less query params parsing (#2553) --- CHANGELOG.md | 1 + UPGRADING.md | 5 ++ lib/grape/exceptions/conflicting_types.rb | 11 ++++ lib/grape/exceptions/invalid_parameters.rb | 11 ++++ lib/grape/exceptions/too_deep_parameters.rb | 11 ++++ lib/grape/locale/en.yml | 4 +- lib/grape/middleware/base.rb | 16 ++++- lib/grape/middleware/error.rb | 2 +- lib/grape/middleware/formatter.rb | 49 +++++++------- lib/grape/middleware/versioner/param.rb | 5 +- lib/grape/request.rb | 8 ++- spec/grape/middleware/base_spec.rb | 32 +++++++++ spec/grape/request_spec.rb | 72 +++++++++++++++++++++ 13 files changed, 193 insertions(+), 34 deletions(-) create mode 100644 lib/grape/exceptions/conflicting_types.rb create mode 100644 lib/grape/exceptions/invalid_parameters.rb create mode 100644 lib/grape/exceptions/too_deep_parameters.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f7a46533..478a6b4d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ * [#2547](https://github.com/ruby-grape/grape/pull/2547): Remove jsonapi related code - [@ericproulx](https://github.com/ericproulx). * [#2548](https://github.com/ruby-grape/grape/pull/2548): Formatting from header acts like versioning from header - [@ericproulx](https://github.com/ericproulx). * [#2552](https://github.com/ruby-grape/grape/pull/2552): Fix declared params optional array - [@ericproulx](https://github.com/ericproulx). +* [#2553](https://github.com/ruby-grape/grape/pull/2553): Improve performance of query params parsing - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 2.3.0 (2025-02-08) diff --git a/UPGRADING.md b/UPGRADING.md index a1d82bf80..9e7b8ade2 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,6 +1,11 @@ Upgrading Grape =============== +### Grape::Middleware::Base + +- Constant `TEXT_HTML` has been removed in favor of using literal string 'text/html'. +- `rack_request` and `query_params` have been added. Feel free to call these in your middlewares. + #### Params Builder - Passing a class to `build_with` or `Grape.config.param_builder` has been deprecated in favor of a symbolized short_name. See `SHORTNAME_LOOKUP` in [params_builder](lib/grape/params_builder.rb). diff --git a/lib/grape/exceptions/conflicting_types.rb b/lib/grape/exceptions/conflicting_types.rb new file mode 100644 index 000000000..9228cd903 --- /dev/null +++ b/lib/grape/exceptions/conflicting_types.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Grape + module Exceptions + class ConflictingTypes < Base + def initialize + super(message: compose_message(:conflicting_types), status: 400) + end + end + end +end diff --git a/lib/grape/exceptions/invalid_parameters.rb b/lib/grape/exceptions/invalid_parameters.rb new file mode 100644 index 000000000..c060c8c08 --- /dev/null +++ b/lib/grape/exceptions/invalid_parameters.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Grape + module Exceptions + class InvalidParameters < Base + def initialize + super(message: compose_message(:invalid_parameters), status: 400) + end + end + end +end diff --git a/lib/grape/exceptions/too_deep_parameters.rb b/lib/grape/exceptions/too_deep_parameters.rb new file mode 100644 index 000000000..c71267705 --- /dev/null +++ b/lib/grape/exceptions/too_deep_parameters.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Grape + module Exceptions + class TooDeepParameters < Base + def initialize(limit) + super(message: compose_message(:too_deep_parameters, limit: limit), status: 400) + end + end + end +end diff --git a/lib/grape/locale/en.yml b/lib/grape/locale/en.yml index 35cdeef4e..212e98999 100644 --- a/lib/grape/locale/en.yml +++ b/lib/grape/locale/en.yml @@ -58,4 +58,6 @@ en: problem: 'invalid version header' resolution: '%{message}' invalid_response: 'Invalid response' - query_parsing: 'query params are not parsable' + conflicting_types: 'query params contains conflicting types' + invalid_parameters: 'query params contains invalid format or byte sequence' + too_deep_parameters: 'query params are recursively nested over the specified limit (%{limit})' diff --git a/lib/grape/middleware/base.rb b/lib/grape/middleware/base.rb index af7195f86..d7a50d598 100644 --- a/lib/grape/middleware/base.rb +++ b/lib/grape/middleware/base.rb @@ -8,8 +8,6 @@ class Base attr_reader :app, :env, :options - TEXT_HTML = 'text/html' - # @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) @@ -54,6 +52,10 @@ def before; end # @return [Response, nil] a Rack SPEC response or nil to call the application afterwards. def after; end + def rack_request + @rack_request ||= Rack::Request.new(env) + end + def response return @app_response if @app_response.is_a?(Rack::Response) @@ -73,7 +75,15 @@ def content_type_for(format) end def content_type - content_type_for(env[Grape::Env::API_FORMAT] || options[:format]) || TEXT_HTML + content_type_for(env[Grape::Env::API_FORMAT] || options[:format]) || 'text/html' + end + + def query_params + rack_request.GET + rescue Rack::QueryParser::ParamsTooDeepError + raise Grape::Exceptions::TooDeepParameters.new(Rack::Utils.param_depth_limit) + rescue Rack::Utils::ParameterTypeError + raise Grape::Exceptions::ConflictingTypes end private diff --git a/lib/grape/middleware/error.rb b/lib/grape/middleware/error.rb index 2dd39bc16..56a6d5443 100644 --- a/lib/grape/middleware/error.rb +++ b/lib/grape/middleware/error.rb @@ -39,7 +39,7 @@ def call!(env) private def rack_response(status, headers, message) - message = Rack::Utils.escape_html(message) if headers[Rack::CONTENT_TYPE] == TEXT_HTML + message = Rack::Utils.escape_html(message) if headers[Rack::CONTENT_TYPE] == 'text/html' Rack::Response.new(Array.wrap(message), Rack::Utils.status_code(status), Grape::Util::Header.new.merge(headers)) end diff --git a/lib/grape/middleware/formatter.rb b/lib/grape/middleware/formatter.rb index b35500c9e..73b12b454 100644 --- a/lib/grape/middleware/formatter.rb +++ b/lib/grape/middleware/formatter.rb @@ -3,9 +3,6 @@ module Grape module Middleware class Formatter < Base - CHUNKED = 'chunked' - FORMAT = 'format' - def default_options { default_format: :txt, @@ -69,34 +66,27 @@ def ensure_content_type(headers) end end - def request - @request ||= Rack::Request.new(env) - end - - # store read input in env['api.request.input'] def read_body_input - return unless - (request.post? || request.put? || request.patch? || request.delete?) && - (!request.form_data? || !request.media_type) && - !request.parseable_data? && - (request.content_length.to_i.positive? || request.env[Grape::Http::Headers::HTTP_TRANSFER_ENCODING] == CHUNKED) - - return unless (input = env[Rack::RACK_INPUT]) + input = rack_request.body # reads RACK_INPUT + return if input.nil? + return unless read_body_input? input.try(:rewind) body = env[Grape::Env::API_REQUEST_INPUT] = input.read begin - read_rack_input(body) if body && !body.empty? + read_rack_input(body) ensure input.try(:rewind) end end - # store parsed input in env['api.request.body'] def read_rack_input(body) - fmt = request.media_type ? mime_types[request.media_type] : options[:default_format] + return if body.empty? + + media_type = rack_request.media_type + fmt = media_type ? mime_types[media_type] : options[:default_format] - throw :error, status: 415, message: "The provided content-type '#{request.media_type}' is not supported." unless content_type_for(fmt) + throw :error, status: 415, message: "The provided content-type '#{media_type}' is not supported." unless content_type_for(fmt) parser = Grape::Parser.parser_for fmt, options[:parsers] if parser begin @@ -119,8 +109,21 @@ def read_rack_input(body) end end + # this middleware will not try to format the following content-types since Rack already handles them + # when calling Rack's `params` function + # - application/x-www-form-urlencoded + # - multipart/form-data + # - multipart/related + # - multipart/mixed + def read_body_input? + (rack_request.post? || rack_request.put? || rack_request.patch? || rack_request.delete?) && + !(rack_request.form_data? && rack_request.content_type) && + !rack_request.parseable_data? && + (rack_request.content_length.to_i.positive? || rack_request.env[Grape::Http::Headers::HTTP_TRANSFER_ENCODING] == 'chunked') + end + def negotiate_content_type - fmt = format_from_extension || format_from_params || options[:format] || format_from_header || options[:default_format] + fmt = format_from_extension || query_params['format'] || options[:format] || format_from_header || options[:default_format] if content_type_for(fmt) env[Grape::Env::API_FORMAT] = fmt.to_sym else @@ -129,7 +132,7 @@ def negotiate_content_type end def format_from_extension - request_path = request.path.try(:scrub) + request_path = rack_request.path.try(:scrub) dot_pos = request_path.rindex('.') return unless dot_pos @@ -137,10 +140,6 @@ def format_from_extension extension if content_type_for(extension) end - def format_from_params - Rack::Utils.parse_nested_query(env[Rack::QUERY_STRING])[FORMAT] - end - def format_from_header accept_header = env[Grape::Http::Headers::HTTP_ACCEPT].try(:scrub) return if accept_header.blank? diff --git a/lib/grape/middleware/versioner/param.rb b/lib/grape/middleware/versioner/param.rb index 771faf616..102d30c55 100644 --- a/lib/grape/middleware/versioner/param.rb +++ b/lib/grape/middleware/versioner/param.rb @@ -20,12 +20,11 @@ module Versioner # env['api.version'] => 'v1' class Param < Base def before - potential_version = Rack::Utils.parse_nested_query(env[Rack::QUERY_STRING])[parameter_key] + potential_version = query_params[parameter_key] return if potential_version.blank? version_not_found! unless potential_version_match?(potential_version) - env[Grape::Env::API_VERSION] = potential_version - env[Rack::RACK_REQUEST_QUERY_HASH].delete(parameter_key) if env.key? Rack::RACK_REQUEST_QUERY_HASH + env[Grape::Env::API_VERSION] = env[Rack::RACK_REQUEST_QUERY_HASH].delete(parameter_key) end end end diff --git a/lib/grape/request.rb b/lib/grape/request.rb index 1b9469c8a..45ee801e7 100644 --- a/lib/grape/request.rb +++ b/lib/grape/request.rb @@ -37,8 +37,14 @@ def make_params @params_builder.call(rack_params).deep_merge!(grape_routing_args) rescue EOFError raise Grape::Exceptions::EmptyMessageBody.new(content_type) - rescue Rack::Multipart::MultipartPartLimitError + rescue Rack::Multipart::MultipartPartLimitError, Rack::Multipart::MultipartTotalPartLimitError raise Grape::Exceptions::TooManyMultipartFiles.new(Rack::Utils.multipart_part_limit) + rescue Rack::QueryParser::ParamsTooDeepError + raise Grape::Exceptions::TooDeepParameters.new(Rack::Utils.param_depth_limit) + rescue Rack::Utils::ParameterTypeError + raise Grape::Exceptions::ConflictingTypes + rescue Rack::Utils::InvalidParameterError + raise Grape::Exceptions::InvalidParameters end def build_headers diff --git a/spec/grape/middleware/base_spec.rb b/spec/grape/middleware/base_spec.rb index a3acc39d5..7bfedcfe6 100644 --- a/spec/grape/middleware/base_spec.rb +++ b/spec/grape/middleware/base_spec.rb @@ -223,4 +223,36 @@ def after expect(last_response.headers['X-Test-Overwriting']).to eq('Bye') end end + + describe 'query_params' do + let(:dummy_middleware) do + Class.new(Grape::Middleware::Base) do + def before + query_params + end + end + end + + let(:app) do + context = self + Rack::Builder.app do + use context.dummy_middleware + run ->(_) { [200, {}, ['Yeah']] } + end + end + + context 'when query params are conflicting' do + it 'raises an ConflictingTypes error' do + expect { get '/?x[y]=1&x[y]z=2' }.to raise_error(Grape::Exceptions::ConflictingTypes) + end + end + + context 'when query params is over the specified limit' do + let(:query_params) { "foo#{'[a]' * Rack::Utils.param_depth_limit}=bar" } + + it 'raises an ConflictingTypes error' do + expect { get "/?foo#{'[a]' * Rack::Utils.param_depth_limit}=bar" }.to raise_error(Grape::Exceptions::TooDeepParameters) + end + end + end end diff --git a/spec/grape/request_spec.rb b/spec/grape/request_spec.rb index 3ed8d8f03..4e00747cf 100644 --- a/spec/grape/request_spec.rb +++ b/spec/grape/request_spec.rb @@ -59,6 +59,78 @@ expect(request.params).to eq(ActiveSupport::HashWithIndifferentAccess.new(a: '123', b: 'xyz', c: 'ccc')) end end + + context 'when rack_params raises an EOF error' do + before do + allow(request).to receive(:rack_params).and_raise(EOFError) + end + + let(:message) { Grape::Exceptions::EmptyMessageBody.new(nil).to_s } + + it 'raises an Grape::Exceptions::EmptyMessageBody' do + expect { request.params }.to raise_error(Grape::Exceptions::EmptyMessageBody, message) + end + end + + context 'when rack_params raises a Rack::Multipart::MultipartPartLimitError' do + before do + allow(request).to receive(:rack_params).and_raise(Rack::Multipart::MultipartPartLimitError) + end + + let(:message) { Grape::Exceptions::TooManyMultipartFiles.new(Rack::Utils.multipart_part_limit).to_s } + + it 'raises an Rack::Multipart::MultipartPartLimitError' do + expect { request.params }.to raise_error(Grape::Exceptions::TooManyMultipartFiles, message) + end + end + + context 'when rack_params raises a Rack::Multipart::MultipartTotalPartLimitError' do + before do + allow(request).to receive(:rack_params).and_raise(Rack::Multipart::MultipartTotalPartLimitError) + end + + let(:message) { Grape::Exceptions::TooManyMultipartFiles.new(Rack::Utils.multipart_part_limit).to_s } + + it 'raises an Rack::Multipart::MultipartPartLimitError' do + expect { request.params }.to raise_error(Grape::Exceptions::TooManyMultipartFiles, message) + end + end + + context 'when rack_params raises a Rack::QueryParser::ParamsTooDeepError' do + before do + allow(request).to receive(:rack_params).and_raise(Rack::QueryParser::ParamsTooDeepError) + end + + let(:message) { Grape::Exceptions::TooDeepParameters.new(Rack::Utils.param_depth_limit).to_s } + + it 'raises a Grape::Exceptions::TooDeepParameters' do + expect { request.params }.to raise_error(Grape::Exceptions::TooDeepParameters, message) + end + end + + context 'when rack_params raises a Rack::Utils::ParameterTypeError' do + before do + allow(request).to receive(:rack_params).and_raise(Rack::Utils::ParameterTypeError) + end + + let(:message) { Grape::Exceptions::ConflictingTypes.new.to_s } + + it 'raises a Grape::Exceptions::ConflictingTypes' do + expect { request.params }.to raise_error(Grape::Exceptions::ConflictingTypes, message) + end + end + + context 'when rack_params raises a Rack::Utils::InvalidParameterError' do + before do + allow(request).to receive(:rack_params).and_raise(Rack::Utils::InvalidParameterError) + end + + let(:message) { Grape::Exceptions::InvalidParameters.new.to_s } + + it 'raises an Rack::Multipart::MultipartPartLimitError' do + expect { request.params }.to raise_error(Grape::Exceptions::InvalidParameters, message) + end + end end describe '#headers' do From 1a75b2862d217c41757b6943feb1b2297eae0a63 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Thu, 10 Apr 2025 14:24:50 +0200 Subject: [PATCH 303/304] Remove Grape::Http::Headers (#2554) * Move HTTP_HEADERS in Grape::Request and renew known headers Use plain HTTP_ or real header name instead of referencing to Http::Headers:: Remove Grape::Util::Lazy::Object * Add few headers * Add other headers * Add other known headers and sort * CHANGELOG.md entry * Fix CHANGELOG.md Add UPGRADING notes Add X-Access-Token in list of headers --- CHANGELOG.md | 1 + UPGRADING.md | 14 +- lib/grape.rb | 10 ++ lib/grape/api/instance.rb | 2 +- lib/grape/dsl/headers.rb | 2 +- lib/grape/dsl/inside_route.rb | 6 +- lib/grape/dsl/routing.rb | 2 +- lib/grape/endpoint.rb | 2 +- lib/grape/http/headers.rb | 56 ------- lib/grape/middleware/formatter.rb | 4 +- .../versioner/accept_version_header.rb | 4 +- lib/grape/middleware/versioner/base.rb | 4 +- lib/grape/middleware/versioner/header.rb | 2 +- lib/grape/request.rb | 142 +++++++++++++++++- lib/grape/router.rb | 6 +- lib/grape/router/route.rb | 2 +- lib/grape/util/lazy/object.rb | 45 ------ spec/grape/api/patch_method_helpers_spec.rb | 8 +- spec/grape/api_spec.rb | 24 +-- spec/grape/dsl/inside_route_spec.rb | 12 +- spec/grape/endpoint_spec.rb | 4 +- .../exceptions/invalid_accept_header_spec.rb | 48 +++--- spec/grape/middleware/formatter_spec.rb | 52 +++---- .../versioner/accept_version_header_spec.rb | 28 ++-- .../grape/middleware/versioner/header_spec.rb | 68 ++++----- spec/integration/rack_3_0/headers_spec.rb | 12 +- spec/support/chunked_response.rb | 4 +- spec/support/versioned_helpers.rb | 4 +- 28 files changed, 307 insertions(+), 261 deletions(-) delete mode 100644 lib/grape/http/headers.rb delete mode 100644 lib/grape/util/lazy/object.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 478a6b4d5..e6b495d2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * [#2540](https://github.com/ruby-grape/grape/pull/2540): Introduce Params builder with symbolized short name - [@ericproulx](https://github.com/ericproulx). * [#2550](https://github.com/ruby-grape/grape/pull/2550): Drop ActiveSupport 6.0 - [@ericproulx](https://github.com/ericproulx). * [#2549](https://github.com/ruby-grape/grape/pull/2549): Delegate cookies management to `Grape::Request` - [@ericproulx](https://github.com/ericproulx). +* [#2554](https://github.com/ruby-grape/grape/pull/2554): Remove `Grape::Http::Headers` and `Grape::Util::Lazy::Object` - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/UPGRADING.md b/UPGRADING.md index 9e7b8ade2..2ace6622d 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,6 +1,18 @@ Upgrading Grape =============== +### Upgrading to >= 2.4.0 + +#### Grape::Http::Headers, Grape::Util::Lazy::Object + +Both have been removed. See [2554](https://github.com/ruby-grape/grape/pull/2554). +Here are the notable changes: + +- Constants like `HTTP_ACCEPT` have been replaced by their literal value. +- `SUPPORTED_METHODS` has been moved to `Grape` module. +- `HTTP_HEADERS` has been moved to `Grape::Request` and renamed `KNOWN_HEADERS`. The last has been refreshed with new headers, and it's not lazy anymore. +- `SUPPORTED_METHODS_WITHOUT_OPTIONS` and `find_supported_method` have been removed. + ### Grape::Middleware::Base - Constant `TEXT_HTML` has been removed in favor of using literal string 'text/html'. @@ -30,8 +42,6 @@ For the non provided case, 1.0 was automatically assigned and in a case of multi Excluding the [header versioning strategy](https://github.com/ruby-grape/grape?tab=readme-ov-file#header), whenever a media type with the [vendor tree](https://datatracker.ietf.org/doc/html/rfc6838#section-3.2) leading facet `vnd.` like `application/vnd.api+json` was provided, Grape would also consider its closest generic when negotiating. In that case, `application/json` was added to the negotiation. Now, it will just consider the provided media types without considering any closest generics, and you'll need to [register](https://github.com/ruby-grape/grape?tab=readme-ov-file#api-formats) it. You can find the official vendor tree registrations on [IANA](https://www.iana.org/assignments/media-types/media-types.xhtml) -### Upgrading to >= 2.4.0 - #### Custom Validators If you now receive an error of `'Grape::Validations.require_validator': unknown validator: your_custom_validation (Grape::Exceptions::UnknownValidator)` after upgrading to 2.4.0 then you will need to ensure that you require the `your_custom_validation` file before your Grape API code is loaded. diff --git a/lib/grape.rb b/lib/grape.rb index 91d3f3f8a..a1e6f511c 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -59,6 +59,16 @@ module Grape include ActiveSupport::Configurable + HTTP_SUPPORTED_METHODS = [ + Rack::GET, + Rack::POST, + Rack::PUT, + Rack::PATCH, + Rack::DELETE, + Rack::HEAD, + Rack::OPTIONS + ].freeze + def self.deprecator @deprecator ||= ActiveSupport::Deprecation.new('2.0', 'Grape') end diff --git a/lib/grape/api/instance.rb b/lib/grape/api/instance.rb index facd0f9f9..aa24263a6 100644 --- a/lib/grape/api/instance.rb +++ b/lib/grape/api/instance.rb @@ -164,7 +164,7 @@ def call(env) status, headers, response = @router.call(env) unless cascade? headers = Grape::Util::Header.new.merge(headers) - headers.delete(Grape::Http::Headers::X_CASCADE) + headers.delete('X-Cascade') end [status, headers, response] diff --git a/lib/grape/dsl/headers.rb b/lib/grape/dsl/headers.rb index a02bdd588..4c193364f 100644 --- a/lib/grape/dsl/headers.rb +++ b/lib/grape/dsl/headers.rb @@ -10,7 +10,7 @@ module Headers # 4. Delete a specifc header key-value pair def header(key = nil, val = nil) if key - val ? header[key.to_s] = val : header.delete(key.to_s) + val ? header[key] = val : header.delete(key) else @header ||= Grape::Util::Header.new end diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index c5861cc92..2d7568afa 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -213,7 +213,7 @@ def redirect(url, permanent: false, body: nil) status 302 body_message ||= "This resource has been moved temporarily to #{url}." end - header Grape::Http::Headers::LOCATION, url + header 'Location', url content_type 'text/plain' body body_message end @@ -330,7 +330,7 @@ def stream(value = nil) return if value.nil? && @stream.nil? header Rack::CONTENT_LENGTH, nil - header Grape::Http::Headers::TRANSFER_ENCODING, nil + header 'Transfer-Encoding', nil header Rack::CACHE_CONTROL, 'no-cache' # Skips ETag generation (reading the response up front) if value.is_a?(String) file_body = Grape::ServeStream::FileBody.new(value) @@ -439,7 +439,7 @@ def entity_representation_for(entity_class, object, options) end def http_version - env.fetch(Grape::Http::Headers::HTTP_VERSION) { env[Rack::SERVER_PROTOCOL] } + env.fetch('HTTP_VERSION') { env[Rack::SERVER_PROTOCOL] } end def api_format(format) diff --git a/lib/grape/dsl/routing.rb b/lib/grape/dsl/routing.rb index c2738f933..8c32db364 100644 --- a/lib/grape/dsl/routing.rb +++ b/lib/grape/dsl/routing.rb @@ -158,7 +158,7 @@ def route(methods, paths = ['/'], route_options = {}, &block) reset_validations! end - Grape::Http::Headers::SUPPORTED_METHODS.each do |supported_method| + Grape::HTTP_SUPPORTED_METHODS.each do |supported_method| define_method supported_method.downcase do |*args, &block| options = args.extract_options! paths = args.first || ['/'] diff --git a/lib/grape/endpoint.rb b/lib/grape/endpoint.rb index a97181fe3..2e5136fda 100644 --- a/lib/grape/endpoint.rb +++ b/lib/grape/endpoint.rb @@ -257,7 +257,7 @@ def run header['Allow'] = env[Grape::Env::GRAPE_ALLOWED_METHODS].join(', ') raise Grape::Exceptions::MethodNotAllowed.new(header) unless options? - header Grape::Http::Headers::ALLOW, header['Allow'] + header 'Allow', header['Allow'] response_object = '' status 204 else diff --git a/lib/grape/http/headers.rb b/lib/grape/http/headers.rb deleted file mode 100644 index eb4d38915..000000000 --- a/lib/grape/http/headers.rb +++ /dev/null @@ -1,56 +0,0 @@ -# frozen_string_literal: true - -module Grape - module Http - module Headers - HTTP_ACCEPT_VERSION = 'HTTP_ACCEPT_VERSION' - HTTP_ACCEPT = 'HTTP_ACCEPT' - HTTP_TRANSFER_ENCODING = 'HTTP_TRANSFER_ENCODING' - HTTP_VERSION = 'HTTP_VERSION' - - ALLOW = 'Allow' - LOCATION = 'Location' - X_CASCADE = 'X-Cascade' - TRANSFER_ENCODING = 'Transfer-Encoding' - - SUPPORTED_METHODS = [ - Rack::GET, - Rack::POST, - Rack::PUT, - Rack::PATCH, - Rack::DELETE, - Rack::HEAD, - Rack::OPTIONS - ].freeze - - SUPPORTED_METHODS_WITHOUT_OPTIONS = (SUPPORTED_METHODS - [Rack::OPTIONS]).freeze - - HTTP_HEADERS = Grape::Util::Lazy::Object.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 - - 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/middleware/formatter.rb b/lib/grape/middleware/formatter.rb index 73b12b454..359792a40 100644 --- a/lib/grape/middleware/formatter.rb +++ b/lib/grape/middleware/formatter.rb @@ -119,7 +119,7 @@ def read_body_input? (rack_request.post? || rack_request.put? || rack_request.patch? || rack_request.delete?) && !(rack_request.form_data? && rack_request.content_type) && !rack_request.parseable_data? && - (rack_request.content_length.to_i.positive? || rack_request.env[Grape::Http::Headers::HTTP_TRANSFER_ENCODING] == 'chunked') + (rack_request.content_length.to_i.positive? || rack_request.env['HTTP_TRANSFER_ENCODING'] == 'chunked') end def negotiate_content_type @@ -141,7 +141,7 @@ def format_from_extension end def format_from_header - accept_header = env[Grape::Http::Headers::HTTP_ACCEPT].try(:scrub) + accept_header = env['HTTP_ACCEPT'].try(:scrub) return if accept_header.blank? media_type = Rack::Utils.best_q_match(accept_header, mime_types.keys) diff --git a/lib/grape/middleware/versioner/accept_version_header.rb b/lib/grape/middleware/versioner/accept_version_header.rb index aa7a8b353..1d24388f1 100644 --- a/lib/grape/middleware/versioner/accept_version_header.rb +++ b/lib/grape/middleware/versioner/accept_version_header.rb @@ -18,9 +18,7 @@ module Versioner # route. class AcceptVersionHeader < Base def before - potential_version = env[Grape::Http::Headers::HTTP_ACCEPT_VERSION] - potential_version = potential_version.scrub unless potential_version.nil? - + potential_version = env['HTTP_ACCEPT_VERSION'].try(:scrub) not_acceptable!('Accept-Version header must be set.') if strict? && potential_version.blank? return if potential_version.blank? diff --git a/lib/grape/middleware/versioner/base.rb b/lib/grape/middleware/versioner/base.rb index 68604f14e..4cd18a8c0 100644 --- a/lib/grape/middleware/versioner/base.rb +++ b/lib/grape/middleware/versioner/base.rb @@ -66,7 +66,7 @@ def vendor end def error_headers - cascade? ? { Grape::Http::Headers::X_CASCADE => 'pass' } : {} + cascade? ? { 'X-Cascade' => 'pass' } : {} end def potential_version_match?(potential_version) @@ -74,7 +74,7 @@ def potential_version_match?(potential_version) end def version_not_found! - throw :error, status: 404, message: '404 API Version Not Found', headers: { Grape::Http::Headers::X_CASCADE => 'pass' } + throw :error, status: 404, message: '404 API Version Not Found', headers: { 'X-Cascade' => 'pass' } end end end diff --git a/lib/grape/middleware/versioner/header.rb b/lib/grape/middleware/versioner/header.rb index a34a80fc7..5470fe170 100644 --- a/lib/grape/middleware/versioner/header.rb +++ b/lib/grape/middleware/versioner/header.rb @@ -49,7 +49,7 @@ def match_best_quality_media_type! end def accept_header - env[Grape::Http::Headers::HTTP_ACCEPT] + env['HTTP_ACCEPT'] end def strict_header_checks! diff --git a/lib/grape/request.rb b/lib/grape/request.rb index 45ee801e7..06c389b75 100644 --- a/lib/grape/request.rb +++ b/lib/grape/request.rb @@ -3,7 +3,139 @@ module Grape class Request < Rack::Request DEFAULT_PARAMS_BUILDER = :hash_with_indifferent_access - HTTP_PREFIX = 'HTTP_' + # Based on rack 3 KNOWN_HEADERS + # https://github.com/rack/rack/blob/4f15e7b814922af79605be4b02c5b7c3044ba206/lib/rack/headers.rb#L10 + + KNOWN_HEADERS = %w[ + Accept + Accept-CH + Accept-Encoding + Accept-Language + Accept-Patch + Accept-Ranges + Accept-Version + Access-Control-Allow-Credentials + Access-Control-Allow-Headers + Access-Control-Allow-Methods + Access-Control-Allow-Origin + Access-Control-Expose-Headers + Access-Control-Max-Age + Age + Allow + Alt-Svc + Authorization + Cache-Control + Client-Ip + Connection + Content-Disposition + Content-Encoding + Content-Language + Content-Length + Content-Location + Content-MD5 + Content-Range + Content-Security-Policy + Content-Security-Policy-Report-Only + Content-Type + Cookie + Date + Delta-Base + Dnt + ETag + Expect-CT + Expires + Feature-Policy + Forwarded + Host + If-Modified-Since + If-None-Match + IM + Last-Modified + Link + Location + NEL + P3P + Permissions-Policy + Pragma + Preference-Applied + Proxy-Authenticate + Public-Key-Pins + Range + Referer + Referrer-Policy + Refresh + Report-To + Retry-After + Sec-Fetch-Dest + Sec-Fetch-Mode + Sec-Fetch-Site + Sec-Fetch-User + Server + Set-Cookie + Status + Strict-Transport-Security + Timing-Allow-Origin + Tk + Trailer + Transfer-Encoding + Upgrade + Upgrade-Insecure-Requests + User-Agent + Vary + Version + Via + Warning + WWW-Authenticate + X-Accel-Buffering + X-Accel-Charset + X-Accel-Expires + X-Accel-Limit-Rate + X-Accel-Mapping + X-Accel-Redirect + X-Access-Token + X-Auth-Request-Access-Token + X-Auth-Request-Email + X-Auth-Request-Groups + X-Auth-Request-Preferred-Username + X-Auth-Request-Redirect + X-Auth-Request-Token + X-Auth-Request-User + X-Cascade + X-Client-Ip + X-Content-Duration + X-Content-Security-Policy + X-Content-Type-Options + X-Correlation-Id + X-Download-Options + X-Forwarded-Access-Token + X-Forwarded-Email + X-Forwarded-For + X-Forwarded-Groups + X-Forwarded-Host + X-Forwarded-Port + X-Forwarded-Preferred-Username + X-Forwarded-Proto + X-Forwarded-Scheme + X-Forwarded-Ssl + X-Forwarded-Uri + X-Forwarded-User + X-Frame-Options + X-HTTP-Method-Override + X-Permitted-Cross-Domain-Policies + X-Powered-By + X-Real-IP + X-Redirect-By + X-Request-Id + X-Requested-With + X-Runtime + X-Sendfile + X-Sendfile-Type + X-UA-Compatible + X-WebKit-CS + X-XSS-Protection + ].each_with_object({}) do |header, response| + response["HTTP_#{header.upcase.tr('-', '_')}"] = header + end.freeze alias rack_params params alias rack_cookies cookies @@ -49,15 +181,11 @@ def make_params def build_headers each_header.with_object(Grape::Util::Header.new) do |(k, v), headers| - next unless k.start_with? HTTP_PREFIX + next unless k.start_with? 'HTTP_' - transformed_header = Grape::Http::Headers::HTTP_HEADERS[k] || transform_header(k) + transformed_header = KNOWN_HEADERS.fetch(k) { -k[5..].tr('_', '-').downcase } headers[transformed_header] = v end end - - def transform_header(header) - -header[5..].tr('_', '-').downcase - end end end diff --git a/lib/grape/router.rb b/lib/grape/router.rb index 99065b6e4..6a9e3f5df 100644 --- a/lib/grape/router.rb +++ b/lib/grape/router.rb @@ -42,7 +42,7 @@ def compile! @union = Regexp.union(@neutral_regexes) @neutral_regexes = nil - (Grape::Http::Headers::SUPPORTED_METHODS + ['*']).each do |method| + (Grape::HTTP_SUPPORTED_METHODS + ['*']).each do |method| next unless map.key?(method) routes = map[method] @@ -156,7 +156,7 @@ def with_optimization end def default_response - headers = Grape::Util::Header.new.merge(Grape::Http::Headers::X_CASCADE => 'pass') + headers = Grape::Util::Header.new.merge('X-Cascade' => 'pass') [404, headers, ['404 Not Found']] end @@ -180,7 +180,7 @@ def prepare_env_from_route(env, route) end def cascade?(response) - response && response[1][Grape::Http::Headers::X_CASCADE] == 'pass' + response && response[1]['X-Cascade'] == 'pass' end def string_for(input) diff --git a/lib/grape/router/route.rb b/lib/grape/router/route.rb index 48599610c..64105be9c 100644 --- a/lib/grape/router/route.rb +++ b/lib/grape/router/route.rb @@ -55,7 +55,7 @@ def params_without_input def upcase_method(method) method_s = method.to_s - Grape::Http::Headers::SUPPORTED_METHODS.detect { |m| m.casecmp(method_s).zero? } || method_s.upcase + Grape::HTTP_SUPPORTED_METHODS.detect { |m| m.casecmp(method_s).zero? } || method_s.upcase end end end diff --git a/lib/grape/util/lazy/object.rb b/lib/grape/util/lazy/object.rb deleted file mode 100644 index 6c10dadfc..000000000 --- a/lib/grape/util/lazy/object.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -# Based on https://github.com/HornsAndHooves/lazy_object - -module Grape - module Util - module Lazy - class Object < 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 -end diff --git a/spec/grape/api/patch_method_helpers_spec.rb b/spec/grape/api/patch_method_helpers_spec.rb index f91f028a5..ad0a162ba 100644 --- a/spec/grape/api/patch_method_helpers_spec.rb +++ b/spec/grape/api/patch_method_helpers_spec.rb @@ -49,12 +49,12 @@ def app context 'patch' do it 'public' do - patch '/', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.grape-public-v1+json' + patch '/', {}, 'HTTP_ACCEPT' => 'application/vnd.grape-public-v1+json' expect(last_response.status).to eq 405 end it 'private' do - patch '/', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.grape-private-v1+json' + patch '/', {}, 'HTTP_ACCEPT' => 'application/vnd.grape-private-v1+json' expect(last_response.status).to eq 405 end @@ -66,13 +66,13 @@ def app context 'default' do it 'public' do - get '/', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.grape-public-v1+json' + get '/', {}, 'HTTP_ACCEPT' => 'application/vnd.grape-public-v1+json' expect(last_response.status).to eq 200 expect(last_response.body).to eq({ ok: 'public' }.to_json) end it 'private' do - get '/', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.grape-private-v1+json' + get '/', {}, 'HTTP_ACCEPT' => 'application/vnd.grape-private-v1+json' expect(last_response.status).to eq 200 expect(last_response.body).to eq({ ok: 'private' }.to_json) end diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index 013e0741b..f16b31ee2 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -510,7 +510,7 @@ def to_txt subject.send(verb) do env[Grape::Env::API_REQUEST_INPUT] end - send verb, '/', Grape::Json.dump(object), 'CONTENT_TYPE' => 'application/json', Grape::Http::Headers::HTTP_TRANSFER_ENCODING => 'chunked' + send verb, '/', Grape::Json.dump(object), 'CONTENT_TYPE' => 'application/json', 'HTTP_TRANSFER_ENCODING' => 'chunked' expect(last_response.status).to eq(verb == :post ? 201 : 200) expect(last_response.body).to eql Grape::Json.dump(object).to_json end @@ -1307,7 +1307,7 @@ def to_txt expect(last_response.content_type).to eq('text/plain') expect(last_response.content_length).to be_nil expect(last_response.headers[Rack::CACHE_CONTROL]).to eq('no-cache') - expect(last_response.headers[Grape::Http::Headers::TRANSFER_ENCODING]).to eq('chunked') + expect(last_response.headers['Transfer-Encoding']).to eq('chunked') expect(last_response.body).to eq("c\r\nThis is some\r\nd\r\n file content\r\n0\r\n\r\n") end @@ -2886,7 +2886,7 @@ def self.call(message, _backtrace, _options, _env, _original_exception) end it 'uses custom formatter' do - get '/simple.custom', Grape::Http::Headers::HTTP_ACCEPT => 'application/custom' + get '/simple.custom', 'HTTP_ACCEPT' => 'application/custom' expect(last_response.body).to eql '{"custom_formatter":"hash"}' end end @@ -2915,7 +2915,7 @@ def self.call(object, _env) end it 'uses custom formatter' do - get '/simple.custom', Grape::Http::Headers::HTTP_ACCEPT => 'application/custom' + get '/simple.custom', 'HTTP_ACCEPT' => 'application/custom' expect(last_response.body).to eql '{"custom_formatter":"hash"}' end end @@ -3558,7 +3558,7 @@ def self.call(object, _env) it 'is able to cascade' do subject.mount lambda { |env| headers = {} - headers[Grape::Http::Headers::X_CASCADE] == 'pass' if env[Rack::PATH_INFO].exclude?('boo') + headers['X-Cascade'] == 'pass' if env[Rack::PATH_INFO].exclude?('boo') [200, headers, ['Farfegnugen']] } => '/' @@ -4058,7 +4058,7 @@ def my_method end it 'forces txt from a non-accepting header' do - get '/meaning_of_life', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/json' + get '/meaning_of_life', {}, 'HTTP_ACCEPT' => 'application/json' expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s) end end @@ -4087,7 +4087,7 @@ def my_method end it 'forces txt from a non-accepting header' do - get '/meaning_of_life', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/json' + get '/meaning_of_life', {}, 'HTTP_ACCEPT' => 'application/json' expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s) end end @@ -4112,7 +4112,7 @@ def my_method end it 'forces json from a non-accepting header' do - get '/meaning_of_life', {}, Grape::Http::Headers::HTTP_ACCEPT => 'text/html' + get '/meaning_of_life', {}, 'HTTP_ACCEPT' => 'text/html' expect(last_response.body).to eq({ meaning_of_life: 42 }.to_json) end @@ -4336,14 +4336,14 @@ def before subject.version 'v1', using: :path, cascade: true get '/v1/hello' expect(last_response).to be_not_found - expect(last_response.headers[Grape::Http::Headers::X_CASCADE]).to eq('pass') + expect(last_response.headers['X-Cascade']).to eq('pass') end it 'does not cascade' do subject.version 'v2', using: :path, cascade: false get '/v2/hello' expect(last_response).to be_not_found - expect(last_response.headers.keys).not_to include Grape::Http::Headers::X_CASCADE + expect(last_response.headers.keys).not_to include 'X-Cascade' end end @@ -4352,14 +4352,14 @@ def before subject.cascade true get '/hello' expect(last_response).to be_not_found - expect(last_response.headers[Grape::Http::Headers::X_CASCADE]).to eq('pass') + expect(last_response.headers['X-Cascade']).to eq('pass') end it 'does not cascade' do subject.cascade false get '/hello' expect(last_response).to be_not_found - expect(last_response.headers.keys).not_to include Grape::Http::Headers::X_CASCADE + expect(last_response.headers.keys).not_to include 'X-Cascade' end end end diff --git a/spec/grape/dsl/inside_route_spec.rb b/spec/grape/dsl/inside_route_spec.rb index 9e72e8087..5e5b8b5d0 100644 --- a/spec/grape/dsl/inside_route_spec.rb +++ b/spec/grape/dsl/inside_route_spec.rb @@ -69,7 +69,7 @@ def initialize end it 'sets location header' do - expect(subject.header[Grape::Http::Headers::LOCATION]).to eq '/' + expect(subject.header['Location']).to eq '/' end end @@ -83,7 +83,7 @@ def initialize end it 'sets location header' do - expect(subject.header[Grape::Http::Headers::LOCATION]).to eq '/' + expect(subject.header['Location']).to eq '/' end end end @@ -205,7 +205,7 @@ def initialize before do subject.header Rack::CACHE_CONTROL, 'cache' subject.header Rack::CONTENT_LENGTH, 123 - subject.header Grape::Http::Headers::TRANSFER_ENCODING, 'base64' + subject.header 'Transfer-Encoding', 'base64' subject.sendfile file_path end @@ -217,7 +217,7 @@ def initialize expect(subject.header).to match( Rack::CACHE_CONTROL => 'cache', Rack::CONTENT_LENGTH => 123, - Grape::Http::Headers::TRANSFER_ENCODING => 'base64' + 'Transfer-Encoding' => 'base64' ) end end @@ -249,7 +249,7 @@ def initialize before do subject.header Rack::CACHE_CONTROL, 'cache' subject.header Rack::CONTENT_LENGTH, 123 - subject.header Grape::Http::Headers::TRANSFER_ENCODING, 'base64' + subject.header 'Transfer-Encoding', 'base64' subject.stream file_path end @@ -272,7 +272,7 @@ def initialize before do subject.header Rack::CACHE_CONTROL, 'cache' subject.header Rack::CONTENT_LENGTH, 123 - subject.header Grape::Http::Headers::TRANSFER_ENCODING, 'base64' + subject.header 'Transfer-Encoding', 'base64' subject.stream stream_object end diff --git a/spec/grape/endpoint_spec.rb b/spec/grape/endpoint_spec.rb index bcdf5f62e..3f98aacb6 100644 --- a/spec/grape/endpoint_spec.rb +++ b/spec/grape/endpoint_spec.rb @@ -1021,12 +1021,12 @@ def memoized end it 'result in a 406 response if they are invalid' do - get '/test', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.ohanapi.v1+json' + get '/test', {}, 'HTTP_ACCEPT' => 'application/vnd.ohanapi.v1+json' expect(last_response.status).to eq(406) end it 'result in a 406 response if they cannot be parsed' do - get '/test', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.ohanapi.v1+json; version=1' + get '/test', {}, 'HTTP_ACCEPT' => 'application/vnd.ohanapi.v1+json; version=1' expect(last_response.status).to eq(406) end end diff --git a/spec/grape/exceptions/invalid_accept_header_spec.rb b/spec/grape/exceptions/invalid_accept_header_spec.rb index a0bf11139..29884f0dd 100644 --- a/spec/grape/exceptions/invalid_accept_header_spec.rb +++ b/spec/grape/exceptions/invalid_accept_header_spec.rb @@ -19,7 +19,7 @@ shared_examples_for 'a not-cascaded request' do it 'does not include the X-Cascade=pass header' do - expect(last_response.headers).not_to have_key(Grape::Http::Headers::X_CASCADE) + expect(last_response.headers).not_to have_key('X-Cascade') end it 'does not accept the request' do @@ -29,7 +29,7 @@ shared_examples_for 'a rescued request' do it 'does not include the X-Cascade=pass header' do - expect(last_response.headers[Grape::Http::Headers::X_CASCADE]).to be_nil + expect(last_response.headers['X-Cascade']).to be_nil end it 'does show rescue handler processing' do @@ -56,7 +56,7 @@ def app end context 'that received a request with correct vendor and version' do - before { get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendorname-v99' } + before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } it_behaves_like 'a valid request' end @@ -64,7 +64,7 @@ def app context 'that receives' do context 'an invalid vendor in the request' do before do - get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.invalidvendor-v99', + get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99', 'CONTENT_TYPE' => 'application/json' end @@ -88,20 +88,20 @@ def app end context 'that received a request with correct vendor and version' do - before { get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendorname-v99' } + before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } it_behaves_like 'a valid request' end context 'that receives' do context 'an invalid version in the request' do - before { get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendorname-v77' } + before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77' } it_behaves_like 'a not-cascaded request' end context 'an invalid vendor in the request' do - before { get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.invalidvendor-v99' } + before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99' } it_behaves_like 'a not-cascaded request' end @@ -131,7 +131,7 @@ def app end context 'that received a request with correct vendor and version' do - before { get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendorname-v99' } + before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } it_behaves_like 'a valid request' end @@ -139,7 +139,7 @@ def app context 'that receives' do context 'an invalid vendor in the request' do before do - get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.invalidvendor-v99', + get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99', 'CONTENT_TYPE' => 'application/json' end @@ -168,20 +168,20 @@ def app end context 'that received a request with correct vendor and version' do - before { get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendorname-v99' } + before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } it_behaves_like 'a valid request' end context 'that receives' do context 'an invalid version in the request' do - before { get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendorname-v77' } + before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77' } it_behaves_like 'a not-cascaded request' end context 'an invalid vendor in the request' do - before { get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.invalidvendor-v99' } + before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99' } it_behaves_like 'a not-cascaded request' end @@ -206,7 +206,7 @@ def app end context 'that received a request with correct vendor and version' do - before { get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendorname-v99' } + before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } it_behaves_like 'a valid request' end @@ -214,7 +214,7 @@ def app context 'that receives' do context 'an invalid version in the request' do before do - get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendorname-v77', + get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77', 'CONTENT_TYPE' => 'application/json' end @@ -223,7 +223,7 @@ def app context 'an invalid vendor in the request' do before do - get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.invalidvendor-v99', + get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99', 'CONTENT_TYPE' => 'application/json' end @@ -247,20 +247,20 @@ def app end context 'that received a request with correct vendor and version' do - before { get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendorname-v99' } + before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } it_behaves_like 'a valid request' end context 'that receives' do context 'an invalid version in the request' do - before { get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendorname-v77' } + before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77' } it_behaves_like 'a cascaded request' end context 'an invalid vendor in the request' do - before { get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.invalidvendor-v99' } + before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99' } it_behaves_like 'a cascaded request' end @@ -290,7 +290,7 @@ def app end context 'that received a request with correct vendor and version' do - before { get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendorname-v99' } + before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } it_behaves_like 'a valid request' end @@ -298,7 +298,7 @@ def app context 'that receives' do context 'an invalid version in the request' do before do - get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendorname-v77', + get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77', 'CONTENT_TYPE' => 'application/json' end @@ -307,7 +307,7 @@ def app context 'an invalid vendor in the request' do before do - get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.invalidvendor-v99', + get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99', 'CONTENT_TYPE' => 'application/json' end @@ -336,20 +336,20 @@ def app end context 'that received a request with correct vendor and version' do - before { get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendorname-v99' } + before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } it_behaves_like 'a valid request' end context 'that receives' do context 'an invalid version in the request' do - before { get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendorname-v77' } + before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77' } it_behaves_like 'a cascaded request' end context 'an invalid vendor in the request' do - before { get '/beer', {}, Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.invalidvendor-v99' } + before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99' } it_behaves_like 'a cascaded request' end diff --git a/spec/grape/middleware/formatter_spec.rb b/spec/grape/middleware/formatter_spec.rb index af72f029d..807edaad4 100644 --- a/spec/grape/middleware/formatter_spec.rb +++ b/spec/grape/middleware/formatter_spec.rb @@ -11,7 +11,7 @@ context 'serialization' do let(:body) { { 'abc' => 'def' } } let(:env) do - { Rack::PATH_INFO => '/somewhere', Grape::Http::Headers::HTTP_ACCEPT => 'application/json' } + { Rack::PATH_INFO => '/somewhere', 'HTTP_ACCEPT' => 'application/json' } end it 'looks at the bodies for possibly serializable data' do @@ -22,7 +22,7 @@ context 'default format' do let(:body) { ['foo'] } let(:env) do - { Rack::PATH_INFO => '/somewhere', Grape::Http::Headers::HTTP_ACCEPT => 'application/json' } + { Rack::PATH_INFO => '/somewhere', 'HTTP_ACCEPT' => 'application/json' } end it 'calls #to_json since default format is json' do @@ -39,7 +39,7 @@ def to_json(*_args) context 'xml' do let(:body) { +'string' } let(:env) do - { Rack::PATH_INFO => '/somewhere.xml', Grape::Http::Headers::HTTP_ACCEPT => 'application/json' } + { Rack::PATH_INFO => '/somewhere.xml', 'HTTP_ACCEPT' => 'application/json' } end it 'calls #to_xml if the content type is xml' do @@ -57,7 +57,7 @@ def to_xml context 'error handling' do let(:formatter) { double(:formatter) } let(:env) do - { Rack::PATH_INFO => '/somewhere.xml', Grape::Http::Headers::HTTP_ACCEPT => 'application/json' } + { Rack::PATH_INFO => '/somewhere.xml', 'HTTP_ACCEPT' => 'application/json' } end before do @@ -76,7 +76,7 @@ def to_xml allow(formatter).to receive(:call) { raise StandardError } expect do - catch(:error) { subject.call(Rack::PATH_INFO => '/somewhere.xml', Grape::Http::Headers::HTTP_ACCEPT => 'application/json') } + catch(:error) { subject.call(Rack::PATH_INFO => '/somewhere.xml', 'HTTP_ACCEPT' => 'application/json') } end.to raise_error(StandardError) end end @@ -109,12 +109,12 @@ def to_xml end it 'uses the requested format if provided in headers' do - subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/json') + subject.call(Rack::PATH_INFO => '/info', 'HTTP_ACCEPT' => 'application/json') expect(subject.env[Grape::Env::API_FORMAT]).to eq(:json) end it 'uses the file extension format if provided before headers' do - subject.call(Rack::PATH_INFO => '/info.txt', Grape::Http::Headers::HTTP_ACCEPT => 'application/json') + subject.call(Rack::PATH_INFO => '/info.txt', 'HTTP_ACCEPT' => 'application/json') expect(subject.env[Grape::Env::API_FORMAT]).to eq(:txt) end end @@ -122,47 +122,47 @@ def to_xml context 'accept header detection' do context 'when header contains invalid byte sequence' do it 'does not raise an exception' do - expect { subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => "Hello \x80") }.not_to raise_error + expect { subject.call(Rack::PATH_INFO => '/info', 'HTTP_ACCEPT' => "Hello \x80") }.not_to raise_error end end it 'detects from the Accept header' do - subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/xml') + subject.call(Rack::PATH_INFO => '/info', 'HTTP_ACCEPT' => 'application/xml') expect(subject.env[Grape::Env::API_FORMAT]).to eq(:xml) end it 'uses quality rankings to determine formats' do - subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/json; q=0.3,application/xml; q=1.0') + subject.call(Rack::PATH_INFO => '/info', 'HTTP_ACCEPT' => 'application/json; q=0.3,application/xml; q=1.0') expect(subject.env[Grape::Env::API_FORMAT]).to eq(:xml) - subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/json; q=1.0,application/xml; q=0.3') + subject.call(Rack::PATH_INFO => '/info', 'HTTP_ACCEPT' => 'application/json; q=1.0,application/xml; q=0.3') expect(subject.env[Grape::Env::API_FORMAT]).to eq(:json) end it 'handles quality rankings mixed with nothing' do - subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/json,application/xml; q=1.0') + subject.call(Rack::PATH_INFO => '/info', 'HTTP_ACCEPT' => 'application/json,application/xml; q=1.0') expect(subject.env[Grape::Env::API_FORMAT]).to eq(:xml) - subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/xml; q=1.0,application/json') + subject.call(Rack::PATH_INFO => '/info', 'HTTP_ACCEPT' => 'application/xml; q=1.0,application/json') expect(subject.env[Grape::Env::API_FORMAT]).to eq(:json) end it 'handles quality rankings that have a default 1.0 value' do - subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/json,application/xml;q=0.5') + subject.call(Rack::PATH_INFO => '/info', 'HTTP_ACCEPT' => 'application/json,application/xml;q=0.5') expect(subject.env[Grape::Env::API_FORMAT]).to eq(:json) - subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/xml;q=0.5,application/json') + subject.call(Rack::PATH_INFO => '/info', 'HTTP_ACCEPT' => 'application/xml;q=0.5,application/json') expect(subject.env[Grape::Env::API_FORMAT]).to eq(:json) end it 'parses headers with other attributes' do - subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/json; abc=2.3; q=1.0,application/xml; q=0.7') + subject.call(Rack::PATH_INFO => '/info', 'HTTP_ACCEPT' => 'application/json; abc=2.3; q=1.0,application/xml; q=0.7') expect(subject.env[Grape::Env::API_FORMAT]).to eq(:json) end it 'ensures that a quality of 0 is less preferred than any other content type' do - subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/json;q=0.0,application/xml') + subject.call(Rack::PATH_INFO => '/info', 'HTTP_ACCEPT' => 'application/json;q=0.0,application/xml') expect(subject.env[Grape::Env::API_FORMAT]).to eq(:xml) - subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/xml,application/json;q=0.0') + subject.call(Rack::PATH_INFO => '/info', 'HTTP_ACCEPT' => 'application/xml,application/json;q=0.0') expect(subject.env[Grape::Env::API_FORMAT]).to eq(:xml) end @@ -171,21 +171,21 @@ def to_xml subject { described_class.new(app, content_types: { custom: 'application/vnd.test+json' }) } it 'uses the custom type' do - subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.test+json') + subject.call(Rack::PATH_INFO => '/info', 'HTTP_ACCEPT' => 'application/vnd.test+json') expect(subject.env[Grape::Env::API_FORMAT]).to eq(:custom) end end context 'when unregistered' do it 'returns the default content type text/plain' do - r = Rack::MockResponse[*subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.test+json')] + r = Rack::MockResponse[*subject.call(Rack::PATH_INFO => '/info', 'HTTP_ACCEPT' => 'application/vnd.test+json')] expect(r.headers[Rack::CONTENT_TYPE]).to eq('text/plain') end end end it 'parses headers with symbols as hash keys' do - subject.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/xml', system_time: '091293') + subject.call(Rack::PATH_INFO => '/info', 'HTTP_ACCEPT' => 'application/xml', system_time: '091293') expect(subject.env[:system_time]).to eq('091293') end end @@ -214,7 +214,7 @@ def to_xml it 'is set for vendored with registered type' do s = described_class.new(app, content_types: { custom: 'application/vnd.test+json' }) - _, headers, = s.call(Rack::PATH_INFO => '/info', Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.test+json') + _, headers, = s.call(Rack::PATH_INFO => '/info', 'HTTP_ACCEPT' => 'application/vnd.test+json') expect(headers[Rack::CONTENT_TYPE]).to eq('application/vnd.test+json') end end @@ -362,7 +362,7 @@ def to_xml Rack::REQUEST_METHOD => method, 'CONTENT_TYPE' => 'application/json', Rack::RACK_INPUT => io, - Grape::Http::Headers::HTTP_TRANSFER_ENCODING => 'chunked' + 'HTTP_TRANSFER_ENCODING' => 'chunked' ) expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]['is_boolean']).to be true expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]['string']).to eq('thing') @@ -376,7 +376,7 @@ def to_xml Rack::REQUEST_METHOD => method, 'CONTENT_TYPE' => 'application/json', Rack::RACK_INPUT => io, - Grape::Http::Headers::HTTP_TRANSFER_ENCODING => 'chunked' + 'HTTP_TRANSFER_ENCODING' => 'chunked' ) expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]['is_boolean']).to be true expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]['string']).to eq('thing') @@ -420,7 +420,7 @@ def to_xml let(:app) { ->(_env) { [200, {}, file_body] } } let(:body) { 'data' } let(:env) do - { Rack::PATH_INFO => '/somewhere', Grape::Http::Headers::HTTP_ACCEPT => 'application/json' } + { Rack::PATH_INFO => '/somewhere', 'HTTP_ACCEPT' => 'application/json' } end let(:headers) do if Gem::Version.new(Rack.release) < Gem::Version.new('3.1') @@ -452,7 +452,7 @@ def self.call(_, _) let(:app) { ->(_env) { [200, {}, ['']] } } let(:env) do - Rack::MockRequest.env_for('/hello.invalid', Grape::Http::Headers::HTTP_ACCEPT => 'application/x-invalid') + Rack::MockRequest.env_for('/hello.invalid', 'HTTP_ACCEPT' => 'application/x-invalid') end it 'returns response by invalid formatter' do diff --git a/spec/grape/middleware/versioner/accept_version_header_spec.rb b/spec/grape/middleware/versioner/accept_version_header_spec.rb index 491abe6f9..7693cb92d 100644 --- a/spec/grape/middleware/versioner/accept_version_header_spec.rb +++ b/spec/grape/middleware/versioner/accept_version_header_spec.rb @@ -20,8 +20,8 @@ it 'does not raise an error' do expect do - subject.call(Grape::Http::Headers::HTTP_ACCEPT_VERSION => "\x80") - end.to throw_symbol(:error, status: 406, headers: { Grape::Http::Headers::X_CASCADE => 'pass' }, message: 'The requested version is not supported.') + subject.call('HTTP_ACCEPT_VERSION' => "\x80") + end.to throw_symbol(:error, status: 406, headers: { 'X-Cascade' => 'pass' }, message: 'The requested version is not supported.') end end @@ -31,37 +31,37 @@ end it 'is set' do - status, _, env = subject.call(Grape::Http::Headers::HTTP_ACCEPT_VERSION => 'v1') + status, _, env = subject.call('HTTP_ACCEPT_VERSION' => 'v1') expect(env[Grape::Env::API_VERSION]).to eql 'v1' expect(status).to eq(200) end it 'is set if format provided' do - status, _, env = subject.call(Grape::Http::Headers::HTTP_ACCEPT_VERSION => 'v1') + status, _, env = subject.call('HTTP_ACCEPT_VERSION' => 'v1') expect(env[Grape::Env::API_VERSION]).to eql 'v1' expect(status).to eq(200) end it 'fails with 406 Not Acceptable if version is not supported' do expect do - subject.call(Grape::Http::Headers::HTTP_ACCEPT_VERSION => 'v2').last + subject.call('HTTP_ACCEPT_VERSION' => 'v2').last end.to throw_symbol( :error, status: 406, - headers: { Grape::Http::Headers::X_CASCADE => 'pass' }, + headers: { 'X-Cascade' => 'pass' }, message: 'The requested version is not supported.' ) end end it 'succeeds if :strict is not set' do - expect(subject.call(Grape::Http::Headers::HTTP_ACCEPT_VERSION => '').first).to eq(200) + expect(subject.call('HTTP_ACCEPT_VERSION' => '').first).to eq(200) expect(subject.call({}).first).to eq(200) end it 'succeeds if :strict is set to false' do @options[:version_options][:strict] = false - expect(subject.call(Grape::Http::Headers::HTTP_ACCEPT_VERSION => '').first).to eq(200) + expect(subject.call('HTTP_ACCEPT_VERSION' => '').first).to eq(200) expect(subject.call({}).first).to eq(200) end @@ -77,24 +77,24 @@ end.to throw_symbol( :error, status: 406, - headers: { Grape::Http::Headers::X_CASCADE => 'pass' }, + headers: { 'X-Cascade' => 'pass' }, message: 'Accept-Version header must be set.' ) end it 'fails with 406 Not Acceptable if header is empty' do expect do - subject.call(Grape::Http::Headers::HTTP_ACCEPT_VERSION => '').last + subject.call('HTTP_ACCEPT_VERSION' => '').last end.to throw_symbol( :error, status: 406, - headers: { Grape::Http::Headers::X_CASCADE => 'pass' }, + headers: { 'X-Cascade' => 'pass' }, message: 'Accept-Version header must be set.' ) end it 'succeeds if proper header is set' do - expect(subject.call(Grape::Http::Headers::HTTP_ACCEPT_VERSION => 'v1').first).to eq(200) + expect(subject.call('HTTP_ACCEPT_VERSION' => 'v1').first).to eq(200) end end @@ -118,7 +118,7 @@ it 'fails with 406 Not Acceptable if header is empty' do expect do - subject.call(Grape::Http::Headers::HTTP_ACCEPT_VERSION => '').last + subject.call('HTTP_ACCEPT_VERSION' => '').last end.to throw_symbol( :error, status: 406, @@ -128,7 +128,7 @@ end it 'succeeds if proper header is set' do - expect(subject.call(Grape::Http::Headers::HTTP_ACCEPT_VERSION => 'v1').first).to eq(200) + expect(subject.call('HTTP_ACCEPT_VERSION' => 'v1').first).to eq(200) end end end diff --git a/spec/grape/middleware/versioner/header_spec.rb b/spec/grape/middleware/versioner/header_spec.rb index 12fd837e0..230133cba 100644 --- a/spec/grape/middleware/versioner/header_spec.rb +++ b/spec/grape/middleware/versioner/header_spec.rb @@ -16,21 +16,21 @@ context 'api.type and api.subtype' do it 'sets type and subtype to first choice of content type if no preference given' do - status, _, env = subject.call(Grape::Http::Headers::HTTP_ACCEPT => '*/*') + status, _, env = subject.call('HTTP_ACCEPT' => '*/*') expect(env[Grape::Env::API_TYPE]).to eql 'application' expect(env[Grape::Env::API_SUBTYPE]).to eql 'vnd.vendor+xml' expect(status).to eq(200) end it 'sets preferred type' do - status, _, env = subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/*') + status, _, env = subject.call('HTTP_ACCEPT' => 'application/*') expect(env[Grape::Env::API_TYPE]).to eql 'application' expect(env[Grape::Env::API_SUBTYPE]).to eql 'vnd.vendor+xml' expect(status).to eq(200) end it 'sets preferred type and subtype' do - status, _, env = subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'text/plain') + status, _, env = subject.call('HTTP_ACCEPT' => 'text/plain') expect(env[Grape::Env::API_TYPE]).to eql 'text' expect(env[Grape::Env::API_SUBTYPE]).to eql 'plain' expect(status).to eq(200) @@ -39,13 +39,13 @@ context 'api.format' do it 'is set' do - status, _, env = subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendor+json') + status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor+json') expect(env[Grape::Env::API_FORMAT]).to eql 'json' expect(status).to eq(200) end it 'is nil if not provided' do - status, _, env = subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendor') + status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor') expect(env[Grape::Env::API_FORMAT]).to be_nil expect(status).to eq(200) end @@ -57,13 +57,13 @@ end it 'is set' do - status, _, env = subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendor-v1+json') + status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json') expect(env[Grape::Env::API_FORMAT]).to eql 'json' expect(status).to eq(200) end it 'is nil if not provided' do - status, _, env = subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendor-v1') + status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1') expect(env[Grape::Env::API_FORMAT]).to be_nil expect(status).to eq(200) end @@ -73,22 +73,22 @@ context 'api.vendor' do it 'is set' do - status, _, env = subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendor') + status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor') expect(env[Grape::Env::API_VENDOR]).to eql 'vendor' expect(status).to eq(200) end it 'is set if format provided' do - status, _, env = subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendor+json') + status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor+json') expect(env[Grape::Env::API_VENDOR]).to eql 'vendor' expect(status).to eq(200) end it 'fails with 406 Not Acceptable if vendor is invalid' do - expect { subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.othervendor+json').last } + expect { subject.call('HTTP_ACCEPT' => 'application/vnd.othervendor+json').last } .to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) - expect(exception.headers).to eql(Grape::Http::Headers::X_CASCADE => 'pass') + expect(exception.headers).to eql('X-Cascade' => 'pass') expect(exception.status).to be 406 expect(exception.message).to include 'API vendor not found' end @@ -100,22 +100,22 @@ end it 'is set' do - status, _, env = subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendor-v1') + status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1') expect(env[Grape::Env::API_VENDOR]).to eql 'vendor' expect(status).to eq(200) end it 'is set if format provided' do - status, _, env = subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendor-v1+json') + status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json') expect(env[Grape::Env::API_VENDOR]).to eql 'vendor' expect(status).to eq(200) end it 'fails with 406 Not Acceptable if vendor is invalid' do - expect { subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.othervendor-v1+json').last } + expect { subject.call('HTTP_ACCEPT' => 'application/vnd.othervendor-v1+json').last } .to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) - expect(exception.headers).to eql(Grape::Http::Headers::X_CASCADE => 'pass') + expect(exception.headers).to eql('X-Cascade' => 'pass') expect(exception.status).to be 406 expect(exception.message).to include('API vendor not found') end @@ -129,21 +129,21 @@ end it 'is set' do - status, _, env = subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendor-v1') + status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1') expect(env[Grape::Env::API_VERSION]).to eql 'v1' expect(status).to eq(200) end it 'is set if format provided' do - status, _, env = subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendor-v1+json') + status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json') expect(env[Grape::Env::API_VERSION]).to eql 'v1' expect(status).to eq(200) end it 'fails with 406 Not Acceptable if version is invalid' do - expect { subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendor-v2+json').last }.to raise_exception do |exception| + expect { subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v2+json').last }.to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidVersionHeader) - expect(exception.headers).to eql(Grape::Http::Headers::X_CASCADE => 'pass') + expect(exception.headers).to eql('X-Cascade' => 'pass') expect(exception.status).to be 406 expect(exception.message).to include('API version not found') end @@ -151,19 +151,19 @@ end it 'succeeds if :strict is not set' do - expect(subject.call(Grape::Http::Headers::HTTP_ACCEPT => '').first).to eq(200) + expect(subject.call('HTTP_ACCEPT' => '').first).to eq(200) expect(subject.call({}).first).to eq(200) end it 'succeeds if :strict is set to false' do @options[:version_options][:strict] = false - expect(subject.call(Grape::Http::Headers::HTTP_ACCEPT => '').first).to eq(200) + expect(subject.call('HTTP_ACCEPT' => '').first).to eq(200) expect(subject.call({}).first).to eq(200) end it 'succeeds if :strict is set to false and given an invalid header' do @options[:version_options][:strict] = false - expect(subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'yaml').first).to eq(200) + expect(subject.call('HTTP_ACCEPT' => 'yaml').first).to eq(200) expect(subject.call({}).first).to eq(200) end @@ -176,23 +176,23 @@ it 'fails with 406 Not Acceptable if header is not set' do expect { subject.call({}).last }.to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) - expect(exception.headers).to eql(Grape::Http::Headers::X_CASCADE => 'pass') + expect(exception.headers).to eql('X-Cascade' => 'pass') expect(exception.status).to be 406 expect(exception.message).to include('Accept header must be set.') end end it 'fails with 406 Not Acceptable if header is empty' do - expect { subject.call(Grape::Http::Headers::HTTP_ACCEPT => '').last }.to raise_exception do |exception| + expect { subject.call('HTTP_ACCEPT' => '').last }.to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) - expect(exception.headers).to eql(Grape::Http::Headers::X_CASCADE => 'pass') + expect(exception.headers).to eql('X-Cascade' => 'pass') expect(exception.status).to be 406 expect(exception.message).to include('Accept header must be set.') end end it 'succeeds if proper header is set' do - expect(subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendor-v1+json').first).to eq(200) + expect(subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json').first).to eq(200) end end @@ -213,7 +213,7 @@ end it 'fails with 406 Not Acceptable if header is application/xml' do - expect { subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/xml').last } + expect { subject.call('HTTP_ACCEPT' => 'application/xml').last } .to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) expect(exception.headers).to eql({}) @@ -223,7 +223,7 @@ end it 'fails with 406 Not Acceptable if header is empty' do - expect { subject.call(Grape::Http::Headers::HTTP_ACCEPT => '').last }.to raise_exception do |exception| + expect { subject.call('HTTP_ACCEPT' => '').last }.to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) expect(exception.headers).to eql({}) expect(exception.status).to be 406 @@ -232,7 +232,7 @@ end it 'fails with 406 Not Acceptable if header contains a single invalid accept' do - expect { subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/json;application/vnd.vendor-v1+json').first } + expect { subject.call('HTTP_ACCEPT' => 'application/json;application/vnd.vendor-v1+json').first } .to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) expect(exception.headers).to eql({}) @@ -242,7 +242,7 @@ end it 'succeeds if proper header is set' do - expect(subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendor-v1+json').first).to eq(200) + expect(subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json').first).to eq(200) end end @@ -252,17 +252,17 @@ end it 'succeeds with v1' do - expect(subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendor-v1+json').first).to eq(200) + expect(subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json').first).to eq(200) end it 'succeeds with v2' do - expect(subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendor-v2+json').first).to eq(200) + expect(subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v2+json').first).to eq(200) end it 'fails with another version' do - expect { subject.call(Grape::Http::Headers::HTTP_ACCEPT => 'application/vnd.vendor-v3+json') }.to raise_exception do |exception| + expect { subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v3+json') }.to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidVersionHeader) - expect(exception.headers).to eql(Grape::Http::Headers::X_CASCADE => 'pass') + expect(exception.headers).to eql('X-Cascade' => 'pass') expect(exception.status).to be 406 expect(exception.message).to include('API version not found') end diff --git a/spec/integration/rack_3_0/headers_spec.rb b/spec/integration/rack_3_0/headers_spec.rb index bd270e129..735807c7e 100644 --- a/spec/integration/rack_3_0/headers_spec.rb +++ b/spec/integration/rack_3_0/headers_spec.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -describe Grape::Http::Headers do +describe Grape::API do subject { last_response.headers } describe 'returned headers should all be in lowercase' do context 'when setting an header in an API' do let(:app) do - Class.new(Grape::API) do + Class.new(described_class) do get do header['GRAPE'] = '1' return_no_content @@ -21,7 +21,7 @@ context 'when error!' do let(:app) do - Class.new(Grape::API) do + Class.new(described_class) do rescue_from ArgumentError do error!('error!', 500, { 'GRAPE' => '1' }) end @@ -37,7 +37,7 @@ context 'when redirect' do let(:app) do - Class.new(Grape::API) do + Class.new(described_class) do get do redirect 'https://www.ruby-grape.org/' end @@ -51,7 +51,7 @@ context 'when options' do let(:app) do - Class.new(Grape::API) do + Class.new(described_class) do get { return_no_content } end end @@ -63,7 +63,7 @@ context 'when cascade' do let(:app) do - Class.new(Grape::API) do + Class.new(described_class) do version 'v0', using: :path, cascade: true get { return_no_content } end diff --git a/spec/support/chunked_response.rb b/spec/support/chunked_response.rb index c74f18f09..4c66e9384 100644 --- a/spec/support/chunked_response.rb +++ b/spec/support/chunked_response.rb @@ -58,9 +58,9 @@ def call(env) if !Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) && !headers[Rack::CONTENT_LENGTH] && - !headers[Grape::Http::Headers::TRANSFER_ENCODING] + !headers['Transfer-Encoding'] - headers[Grape::Http::Headers::TRANSFER_ENCODING] = 'chunked' + headers['Transfer-Encoding'] = 'chunked' response[2] = if headers['trailer'] TrailerBody.new(body) else diff --git a/spec/support/versioned_helpers.rb b/spec/support/versioned_helpers.rb index c4e841249..ce682e0d7 100644 --- a/spec/support/versioned_helpers.rb +++ b/spec/support/versioned_helpers.rb @@ -23,14 +23,14 @@ def versioned_headers(options) {} when :header { - Grape::Http::Headers::HTTP_ACCEPT => [ + 'HTTP_ACCEPT' => [ "application/vnd.#{options[:vendor]}-#{options[:version]}", options[:format] ].compact.join('+') } when :accept_version_header { - Grape::Http::Headers::HTTP_ACCEPT_VERSION => options[:version].to_s + 'HTTP_ACCEPT_VERSION' => options[:version].to_s } else raise ArgumentError.new("unknown versioning strategy: #{options[:using]}") From a80d2d456c279197459548e520dcb6b52258c49b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20L=C3=B6vmo?= <84896037+eriklovmo@users.noreply.github.com> Date: Fri, 11 Apr 2025 14:33:26 +0200 Subject: [PATCH 304/304] Remove unused `Request::DEFAULT_PARAMS_BUILDER` constant (#2556) * Remove unused Request::DEFAULT_PARAMS_BUILDER The constant was introduced in 45abe0dd9f18fcc0e1993b1d5a2988135cb216e1 but is never referenced and hasn't been since its inception. * Update CHANGELOG.md --- CHANGELOG.md | 1 + lib/grape/request.rb | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6b495d2f..e7ef011ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ * [#2550](https://github.com/ruby-grape/grape/pull/2550): Drop ActiveSupport 6.0 - [@ericproulx](https://github.com/ericproulx). * [#2549](https://github.com/ruby-grape/grape/pull/2549): Delegate cookies management to `Grape::Request` - [@ericproulx](https://github.com/ericproulx). * [#2554](https://github.com/ruby-grape/grape/pull/2554): Remove `Grape::Http::Headers` and `Grape::Util::Lazy::Object` - [@ericproulx](https://github.com/ericproulx). +* [#2556](https://github.com/ruby-grape/grape/pull/2556): Remove unused `Grape::Request::DEFAULT_PARAMS_BUILDER` constant - [@eriklovmo](https://github.com/eriklovmo). * Your contribution here. #### Fixes diff --git a/lib/grape/request.rb b/lib/grape/request.rb index 06c389b75..997edac7c 100644 --- a/lib/grape/request.rb +++ b/lib/grape/request.rb @@ -2,7 +2,6 @@ module Grape class Request < Rack::Request - DEFAULT_PARAMS_BUILDER = :hash_with_indifferent_access # Based on rack 3 KNOWN_HEADERS # https://github.com/rack/rack/blob/4f15e7b814922af79605be4b02c5b7c3044ba206/lib/rack/headers.rb#L10