yap 1.4.0 → 1.4.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 90a7fb8f804bd7b631c632deeb7241bfcdac00ee
4
- data.tar.gz: 4d2de49002fcdc9d79dd60786a3c191fac6275cb
3
+ metadata.gz: 04100f8b32c43f3281e414e58d2ac06bc736351f
4
+ data.tar.gz: 8242bbd5e3b635a25feb3d69264a057d491bffda
5
5
  SHA512:
6
- metadata.gz: 1669e8d881f42da049c61c49c1b1c483340ece46642f281831dd104f999198320eca72b82c097d8d1df534830b8c97223a559c080fdae695671c8f9a9a36a89e
7
- data.tar.gz: fac27392172ca4fbbf5fb96332f79bcf157893a87076335e094d50908589b9e85e6f1ee5cbaa55d900538bf688cbc7451751d985ec57af61a7e32af9db2d564e
6
+ metadata.gz: 276f3729be6894e0ce980a4625d3f48f7b4f03730c1a89cc2468441b08c85aba33abb5aa213f2a7e2d92c08a01cb5fb8d6275af2dc65570943e67b806d3d15d8
7
+ data.tar.gz: a3424e43274c8bc5c27c6b3ebe11a5a09576a00b07685fadd6c54b64fa9234f9a0f45a9b51ea03121dbeb5180557b6c76bad8b2c54a0f613cf19ee9056467d46
data/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ [![Build Status](https://travis-ci.org/1and1/yap.svg?branch=master)](https://travis-ci.org/1and1/yap) [![Test Coverage](https://codeclimate.com/github/1and1/yap/badges/coverage.svg)](https://codeclimate.com/github/1and1/yap/coverage) [![Code Climate](https://codeclimate.com/github/1and1/yap/badges/gpa.svg)](https://codeclimate.com/github/1and1/yap)
2
+
1
3
  Yet another paginator for Ruby on Rails, which adds a `paginate` scope to your ActiveRecords.
2
4
 
3
5
  ## Setup
@@ -3,8 +3,8 @@ class StringInfinity < String
3
3
  String::INFINITY_NEGATIVE
4
4
  end
5
5
 
6
- def ==(value)
7
- self.class == value.class
6
+ def ==(other)
7
+ self.class == other.class
8
8
  end
9
9
  end
10
10
 
data/lib/yap.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  require 'active_support/concern'
2
2
  require 'yap/active_record/relation'
3
- require 'yap/column_mapper'
3
+ require 'yap/params_extractor'
4
4
  require 'yap/filterable'
5
5
  require 'yap/exceptions'
6
6
  require 'string_infinity'
@@ -28,7 +28,7 @@ module Yap
28
28
  include Filterable
29
29
 
30
30
  DEFAULTS = Struct.new(:page, :per_page, :hard_limit, :sort, :direction, :disable_warnings)
31
- .new(1, 10, nil, :id, :asc, false)
31
+ .new(1, 10, nil, :id, :asc, false)
32
32
 
33
33
  def self.configure
34
34
  raise ArgumentError, 'No block given.' unless block_given?
@@ -40,85 +40,16 @@ module Yap
40
40
  end
41
41
 
42
42
  included do
43
- extend ColumnMapper
43
+ extend ParamsExtractor
44
44
 
45
45
  ##
46
46
  # The paginate scope takes a hash as parameter. All options are optional and can be combined arbitrarily.
47
47
  #
48
- # @param [Hash] params The parameters used for pagination (:page, :per_page, :sort, :direction)
48
+ # @param [Hash] params The parameters used for pagination (:page, :per_page, :sort, :direction, :filter)
49
49
  #
50
- scope :paginate, -> (params = {}) {
50
+ scope :paginate, lambda { |params = {}|
51
51
  page, per_page, order_by = extract_pagination_params(params)
52
- filter(params[:filter]).limit(per_page).offset((page-1)*per_page).order(order_by)
52
+ filter(params[:filter]).limit(per_page).offset((page - 1) * per_page).order(order_by)
53
53
  }
54
-
55
- private
56
-
57
- def self.extract_pagination_params(params)
58
- page = extract_number(params[:page], DEFAULTS.page)
59
- per_page = extract_number(params[:per_page], DEFAULTS.per_page)
60
- if DEFAULTS.hard_limit && per_page > DEFAULTS.hard_limit
61
- raise PaginationError.new("Not more than #{DEFAULTS.hard_limit} items per page accepted.")
62
- end
63
- sort = extract_order(params[:sort], params[:direction])
64
-
65
- return page, per_page, sort
66
- end
67
-
68
- def self.extract_number(number, default)
69
- number ||= default
70
- begin
71
- number = Integer(number)
72
- rescue
73
- raise PaginationError.new("'#{number}' is not a valid number.")
74
- end
75
-
76
- raise PaginationError.new('Only positive numbers are accepted.') unless number > 0
77
- number
78
- end
79
-
80
- def self.extract_order(sort, direction)
81
- sort = sort.split(',') if sort.is_a?(String) && sort =~ /,/
82
- direction = direction.split(',') if direction.is_a?(String) && direction =~ /,/
83
-
84
- case sort
85
- when Array
86
- order = []
87
- direction = Array.wrap direction
88
- sort.each_with_index do |s, i|
89
- order << build_order_by(s, direction[i] || DEFAULTS.direction)
90
- end
91
-
92
- order
93
- when Hash
94
- sort.map do |s, d|
95
- build_order_by(s, d)
96
- end
97
- else
98
- build_order_by sort, direction
99
- end
100
- end
101
-
102
- def self.build_order_by(sort, direction)
103
- sort = extract_column(sort)
104
- direction = extract_direction(direction)
105
-
106
- (sort =~ /\./ ? "#{sort} #{direction}" : { sort => direction })
107
- end
108
- private_class_method :build_order_by
109
-
110
- def self.extract_column(sort)
111
- sort ||= DEFAULTS.sort
112
- column = map_column(sort.to_s.downcase)
113
- raise PaginationError.new("Cannot sort by '#{sort}'.") unless column
114
- column
115
- end
116
-
117
- def self.extract_direction(direction)
118
- direction ||= DEFAULTS.direction
119
- dir = (direction).to_sym.downcase
120
- raise PaginationError.new("'#{direction}' is not a valid direction. Use 'asc' or 'desc'.") unless [:asc, :desc].include? dir
121
- dir
122
- end
123
54
  end
124
55
  end
@@ -20,7 +20,7 @@ module ActiveRecord
20
20
  # @return [Integer] Total number of results
21
21
  #
22
22
  def total
23
- @total ||= without_pagination { |rel| rel.count }
23
+ @total ||= without_pagination(&:count)
24
24
  end
25
25
 
26
26
  ##
@@ -42,8 +42,8 @@ module ActiveRecord
42
42
  # @return [Hash] Values defining the range of the current page.
43
43
  #
44
44
  def range(include_total = false)
45
- from = offset_value+1
46
- to = offset_value+limit_value
45
+ from = offset_value + 1
46
+ to = offset_value + limit_value
47
47
  to = total if total < to && include_total
48
48
 
49
49
  range = { from: from, to: to }
@@ -52,4 +52,4 @@ module ActiveRecord
52
52
  range.merge(total: total)
53
53
  end
54
54
  end
55
- end
55
+ end
@@ -1,12 +1,14 @@
1
1
  module ColumnMapper
2
- private
3
-
4
2
  def map_column(name)
5
- column_names.include?(name) ? name : if respond_to? :map_name_to_column
3
+ actual_column = name if column_names.include?(name)
4
+
5
+ column_alias = if respond_to? :map_name_to_column
6
6
  map_name_to_column(name)
7
- else
8
- warn "#{self.name} does not implement map_name_to_column. If you do not need column mapping set disable_warnings=true" unless Yap::DEFAULTS.disable_warnings
9
- nil
7
+ elsif actual_column.nil? && !Yap::DEFAULTS.disable_warnings
8
+ warn "#{self.name} does not implement map_name_to_column. If you do not need column mapping set " \
9
+ 'disable_warnings=true'
10
10
  end
11
+
12
+ column_alias || actual_column
11
13
  end
12
- end
14
+ end
@@ -2,4 +2,4 @@ module Yap
2
2
  class YapError < StandardError; end
3
3
  class PaginationError < YapError; end
4
4
  class FilterError < YapError; end
5
- end
5
+ end
@@ -1,24 +1,23 @@
1
1
  module Yap
2
+ ##
3
+ # Extends Range by StringInfinity.
4
+ #
2
5
  class ExtendedRange < Range
3
6
  def begin
4
- infinity_or_do_not_change super
7
+ handle_infinity super
5
8
  end
6
9
 
7
10
  def end
8
- infinity_or_do_not_change super
11
+ handle_infinity super
9
12
  end
10
13
 
11
- def infinity_or_do_not_change(value)
12
- if value.is_a? StringInfinity
13
- if value == String::INFINITY
14
- Float::INFINITY
15
- elsif value == -String::INFINITY
16
- -Float::INFINITY
17
- else
18
- raise ArgumentError, "Invalid value for StringInfinity."
19
- end
14
+ def handle_infinity(value)
15
+ return value unless value.is_a? StringInfinity
16
+
17
+ if value.is_a?(StringInfinityNegative)
18
+ -Float::INFINITY
20
19
  else
21
- value
20
+ Float::INFINITY
22
21
  end
23
22
  end
24
23
  end
@@ -1,58 +1,52 @@
1
- require 'yap/extended_range'
1
+ require 'yap/filter_column'
2
2
 
3
3
  module Yap
4
- class Filter < Hash
4
+ class Filter
5
5
  def initialize
6
- self[:where] = {}
7
- self[:not] = {}
6
+ @failed = []
7
+ @columns = []
8
8
  end
9
9
 
10
- def parse!(column, values)
11
- values.to_s.split(',').each do |value|
12
- # Perform negative match if value starts with '!'.
13
- if value =~/^!(.+)$/
14
- match = :not
15
- value = $1
16
- else
17
- match = :where
18
- end
10
+ def parse!(model, params)
11
+ params.each do |attribute, values|
12
+ parse_arrtibute(model, attribute, values)
13
+ end
19
14
 
20
- case value
21
- when /([<>]=?)(.+)/
22
- match, value = handle_comparison_operators(match, column, $1.to_sym, $2)
23
- when /(.+)\.{3}(.+)/
24
- value = $1...$2
25
- when /(.+)\.{2}(.+)/
26
- value = $1..$2
27
- else
28
- # Convert null to ruby nil to use 'IS NULL' in SQL.
29
- value = value.downcase == 'null' ? nil : value
30
- end
15
+ raise FilterError, "Cannot filter by: #{@failed.join(', ')}" unless @failed.empty?
16
+ end
31
17
 
32
- # Ensure filter contains an array to append to.
33
- self[match][column] ||= []
18
+ def where
19
+ extract_filters(:where)
20
+ end
34
21
 
35
- self[match][column] << value
36
- end
22
+ def not
23
+ extract_filters(:not)
37
24
  end
38
25
 
39
26
  private
40
27
 
41
- def handle_comparison_operators(match, column, operator, value)
42
- case operator
43
- when :<
44
- handle_comparison_operators(toggle_match(match), column, :>=, value)
45
- when :>
46
- handle_comparison_operators(toggle_match(match), column, :<=, value)
47
- when :<=
48
- return match, ExtendedRange.new(-String::INFINITY, value)
49
- when :>=
50
- return match, ExtendedRange.new(value, String::INFINITY)
28
+ def extract_filters(condition)
29
+ @columns.inject({}) do |filter, column|
30
+ values = column.send(condition)
31
+
32
+ if values.empty?
33
+ filter
34
+ else
35
+ filter.merge(column.name => values.size == 1 ? values.first : values)
36
+ end
51
37
  end
52
38
  end
53
39
 
54
- def toggle_match(match)
55
- match == :where ? :not : :where
40
+ def parse_arrtibute(model, attribute, values)
41
+ column = model.map_column(attribute.to_s.downcase)
42
+ if column.nil?
43
+ @failed << attribute
44
+ return
45
+ end
46
+
47
+ filter_column = FilterColumn.new(column)
48
+ filter_column.parse_values(values)
49
+ @columns << filter_column
56
50
  end
57
51
  end
58
52
  end
@@ -0,0 +1,35 @@
1
+ require 'yap/filter_value'
2
+
3
+ module Yap
4
+ ##
5
+ # Multiple filter for a column can be separated by comma (,).
6
+ #
7
+ class FilterColumn
8
+ attr_reader :name
9
+
10
+ def initialize(name)
11
+ @name = name
12
+ @values = []
13
+ end
14
+
15
+ def parse_values(values)
16
+ values.to_s.split(',').each do |value|
17
+ filter_value = FilterValue.new
18
+ filter_value.parse_value(value)
19
+ @values << filter_value
20
+ end
21
+ end
22
+
23
+ def where
24
+ @values.select do |v|
25
+ v.condition == :where
26
+ end.map(&:value)
27
+ end
28
+
29
+ def not
30
+ @values.select do |v|
31
+ v.condition == :not
32
+ end.map(&:value)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,67 @@
1
+ require 'yap/extended_range'
2
+
3
+ module Yap
4
+ ##
5
+ # Filter values can have the following formats:
6
+ #
7
+ # * Integer - e.g. 100
8
+ # * String - e.g. text
9
+ # * Comparisons - e.g. >10, <=B
10
+ # * Dates - e.g. 1970-01-01 (depends on the database backend)
11
+ # * Ranges - e.g. 1...2, Jones..Smith
12
+ # * NULL - e.g. NULL, null, Null
13
+ # * Negation - e.g. !value, !1..10
14
+ #
15
+ class FilterValue
16
+ OPERATOR_INVERSION_MAP = { :< => :>=, :> => :<= }.freeze
17
+
18
+ attr_reader :condition, :value
19
+
20
+ def parse_value(value)
21
+ value = handle_negation(value)
22
+
23
+ @value = case value
24
+ when /([<>]=?)(.+)/
25
+ handle_comparison_operators($1.to_sym, $2)
26
+ when /(.+)(\.{2,3})(.+)/
27
+ Range.new $1, $3, $2 == '...'
28
+ else
29
+ handle_null(value)
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def handle_negation(value)
36
+ if value =~ /^!(.+)$/
37
+ @condition = :not
38
+ value = $1
39
+ else
40
+ @condition = :where
41
+ end
42
+
43
+ value
44
+ end
45
+
46
+ def handle_comparison_operators(operator, value)
47
+ case operator
48
+ when :<, :> then invert_comparison_operator(operator, value)
49
+ when :<= then ExtendedRange.new(-String::INFINITY, value)
50
+ when :>= then ExtendedRange.new(value, String::INFINITY)
51
+ end
52
+ end
53
+
54
+ def invert_comparison_operator(operator, value)
55
+ toggle_condition!
56
+ handle_comparison_operators(OPERATOR_INVERSION_MAP[operator], value)
57
+ end
58
+
59
+ def toggle_condition!
60
+ @condition = @condition == :where ? :not : :where
61
+ end
62
+
63
+ def handle_null(value)
64
+ value.casecmp('null').zero? ? nil : value
65
+ end
66
+ end
67
+ end
@@ -32,31 +32,18 @@ module Yap
32
32
  #
33
33
  # @param [Hash] Attribute/value pairs to filter ( { 'gender' => 'f' } )
34
34
  #
35
- scope :filter, -> (params = nil) {
35
+ scope :filter, lambda { |params = nil|
36
36
  if params.blank?
37
37
  all
38
38
  else
39
39
  filter = extract_filter_params(params)
40
- where(filter[:where]).where.not(filter[:not])
40
+ where(filter.where).where.not(filter.not)
41
41
  end
42
42
  }
43
43
 
44
- private
45
-
46
44
  def self.extract_filter_params(params)
47
45
  filter = Filter.new
48
-
49
- failed = []
50
- params.each do |key, values|
51
- column = map_column(key.to_s.downcase)
52
- if column
53
- filter.parse!(column, values)
54
- else
55
- failed << key
56
- end
57
- end
58
-
59
- raise FilterError.new('Cannot filter by: ' + failed.join(', ')) unless failed.empty?
46
+ filter.parse!(self, params)
60
47
 
61
48
  filter
62
49
  end
@@ -0,0 +1,102 @@
1
+ require 'yap/column_mapper'
2
+
3
+ module Yap
4
+ ##
5
+ # Methods for extracting valid pagination parameters from rails params.
6
+ #
7
+ module ParamsExtractor
8
+ def extract_pagination_params(params)
9
+ page = extract_page(params)
10
+ per_page = extract_per_page(params)
11
+ sort = extract_order(params)
12
+
13
+ [page, per_page, sort]
14
+ end
15
+
16
+ def extract_page(params)
17
+ extract_number(params[:page], DEFAULTS.page)
18
+ end
19
+
20
+ def extract_per_page(params)
21
+ per_page = extract_number(params[:per_page], DEFAULTS.per_page)
22
+
23
+ if DEFAULTS.hard_limit && per_page > DEFAULTS.hard_limit
24
+ raise PaginationError, "Not more than #{DEFAULTS.hard_limit} items per page accepted."
25
+ end
26
+
27
+ per_page
28
+ end
29
+
30
+ def extract_number(number, default)
31
+ number ||= default
32
+ begin
33
+ number = Integer(number)
34
+ rescue ArgumentError
35
+ raise PaginationError, "'#{number}' is not a valid number."
36
+ end
37
+
38
+ raise PaginationError, 'Only positive numbers are accepted.' unless number > 0
39
+ number
40
+ end
41
+
42
+ def extract_order(params)
43
+ sort, direction = params.values_at(:sort, :direction)
44
+
45
+ case sort
46
+ when Array, String
47
+ build_order_from_array(sort, direction)
48
+ when Hash
49
+ build_order_from_hash(sort)
50
+ else # nil or symbol
51
+ build_order sort, direction
52
+ end
53
+ end
54
+
55
+ def build_order_from_array(sort, direction = [])
56
+ sort = convert_to_array(sort)
57
+ direction = convert_to_array(direction)
58
+ order = []
59
+ sort.each_with_index do |s, i|
60
+ order << build_order(s, direction[i] || DEFAULTS.direction)
61
+ end
62
+
63
+ order
64
+ end
65
+
66
+ def convert_to_array(object)
67
+ object = object.split(',') if object.is_a?(String)
68
+
69
+ Array.wrap(object)
70
+ end
71
+
72
+ def build_order_from_hash(sort)
73
+ sort.map do |s, d|
74
+ build_order(s, d)
75
+ end
76
+ end
77
+
78
+ def build_order(sort, direction)
79
+ sort = extract_column(sort || DEFAULTS.sort)
80
+ direction = extract_direction(direction)
81
+
82
+ (sort =~ /\./ ? "#{sort} #{direction}" : { sort => direction })
83
+ end
84
+
85
+ def extract_column(sort)
86
+ column = map_column(sort.to_s.downcase)
87
+ raise PaginationError, "Cannot sort by '#{sort}'." unless column
88
+
89
+ column
90
+ end
91
+
92
+ def extract_direction(direction)
93
+ direction ||= DEFAULTS.direction
94
+ dir = direction.to_sym.downcase
95
+ unless [:asc, :desc].include? dir
96
+ raise PaginationError, "'#{direction}' is not a valid direction. Use 'asc' or 'desc'."
97
+ end
98
+
99
+ dir
100
+ end
101
+ end
102
+ end
@@ -1,3 +1,3 @@
1
1
  module Yap
2
- VERSION = '1.4.0'
2
+ VERSION = '1.4.2'.freeze
3
3
  end
metadata CHANGED
@@ -1,29 +1,35 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yap
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Finn Glöe
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-01-25 00:00:00.000000000 Z
11
+ date: 2017-02-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '4.2'
20
+ - - "<"
18
21
  - !ruby/object:Gem::Version
19
- version: '4.1'
22
+ version: '6'
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
23
26
  requirements:
24
- - - "~>"
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '4.2'
30
+ - - "<"
25
31
  - !ruby/object:Gem::Version
26
- version: '4.1'
32
+ version: '6'
27
33
  - !ruby/object:Gem::Dependency
28
34
  name: sqlite3
29
35
  requirement: !ruby/object:Gem::Requirement
@@ -38,7 +44,21 @@ dependencies:
38
44
  - - "~>"
39
45
  - !ruby/object:Gem::Version
40
46
  version: '1.3'
41
- description: Yet another paginator for Ruby on Rails adding a pagination and filtering
47
+ - !ruby/object:Gem::Dependency
48
+ name: rake
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '11.1'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '11.1'
61
+ description: Yet another paginator for Ruby on Rails adding pagination and filtering
42
62
  interface to ActiveRecords.
43
63
  email: finn.gloee@1und1.de
44
64
  executables: []
@@ -54,7 +74,10 @@ files:
54
74
  - lib/yap/exceptions.rb
55
75
  - lib/yap/extended_range.rb
56
76
  - lib/yap/filter.rb
77
+ - lib/yap/filter_column.rb
78
+ - lib/yap/filter_value.rb
57
79
  - lib/yap/filterable.rb
80
+ - lib/yap/params_extractor.rb
58
81
  - lib/yap/version.rb
59
82
  homepage: https://github.com/1and1/yap
60
83
  licenses:
@@ -66,9 +89,9 @@ require_paths:
66
89
  - lib
67
90
  required_ruby_version: !ruby/object:Gem::Requirement
68
91
  requirements:
69
- - - "~>"
92
+ - - ">="
70
93
  - !ruby/object:Gem::Version
71
- version: '2.0'
94
+ version: 1.9.3
72
95
  required_rubygems_version: !ruby/object:Gem::Requirement
73
96
  requirements:
74
97
  - - ">="
@@ -76,7 +99,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
76
99
  version: '0'
77
100
  requirements: []
78
101
  rubyforge_project:
79
- rubygems_version: 2.5.1
102
+ rubygems_version: 2.6.10
80
103
  signing_key:
81
104
  specification_version: 4
82
105
  summary: Yet another paginator for Ruby on Rails