Skip to content

Commit f12fbc1

Browse files
authored
Merge pull request #33 from procore/support_json_array
Support json array
2 parents 10f6d78 + e9df805 commit f12fbc1

File tree

10 files changed

+213
-18
lines changed

10 files changed

+213
-18
lines changed

.rubocop.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ Style/TrailingCommaInArguments:
373373
- no_comma
374374
Enabled: true
375375

376-
Style/TrailingCommaInLiteral:
376+
Style/TrailingCommaInArrayLiteral:
377377
Description: 'Checks for trailing comma in array and hash literals.'
378378
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas'
379379
EnforcedStyleForMultiline: comma

README.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,26 @@ class PostsController < ApplicationController
109109
end
110110
```
111111

112+
### Filter on Ranges
113+
Some parameter types support ranges. Ranges are expected to be a string with the bounding values separated by `...`
114+
115+
For example `?filters[price]=3...50` would return records with a price between 3 and 50.
116+
117+
The following types support ranges
118+
* int
119+
* decimal
120+
* boolean
121+
* date
122+
* time
123+
* datetime
124+
125+
### Filter on JSON Array
126+
`int` type filters support sending the values as an array in the URL Query parameters. For example `?filters[id]=[1,2]`. This is a way to keep payloads smaller for GET requests. When URI encoded this will become `filters%5Bid%5D=%5B1,2%5D` which is much smaller the standard format of `filters%5Bid%5D%5B%5D=1&&filters%5Bid%5D%5B%5D=2`.
127+
128+
On the server side, the params will be received as:
129+
`"filters"=>{"id"=>"[1,2]"}` compared to the standard format of
130+
`"filters"=>{"id"=>["1", "2"]}`.
131+
112132
### Sort Types
113133
Every sort must have a type, so that Brita knows what to do with it. The current valid sort types are:
114134
* int - Sort on an integer column
@@ -189,7 +209,11 @@ $ gem install brita
189209

190210
## Contributing
191211

192-
Contribution directions go here.
212+
Running tests:
213+
```bash
214+
$ bundle exec rake test
215+
```
216+
Bash shell is recommend if tests are not working in your shell of choice.
193217

194218
## License
195219

lib/brita.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
require "brita/subset_comparator"
66
require "brita/type_validator"
77
require "brita/parameter"
8+
require "brita/value_parser"
89
require "brita/scope_handler"
910
require "brita/where_handler"
1011
require "brita/validators/valid_int_validator"

lib/brita/filter.rb

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ def param
5151

5252
private
5353

54+
def parameterize(value)
55+
ValueParser.new(value: value, options: parameter.parse_options).parse
56+
end
57+
5458
def not_processable?(value)
5559
value.nil? && default.nil?
5660
end
@@ -65,20 +69,6 @@ def mapped_scope_params(params)
6569
end
6670
end
6771

68-
def parameterize(value)
69-
if supports_ranges? && value.to_s.include?("...")
70-
Range.new(*value.split("..."))
71-
elsif type == :boolean
72-
if Rails.version.starts_with?("5")
73-
ActiveRecord::Type::Boolean.new.cast(value)
74-
else
75-
ActiveRecord::Type::Boolean.new.type_cast_from_user(value)
76-
end
77-
else
78-
value
79-
end
80-
end
81-
8272
def valid_scope_params?(scope_params)
8373
scope_params.is_a?(Array) && scope_params.all? { |symbol| symbol.is_a?(Symbol) }
8474
end

lib/brita/parameter.rb

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,12 @@ def initialize(param, type, internal_name = param)
99
@internal_name = internal_name
1010
end
1111

12-
def supports_ranges?
13-
![:string, :text, :scope].include?(type)
12+
def parse_options
13+
{
14+
supports_boolean: supports_boolean?,
15+
supports_ranges: supports_ranges?,
16+
supports_json: supports_json?
17+
}
1418
end
1519

1620
def handler
@@ -20,5 +24,19 @@ def handler
2024
WhereHandler.new(self)
2125
end
2226
end
27+
28+
private
29+
30+
def supports_ranges?
31+
![:string, :text, :scope].include?(type)
32+
end
33+
34+
def supports_json?
35+
type == :int
36+
end
37+
38+
def supports_boolean?
39+
type == :boolean
40+
end
2341
end
2442
end

lib/brita/validators/valid_int_validator.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ def valid_int?(value)
1111
end
1212

1313
def integer_array?(value)
14+
if value.is_a?(String)
15+
value = Brita::ValueParser.new(value: value).array_from_json
16+
end
17+
1418
value.is_a?(Array) && value.any? && value.all? { |v| integer_or_range?(v) }
1519
end
1620

lib/brita/value_parser.rb

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
module Brita
2+
class ValueParser
3+
attr_accessor :value, :supports_boolean, :supports_json, :supports_ranges
4+
def initialize(value:, options: {})
5+
@value = value
6+
@supports_boolean = options.fetch(:supports_boolean, false)
7+
@supports_ranges = options.fetch(:supports_ranges, false)
8+
@supports_json = options.fetch(:supports_json, false)
9+
end
10+
11+
def parse
12+
@_result ||=
13+
if parse_as_range?
14+
range_value
15+
elsif parse_as_boolean?
16+
boolean_value
17+
elsif parse_as_json?
18+
array_from_json
19+
else
20+
value
21+
end
22+
end
23+
24+
def array_from_json
25+
result = JSON.parse(value)
26+
if result.is_a?(Array)
27+
result
28+
else
29+
value
30+
end
31+
rescue JSON::ParserError
32+
value
33+
end
34+
35+
private
36+
37+
def parse_as_range?
38+
supports_ranges && value.to_s.include?("...")
39+
end
40+
41+
def range_value
42+
Range.new(*value.split("..."))
43+
end
44+
45+
def parse_as_json?
46+
supports_json && value.is_a?(String)
47+
end
48+
49+
def parse_as_boolean?
50+
supports_boolean
51+
end
52+
53+
def boolean_value
54+
if Rails.version.starts_with?("5")
55+
ActiveRecord::Type::Boolean.new.cast(value)
56+
else
57+
ActiveRecord::Type::Boolean.new.type_cast_from_user(value)
58+
end
59+
end
60+
end
61+
end

test/controller_test.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,19 @@ class PostsControllerTest < ActionDispatch::IntegrationTest
2222
assert_equal post.id, json.first["id"]
2323
end
2424

25+
test "it filters on id by value for a JSON string array" do
26+
post1 = Post.create!
27+
post2 = Post.create!
28+
post3 = Post.create!
29+
30+
get("/posts", params: { filters: { id: "[#{post1.id},#{post2.id}]" } })
31+
32+
json = JSON.parse(@response.body)
33+
assert_equal 2, json.size
34+
ids_array = json.map { |json_hash| json_hash["id"] }
35+
assert_equal [post1.id, post2.id], ids_array
36+
end
37+
2538
test "it filters on decimals" do
2639
post = Post.create!(rating: 1.25)
2740
Post.create!

test/type_validator_test.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,11 @@ class TypeValidatorTest < ActiveSupport::TestCase
3333

3434
assert_equal expected_validation, validator.validate
3535
end
36+
37+
test "it accepts a json array for type int" do
38+
validator = Brita::TypeValidator.new("[1,10]", :int)
39+
expected_validation = { valid_int: true }
40+
41+
assert_equal expected_validation, validator.validate
42+
end
3643
end

test/value_parser_test.rb

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
require "test_helper"
2+
3+
class FilterTest < ActiveSupport::TestCase
4+
test "returns the original value without options" do
5+
parser = Brita::ValueParser.new(value: "hi")
6+
7+
assert_equal "hi", parser.parse
8+
end
9+
10+
test "With options an array of integers results in an array of integers" do
11+
options = {
12+
supports_ranges: true,
13+
supports_json: true
14+
}
15+
parser = Brita::ValueParser.new(value: [1,2,3])
16+
17+
assert_equal [1,2,3], parser.parse
18+
end
19+
20+
test "With options a json string array of integers results in an array of integers" do
21+
options = {
22+
supports_ranges: true,
23+
supports_json: true
24+
}
25+
parser = Brita::ValueParser.new(value: "[1,2,3]", options: options)
26+
27+
assert_equal [1,2,3], parser.parse
28+
end
29+
30+
test "with invalid json returns original value" do
31+
options = {
32+
supports_ranges: true,
33+
supports_json: true
34+
}
35+
parser = Brita::ValueParser.new(value: "[1,2,3", options: options)
36+
37+
assert_equal "[1,2,3", parser.parse
38+
end
39+
40+
test "JSON parsing only supports arrays" do
41+
options = {
42+
supports_json: true
43+
}
44+
json_string = "{\"a\":4}"
45+
parser = Brita::ValueParser.new(value: json_string, options: options)
46+
47+
assert_equal json_string, parser.parse
48+
end
49+
50+
test "With options a range string of integers results in a range" do
51+
options = {
52+
supports_ranges: true,
53+
supports_json: true
54+
}
55+
parser = Brita::ValueParser.new(value: "1...3", options: options)
56+
57+
assert_instance_of Range, parser.parse
58+
end
59+
60+
test "parses true from 1" do
61+
options = {
62+
supports_boolean: true
63+
}
64+
parser = Brita::ValueParser.new(value: 1, options: options)
65+
66+
assert_equal true, parser.parse
67+
end
68+
69+
test "parses false from 0" do
70+
options = {
71+
supports_boolean: true
72+
}
73+
parser = Brita::ValueParser.new(value: 0, options: options)
74+
75+
assert_equal false, parser.parse
76+
end
77+
end

0 commit comments

Comments
 (0)