whereable 0.1.2 → 0.2.0

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
  SHA256:
3
- metadata.gz: ea1b122fb55b9fca4d22efd7d32057bbc198ef97f4090bdf8bdaa42da6b15a7f
4
- data.tar.gz: 8a0d13ea659a80ca2f34df1e7e970ee6716bed272a36e288472a1827c6b61855
3
+ metadata.gz: a63f7a79cff3aa76f6c0d05d15bb318476188870cc121e6e7d1d9d44de2e4349
4
+ data.tar.gz: 58283edffdd9ccbe6283f0c7cba5d52342a0b37460fac7e747393442f3746e52
5
5
  SHA512:
6
- metadata.gz: 1e66e7d892444322c226af98aedd2639b31d3ebb1444bd6337582c6e40248dd2276b183ad70e6794430f0ed0cfa6618fccc37a9308ddf2a50c53453db74e05ac
7
- data.tar.gz: 2552253cb5f31f90d5830a95db7f0f328b05f477e8f31709cb76234ebcfb3782709e322e1fa8c45b48b8645e413ac5d40f0f81045b7521979875d7432fece3bd
6
+ metadata.gz: a9bd520efc357f0f8391bcfe56af756ca462f107a7a7919c81bb9825e0790eb99499f372a570ddf3d3d8aea18ae503b11b07ef4cd64405a407b160c0bb0ff18d
7
+ data.tar.gz: af2956f56cb244f79d96bea6301f41a87b8e5bb8ede3df4bf0e71223e7dc71b1dea3e8a53dde6d471d8a74dcbc5f12e5e2af130cee77cc23b585aee48c9f6f2e
@@ -1,8 +1,11 @@
1
1
  Release History
2
2
  ===============
3
+ # 0.2.0
4
+ * Add support for IN and BETWEEN
5
+
3
6
  # 0.1.2
4
- * Appears to work with Ruby 2.3.0 and ActiveRecord 4.1.0 or newer.
5
- However, there's a version conflict surrounding Treetop in Rails < 4.1.6.
7
+ * Minor changes to play nice with older versions of Rails
8
+ * Update gemspec dependencies based on additional testing
6
9
 
7
10
  # 0.1.1
8
11
  * Make operators case-insensitive
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  # Whereable
4
4
 
5
5
  Translates where-like filter syntax into an Arel-based ActiveRecord scope, so you can safely use SQL syntax in Rails controller parameters.
6
- Not as powerful as [Ransack](https://github.com/activerecord-hackery/ransack), but simple and lightweight.
6
+ What it lacks in power, it gains in simplicity and ease of use for API consumers.
7
7
 
8
8
  ## Installation
9
9
 
@@ -30,6 +30,8 @@ class User < ActiveRecord::Base
30
30
 
31
31
  validates :username, presence: true, uniqueness: true
32
32
 
33
+ validates :born_on, presence: true
34
+
33
35
  enum role: { standard: 0, admin: 1 }
34
36
  end
35
37
  ```
@@ -37,15 +39,16 @@ With this data:
37
39
  ``` ruby
38
40
  User.create!(username: 'Morpheus', role: :admin, born_on: '1961-07-30')
39
41
  User.create!(username: 'Neo', role: :standard, born_on: '1964-09-02')
42
+ User.create!(username: 'Trinity', role: :standard, born_on: '1967-08-21')
40
43
  ```
41
44
  Let's assume you're allowing filtered API access to your Users,
42
45
  but using the `#standard` scope to keep admins hidden. So your controller might include:
43
46
  ``` ruby
44
47
  User.standard.where(params[:filter])
45
48
  ```
46
- And your white hat API consumers pass in `filter=born_on < '1970-11-11'` to get Users over 50, and &hellip;
49
+ And your white hat API consumers pass in `filter=born_on < '1967-01-01'`, and &hellip;
47
50
  ``` ruby
48
- User.standard.where("born_on < '1970-11-11'")
51
+ User.standard.where("born_on < '1967-01-01'")
49
52
  ```
50
53
  returns Neo as expected, so we're all good.
51
54
 
@@ -65,7 +68,7 @@ User.standard.whereable(params[:filter])
65
68
  ```
66
69
  And then &hellip;
67
70
  ``` ruby
68
- User.standard.whereable("born_on < '1970-11-11'")
71
+ User.standard.whereable("born_on < '1967-01-01'")
69
72
  ```
70
73
  returns Neo as before, but &hellip;
71
74
  ``` ruby
@@ -78,9 +81,13 @@ Whereable::FilterInvalid ('Invalid filter at ) or (true')
78
81
 
79
82
  ### Syntax
80
83
  * Supports and/or with nested parentheses as needed
81
- * Recognizes these operators: `eq ne gte gt lte lt = != <> >= > <= <`
82
- * Column must be to left of operator, and literal to right
83
- * Comparing columns is *not* supported
84
+ * Recognizes these operators: `eq ne gte gt lte lt = != <> >= > <= <`, plus `IN` and `BETWEEN`
85
+ * Column must be to left of operator, and literal(s) to right
86
+ * Comparing columns to each other is *not* supported
87
+ * `BETWEEN` requires two literals separated by `AND`
88
+ * Example: `publish_at between '2020-11-01 12:00 EST' and '2020-11-15 23:59:59 EST'`
89
+ * `IN` requires comma-separated literals in parentheses
90
+ * Example: `username in (Morpheus, Trinity)`
84
91
  * Quotes are optional unless the literal contains spaces or quotes
85
92
  * Supports double or single quotes, and embedded quotes may be backslash escaped
86
93
  * Also supports the PostgreSQL double-single embedded quote
@@ -88,6 +95,9 @@ Whereable::FilterInvalid ('Invalid filter at ) or (true')
88
95
  * 👍 `User.whereable('role = admin')`
89
96
  * 👎 `User.whereable('role = 1')`
90
97
 
98
+ ### More
99
+ See the [Wiki](https://github.com/MacksMind/whereable/wiki) for more.
100
+
91
101
  ## Development
92
102
 
93
103
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -4,8 +4,7 @@
4
4
 
5
5
  require 'whereable/version'
6
6
  require 'active_record'
7
- require 'treetop'
8
- require 'whereable.treetop'
7
+ require 'whereable_clause'
9
8
 
10
9
  # Whereable module
11
10
  module Whereable
@@ -13,85 +12,6 @@ module Whereable
13
12
 
14
13
  class FilterInvalid < StandardError; end
15
14
 
16
- module Disjunction
17
- # Hash for OR
18
- def to_h
19
- return conjunction.to_h if opt.empty?
20
-
21
- {
22
- or: [conjunction.to_h] + opt.elements.map { |o| o.conjunction.to_h },
23
- }
24
- end
25
- end
26
-
27
- module Conjunction
28
- # Hash for AND
29
- def to_h
30
- return term.to_h if opt.empty?
31
-
32
- {
33
- and: [term.to_h] + opt.elements.map { |o| o.term.to_h },
34
- }
35
- end
36
- end
37
-
38
- module NestedExpression
39
- # Hash for nested expression
40
- delegate :to_h, to: :expression
41
- end
42
-
43
- module Condition
44
- # Hash for comparison
45
- def to_h
46
- {
47
- operator.to_sym => {
48
- column: column.to_s,
49
- literal: literal.to_s,
50
- },
51
- }
52
- end
53
- end
54
-
55
- module Column
56
- # String column name
57
- def to_s
58
- text_value
59
- end
60
- end
61
-
62
- # Operators such as 'eq' that match their Arel equivalent aren't needed here
63
- OP_SYMS = {
64
- 'ne' => :not_eq,
65
- 'lte' => :lteq,
66
- 'gte' => :gteq,
67
- '=' => :eq,
68
- '!=' => :not_eq,
69
- '<>' => :not_eq,
70
- '<' => :lt,
71
- '<=' => :lteq,
72
- '>' => :gt,
73
- '>=' => :gteq,
74
- }.freeze
75
- private_constant :OP_SYMS
76
-
77
- module Operator
78
- # Arel comparion method
79
- def to_sym
80
- OP_SYMS[text_value.downcase] || text_value.downcase.to_sym
81
- end
82
- end
83
-
84
- module Literal
85
- # Strip enclosing quotes and tranlate embedded quote patterns
86
- def to_s
87
- text_value
88
- .gsub(/\A['"]|['"]\z/, '')
89
- .gsub("''", "'")
90
- .gsub("\\'", "'")
91
- .gsub('\"', '"')
92
- end
93
- end
94
-
95
15
  included do
96
16
  scope(
97
17
  :whereable,
@@ -108,14 +28,32 @@ module Whereable
108
28
  module ClassMethods
109
29
  # Parse filter to hash tree using Treetop PEG
110
30
  def whereable_hash_tree(filter)
111
- parser = WhereableParser.new
112
- hash = parser.parse(filter)&.to_h
31
+ parser = WhereableClauseParser.new
32
+ hash = parser.parse(filter.strip)&.to_h
113
33
 
114
34
  raise FilterInvalid, "Invalid filter at #{filter[parser.max_terminal_failure_index..-1]}" if hash.nil?
115
35
 
116
36
  hash
117
37
  end
118
38
 
39
+ # Test column validity
40
+ def whereable_valid_column(column)
41
+ raise FilterInvalid, "Invalid column #{column}" unless column_names.include?(column)
42
+
43
+ column
44
+ end
45
+
46
+ # Test literal validity
47
+ def whereable_valid_literal(column, literal)
48
+ if defined_enums.key?(column)
49
+ raise(FilterInvalid, "Invalid value #{literal} for #{column}") unless defined_enums[column].key?(literal)
50
+
51
+ defined_enums[column][literal]
52
+ else
53
+ literal
54
+ end
55
+ end
56
+
119
57
  # deparse hash tree to Arel
120
58
  def whereable_deparse(hash)
121
59
  raise FilterInvalid, "Invalid hash #{hash}" if hash.size > 1
@@ -130,14 +68,20 @@ module Whereable
130
68
  end
131
69
  arel
132
70
  when :eq, :not_eq, :gt, :gteq, :lt, :lteq
133
- column = value[:column]
134
- literal = value[:literal]
135
- raise FilterInvalid, "Invalid column #{column}" unless column_names.include?(column)
136
-
137
- if defined_enums.key?(column)
138
- literal = defined_enums[column][literal] || raise(FilterInvalid, "Invalid value #{literal} for #{column}")
139
- end
71
+ column = whereable_valid_column(value[:column])
72
+ literal = whereable_valid_literal(column, value[:literal])
140
73
  arel_table[column].public_send(key, literal)
74
+ when :between
75
+ column = whereable_valid_column(value[:column])
76
+ literals = value[:literals]
77
+ raise(FilterInvalid, "Invalid operation for #{column}") if defined_enums.key?(column)
78
+
79
+ arel_table[column].between(literals)
80
+ when :in
81
+ column = whereable_valid_column(value[:column])
82
+ literals = value[:literals].map { |l| whereable_valid_literal(column, l) }
83
+
84
+ arel_table[column].in(literals)
141
85
  else
142
86
  raise FilterInvalid, "Invalid hash #{hash}"
143
87
  end
@@ -3,6 +3,6 @@
3
3
  # Copyright 2020 Mack Earnhardt
4
4
 
5
5
  module Whereable
6
- VERSION = '0.1.2'
6
+ VERSION = '0.2.0'
7
7
  public_constant :VERSION
8
8
  end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2020 Mack Earnhardt
4
+
5
+ require 'treetop'
6
+
7
+ module WhereableClause
8
+ Treetop.load(File.join(__dir__, 'whereable_clause.treetop'))
9
+
10
+ module Disjunction
11
+ # Hash for OR
12
+ def to_h
13
+ return conjunction.to_h if opt.empty?
14
+
15
+ {
16
+ or: [conjunction.to_h] + opt.elements.map { |o| o.conjunction.to_h },
17
+ }
18
+ end
19
+ end
20
+
21
+ module Conjunction
22
+ # Hash for AND
23
+ def to_h
24
+ return term.to_h if opt.empty?
25
+
26
+ {
27
+ and: [term.to_h] + opt.elements.map { |o| o.term.to_h },
28
+ }
29
+ end
30
+ end
31
+
32
+ module NestedExpression
33
+ # Hash for nested expression
34
+ delegate :to_h, to: :expression
35
+ end
36
+
37
+ module Condition
38
+ # Hash for comparison
39
+ def to_h
40
+ {
41
+ operator.to_sym => {
42
+ column: column.to_s,
43
+ literal: literal.to_s,
44
+ },
45
+ }
46
+ end
47
+ end
48
+
49
+ module ConditionBetween
50
+ # Hash for between operator
51
+ def to_h
52
+ {
53
+ between: {
54
+ column: column.to_s,
55
+ literals: left.to_s..right.to_s,
56
+ },
57
+ }
58
+ end
59
+ end
60
+
61
+ module ConditionIn
62
+ # Hash for in operator
63
+ def to_h
64
+ if opt.empty?
65
+ {
66
+ eq: {
67
+ column: column.to_s,
68
+ literal: literal.to_s,
69
+ },
70
+ }
71
+ else
72
+ {
73
+ in: {
74
+ column: column.to_s,
75
+ literals: [literal.to_s] + opt.elements.map { |o| o.literal.to_s },
76
+ },
77
+ }
78
+ end
79
+ end
80
+ end
81
+
82
+ module Column
83
+ # String column name
84
+ def to_s
85
+ text_value
86
+ end
87
+ end
88
+
89
+ # Operators such as 'eq' that match their Arel equivalent aren't needed here
90
+ OP_SYMS = {
91
+ 'ne' => :not_eq,
92
+ 'lte' => :lteq,
93
+ 'gte' => :gteq,
94
+ '=' => :eq,
95
+ '!=' => :not_eq,
96
+ '<>' => :not_eq,
97
+ '<' => :lt,
98
+ '<=' => :lteq,
99
+ '>' => :gt,
100
+ '>=' => :gteq,
101
+ }.freeze
102
+ private_constant :OP_SYMS
103
+
104
+ module Operator
105
+ # Arel comparion method
106
+ def to_sym
107
+ OP_SYMS[text_value.downcase] || text_value.downcase.to_sym
108
+ end
109
+ end
110
+
111
+ module Literal
112
+ # Strip enclosing quotes and tranlate embedded quote patterns
113
+ def to_s
114
+ text_value
115
+ .gsub(/\A['"]|['"]\z/, '')
116
+ .gsub("''", "'")
117
+ .gsub("\\'", "'")
118
+ .gsub('\"', '"')
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,45 @@
1
+ grammar WhereableClause
2
+ rule expression
3
+ disjunction
4
+ end
5
+
6
+ rule disjunction
7
+ conjunction opt:( _sp? 'or'i _sp? conjunction )* <Disjunction>
8
+ end
9
+
10
+ rule conjunction
11
+ term opt:( _sp? 'and'i _sp? term )* <Conjunction>
12
+ end
13
+
14
+ rule term
15
+ condition / condition_between / condition_in / '(' _sp? expression _sp? ')' <NestedExpression>
16
+ end
17
+
18
+ rule condition
19
+ column _sp? operator _sp? literal <Condition>
20
+ end
21
+
22
+ rule condition_between
23
+ column _sp 'between'i _sp left:literal _sp 'and'i _sp right:literal <ConditionBetween>
24
+ end
25
+
26
+ rule condition_in
27
+ column _sp 'in'i _sp? '(' _sp? literal _sp? opt:( ',' _sp? literal _sp? )* ')' <ConditionIn>
28
+ end
29
+
30
+ rule _sp
31
+ [\s]+
32
+ end
33
+
34
+ rule column
35
+ [a-zA-Z0-9_]+ <Column>
36
+ end
37
+
38
+ rule operator
39
+ ( 'eq'i &_sp / 'ne'i &_sp / 'gte'i &_sp / 'gt'i &_sp / 'lte'i &_sp / 'lt'i &_sp / '=' / '!=' / '<>' / '>=' / '>' / '<=' / '<' ) <Operator>
40
+ end
41
+
42
+ rule literal
43
+ ( '"' ( '\"' / [^"] )+ '"' / "'" ( "''" / "\\'" / [^'] )+ "'" / [^\s,)]+ ) <Literal>
44
+ end
45
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: whereable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mack Earnhardt
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-11-12 00:00:00.000000000 Z
11
+ date: 2020-11-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -41,7 +41,7 @@ dependencies:
41
41
  description: |
42
42
  Translates where-like filter syntax into an Arel-based ActiveRecord scope,
43
43
  so you can safely use SQL syntax in Rails controller parameters.
44
- Not as powerful as Ransack, but simple and lightweight.
44
+ What it lacks in power, it gains in simplicity and ease of use for API consumers.
45
45
  email:
46
46
  - mack@agilereasoning.com
47
47
  executables: []
@@ -51,16 +51,17 @@ files:
51
51
  - "./CHANGELOG.md"
52
52
  - "./LICENSE.txt"
53
53
  - "./README.md"
54
- - "./lib/whereable.treetop"
54
+ - "./lib/whereable_clause.treetop"
55
55
  - lib/whereable.rb
56
56
  - lib/whereable/version.rb
57
+ - lib/whereable_clause.rb
57
58
  homepage: https://github.com/MacksMind/whereable
58
59
  licenses:
59
60
  - MIT
60
61
  metadata:
61
62
  homepage_uri: https://github.com/MacksMind/whereable
62
63
  source_code_uri: https://github.com/MacksMind/whereable
63
- changelog_uri: https://github.com/MacksMind/whereable/blob/master/CHANGELOG.md
64
+ changelog_uri: https://github.com/MacksMind/whereable/blob/main/CHANGELOG.md
64
65
  post_install_message:
65
66
  rdoc_options: []
66
67
  require_paths:
@@ -1,37 +0,0 @@
1
- grammar Whereable
2
- rule expression
3
- disjunction
4
- end
5
-
6
- rule disjunction
7
- conjunction opt:( _? 'or'i _? conjunction )* <Disjunction>
8
- end
9
-
10
- rule conjunction
11
- term opt:( _? 'and'i _? term )* <Conjunction>
12
- end
13
-
14
- rule term
15
- condition / '(' _? expression _? ')' <NestedExpression>
16
- end
17
-
18
- rule condition
19
- column _? operator _? literal <Condition>
20
- end
21
-
22
- rule _
23
- [\s]+
24
- end
25
-
26
- rule column
27
- [a-zA-Z0-9_]+ <Column>
28
- end
29
-
30
- rule operator
31
- ( 'eq'i / 'ne'i / 'gte'i / 'gt'i / 'lte'i / 'lt'i / '=' / '!=' / '<>' / '>=' / '>' / '<=' / '<' ) <Operator>
32
- end
33
-
34
- rule literal
35
- ( '"' ( '\"' / [^"] )+ '"' / "'" ( "''" / "\\'" / [^'] )+ "'" / [^\s)]+ ) <Literal>
36
- end
37
- end