hario 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +20 -4
- data/hario.gemspec +3 -3
- data/lib/hario/behaviours/filter.rb +56 -41
- data/lib/hario/behaviours/pluck.rb +30 -28
- data/lib/hario/behaviours/utils.rb +5 -1
- data/lib/hario/version.rb +1 -1
- data/test/filter_test.rb +21 -0
- data/test/fixtures.rb +4 -1
- data/test/schema.rb +2 -0
- data/test/test_helper.rb +3 -1
- metadata +9 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 51eaa13608caa623aa57c2e1c2b275aab5419e0a
|
4
|
+
data.tar.gz: 0bdab870d96a09ccdaf3357f0c45107dc366a50e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2b0236ad432571151b232b5d2f869cc46048a60bbe058ee550347fa7940664728952c1a84f5f413bbc166aacd15eae613af47528d19645c8e451577fca862bb3
|
7
|
+
data.tar.gz: 7c598b57e7310d36620783a0bcd3dd17878e79a4dbe0b65221495fd2e7f52c221f7ea7ba23e5188f560483944af7fbc06d99150fe29c3e663326d2910188f43b
|
data/README.md
CHANGED
@@ -20,7 +20,7 @@ Or install it yourself as:
|
|
20
20
|
|
21
21
|
### Setup
|
22
22
|
|
23
|
-
Add `
|
23
|
+
Add `extend Hario::Filterable` to your AR model to add the `search` method, for instance (we'll use these classes as examples throughout):
|
24
24
|
|
25
25
|
```ruby
|
26
26
|
def Brand < ActiveRecord::Base
|
@@ -46,7 +46,9 @@ class BrandsController < ApplicationController
|
|
46
46
|
respond_to :json
|
47
47
|
|
48
48
|
def index
|
49
|
-
@brands = Brand.search(params[:filters])
|
49
|
+
@brands = Brand.search(params[:filters], params[:pluck])
|
50
|
+
# or to only use the filterable functionality
|
51
|
+
# @brands = Brand.search(params[:filters])
|
50
52
|
|
51
53
|
respond_with(@brands)
|
52
54
|
end
|
@@ -79,9 +81,23 @@ The available operators are:
|
|
79
81
|
- like (sql like)
|
80
82
|
- equals
|
81
83
|
|
82
|
-
|
84
|
+
### Pluck
|
83
85
|
|
84
|
-
|
86
|
+
Oftentimes you might only need a particular attribute, or a few attributes, from the resource, in which case getting the whole of the resources would be inefficient.
|
87
|
+
|
88
|
+
This is where pluck comes in handy, it allows you to specify which attributes you're interested in in the request, reducing the amount of data that has to be retrieved from the database, and more importantly, sent over the wire:
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
@brands = Brand.search(nil, ["name"])
|
92
|
+
# will return an array of hashes containing only the primary key and the name attribute for each brand.
|
93
|
+
```
|
94
|
+
|
95
|
+
Like filters, you can also pluck associated columns from other models using the dot notation, e.g.:
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
# assuming Country belongs_to :continent
|
99
|
+
@countries = Country.search(nil, ["name", "continent.name"])
|
100
|
+
```
|
85
101
|
|
86
102
|
## Tests
|
87
103
|
|
data/hario.gemspec
CHANGED
@@ -17,10 +17,10 @@ Gem::Specification.new do |spec|
|
|
17
17
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
18
|
spec.require_paths = ["lib"]
|
19
19
|
|
20
|
-
spec.add_runtime_dependency "activerecord", "
|
20
|
+
spec.add_runtime_dependency "activerecord", ">= 4.0"
|
21
21
|
|
22
22
|
spec.add_development_dependency "bundler", "~> 1.6"
|
23
|
-
spec.add_development_dependency "rake"
|
23
|
+
spec.add_development_dependency "rake"
|
24
24
|
spec.add_development_dependency "sqlite3", "~> 1.3.5"
|
25
|
-
spec.add_development_dependency "database_rewinder"
|
25
|
+
spec.add_development_dependency "database_rewinder"
|
26
26
|
end
|
@@ -1,57 +1,72 @@
|
|
1
1
|
require "hario/behaviours/utils"
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
module Hario
|
4
|
+
class FilterParser
|
5
|
+
include ParserUtils
|
5
6
|
|
6
|
-
|
7
|
+
OPERATORS = { lt: '<', gt: '>', lte: '<=', gte: '>=', like: 'like', equals: '=' }
|
7
8
|
|
8
|
-
|
9
|
+
attr_accessor :join_clause, :where_clauses
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
11
|
+
def initialize(filters, klass)
|
12
|
+
@filters = filters
|
13
|
+
@klass = klass
|
13
14
|
|
14
|
-
|
15
|
-
|
15
|
+
parse_filters
|
16
|
+
end
|
16
17
|
|
17
|
-
|
18
|
-
def parse_filters
|
19
|
-
@join_clause, @where_clauses = @filters.inject([{}, []]) do |m, (descriptor, value)|
|
20
|
-
association_chain, attribute, operator = parse_descriptor(descriptor)
|
21
|
-
condition = build_condition(association_chain, attribute, operator, value)
|
18
|
+
InvalidAttributeError = Class.new(RuntimeError)
|
22
19
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
end
|
20
|
+
private
|
21
|
+
def parse_filters
|
22
|
+
@join_clause, @where_clauses = @filters.inject([{}, []]) do |m, (descriptor, value)|
|
23
|
+
association_chain, attribute, operator = parse_descriptor(descriptor)
|
24
|
+
condition = build_condition(association_chain, attribute, operator, value)
|
29
25
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
26
|
+
nested_associations = (association_chain.dup << {}).reverse.inject { |v, key| { key => v } }
|
27
|
+
joins = m[0].deep_merge(nested_associations)
|
28
|
+
wheres = m[1] + [condition]
|
29
|
+
[joins, wheres]
|
30
|
+
end
|
31
|
+
end
|
35
32
|
|
36
|
-
|
37
|
-
|
33
|
+
def parse_descriptor(descriptor)
|
34
|
+
parts = descriptor.split('.')
|
35
|
+
operator = parts.pop.to_sym
|
36
|
+
attribute = parts.pop
|
37
|
+
association_chain = parts
|
38
38
|
|
39
|
-
|
40
|
-
if association_chain.any?
|
41
|
-
attribute_table = table_name_from_association_chain(association_chain)
|
39
|
+
[association_chain, attribute, operator]
|
42
40
|
end
|
43
41
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
42
|
+
def build_condition(association_chain, attribute, operator, value)
|
43
|
+
if association_chain.any?
|
44
|
+
end_model = end_model_from_association_chain(association_chain)
|
45
|
+
attribute_table = end_model.table_name
|
46
|
+
else
|
47
|
+
end_model = @klass
|
48
|
+
end
|
49
|
+
|
50
|
+
raise_if_invalid_attribute!(attribute, end_model)
|
51
|
+
|
52
|
+
case operator
|
53
|
+
when :equals
|
54
|
+
condition = { attribute => value }
|
55
|
+
condition = { attribute_table => condition } if attribute_table
|
56
|
+
else
|
57
|
+
operator = OPERATORS[operator]
|
58
|
+
condition = ["#{attribute} #{operator} ?", value]
|
59
|
+
condition[0].prepend("#{attribute_table || @klass.table_name}.")
|
60
|
+
end
|
61
|
+
|
62
|
+
condition
|
53
63
|
end
|
54
64
|
|
55
|
-
|
56
|
-
|
65
|
+
def raise_if_invalid_attribute!(attribute, end_model)
|
66
|
+
unless end_model.column_names.include?(attribute)
|
67
|
+
raise InvalidAttributeError,
|
68
|
+
"'#{attribute}' is not a valid column name for '#{end_model.table_name}'"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
57
72
|
end
|
@@ -1,42 +1,44 @@
|
|
1
1
|
require "hario/behaviours/utils"
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
module Hario
|
4
|
+
class PluckParser
|
5
|
+
include ParserUtils
|
5
6
|
|
6
|
-
|
7
|
+
attr_accessor :join_clause, :pluck_clause
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
9
|
+
def initialize(pluck, klass)
|
10
|
+
@pluck = pluck
|
11
|
+
@klass = klass
|
11
12
|
|
12
|
-
|
13
|
-
|
13
|
+
parse_pluck
|
14
|
+
end
|
14
15
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
16
|
+
private
|
17
|
+
def parse_pluck
|
18
|
+
@join_clause = {}
|
19
|
+
@pluck_clause = [[@klass.table_name, 'id'].join('.')]
|
19
20
|
|
20
|
-
|
21
|
+
ns, no_ns = @pluck.partition{ |p| p.include?('.') }
|
21
22
|
|
22
|
-
|
23
|
+
no_ns.each{ |p| @pluck_clause << [@klass.table_name, p].join('.') }
|
23
24
|
|
24
|
-
|
25
|
-
|
25
|
+
ns.each do |p|
|
26
|
+
association_chain, attribute = parse_namespace(p)
|
26
27
|
|
27
|
-
|
28
|
-
|
28
|
+
nested_associations = (association_chain.dup << {}).reverse.inject { |v, key| { key => v } }
|
29
|
+
@join_clause.deep_merge!(nested_associations)
|
29
30
|
|
30
|
-
|
31
|
-
|
31
|
+
attribute_table = table_name_from_association_chain(association_chain)
|
32
|
+
@pluck_clause << [attribute_table, attribute].join('.')
|
33
|
+
end
|
32
34
|
end
|
33
|
-
end
|
34
35
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
36
|
+
def parse_namespace(namespace)
|
37
|
+
parts = namespace.split('.')
|
38
|
+
attribute = parts.pop
|
39
|
+
association_chain = parts
|
40
|
+
|
41
|
+
[association_chain, attribute]
|
42
|
+
end
|
43
|
+
end
|
42
44
|
end
|
@@ -1,11 +1,15 @@
|
|
1
1
|
module ParserUtils
|
2
2
|
def table_name_from_association_chain(association_chain)
|
3
|
+
end_model_from_association_chain(association_chain).table_name
|
4
|
+
end
|
5
|
+
|
6
|
+
def end_model_from_association_chain(association_chain)
|
3
7
|
head = @klass
|
4
8
|
|
5
9
|
association_chain.each do |a_name|
|
6
10
|
head = head.reflect_on_all_associations.find{ |a| a.name.to_s == a_name }.klass
|
7
11
|
end
|
8
12
|
|
9
|
-
head
|
13
|
+
head
|
10
14
|
end
|
11
15
|
end
|
data/lib/hario/version.rb
CHANGED
data/test/filter_test.rb
CHANGED
@@ -1,6 +1,12 @@
|
|
1
1
|
require_relative 'test_helper'
|
2
|
+
require 'date'
|
2
3
|
|
3
4
|
class FilterTest < Hario::Test
|
5
|
+
def test_test_data_loaded_properly
|
6
|
+
assert Brand.count > 0,
|
7
|
+
"Test data not loaded"
|
8
|
+
end
|
9
|
+
|
4
10
|
def test_simple_filter
|
5
11
|
filters = { 'name.equals' => "Adidas" }
|
6
12
|
brands = Brand.search(filters)
|
@@ -27,4 +33,19 @@ class FilterTest < Hario::Test
|
|
27
33
|
assert_equal 1, brands.count
|
28
34
|
assert brand.name == "Adidas"
|
29
35
|
end
|
36
|
+
|
37
|
+
def test_filter_with_date_condition
|
38
|
+
filters = { 'created_at.gt' => (DateTime.now - 5).iso8601 }
|
39
|
+
products = Product.search(filters)
|
40
|
+
|
41
|
+
assert_equal 2, products.count
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_invalid_attribute_raises
|
45
|
+
filters = { 'foobar.equals' => "ehehehe" }
|
46
|
+
|
47
|
+
assert_raises Hario::FilterParser::InvalidAttributeError do
|
48
|
+
Product.search(filters)
|
49
|
+
end
|
50
|
+
end
|
30
51
|
end
|
data/test/fixtures.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
1
3
|
@adidas = Brand.create!(name: "Adidas")
|
2
4
|
@shoe = ProductCategory.create!(name: "Shoe")
|
3
5
|
@tee = ProductCategory.create!(name: "T-shirt")
|
@@ -5,7 +7,8 @@
|
|
5
7
|
Product.create!(
|
6
8
|
brand_id: @adidas.id,
|
7
9
|
category_id: @shoe.id,
|
8
|
-
name: "Gazelle OG"
|
10
|
+
name: "Gazelle OG",
|
11
|
+
created_at: DateTime.now - 10
|
9
12
|
)
|
10
13
|
Product.create!(
|
11
14
|
brand_id: @adidas.id,
|
data/test/schema.rb
CHANGED
@@ -1,12 +1,14 @@
|
|
1
1
|
ActiveRecord::Schema.define(:version => 1) do
|
2
2
|
create_table :brands, :force => true do |t|
|
3
3
|
t.column :name, :string
|
4
|
+
t.timestamps null: false
|
4
5
|
end
|
5
6
|
|
6
7
|
create_table :products, :force => true do |t|
|
7
8
|
t.column :name, :string
|
8
9
|
t.column :category_id, :integer
|
9
10
|
t.column :brand_id, :integer
|
11
|
+
t.timestamps null: false
|
10
12
|
end
|
11
13
|
|
12
14
|
create_table :product_categories, :force => true do |t|
|
data/test/test_helper.rb
CHANGED
@@ -8,7 +8,9 @@ require "models"
|
|
8
8
|
|
9
9
|
ActiveRecord::Base.configurations = YAML.load_file(File.join(File.dirname(__FILE__), 'database.yml'))
|
10
10
|
ActiveRecord::Base.establish_connection(ENV['DB'] || :sqlite3)
|
11
|
-
|
11
|
+
ActiveRecord::Migration.suppress_messages do
|
12
|
+
load(File.join(File.dirname(__FILE__), "schema.rb"))
|
13
|
+
end
|
12
14
|
|
13
15
|
ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__), "debug.log"))
|
14
16
|
|
metadata
CHANGED
@@ -1,27 +1,27 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hario
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mike Campbell
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2016-03-17 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
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '4.0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '4.0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
@@ -42,14 +42,14 @@ dependencies:
|
|
42
42
|
name: rake
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- - "
|
45
|
+
- - ">="
|
46
46
|
- !ruby/object:Gem::Version
|
47
47
|
version: '0'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- - "
|
52
|
+
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
@@ -70,14 +70,14 @@ dependencies:
|
|
70
70
|
name: database_rewinder
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
|
-
- - "
|
73
|
+
- - ">="
|
74
74
|
- !ruby/object:Gem::Version
|
75
75
|
version: '0'
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
|
-
- - "
|
80
|
+
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
83
|
description:
|
@@ -126,7 +126,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
126
126
|
version: '0'
|
127
127
|
requirements: []
|
128
128
|
rubyforge_project:
|
129
|
-
rubygems_version: 2.
|
129
|
+
rubygems_version: 2.4.6
|
130
130
|
signing_key:
|
131
131
|
specification_version: 4
|
132
132
|
summary: Hario provides ActiveRecord filtering for Rails APIs.
|