whereable 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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