scopiform 0.1.1

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 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: []