active_hash_relation 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4b491ebb86ce76c8bd1380597b0c1fcba6c787b5
4
+ data.tar.gz: 59ce2d3323cfe3b85aa5fc50b9fc81c9414cd666
5
+ SHA512:
6
+ metadata.gz: 6580c8a889a299e994b2af5dd32d937281afce27c9583838fc793b0d93c0ef9638f7ea0e89bb283bd2bb699aae8fe1dc804953e6e7ef284890b5da0c063c220c
7
+ data.tar.gz: e63134ea30cec6c28cc92dfbf46bd0689465464542c6b2d87d36d90c7fe5a57491ea0211fc3f948a17530589baa8a809af0a0191413fb1179528494ca2b236e0
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in active_hash_relation.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Filippos Vasilakis
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # ActiveHashRelation
2
+
3
+ ## Introduction
4
+ Simple gem that allows you to manipulate ActiveRecord::Relation using JSON. For instance:
5
+ ```ruby
6
+ apply_filters(resource, {name: 'RPK', id: [1,2,3,4,5,6,7,8,9], start_date: {leq: "2014-10-19"}, act_status: "ongoing"})
7
+ ```
8
+ filter a resource based on it's associations:
9
+ ```ruby
10
+ apply_filters(resource, {updated_at: { geq: "2014-11-2 14:25:04"}, unit: {id: 9})
11
+ ```
12
+ or even filter a resource based on it's associations' associations:
13
+ ```ruby
14
+ apply_filters(resource, {updated_at: { geq: "2014-11-2 14:25:04"}, unit: {id: 9, areas: {id: 22} }})
15
+ ```
16
+ and the list could go on.. Basically your whole db is exposed there. It's perfect for filtering a collection of resources on APIs.
17
+
18
+ ## Installation
19
+
20
+ Add this line to your application's Gemfile:
21
+
22
+ gem 'active_hash_relation'
23
+
24
+ And then execute:
25
+
26
+ $ bundle
27
+
28
+ Or install it yourself as:
29
+
30
+ $ gem install active_hash_relation
31
+ ## How to use
32
+ The gem exposes only one method: `apply_filters(resource, hash_params, include_associations: true, model: nil)`. `resource` is expected to be an ActiveRecord::Relation.
33
+ That way, you can add your custom filters before passing the `Relation` to `ActiveHashRelation`.
34
+
35
+ In order to use it you have to include ActiveHashRelation module in your class. For instance in a Rails API controller you would do:
36
+
37
+ ```ruby
38
+ class Api::V1::ResourceController < Api::V1::BaseController
39
+ include ActiveHashRelation
40
+
41
+ def index
42
+ resources = apply_filters(Resource.all, params)
43
+
44
+ authorized_resources = policy_scope(resource)
45
+
46
+ render json: resources, each_serializer: Api::V1::ResourceSerializer
47
+ end
48
+ end
49
+ ```
50
+
51
+ ## The API
52
+ ### Columns
53
+ For each param, `apply_filters` method will search in the model's (derived from the first param, or explicitly defined as the last param) all the record's column names and associations. (filtering based on scopes are not working at the moment but will be supported soon). For each column, if there is such a param, it will apply the filter based on the column type. The following column types are supported:
54
+
55
+ #### Primary
56
+ You can apply a filter a column which is a primary key by value or using an array like:
57
+ * `{primary_key_column: 5}`
58
+ * `{primary_key)column: [1,3,4,5,6,7]}`
59
+
60
+ #### Integer, Float, Decimal, Date, Time or Datetime/Timestamp
61
+ You can apply an equality filter:
62
+ * `{example_column: 500}`
63
+ or using a hash as a value you get more options:
64
+ * `{example_column: {le: 500}}`
65
+ * `{example_column: {leq: 500}}`
66
+ * `{example_column: {ge: 500}}`
67
+ * `{example_column: {geq: 500}}`
68
+
69
+ Of course you can provide a compination of those like:
70
+ * `{example_column: {geq: 500, le: 1000}}`
71
+
72
+ The same api is for Date, Time or Datetime/Timestamp.
73
+
74
+ #### Boolean
75
+ The boolean value is converted from string using ActiveRecord's `TRUE_VALUES` through `value_to_boolean` method.. So for a value to be true must be one of the following: `[true, 1, '1', 't', 'T', 'true', 'TRUE']`. Anything else is false.
76
+ * `{example_column: true}`
77
+ * `{example_column: 0}`
78
+
79
+ #### String or Text
80
+ You can apply an incensitive matching filter (currently working only for Postgres):
81
+ * `{example_column: test}`
82
+
83
+ The above filter will search all records that include `test` in the `example_column` field. A better would be nice here, for instance, setting the search sensitive or insensitive, start or end with a string etch
84
+
85
+
86
+ ### Associations
87
+ If the association is a `belongs_to` or `has_one`, then the hash key name must be in singular. If the association is `has_many` the attribute must be in plural reflecting the association type. When you have, in your hash, filters for an association, the sub-hash is passed in the association's model. For instance, let's say a user has many microposts and the following filter is applied (could be through an HTTP GET request on controller's index method):
88
+ * `{email: test@user.com, microposts: {created_at { leq: 12-9-2014} }`
89
+
90
+ Internally, ActiveHashRelation, extracts `{created_at { leq: 12-9-2014} }` and runs it on Micropost model. So the final query will look like:
91
+
92
+ ```ruby
93
+ micropost_filter = Micropost.all.where("CREATED_AT =< ?", '12-9-2014'.to_datetime)
94
+ User.where(email: 'test@user.com').joins(:microposts).merge(micropost_filter)
95
+ ```
96
+
97
+ ### Scopes
98
+ Scopes are supported via a tiny monkeypatch in the ActiveRecord's scope class method which holds the name of each scope. Only scopes that don't accept arguments are supported. The rest could also be supported but it wouldn't make much sense.. If you want to filter based on a scope in a model, the scope names should go under `scopes` sub-hash. For instance the following:
99
+ * `{ scopes: { planned: true } }`
100
+
101
+ will run the `.planned` scope on the resource.
102
+
103
+ ### Whitelisting
104
+ If you don't want to allow a column/association/scope just remove it from the params hash.
105
+
106
+ ## Contributing
107
+
108
+ 1. Fork it ( https://github.com/[my-github-username]/active_hash_relation/fork )
109
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
110
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
111
+ 4. Push to the branch (`git push origin my-new-feature`)
112
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'active_hash_relation/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "active_hash_relation"
8
+ spec.version = ActiveHashRelation::VERSION
9
+ spec.authors = ["Filippos Vasilakis"]
10
+ spec.email = ["vasilakisfil@gmail.com"]
11
+ spec.summary = %q{Simple gem that allows you to run multiple ActiveRecord::Relation using hash. Perfect for APIs.}
12
+ spec.description = spec.summary
13
+ spec.homepage = "https://www.kollegorna.se"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.6"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ end
@@ -0,0 +1,21 @@
1
+ module ActiveHashRelation::AssociationFilters
2
+ def filter_associations(resource, params, model: nil)
3
+ unless model
4
+ model = model_class_name(resource)
5
+ end
6
+
7
+ model.reflect_on_all_associations.map(&:name).each do |association|
8
+ if params[association]
9
+ association_name = association.to_s.titleize.split.join
10
+ association_filters = ActiveHashRelation::FilterApplier.new(
11
+ association_name.singularize.constantize.all,
12
+ params[association],
13
+ include_associations: true
14
+ ).apply_filters
15
+ resource = resource.joins(association).merge(association_filters)
16
+ end
17
+ end
18
+
19
+ return resource
20
+ end
21
+ end
@@ -0,0 +1,77 @@
1
+ module ActiveHashRelation::ColumnFilters
2
+ def model_class_name(resource)
3
+ resource.class.to_s.split('::').first.constantize
4
+ end
5
+
6
+ def filter_primary(resource, column, param)
7
+ resource = resource.where(id: param)
8
+ end
9
+
10
+ def filter_integer(resource, column, table_name, param)
11
+ if !param.is_a? Hash
12
+ return resource.where(column => param)
13
+ else
14
+ return apply_leq_geq_le_ge_filters(resource, table_name, column, param)
15
+ end
16
+ end
17
+
18
+ def filter_float(resource, column, table_name, param)
19
+ filter_integer(resource, column, table_name, param)
20
+ end
21
+
22
+ def filter_decimal(resource, column, table_name, param)
23
+ filter_integer(resource, column, table_name, param)
24
+ end
25
+
26
+ def filter_string(resource, column, table_name, param)
27
+ resource = resource.where("#{table_name}.#{column} ILIKE ?", "%#{param}%")
28
+ end
29
+
30
+ def filter_text(resource, column, param)
31
+ return filter_string(resource, column, param)
32
+ end
33
+
34
+ def filter_date(resource, column, table_name, param)
35
+ if !param.is_a? Hash
36
+ resource = resource.where(column => param[column])
37
+ else
38
+ return apply_leq_geq_le_ge_filters(resource, table_name, column, param)
39
+ end
40
+
41
+ return resource
42
+ end
43
+
44
+ def filter_datetime(resource, column, table_name, param)
45
+ if !param.is_a? Hash
46
+ resource = resource.where(column => param[column])
47
+ else
48
+ return apply_leq_geq_le_ge_filters(resource, table_name, column, param)
49
+ end
50
+
51
+ return resource
52
+ end
53
+
54
+ def filter_boolean(resource, column, param)
55
+ b_param = ActiveRecord::ConnectionAdapters::Column.value_to_boolean(param)
56
+
57
+ resource = resource.where(column => b_param)
58
+ end
59
+
60
+ private
61
+
62
+ def apply_leq_geq_le_ge_filters(resource, table_name, column, param)
63
+ if param[:leq]
64
+ resource = resource.where("#{table_name}.#{column} <= ?", param[:leq])
65
+ elsif param[:le]
66
+ resource = resource.where("#{table_name}.#{column} < ?", param[:leq])
67
+ end
68
+
69
+ if param[:geq]
70
+ resource = resource.where("#{table_name}.#{column} >= ?", param[:geq])
71
+ elsif param[:ge]
72
+ resource = resource.where("#{table_name}.#{column} > ?", param[:geq])
73
+ end
74
+
75
+ return resource
76
+ end
77
+ end
@@ -0,0 +1,50 @@
1
+ module ActiveHashRelation
2
+ class FilterApplier
3
+ include ColumnFilters
4
+ include AssociationFilters
5
+ include ScopeFilters
6
+
7
+ def initialize(resource, params, include_associations: false, model: nil)
8
+ @resource = resource
9
+ @params = HashWithIndifferentAccess.new(params)
10
+ @include_associations = include_associations
11
+ @model = model
12
+ end
13
+
14
+
15
+ def apply_filters
16
+ unless @model
17
+ @model = model_class_name(@resource)
18
+ end
19
+ table_name = @model.table_name
20
+ @model.columns.each do |c|
21
+ next if @params[c.name.to_s].nil?
22
+
23
+ @resource = filter_primary(@resource, c.name, @params[c.name]) and next if c.primary
24
+ case c.type
25
+ when :integer
26
+ @resource = filter_integer(@resource, c.name, table_name, @params[c.name])
27
+ when :float
28
+ @resource = filter_float(@resource, c.name, table_name, @params[c.name])
29
+ when :decimal
30
+ @resource = filter_decimal(@resource, c.name, table_name, @params[c.name])
31
+ when :string
32
+ @resource = filter_string(@resource, c.name, table_name, @params[c.name])
33
+ when :date
34
+ @resource = filter_date(@resource, c.name, table_name, @params[c.name])
35
+ when :datetime, :timestamp
36
+ @resource = filter_datetime(@resource, c.name, table_name, @params[c.name])
37
+ when :boolean
38
+ @resource = filter_boolean(@resource, c.name, @params[c.name])
39
+ end
40
+ end
41
+
42
+
43
+ @resource = filter_scopes(@resource, @params[:scopes]) if @params.include?(:scopes)
44
+ @resource = filter_associations(@resource, @params)
45
+
46
+ return @resource
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,15 @@
1
+ module ActiveHashRelation::ScopeFilters
2
+ def filter_scopes(resource, params, model: nil)
3
+ unless model
4
+ model = model_class_name(resource)
5
+ end
6
+
7
+ model.scope_names.each do |scope|
8
+ if params.include?(scope)
9
+ resource = resource.send(scope)
10
+ end
11
+ end
12
+
13
+ return resource
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ module ActiveHashRelation
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,17 @@
1
+ require "active_record/scope_names"
2
+ require "active_hash_relation/version"
3
+ require "active_hash_relation/column_filters"
4
+ require "active_hash_relation/scope_filters"
5
+ require "active_hash_relation/association_filters"
6
+ require "active_hash_relation/filter_applier"
7
+
8
+ module ActiveHashRelation
9
+ def apply_filters(resource, params, include_associations: false, model: nil)
10
+ FilterApplier.new(
11
+ resource,
12
+ params,
13
+ include_associations: include_associations,
14
+ model: model
15
+ ).apply_filters
16
+ end
17
+ end
@@ -0,0 +1,34 @@
1
+ module ActiveRecord
2
+ module Scoping
3
+ module Named
4
+ module ClassMethods
5
+ attr_reader :scope_names
6
+
7
+ def scope(name, body, &block)
8
+ @scope_names ||= []
9
+ unless body.respond_to?(:call)
10
+ raise ArgumentError, 'The scope body needs to be callable.'
11
+ end
12
+
13
+ if dangerous_class_method?(name)
14
+ raise ArgumentError, "You tried to define a scope named \"#{name}\" " \
15
+ "on the model \"#{self.name}\", but Active Record already defined " \
16
+ "a class method with the same name."
17
+ end
18
+
19
+ extension = Module.new(&block) if block
20
+
21
+ singleton_class.send(:define_method, name) do |*args|
22
+ scope = all.scoping { body.call(*args) }
23
+ scope = scope.extending(extension) if extension
24
+
25
+ scope || all
26
+ end
27
+
28
+
29
+ @scope_names << name if body.arity.eql?(0)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_hash_relation
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Filippos Vasilakis
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-01-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ description: Simple gem that allows you to run multiple ActiveRecord::Relation using
42
+ hash. Perfect for APIs.
43
+ email:
44
+ - vasilakisfil@gmail.com
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - ".gitignore"
50
+ - Gemfile
51
+ - LICENSE.txt
52
+ - README.md
53
+ - Rakefile
54
+ - active_hash_relation.gemspec
55
+ - lib/active_hash_relation.rb
56
+ - lib/active_hash_relation/association_filters.rb
57
+ - lib/active_hash_relation/column_filters.rb
58
+ - lib/active_hash_relation/filter_applier.rb
59
+ - lib/active_hash_relation/scope_filters.rb
60
+ - lib/active_hash_relation/version.rb
61
+ - lib/active_record/scope_names.rb
62
+ homepage: https://www.kollegorna.se
63
+ licenses:
64
+ - MIT
65
+ metadata: {}
66
+ post_install_message:
67
+ rdoc_options: []
68
+ require_paths:
69
+ - lib
70
+ required_ruby_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ requirements: []
81
+ rubyforge_project:
82
+ rubygems_version: 2.4.5
83
+ signing_key:
84
+ specification_version: 4
85
+ summary: Simple gem that allows you to run multiple ActiveRecord::Relation using hash.
86
+ Perfect for APIs.
87
+ test_files: []