diff --git a/.rubocop.yml b/.rubocop.yml index d1b6e0f..adc8fe5 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -372,7 +372,7 @@ Style/StringLiterals: Style/TrailingCommaInArguments: Description: 'Checks for trailing comma in argument lists.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas' - EnforcedStyleForMultiline: comma + EnforcedStyleForMultiline: no_comma SupportedStylesForMultiline: - comma - consistent_comma diff --git a/README.md b/README.md index 9e3c469..81d79fe 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,63 @@ end Passing `?filters[user_posts_on_date]=10/12/20` will call the `user_posts_on_date` scope with `10/12/20` as the the first argument, and will grab the `user_id` and `blog_id` out of the params and pass them as a hash, as the second argument. +### Scope Arguments with Types + +Sometimes you need the argument to a scope to be parsed using Sift types. Scope argument types can be specified by passing a hash to the `type:` keyword, +as follows: + +```ruby +class Post < ActiveRecord::Base + scope :with_priority, ->(int_array) { where(priority: int_array) } +end + +class PostsController < ApplicationController + include Sift + + filter_on :with_priority, type: { scope: :int } + + def index + render json: filtrate(Post.all) + end +end +``` +Passing `?filters[with_priority]=[1,2]` will call the `with_priority` scope with the array, `[1, 2]`, instead of `"[1,2]"` +`filters[with_priority]` can also be validated with the `:int` type using `filters_valid?` (see below). + +Types can also be specified for `scope_params` like this: + +```ruby +class Post < ActiveRecord::Base + scope :user_posts_on_date, ->(date, options) { + where(user_id: options[:user_id], blog_id: options[:blog_id], date: date) + } +end + +class UsersController < ApplicationController + include Sift + + filter_on :user_posts_on_date, + type: { + scope: [ + :datetime, + { blog_id: :int } + ] + }, + scope_params: [:user_id, :blog_id] + + def show + render json: filtrate(Post.all) + end +end +``` +Passing `?filters[user_posts_on_date]=2010-12-20&user_id=3blog_id=[4,5]` will result in the filter +`Post.all.user_posts_on_date(DateTime.parse('2010-12-20'), user_id: "3", blog_id: [4, 5])`. +Unfortunately, validation with `filters_valid?` is not yet supported on params used by scope options. + +Note that if `scope_params` is omitted it will default to the keys provided the scope option types. +For example, `filter_on :foo, type: {scope: [:int, { bar: :int, baz: :int }]}` implicitly defines `scope_params` +of `[:bar, :baz]` + ### Renaming Filter Params A filter param can have a different field name than the column or scope. Use `internal_name` with the correct name of the column or scope. diff --git a/lib/procore-sift.rb b/lib/procore-sift.rb index 8b11c31..9464f13 100644 --- a/lib/procore-sift.rb +++ b/lib/procore-sift.rb @@ -1,4 +1,6 @@ require "sift/filter" +require "sift/filter/scope" +require "sift/filter/where" require "sift/filter_validator" require "sift/filtrator" require "sift/sort" @@ -66,7 +68,12 @@ def sort_fields class_methods do def filter_on(parameter, type:, internal_name: parameter, default: nil, validate: nil, scope_params: []) - filters << Filter.new(parameter, type, internal_name, default, validate, scope_params) + filters << + if Sift::Filter.scope_type?(type) + Sift::Filter::Scope.new(parameter, type, internal_name, default, validate, scope_params) + else + Sift::Filter::Where.new(parameter, type, internal_name, default, validate) + end end def filters diff --git a/lib/sift/filter.rb b/lib/sift/filter.rb index 6080a6a..e636310 100644 --- a/lib/sift/filter.rb +++ b/lib/sift/filter.rb @@ -2,14 +2,16 @@ module Sift # Filter describes the way a parameter maps to a database column # and the type information helpful for validating input. class Filter - attr_reader :parameter, :default, :custom_validate, :scope_params + attr_reader :parameter, :default, :custom_validate - def initialize(param, type, internal_name, default, custom_validate = nil, scope_params = []) + def self.scope_type?(type) + type == :scope || type.respond_to?(:key?) + end + + def initialize(param, type, internal_name, default, custom_validate = nil) @parameter = Parameter.new(param, type, internal_name) @default = default @custom_validate = custom_validate - @scope_params = scope_params - raise ArgumentError, "scope_params must be an array of symbols" unless valid_scope_params?(scope_params) raise "unknown filter type: #{type}" unless type_validator.valid_type? end @@ -21,10 +23,8 @@ def validation(_sort) def apply!(collection, value:, active_sorts_hash:, params: {}) if not_processable?(value) collection - elsif should_apply_default?(value) - default.call(collection) else - handler.call(collection, parameterize(value), params, scope_params) + default_or_handler(value, params).call(collection) end end # rubocop:enable Lint/UnusedMethodArgument @@ -49,10 +49,21 @@ def param parameter.param end + protected + + # Subclasses should override. Default behavior is to return none + def handler(_value, _params) + ->(collection) { collection.none } + end + private - def parameterize(value) - ValueParser.new(value: value, type: parameter.type, options: parameter.parse_options).parse + def default_or_handler(value, params) + if should_apply_default?(value) + default + else + handler(value, params) + end end def not_processable?(value) @@ -63,22 +74,12 @@ def should_apply_default?(value) value.nil? && !default.nil? end - def mapped_scope_params(params) - scope_params.each_with_object({}) do |scope_param, hash| - hash[scope_param] = params.fetch(scope_param) + def validation_type + if type != :scope || scope_types.empty? + type + else + scope_types.first end end - - def valid_scope_params?(scope_params) - scope_params.is_a?(Array) && scope_params.all? { |symbol| symbol.is_a?(Symbol) } - end - - def handler - parameter.handler - end - - def supports_ranges? - parameter.supports_ranges? - end end end diff --git a/lib/sift/filter/scope.rb b/lib/sift/filter/scope.rb new file mode 100644 index 0000000..3fb2fb5 --- /dev/null +++ b/lib/sift/filter/scope.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module Sift + class Filter + class Scope < Sift::Filter + attr_reader :scope_params, :scope_types + + def initialize(param, raw_type, internal_name, default, custom_validate = nil, scope_params = []) + super(param, :scope, internal_name, default, custom_validate) + @scope_types = [] + @scope_params = scope_params || [] + raise ArgumentError, "scope_params must be an array of symbols" unless valid_scope_params?(scope_params) + + if raw_type.respond_to?(:key?) + @scope_types = Array(raw_type[:scope]).compact + validate_scope_types! + + @type_validator = Sift::TypeValidator.new(param, @scope_types.first || :scope) + end + end + + protected + + def handler(value, params) + Sift::ScopeHandler.new(value, mapped_scope_params(params), parameter, scope_types) + end + + private + + def mapped_scope_params(params) + scope_params.each_with_object({}) do |scope_param, hash| + hash[scope_param] = params.fetch(scope_param) + end + end + + def valid_scope_params?(scope_params) + scope_params.is_a?(Array) && scope_params.all? { |symbol| symbol.is_a?(Symbol) } + end + + def validate_scope_types! + return if scope_types.empty? + + unless Sift::TypeValidator.new(parameter.param, scope_types.first).valid_type? + raise ArgumentError, "scope_types must contain a valid filter type for the scope parameter" + end + return if scope_types.size == 1 + + if scope_types.size > 2 || !valid_scope_option_types!(scope_types[1]) + raise ArgumentError, "type: scope: expected to have this structure: [type, {#{scope_params.map { |sp| "#{sp}: type" }.join(', ')}}]" + end + end + + def valid_scope_option_types!(hash) + valid_form = hash.respond_to?(:keys) && hash.all? { |key, value| Sift::TypeValidator.new(key, value).valid_type? } + if valid_form && scope_params.empty? + # default scope_params + @scope_params = hash.keys + end + valid_form && (hash.keys - scope_params).empty? + end + end + end +end diff --git a/lib/sift/filter/where.rb b/lib/sift/filter/where.rb new file mode 100644 index 0000000..5857400 --- /dev/null +++ b/lib/sift/filter/where.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Sift + class Filter + class Where < Sift::Filter + protected + + def handler(value, _params) + Sift::WhereHandler.new(value, parameter) + end + end + end +end diff --git a/lib/sift/parameter.rb b/lib/sift/parameter.rb index 403ce0a..9704167 100644 --- a/lib/sift/parameter.rb +++ b/lib/sift/parameter.rb @@ -8,35 +8,5 @@ def initialize(param, type, internal_name = param) @type = type @internal_name = internal_name end - - def parse_options - { - supports_boolean: supports_boolean?, - supports_ranges: supports_ranges?, - supports_json: supports_json? - } - end - - def handler - if type == :scope - ScopeHandler.new(self) - else - WhereHandler.new(self) - end - end - - private - - def supports_ranges? - ![:string, :text, :scope].include?(type) - end - - def supports_json? - type == :int - end - - def supports_boolean? - type == :boolean - end end end diff --git a/lib/sift/scope_handler.rb b/lib/sift/scope_handler.rb index 2dc6e48..58946e6 100644 --- a/lib/sift/scope_handler.rb +++ b/lib/sift/scope_handler.rb @@ -1,24 +1,36 @@ module Sift class ScopeHandler - def initialize(param) - @param = param + def initialize(raw_value, raw_scope_options, parameter, scope_types) + @value = ValueParser.new(value: raw_value, type: scope_types.first || parameter.type).parse + @param = parameter + @scope_options = parsed_scope_options(raw_scope_options, scope_types) end - def call(collection, value, params, scope_params) - collection.public_send(@param.internal_name, *scope_parameters(value, params, scope_params)) + def call(collection) + collection.public_send(@param.internal_name, *scope_parameters) end - def scope_parameters(value, params, scope_params) - if scope_params.empty? - [value] + private + + def scope_parameters + if @scope_options.present? + [@value, @scope_options] else - [value, mapped_scope_params(params, scope_params)] + [@value] end end - def mapped_scope_params(params, scope_params) - scope_params.each_with_object({}) do |scope_param, hash| - hash[scope_param] = params.fetch(scope_param) + def parsed_scope_options(raw_scope_options, scope_types) + return nil if raw_scope_options.empty? + + scope_option_types = scope_types[1] || {} + raw_scope_options.each_with_object({}) do |(key, raw_param), hash| + hash[key] = + if scope_option_types[key].present? + ValueParser.new(value: raw_param, type: scope_option_types[key]).parse + else + raw_param + end end end end diff --git a/lib/sift/value_parser.rb b/lib/sift/value_parser.rb index 469cbd5..10fee35 100644 --- a/lib/sift/value_parser.rb +++ b/lib/sift/value_parser.rb @@ -1,11 +1,8 @@ module Sift class ValueParser - def initialize(value:, type: nil, options: {}) - @value = value - @supports_boolean = options.fetch(:supports_boolean, false) - @supports_ranges = options.fetch(:supports_ranges, false) - @supports_json = options.fetch(:supports_json, false) + def initialize(value:, type: nil) @value = normalized_value(value, type) + @type = type end def parse @@ -34,10 +31,22 @@ def array_from_json private - attr_reader :value, :type, :supports_boolean, :supports_json, :supports_ranges + def supports_ranges? + ![:string, :text, :scope].include?(type) + end + + def supports_json? + type == :int + end + + def supports_boolean? + type == :boolean + end + + attr_reader :value, :type - def parse_as_range?(raw_value=value) - supports_ranges && raw_value.to_s.include?("...") + def parse_as_range?(raw_value = value) + supports_ranges? && raw_value.to_s.include?("...") end def range_value @@ -45,11 +54,11 @@ def range_value end def parse_as_json? - supports_json && value.is_a?(String) + supports_json? && value.is_a?(String) end def parse_as_boolean? - supports_boolean + supports_boolean? end def boolean_value diff --git a/lib/sift/version.rb b/lib/sift/version.rb index d78bf86..aa72a9a 100644 --- a/lib/sift/version.rb +++ b/lib/sift/version.rb @@ -1,3 +1,3 @@ module Sift - VERSION = "0.13.0".freeze + VERSION = "0.14.0".freeze end diff --git a/lib/sift/where_handler.rb b/lib/sift/where_handler.rb index 48fff10..faca915 100644 --- a/lib/sift/where_handler.rb +++ b/lib/sift/where_handler.rb @@ -1,11 +1,12 @@ module Sift class WhereHandler - def initialize(param) - @param = param + def initialize(raw_value, parameter) + @param = parameter + @value = ValueParser.new(value: raw_value, type: parameter.type).parse end - def call(collection, value, _params, _scope_params) - collection.where(@param.internal_name => value) + def call(collection) + collection.where(@param.internal_name => @value) end end end diff --git a/test/filter_test.rb b/test/filter_test.rb index 3aa2f87..0e87778 100644 --- a/test/filter_test.rb +++ b/test/filter_test.rb @@ -2,7 +2,7 @@ class FilterTest < ActiveSupport::TestCase test "it is initialized with the a param and a type" do - filter = Sift::Filter.new("hi", :int, "hi", nil) + filter = Sift::Filter::Where.new("hi", :int, "hi", nil) assert_equal "hi", filter.param assert_equal :int, filter.type @@ -11,66 +11,72 @@ class FilterTest < ActiveSupport::TestCase test "it raises if the type is unknown" do assert_raise RuntimeError do - Sift::Filter.new("hi", :foo, "hi", nil) + Sift::Filter::Where.new("hi", :foo, "hi", nil) end end test "it raises an exception if scope_params is not an array" do assert_raise ArgumentError do - Sift::Filter.new("hi", :scope, "hi", nil, nil, {}) + Sift::Filter::Scope.new("hi", :scope, "hi", nil, nil, {}) end end test "it raises an exception if scope_params does not contain symbols" do assert_raise ArgumentError do - Sift::Filter.new("hi", :scope, "hi", nil, nil, ["foo"]) + Sift::Filter::Scope.new("hi", :scope, "hi", nil, nil, ["foo"]) + end + end + + test "it raises an exception if type scope has invalid scope types" do + assert_raise ArgumentError do + Sift::Filter::Scope.new("hi", :scope, "hi", nil, nil, [:foo], [:int, :foo]) end end test "it knows what validation it needs when a datetime" do - filter = Sift::Filter.new("hi", :datetime, "hi", nil) + filter = Sift::Filter::Where.new("hi", :datetime, "hi", nil) expected_validation = { format: { with: /\A.+(?:[^.]\.\.\.[^.]).+\z/, message: "must be a range" }, valid_date_range: true } assert_equal expected_validation, filter.validation(nil) end test "it knows what validation it needs when an int" do - filter = Sift::Filter.new("hi", :int, "hi", nil) + filter = Sift::Filter::Where.new("hi", :int, "hi", nil) expected_validation = { valid_int: true } assert_equal expected_validation, filter.validation(nil) end test "it accepts a singular int or array of ints" do - filter = Sift::Filter.new([1, 2], :int, [1, 2], nil) + filter = Sift::Filter::Where.new([1, 2], :int, [1, 2], nil) expected_validation = { valid_int: true } assert_equal expected_validation, filter.validation(nil) end test "it does not accept a mixed array when the type is int" do - filter = Sift::Filter.new([1, 2, "a"], :int, [1, 2, "a"], nil) + filter = Sift::Filter::Where.new([1, 2, "a"], :int, [1, 2, "a"], nil) expected_validation = { valid_int: true } assert_equal expected_validation, filter.validation(nil) end test "it does not accept an empty array for type int" do - filter = Sift::Filter.new([], :int, [], nil) + filter = Sift::Filter::Where.new([], :int, [], nil) expected_validation = { valid_int: true } assert_equal expected_validation, filter.validation(nil) end test "it knows what validation it needs when a decimal" do - filter = Sift::Filter.new("hi", :decimal, "hi", nil) + filter = Sift::Filter::Where.new("hi", :decimal, "hi", nil) expected_validation = { numericality: true, allow_nil: true } assert_equal expected_validation, filter.validation(nil) end test "it knows what validation it needs when a boolean" do - filter = Sift::Filter.new("hi", :boolean, "hi", nil) + filter = Sift::Filter::Where.new("hi", :boolean, "hi", nil) expected_validation = { inclusion: { in: [true, false] }, allow_nil: true } assert_equal expected_validation, filter.validation(nil) diff --git a/test/filter_validator_test.rb b/test/filter_validator_test.rb index b17713f..d2ddfdc 100644 --- a/test/filter_validator_test.rb +++ b/test/filter_validator_test.rb @@ -2,7 +2,7 @@ class FilterValidatorTest < ActiveSupport::TestCase test "it validates that integers are string integers" do - filter = Sift::Filter.new(:hi, :int, :hi, nil) + filter = Sift::Filter::Where.new(:hi, :int, :hi, nil) validator = Sift::FilterValidator.build( filters: [filter], sort_fields: [], @@ -15,7 +15,7 @@ class FilterValidatorTest < ActiveSupport::TestCase end test "it validates that integers are numeric integers" do - filter = Sift::Filter.new(:hola, :int, :hola, nil) + filter = Sift::Filter::Where.new(:hola, :int, :hola, nil) validator = Sift::FilterValidator.build( filters: [filter], sort_fields: [], @@ -28,7 +28,7 @@ class FilterValidatorTest < ActiveSupport::TestCase end test "it validates integers cannot be strings" do - filter = Sift::Filter.new(:hi, :int, :hi, nil) + filter = Sift::Filter::Where.new(:hi, :int, :hi, nil) expected_messages = { hi: ["must be integer, array of integers, or range"] } validator = Sift::FilterValidator.build( @@ -42,7 +42,7 @@ class FilterValidatorTest < ActiveSupport::TestCase end test "it validates decimals are numerical" do - filter = Sift::Filter.new(:hi, :decimal, :hi, nil) + filter = Sift::Filter::Where.new(:hi, :decimal, :hi, nil) validator = Sift::FilterValidator.build( filters: [filter], @@ -55,7 +55,7 @@ class FilterValidatorTest < ActiveSupport::TestCase end test "it validates decimals cannot be strings" do - filter = Sift::Filter.new(:hi, :decimal, :hi, nil) + filter = Sift::Filter::Where.new(:hi, :decimal, :hi, nil) expected_messages = { hi: ["is not a number"] } validator = Sift::FilterValidator.build( @@ -69,7 +69,7 @@ class FilterValidatorTest < ActiveSupport::TestCase end test "it validates booleans are 0 or 1" do - filter = Sift::Filter.new(:hi, :boolean, :hi, nil) + filter = Sift::Filter::Where.new(:hi, :boolean, :hi, nil) validator = Sift::FilterValidator.build( filters: [filter], @@ -82,8 +82,8 @@ class FilterValidatorTest < ActiveSupport::TestCase end test "it validates multiple fields" do - bool_filter = Sift::Filter.new(:hi, :boolean, :hi, nil) - dec_filter = Sift::Filter.new(:bye, :decimal, :bye, nil) + bool_filter = Sift::Filter::Where.new(:hi, :boolean, :hi, nil) + dec_filter = Sift::Filter::Where.new(:bye, :decimal, :bye, nil) validator = Sift::FilterValidator.build( filters: [bool_filter, dec_filter], @@ -96,8 +96,8 @@ class FilterValidatorTest < ActiveSupport::TestCase end test "it invalidates when one of two filters is invalid" do - bool_filter = Sift::Filter.new(:hi, :boolean, :hi, nil) - dec_filter = Sift::Filter.new(:bye, :decimal, :bye, nil) + bool_filter = Sift::Filter::Where.new(:hi, :boolean, :hi, nil) + dec_filter = Sift::Filter::Where.new(:bye, :decimal, :bye, nil) expected_messages = { bye: ["is not a number"] } validator = Sift::FilterValidator.build( @@ -111,8 +111,8 @@ class FilterValidatorTest < ActiveSupport::TestCase end test "it invalidates when both fields are invalid" do - bool_filter = Sift::Filter.new(:hi, :date, :hi, nil) - dec_filter = Sift::Filter.new(:bye, :decimal, :bye, nil) + bool_filter = Sift::Filter::Where.new(:hi, :date, :hi, nil) + dec_filter = Sift::Filter::Where.new(:bye, :decimal, :bye, nil) expected_messages = { hi: ["must be a range"], bye: ["is not a number"] } validator = Sift::FilterValidator.build( @@ -126,8 +126,8 @@ class FilterValidatorTest < ActiveSupport::TestCase end test "it ignores validations for filters that are not being used" do - bool_filter = Sift::Filter.new(:hi, :boolean, :hi, nil) - dec_filter = Sift::Filter.new(:bye, :decimal, :bye, nil) + bool_filter = Sift::Filter::Where.new(:hi, :boolean, :hi, nil) + dec_filter = Sift::Filter::Where.new(:bye, :decimal, :bye, nil) validator = Sift::FilterValidator.build( filters: [bool_filter, dec_filter], @@ -140,7 +140,7 @@ class FilterValidatorTest < ActiveSupport::TestCase end test "it allows ranges" do - filter = Sift::Filter.new(:hi, :int, :hi, nil) + filter = Sift::Filter::Where.new(:hi, :int, :hi, nil) validator = Sift::FilterValidator.build( filters: [filter], @@ -153,7 +153,7 @@ class FilterValidatorTest < ActiveSupport::TestCase end test "datetimes are invalid unless they are a range" do - filter = Sift::Filter.new(:hi, :datetime, :hi, nil) + filter = Sift::Filter::Where.new(:hi, :datetime, :hi, nil) validator = Sift::FilterValidator.build( filters: [filter], @@ -166,7 +166,7 @@ class FilterValidatorTest < ActiveSupport::TestCase end test "datetimes are invalid when not a range" do - filter = Sift::Filter.new(:hi, :datetime, :hi, nil) + filter = Sift::Filter::Where.new(:hi, :datetime, :hi, nil) expected_messages = { hi: ["must be a range"] } validator = Sift::FilterValidator.build( @@ -180,7 +180,7 @@ class FilterValidatorTest < ActiveSupport::TestCase end test "datetimes are invalid if any of the boundaries is invalid date" do - filter = Sift::Filter.new(:hi, :datetime, :hi, nil) + filter = Sift::Filter::Where.new(:hi, :datetime, :hi, nil) expected_messages = { hi: ["is invalid"] } validator = Sift::FilterValidator.build( @@ -209,7 +209,7 @@ class FilterValidatorTest < ActiveSupport::TestCase test "it respects a custom validation" do error_message = "super duper error message" - filter = Sift::Filter.new(:hi, :int, :hi, nil, ->(validator) { + filter = Sift::Filter::Where.new(:hi, :int, :hi, nil, ->(validator) { validator.errors.add(:base, error_message) }) expected_messages = { base: [error_message] } @@ -225,7 +225,7 @@ class FilterValidatorTest < ActiveSupport::TestCase end test "custom validation supercedes type validation" do - filter = Sift::Filter.new(:hi, :int, :hi, nil, ->(validator) {}) + filter = Sift::Filter::Where.new(:hi, :int, :hi, nil, ->(validator) {}) validator = Sift::FilterValidator.build( filters: [filter], diff --git a/test/filtrator_test.rb b/test/filtrator_test.rb index c46bf44..fceda96 100644 --- a/test/filtrator_test.rb +++ b/test/filtrator_test.rb @@ -7,7 +7,7 @@ class FiltratorTest < ActiveSupport::TestCase test "it filters by all the filters you pass it" do post = Post.create! - filter = Sift::Filter.new(:id, :int, :id, nil) + filter = Sift::Filter::Where.new(:id, :int, :id, nil) collection = Sift::Filtrator.filter( Post.all, @@ -20,7 +20,7 @@ class FiltratorTest < ActiveSupport::TestCase test "it will not try to make a range out of a string field that includes ..." do post = Post.create!(title: "wow...man") - filter = Sift::Filter.new(:title, :string, :title, nil) + filter = Sift::Filter::Where.new(:title, :string, :title, nil) collection = Sift::Filtrator.filter( Post.all, @@ -34,7 +34,7 @@ class FiltratorTest < ActiveSupport::TestCase test "it returns default when filter param not passed" do Post.create!(body: "foo") Post.create!(body: "bar") - filter = Sift::Filter.new(:body2, :scope, :body2, ->(c) { c.order(:body) }) + filter = Sift::Filter::Scope.new(:body2, :scope, :body2, ->(c) { c.order(:body) }) collection = Sift::Filtrator.filter(Post.all, {}, [filter]) assert_equal [Post.second, Post.first], collection.to_a @@ -43,7 +43,7 @@ class FiltratorTest < ActiveSupport::TestCase test "it will not return default if param passed" do Post.create!(body: "foo") filtered_post = Post.create!(body: "bar") - filter = Sift::Filter.new(:body2, :scope, :body2, nil) + filter = Sift::Filter::Scope.new(:body2, :scope, :body2, nil) collection = Sift::Filtrator.filter( Post.all, { filters: { body2: "bar" } }, @@ -57,7 +57,7 @@ class FiltratorTest < ActiveSupport::TestCase Post.create!(priority: 5, expiration: "2017-01-01") Post.create!(priority: 5, expiration: "2017-01-02") Post.create!(priority: 7, expiration: "2020-10-20") - filter = Sift::Filter.new( + filter = Sift::Filter::Scope.new( :expired_before_and_priority, :scope, :expired_before_and_priority, @@ -86,7 +86,7 @@ class FiltratorTest < ActiveSupport::TestCase Post.create!(priority: 5, expiration: "2017-01-02") Post.create!(priority: 7, expiration: "2020-10-20") - filter = Sift::Filter.new( + filter = Sift::Filter::Scope.new( :ordered_expired_before_and_priority, :scope, :ordered_expired_before_and_priority, diff --git a/test/scope_handler_test.rb b/test/scope_handler_test.rb new file mode 100644 index 0000000..d44bc30 --- /dev/null +++ b/test/scope_handler_test.rb @@ -0,0 +1,31 @@ +require "test_helper" + +class ScopeHandlerTest < ActiveSupport::TestCase + test "it passes values as strings without scope types" do + post1 = Post.create!(priority: 6, expiration: "2017-01-01") + Post.create!(priority: 5, expiration: "2017-01-02") + Post.create!(priority: 6, expiration: "2020-10-20") + handler = Sift::ScopeHandler.new( + "2020-09-01", + { priority: "6" }, + Sift::Parameter.new(:expired_before_and_priority, :scope), + [] + ) + + assert_equal [post1.id], handler.call(Post).pluck(:id) + end + + test "it parses values using scope types" do + post1 = Post.create!(priority: 6, expiration: "2017-01-01") + post2 = Post.create!(priority: 5, expiration: "2017-01-02") + Post.create!(priority: 6, expiration: "2020-10-20") + handler = Sift::ScopeHandler.new( + "2020-09-01", + { priority: "[5,6]" }, + Sift::Parameter.new(:expired_before_and_priority, :scope), + [:datetime, { priority: :int }] + ) + + assert_equal [post1.id, post2.id], handler.call(Post).pluck(:id) + end +end diff --git a/test/sift_test.rb b/test/sift_test.rb index ac8856c..54c695a 100644 --- a/test/sift_test.rb +++ b/test/sift_test.rb @@ -22,6 +22,13 @@ def params assert_equal [:id], MyClass.filters.map(&:param) end + test "it defaults scope params to scope subtypes" do + MyClass.reset_filters + MyClass.filter_on(:foo, type: { scope: [:string, { bar: :int, biz: :string }] }) + + assert_equal [:bar, :biz], MyClass.filters.first.scope_params + end + test "it registers sorts with sort_on" do MyClass.reset_filters MyClass.sort_on(:id, type: :int) diff --git a/test/value_parser_test.rb b/test/value_parser_test.rb index 7f0bceb..60d06ba 100644 --- a/test/value_parser_test.rb +++ b/test/value_parser_test.rb @@ -14,67 +14,36 @@ class FilterTest < ActiveSupport::TestCase end test "With options a json string array of integers results in an array of integers" do - options = { - supports_ranges: true, - supports_json: true - } - parser = Sift::ValueParser.new(value: "[1,2,3]", options: options) + parser = Sift::ValueParser.new(value: "[1,2,3]", type: :int) assert_equal [1, 2, 3], parser.parse end test "with invalid json returns original value" do - options = { - supports_ranges: true, - supports_json: true - } - parser = Sift::ValueParser.new(value: "[1,2,3", options: options) + parser = Sift::ValueParser.new(value: "[1,2,3", type: :int) assert_equal "[1,2,3", parser.parse end - test "JSON parsing only supports arrays" do - options = { - supports_json: true - } - json_string = "{\"a\":4}" - parser = Sift::ValueParser.new(value: json_string, options: options) - - assert_equal json_string, parser.parse - end - test "With options a range string of integers results in a range" do - options = { - supports_ranges: true, - supports_json: true - } - parser = Sift::ValueParser.new(value: "1...3", options: options) + parser = Sift::ValueParser.new(value: "1...3", type: :int) assert_instance_of Range, parser.parse end test "parses true from 1" do - options = { - supports_boolean: true - } - parser = Sift::ValueParser.new(value: 1, options: options) + parser = Sift::ValueParser.new(value: 1, type: :boolean) assert_equal true, parser.parse end test "parses false from 0" do - options = { - supports_boolean: true - } - parser = Sift::ValueParser.new(value: 0, options: options) + parser = Sift::ValueParser.new(value: 0, type: :boolean) assert_equal false, parser.parse end test "parses range for range values" do - options = { - supports_ranges: true - } test_sets = [ { type: :date, @@ -109,7 +78,7 @@ class FilterTest < ActiveSupport::TestCase test_sets.each do |set| range_string = "#{set[:start_value]}...#{set[:end_value]}" - parser = Sift::ValueParser.new(value: range_string, type: set[:type], options: options) + parser = Sift::ValueParser.new(value: range_string, type: set[:type]) result = parser.parse assert_instance_of Range, result @@ -118,14 +87,10 @@ class FilterTest < ActiveSupport::TestCase end test "parses range for Date string range and normalizes DateTime values" do - options = { - supports_ranges: true - } - start_date = "2018-01-01T10:00:00Z[Etc/UTC]" end_date = "2018-01-01T12:00:00Z[Etc/UTC]" range_string = "#{start_date}...#{end_date}" - parser = Sift::ValueParser.new(value: range_string, type: :datetime, options: options) + parser = Sift::ValueParser.new(value: range_string, type: :datetime) result = parser.parse assert_instance_of Range, result diff --git a/test/where_handler_test.rb b/test/where_handler_test.rb new file mode 100644 index 0000000..5c36ebb --- /dev/null +++ b/test/where_handler_test.rb @@ -0,0 +1,27 @@ +require "test_helper" + +class WhereHandlerTest < ActiveSupport::TestCase + test "it handles non-array arguments" do + post1 = Post.create!(priority: 6, expiration: "2017-01-01") + Post.create!(priority: 5, expiration: "2017-01-02") + post3 = Post.create!(priority: 6, expiration: "2020-10-20") + handler = Sift::WhereHandler.new( + "6", + Sift::Parameter.new(:priority, :int) + ) + + assert_equal [post1.id, post3.id], handler.call(Post).pluck(:id) + end + + test "it parses array arguments" do + post1 = Post.create!(priority: 6, expiration: "2017-01-01") + Post.create!(priority: 5, expiration: "2017-01-02") + post3 = Post.create!(priority: 7, expiration: "2020-10-20") + handler = Sift::WhereHandler.new( + "[6,7]", + Sift::Parameter.new(:priority, :int) + ) + + assert_equal [post1.id, post3.id], handler.call(Post).pluck(:id) + end +end