scopiform 0.1.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
+ SHA256:
3
+ metadata.gz: 394c2b67ce4035049ec008ebb55628c2ccff5b37f7bed36d3dee01b3ed16d7bf
4
+ data.tar.gz: 4e08965af4db81bd6694c999ab3718f1ad81bcfceebbcbb3f666e4faaf7eaef5
5
+ SHA512:
6
+ metadata.gz: 7f412f41367de72362799423196deeb247926941e0d710faa4e94bd41f9af79b76182c52d0ba90f9b7366fb65c192848bbf94fe22ceede2bf5644b59f567b576
7
+ data.tar.gz: e955b4a42b3f4aea8ea456424e6b54e637cc8592448024a0c31ddf277acb7a219d1ab7577645aeff20dc1fd2892239fa4c674907f21adf02a0755f0164a9394d
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2020 jayce.pulsipher
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # Scopiform
2
+ Short description and motivation.
3
+
4
+ ## Usage
5
+ How to use my plugin.
6
+
7
+ ## Installation
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'scopiform'
12
+ ```
13
+
14
+ And then execute:
15
+ ```bash
16
+ $ bundle
17
+ ```
18
+
19
+ Or install it yourself as:
20
+ ```bash
21
+ $ gem install scopiform
22
+ ```
23
+
24
+ ## Public ActiveRecord Methods
25
+ :aliases_for
26
+ :apply_filters
27
+ :apply_orders
28
+ :association
29
+ :attribute_aliases_inverted
30
+ :auto_scope_add
31
+ :auto_scope?
32
+ :auto_scopes_by_attribute
33
+ :auto_scopes
34
+ :column
35
+ :reflection_added
36
+ :resolve_alias
37
+
38
+ ## Contributing
39
+ Contribution directions go here.
40
+
41
+ ## License
42
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,27 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Scopiform'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ require 'bundler/gem_tasks'
18
+
19
+ require 'rake/testtask'
20
+
21
+ Rake::TestTask.new(:test) do |t|
22
+ t.libs << 'test'
23
+ t.pattern = 'test/**/*_test.rb'
24
+ t.verbose = false
25
+ end
26
+
27
+ task default: :test
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+
5
+ module Scopiform
6
+ module AssociationScopes
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ setup_associations_auto_scopes
11
+ end
12
+
13
+ module ClassMethods
14
+ def reflection_added(_name, reflection)
15
+ setup_association_auto_scopes(reflection)
16
+ end
17
+
18
+ def scopiform_joins(*args, **kargs)
19
+ respond_to?(:left_outer_joins) ? left_outer_joins(*args, **kargs) : eager_load(*args, **kargs)
20
+ end
21
+
22
+ private
23
+
24
+ def setup_associations_auto_scopes
25
+ reflect_on_all_associations.each do |association|
26
+ setup_association_auto_scopes(association)
27
+ end
28
+ end
29
+
30
+ def setup_association_auto_scopes(association)
31
+ auto_scope_add(
32
+ association.name,
33
+ Proc.new { |value, joins: nil|
34
+ is_root = joins.nil?
35
+ joins = {} if is_root
36
+
37
+ joins[association.name] ||= {}
38
+ applied = association.klass.apply_filters(value, joins: joins[association.name])
39
+
40
+ if is_root
41
+ scopiform_joins(joins).merge(applied)
42
+ else
43
+ all.merge(applied)
44
+ end
45
+ },
46
+ suffix: '_is',
47
+ argument_type: :hash
48
+ )
49
+
50
+ # Ordering
51
+ auto_scope_add(
52
+ association.name,
53
+ Proc.new { |value| scopiform_joins(association.name).merge(association.klass.apply_orders(value)) },
54
+ prefix: 'order_by_',
55
+ argument_type: :hash,
56
+ type: :order
57
+ )
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+
5
+ module Scopiform
6
+ module CommonScopes
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ setup_common_auto_scopes
11
+ end
12
+
13
+ module ClassMethods
14
+ private
15
+
16
+ def setup_common_auto_scopes
17
+ safe_columns.each do |column|
18
+ name = column.name
19
+ name_sym = name.to_sym
20
+ type = column.type
21
+
22
+ auto_scope_add(
23
+ name,
24
+ Proc.new { |value| where(name_sym => value) },
25
+ suffix: '_is',
26
+ argument_type: type
27
+ )
28
+ auto_scope_add(
29
+ name,
30
+ Proc.new { |value| where.not(name_sym => value) },
31
+ suffix: '_not',
32
+ argument_type: type
33
+ )
34
+
35
+ # Ordering
36
+ auto_scope_add(
37
+ name,
38
+ ->(value = :asc) { order(name_sym => value) },
39
+ prefix: 'order_by_',
40
+ argument_type: :string,
41
+ type: :order
42
+ )
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+ require 'scopiform/scope_definition'
5
+
6
+ module Scopiform
7
+ module Core
8
+ extend ActiveSupport::Concern
9
+
10
+ module ClassMethods
11
+ def auto_scopes
12
+ @auto_scopes || []
13
+ end
14
+
15
+ def auto_scopes_by_attribute(attribute)
16
+ attribute = attribute.to_sym
17
+ auto_scopes.select { |scope| scope.attribute == attribute }
18
+ end
19
+
20
+ def auto_scope?(name)
21
+ auto_scopes.find { |scope| scope.name == name }
22
+ end
23
+
24
+ def auto_scope_add(attribute, block, prefix: nil, suffix: nil, **options)
25
+ scope_name = "#{prefix}#{attribute}#{suffix}".underscore
26
+ scope_name_sym = scope_name.to_sym
27
+ scope scope_name_sym, block
28
+
29
+ scope_definition = auto_scope_add_definition(attribute, prefix: prefix, suffix: suffix, **options)
30
+
31
+ aliases_for(attribute).each do |alias_name|
32
+ auto_scope_for_alias(alias_name, scope_definition)
33
+ end
34
+ end
35
+
36
+ def alias_attribute(new_name, old_name)
37
+ super(new_name, old_name)
38
+
39
+ auto_scopes_by_attribute(old_name).each do |scope_definition|
40
+ auto_scope_for_alias(new_name, scope_definition)
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def auto_scope_add_definition(attribute, **options)
47
+ definition = ScopeDefinition.new(attribute, **options)
48
+ @auto_scopes ||= []
49
+ @auto_scopes << definition
50
+
51
+ definition
52
+ end
53
+
54
+ def auto_scope_for_alias(alias_name, scope_definition)
55
+ scope_name = "#{scope_definition.prefix}#{scope_definition.attribute}#{scope_definition.suffix}".underscore
56
+ alias_method_name = "#{scope_definition.prefix}#{alias_name}#{scope_definition.suffix}".underscore
57
+ singleton_class.send(:alias_method, alias_method_name.to_sym, scope_name.to_sym)
58
+ auto_scope_add_definition(alias_name, prefix: scope_definition.prefix, suffix: scope_definition.suffix, **scope_definition.options)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+
5
+ module Scopiform
6
+ module Filters
7
+ extend ActiveSupport::Concern
8
+
9
+ module ClassMethods
10
+ def apply_filters(filters_hash, injecting: all, joins: nil)
11
+ filters_hash.keys.inject(injecting) { |out, filter_name| resolve_filter(out, filter_name, filters_hash[filter_name], joins) }
12
+ end
13
+
14
+ def apply_orders(orders_hash, injecting = all)
15
+ orders_hash.keys.inject(injecting) { |out, order_name| resolve_order(out, order_name, orders_hash[order_name]) }
16
+ end
17
+
18
+ private
19
+
20
+ def resolve_filter(out, filter_name, filter_argument, joins)
21
+ if filter_name.to_s.casecmp('OR').zero?
22
+ or_joins = {}
23
+
24
+ return (
25
+ filter_argument
26
+ .map { |or_filters_hash| apply_filters(or_filters_hash, injecting: out, joins: or_joins) }
27
+ .map { |a| a.scopiform_joins(or_joins) }
28
+ .inject { |chain, applied| chain.or(applied) }
29
+ )
30
+ end
31
+ out.send(filter_name, filter_argument, joins: joins)
32
+ end
33
+
34
+ def resolve_order(out, order_name, order_argument)
35
+ method_name = "order_by_#{order_name}"
36
+ out.send(method_name, order_argument)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+
5
+ module Scopiform
6
+ module Helpers
7
+ extend ActiveSupport::Concern
8
+
9
+ STRING_TYPES = %i[string text].freeze
10
+ NUMBER_TYPES = %i[integer float decimal].freeze
11
+ DATE_TYPES = %i[date time datetime timestamp].freeze
12
+
13
+ module ClassMethods
14
+ def attribute_aliases
15
+ alias_hash = super
16
+
17
+ key = primary_key.to_s
18
+ alias_hash = alias_hash.merge('id' => key) if key != 'id'
19
+
20
+ alias_hash
21
+ end
22
+
23
+ def attribute_aliases_inverted
24
+ attribute_aliases.each_with_object({}) { |(k, v), o| (o[v] ||= []) << k }
25
+ end
26
+
27
+ def resolve_alias(name)
28
+ name_str = name.to_s
29
+ return attribute_aliases[name_str] if attribute_aliases[name_str].present?
30
+
31
+ name_str
32
+ end
33
+
34
+ def aliases_for(name)
35
+ attribute_aliases_inverted[name.to_s] || []
36
+ end
37
+
38
+ def column(name)
39
+ name = resolve_alias(name)
40
+ safe_columns_hash[name.to_s]
41
+ end
42
+
43
+ def association(name)
44
+ name = resolve_alias(name)
45
+ association = reflect_on_association(name)
46
+
47
+ association.klass if association.present?
48
+ association
49
+ rescue NameError
50
+ logger.warn "Unable to load class for association `#{name}` in model `#{self.name}`"
51
+ nil
52
+ end
53
+
54
+ protected
55
+
56
+ def safe_columns
57
+ columns
58
+ rescue ActiveRecord::StatementInvalid, Mysql::Error, Mysql2::Error
59
+ []
60
+ end
61
+
62
+ def safe_columns_hash
63
+ columns_hash
64
+ rescue ActiveRecord::StatementInvalid, Mysql::Error, Mysql2::Error
65
+ {}
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,9 @@
1
+ module Scopiform
2
+ module ReflectionPlugin
3
+ def add_reflection(ar, name, reflection)
4
+ super(ar, name, reflection)
5
+
6
+ ar.reflection_added(name, reflection) if ar.respond_to? :reflection_added
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ module Scopiform
2
+ class ScopeDefinition
3
+ attr_accessor :attribute, :prefix, :suffix, :options
4
+
5
+ def initialize(attribute, prefix: nil, suffix: nil, **options)
6
+ @attribute = attribute.to_sym
7
+ @prefix = prefix
8
+ @suffix = suffix
9
+ @options = options
10
+ end
11
+
12
+ def name
13
+ "#{prefix}#{attribute}#{suffix}".underscore
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+
5
+ module Scopiform
6
+ module StringNumberDateScopes
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ setup_string_number_and_date_auto_scopes
11
+ end
12
+
13
+ module ClassMethods
14
+ private
15
+
16
+ def setup_string_number_and_date_auto_scopes
17
+ string_number_dates = Helpers::STRING_TYPES + Helpers::NUMBER_TYPES + Helpers::DATE_TYPES
18
+ string_number_date_columns = safe_columns.select { |column| string_number_dates.include? column.type }
19
+ string_number_date_columns.each do |column|
20
+ name = column.name
21
+ name_sym = name.to_sym
22
+ type = column.type
23
+ arel_column = arel_table[name]
24
+
25
+ auto_scope_add(
26
+ name,
27
+ Proc.new { |value| where(name_sym => value) },
28
+ suffix: '_in',
29
+ argument_type: [type]
30
+ )
31
+
32
+ auto_scope_add(
33
+ name,
34
+ Proc.new { |value| where.not(name_sym => value) },
35
+ suffix: '_not_in',
36
+ argument_type: [type]
37
+ )
38
+
39
+ auto_scope_add(
40
+ name,
41
+ Proc.new { |value| where(arel_column.lt(value)) },
42
+ suffix: '_lt',
43
+ argument_type: type
44
+ )
45
+
46
+ auto_scope_add(
47
+ name,
48
+ Proc.new { |value| where(arel_column.lteq(value)) },
49
+ suffix: '_lte',
50
+ argument_type: type
51
+ )
52
+
53
+ auto_scope_add(
54
+ name,
55
+ Proc.new { |value| where(arel_column.gt(value)) },
56
+ suffix: '_gt',
57
+ argument_type: type
58
+ )
59
+
60
+ auto_scope_add(
61
+ name,
62
+ Proc.new { |value| where(arel_column.gteq(value)) },
63
+ suffix: '_gte',
64
+ argument_type: type
65
+ )
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+
5
+ module Scopiform
6
+ module StringNumberScopes
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ setup_string_and_number_auto_scopes
11
+ end
12
+
13
+ module ClassMethods
14
+ private
15
+
16
+ def setup_string_and_number_auto_scopes
17
+ string_numbers = Helpers::STRING_TYPES + Helpers::NUMBER_TYPES
18
+ string_number_columns = safe_columns.select { |column| string_numbers.include? column.type }
19
+ string_number_columns.each do |column|
20
+ name = column.name
21
+ type = column.type
22
+ arel_column = arel_table[name]
23
+
24
+ # Numeric values don't work properly with `.matches`. Using workaround
25
+ # https://coderwall.com/p/qtgvdq/using-arel_table-for-ilike-yes-even-with-integers
26
+ if Helpers::NUMBER_TYPES.include? column.type
27
+ arel_column = Arel::Nodes::NamedFunction.new(
28
+ 'CAST',
29
+ [arel_column.as('TEXT')]
30
+ )
31
+
32
+ type = :string
33
+ end
34
+
35
+ auto_scope_add(
36
+ name,
37
+ Proc.new { |value| where(arel_column.matches("%#{ActiveRecord::Base.sanitize_sql_like(value.to_s)}%")) },
38
+ suffix: '_contains',
39
+ type: type
40
+ )
41
+
42
+ auto_scope_add(
43
+ name,
44
+ Proc.new { |value| where.not(arel_column.matches("%#{ActiveRecord::Base.sanitize_sql_like(value.to_s)}%")) },
45
+ suffix: '_not_contains',
46
+ type: type
47
+ )
48
+
49
+ auto_scope_add(
50
+ name,
51
+ Proc.new { |value| where(arel_column.matches("#{ActiveRecord::Base.sanitize_sql_like(value.to_s)}%")) },
52
+ suffix: '_starts_with',
53
+ type: type
54
+ )
55
+
56
+ auto_scope_add(
57
+ name,
58
+ Proc.new { |value| where.not(arel_column.matches("#{ActiveRecord::Base.sanitize_sql_like(value.to_s)}%")) },
59
+ suffix: '_not_starts_with',
60
+ type: type
61
+ )
62
+
63
+ auto_scope_add(
64
+ name,
65
+ Proc.new { |value| where(arel_column.matches("%#{ActiveRecord::Base.sanitize_sql_like(value.to_s)}")) },
66
+ suffix: '_ends_with',
67
+ type: type
68
+ )
69
+
70
+ auto_scope_add(
71
+ name,
72
+ Proc.new { |value| where.not(arel_column.matches("%#{ActiveRecord::Base.sanitize_sql_like(value.to_s)}")) },
73
+ suffix: '_not_ends_with',
74
+ type: type
75
+ )
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,3 @@
1
+ module Scopiform
2
+ VERSION = '0.1.1'
3
+ end
data/lib/scopiform.rb ADDED
@@ -0,0 +1,24 @@
1
+ require 'scopiform/helpers'
2
+ require 'scopiform/core'
3
+ require 'scopiform/common_scopes'
4
+ require 'scopiform/string_number_scopes'
5
+ require 'scopiform/string_number_date_scopes'
6
+ require 'scopiform/association_scopes'
7
+ require 'scopiform/reflection_plugin'
8
+ require 'scopiform/filters'
9
+
10
+ module Scopiform
11
+ def self.included(base)
12
+ base.class_eval do
13
+ include Scopiform::Helpers
14
+ include Scopiform::Core
15
+ include Scopiform::CommonScopes
16
+ include Scopiform::StringNumberScopes
17
+ include Scopiform::StringNumberDateScopes
18
+ include Scopiform::AssociationScopes
19
+ include Scopiform::Filters
20
+ end
21
+ end
22
+ end
23
+
24
+ ActiveRecord::Reflection.singleton_class.prepend Scopiform::ReflectionPlugin
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :scopiform do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: scopiform
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - jayce.pulsipher
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-03-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 4.2.7
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 4.2.7
27
+ - !ruby/object:Gem::Dependency
28
+ name: rails_or
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: sqlite3
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description:
56
+ email:
57
+ - jayce.pulsipher@3-form.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - MIT-LICENSE
63
+ - README.md
64
+ - Rakefile
65
+ - lib/scopiform.rb
66
+ - lib/scopiform/association_scopes.rb
67
+ - lib/scopiform/common_scopes.rb
68
+ - lib/scopiform/core.rb
69
+ - lib/scopiform/filters.rb
70
+ - lib/scopiform/helpers.rb
71
+ - lib/scopiform/reflection_plugin.rb
72
+ - lib/scopiform/scope_definition.rb
73
+ - lib/scopiform/string_number_date_scopes.rb
74
+ - lib/scopiform/string_number_scopes.rb
75
+ - lib/scopiform/version.rb
76
+ - lib/tasks/scopiform_tasks.rake
77
+ homepage: https://github.com/3-form/scopiform
78
+ licenses:
79
+ - MIT
80
+ metadata: {}
81
+ post_install_message:
82
+ rdoc_options: []
83
+ require_paths:
84
+ - lib
85
+ required_ruby_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ requirements: []
96
+ rubygems_version: 3.0.3
97
+ signing_key:
98
+ specification_version: 4
99
+ summary: Generate scope methods to ActiveRecord classes based on columns and associations
100
+ test_files: []