required_scopes 1.0.0

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.
@@ -0,0 +1,121 @@
1
+ require 'active_record'
2
+ require 'required_scopes/errors'
3
+ require 'required_scopes/active_record/version_compatibility'
4
+
5
+ # This file simply adds a few small methods to ::ActiveRecord::Relation to allow tracking which scope categories have
6
+ # been satisfied on a relation.
7
+ ::ActiveRecord::Relation.class_eval do
8
+ # Call this method inline, exactly as you would any class-defined scope, to indicate that a particular category
9
+ # or categories have been satisfied. It's really intended for use in a class method, but both of these will
10
+ # work:
11
+ #
12
+ # class User < ActiveRecord::Base
13
+ # must_scope_by :client, :deleted
14
+ #
15
+ # class << self
16
+ # def active_for_client_named(client_name)
17
+ # client_id = CLIENT_MAP[client_name]
18
+ # where(:client_id => client_id).where(:deleted => false).scope_categories_satisfied(:client)
19
+ # end
20
+ # end
21
+ # end
22
+ #
23
+ # User.active_for_client_named('foo').first
24
+ # User.where(:client_id => client_id).where(:deleted => false).scope_categories_satisfied(:client, :deleted).first
25
+ def scope_categories_satisfied(*categories, &block)
26
+ categories = categories.flatten
27
+
28
+ new_scope = if categories.length == 0
29
+ self
30
+ else
31
+ out = clone
32
+ out.scope_categories_satisfied!(categories)
33
+ out
34
+ end
35
+
36
+ if block
37
+ new_scope.scoping(&block)
38
+ else
39
+ new_scope
40
+ end
41
+ end
42
+
43
+ # Alias for #scope_categories_satisfied.
44
+ def scope_category_satisfied(category, &block)
45
+ scope_categories_satisfied(category, &block)
46
+ end
47
+
48
+ # Tells this Relation that one or more categories have been satisfied.
49
+ def scope_categories_satisfied!(categories)
50
+ categories = categories.flatten
51
+
52
+ @satisfied_scope_categories ||= [ ]
53
+ @satisfied_scope_categories |= categories
54
+ end
55
+
56
+ # Tells this Relation that _all_ categories have been satisfied.
57
+ def all_scope_categories_satisfied!
58
+ scope_categories_satisfied!(required_scope_categories)
59
+ end
60
+
61
+ def all_scope_categories_satisfied(&block)
62
+ scope_categories_satisfied(required_scope_categories, &block)
63
+ end
64
+
65
+ # Returns the set of scope categories that have been satisfied.
66
+ def satisfied_scope_categories
67
+ @satisfied_scope_categories ||= [ ]
68
+ end
69
+
70
+ # Overrides #merge to merge the information about which scope categories have been satisfied, too.
71
+ def merge(other_relation)
72
+ super.scope_categories_satisfied(satisfied_scope_categories | other_relation.satisfied_scope_categories)
73
+ end
74
+
75
+ delegate :required_scope_categories, :to => :klass
76
+
77
+
78
+ private
79
+ # Raises an exception if there is at least one required scope category that has not yet been satisfied.
80
+ # +triggering_method+ is the name of the method called that triggered this check; we include this in the error
81
+ # we raise.
82
+ def ensure_categories_satisfied!(triggering_method)
83
+ required_categories = required_scope_categories
84
+ missing_categories = required_categories - satisfied_scope_categories
85
+
86
+ if missing_categories.length > 0
87
+ # We return a special exception for the category +:base+, because we want to give a simpler, cleaner error
88
+ # message for users who are just using the #base_scope_required! syntactic sugar instead of the full categories
89
+ # system.
90
+ # $stderr.puts "RAISING AT: #{caller.join("\n ")}"
91
+ if missing_categories == [ :base ]
92
+ raise RequiredScopes::Errors::BaseScopeNotSatisfiedError.new(klass, self, triggering_method)
93
+ else
94
+ raise RequiredScopes::Errors::RequiredScopeCategoriesNotSatisfiedError.new(
95
+ klass, self, triggering_method, required_categories, satisfied_scope_categories)
96
+ end
97
+ end
98
+ end
99
+
100
+ # Override certain key methods in ActiveRecord::Relation to make sure they check for category satisfaction before
101
+ # running.
102
+ [ :exec_queries, :perform_calculation, :update_all, :delete_all, :exists?, :pluck ].each do |method_name|
103
+ method_base_name = method_name
104
+ method_suffix = ""
105
+
106
+ if method_base_name.to_s =~ /^(.*?)([\?\!])$/
107
+ method_base_name = $1
108
+ method_suffix = $2
109
+ end
110
+
111
+ with_name = "#{method_base_name}_with_scope_categories_check#{method_suffix}"
112
+ without_name = "#{method_base_name}_without_scope_categories_check#{method_suffix}"
113
+
114
+ define_method(with_name) do |*args, &block|
115
+ ensure_categories_satisfied!(method_name) unless RequiredScopes::ActiveRecord::VersionCompatibility.is_association_relation?(self)
116
+ send(without_name, *args, &block)
117
+ end
118
+
119
+ alias_method_chain method_name, :scope_categories_check
120
+ end
121
+ end
@@ -0,0 +1,157 @@
1
+ module RequiredScopes
2
+ module ActiveRecord
3
+ module VersionCompatibility
4
+ class << self
5
+ delegate :is_association_relation?, :supports_references_method?, :apply_version_specific_fixes!,
6
+ :supports_find_by?, :relation_method_for_ignoring_scopes, :supports_load?, :supports_take?,
7
+ :supports_ids?, :supports_spawn?, :supports_bang_methods?, :supports_references?,
8
+ :supports_unscope?, :supports_none?, :supports_distinct?, :to => :impl
9
+
10
+ private
11
+ def impl
12
+ @impl ||= if ::ActiveRecord::VERSION::MAJOR == 4
13
+ ActiveRecord4.new
14
+ elsif ::ActiveRecord::VERSION::MAJOR == 3
15
+ ActiveRecord3.new
16
+ else
17
+ raise "RequiredScopes does not support ActiveRecord version #{ActiveRecord::VERSION::STRING} currently."
18
+ end
19
+ end
20
+ end
21
+
22
+ class ActiveRecord4
23
+ def is_association_relation?(relation)
24
+ relation.kind_of?(::ActiveRecord::AssociationRelation)
25
+ end
26
+
27
+ def supports_references_method?
28
+ true
29
+ end
30
+
31
+ def apply_version_specific_fixes!
32
+
33
+ end
34
+
35
+ def supports_find_by?
36
+ true
37
+ end
38
+
39
+ def supports_load?
40
+ true
41
+ end
42
+
43
+ def supports_take?
44
+ true
45
+ end
46
+
47
+ def supports_ids?
48
+ true
49
+ end
50
+
51
+ def supports_spawn?
52
+ true
53
+ end
54
+
55
+ def supports_bang_methods?
56
+ true
57
+ end
58
+
59
+ def supports_references?
60
+ true
61
+ end
62
+
63
+ def supports_unscope?
64
+ true
65
+ end
66
+
67
+ def supports_none?
68
+ true
69
+ end
70
+
71
+ def supports_distinct?
72
+ true
73
+ end
74
+
75
+ def relation_method_for_ignoring_scopes
76
+ :all
77
+ end
78
+ end
79
+
80
+ class ActiveRecord3
81
+ def is_association_relation?(relation)
82
+ false
83
+ end
84
+
85
+ def supports_references_method?
86
+ false
87
+ end
88
+
89
+ def apply_version_specific_fixes!
90
+ ::ActiveRecord::Associations::Association.class_eval do
91
+ def target_scope_with_required_scopes_removed
92
+ out = target_scope_without_required_scopes_removed
93
+ out.all_scope_categories_satisfied!
94
+ out
95
+ end
96
+
97
+ alias_method_chain :target_scope, :required_scopes_removed
98
+ end
99
+
100
+ ::ActiveRecord::Base.class_eval do
101
+ def destroy_with_required_scopes_removed
102
+ self.class.all_scope_categories_satisfied do
103
+ destroy_without_required_scopes_removed
104
+ end
105
+ end
106
+
107
+ alias_method_chain :destroy, :required_scopes_removed
108
+ end
109
+ end
110
+
111
+ def supports_find_by?
112
+ false
113
+ end
114
+
115
+ def supports_load?
116
+ false
117
+ end
118
+
119
+ def supports_take?
120
+ false
121
+ end
122
+
123
+ def supports_ids?
124
+ false
125
+ end
126
+
127
+ def supports_spawn?
128
+ false
129
+ end
130
+
131
+ def supports_bang_methods?
132
+ false
133
+ end
134
+
135
+ def supports_references?
136
+ false
137
+ end
138
+
139
+ def supports_unscope?
140
+ false
141
+ end
142
+
143
+ def supports_none?
144
+ false
145
+ end
146
+
147
+ def supports_distinct?
148
+ false
149
+ end
150
+
151
+ def relation_method_for_ignoring_scopes
152
+ :relation
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,48 @@
1
+ module RequiredScopes
2
+ module Errors
3
+ # The parent of all errors raised by RequiredScopes.
4
+ class Base < StandardError; end
5
+
6
+ # Raised if you try to execute an operation on a model or relation without having satisfied one or more
7
+ # scopes.
8
+ class RequiredScopeCategoriesNotSatisfiedError < Base
9
+ attr_reader :model_class, :current_relation, :triggering_method, :required_categories, :satisfied_categories, :missing_categories
10
+
11
+ def initialize(model_class, current_relation, triggering_method, required_categories, satisfied_categories)
12
+ @model_class = model_class
13
+ @current_relation = current_relation
14
+ @triggering_method = triggering_method
15
+ @required_categories = required_categories
16
+ @satisfied_categories = satisfied_categories
17
+ @missing_categories = @required_categories - @satisfied_categories
18
+
19
+ super(build_message)
20
+ end
21
+
22
+ private
23
+ def build_message
24
+ %{Model #{model_class.name} requires that you apply scope(s) satisfying the following
25
+ categories before you use it: #{missing_categories.sort_by(&:to_s).join(", ")}.
26
+
27
+ Satisfy these categories by including scopes in your query that are tagged with
28
+ :satisfies => <category name>, for each of the categories.}
29
+ end
30
+ end
31
+
32
+ # Raised if you try to execute an operation on a model or relation without having satisfied the base scope.
33
+ # (We use this instead of RequiredScopeCategoriesNotSatisfiedError, above, simply to make the error message
34
+ # simpler and more comprehensible to users who only are using the "base scope" syntactic sugar.)
35
+ class BaseScopeNotSatisfiedError < RequiredScopeCategoriesNotSatisfiedError
36
+ def initialize(model_class, current_relation, triggering_method)
37
+ super(model_class, current_relation, triggering_method, [ :base ], [ ])
38
+ end
39
+
40
+ private
41
+ def build_message
42
+ %{Model #{model_class.name} requires specification of a base scope before using it in a query
43
+ or other such operation. (Base scopes are those declared with #base_scope rather than just #scope,
44
+ or class methods that include #base_scope_satisfied.)}
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,3 @@
1
+ module RequiredScopes
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,56 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'required_scopes/version'
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "required_scopes"
8
+ s.version = RequiredScopes::VERSION
9
+ s.authors = ["Andrew Geweke"]
10
+ s.email = ["andrew@geweke.org"]
11
+ s.description = %q{Don't let developers forget about critical scopes for queries.}
12
+ s.summary = %q{Don't let developers forget about critical scopes for queries.}
13
+ s.homepage = "https://github.com/ageweke/required_scopes"
14
+ s.license = "MIT"
15
+
16
+ s.files = `git ls-files`.split($/)
17
+ s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_development_dependency "bundler", "~> 1.3"
22
+ s.add_development_dependency "rake"
23
+ s.add_development_dependency "rspec", "~> 2.14"
24
+
25
+ if (RUBY_VERSION =~ /^1\.9\./ || RUBY_VERSION =~ /^2\./) && ((! defined?(RUBY_ENGINE)) || (RUBY_ENGINE != 'jruby'))
26
+ s.add_development_dependency "pry"
27
+ s.add_development_dependency "pry-debugger"
28
+ s.add_development_dependency "pry-stack_explorer"
29
+ end
30
+
31
+ ar_version = ENV['REQUIRED_SCOPES_AR_TEST_VERSION']
32
+ ar_version = ar_version.strip if ar_version
33
+
34
+ version_spec = case ar_version
35
+ when nil then [ ">= 3.0", "<= 4.99.99" ]
36
+ when 'master' then nil
37
+ else [ "=#{ar_version}" ]
38
+ end
39
+
40
+ if version_spec
41
+ s.add_dependency("activerecord", *version_spec)
42
+ end
43
+
44
+ s.add_dependency "activesupport", ">= 3.0", "<= 4.99.99"
45
+
46
+ require File.expand_path(File.join(File.dirname(__FILE__), 'spec', 'required_scopes', 'helpers', 'database_helper'))
47
+ database_gem_name = RequiredScopes::Helpers::DatabaseHelper.maybe_database_gem_name
48
+
49
+ # Ugh. Later versions of the 'mysql2' gem are incompatible with AR 3.0.x; so, here, we explicitly trap that case
50
+ # and use an earlier version of that Gem.
51
+ if database_gem_name && database_gem_name == 'mysql2' && ar_version && ar_version =~ /^3\.0\./
52
+ s.add_development_dependency('mysql2', '~> 0.2.0')
53
+ else
54
+ s.add_development_dependency(database_gem_name)
55
+ end
56
+ end
@@ -0,0 +1,174 @@
1
+ module RequiredScopes
2
+ module Helpers
3
+ class DatabaseHelper
4
+ class InvalidDatabaseConfigurationError < StandardError; end
5
+
6
+ class << self
7
+ def maybe_database_gem_name
8
+ begin
9
+ dh = new
10
+ dh.database_gem_name
11
+ rescue InvalidDatabaseConfigurationError => idce
12
+ nil
13
+ end
14
+ end
15
+ end
16
+
17
+ def initialize
18
+ config # make sure we raise on instantiation if configuration is invalid
19
+ end
20
+
21
+ def database_type
22
+ case database_gem_name
23
+ when /mysql/i then :mysql
24
+ when /sqlite/i then :sqlite
25
+ when /pg/i, /postgres/i then :postgres
26
+ else raise "Unknown database type for Gem name: #{database_gem_name.inspect}"
27
+ end
28
+ end
29
+
30
+ def setup_activerecord!
31
+ require 'active_record'
32
+ require config[:require]
33
+ ::ActiveRecord::Base.establish_connection(config[:config])
34
+
35
+ require 'logger'
36
+ require 'stringio'
37
+ @logs = StringIO.new
38
+ ::ActiveRecord::Base.logger = Logger.new(@logs)
39
+
40
+ if config[:config][:adapter] == 'sqlite3'
41
+ sqlite_version = ::ActiveRecord::Base.connection.send(:sqlite_version).instance_variable_get("@version").inspect rescue "unknown"
42
+ end
43
+ end
44
+
45
+ def table_name(name)
46
+ "rec_spec_#{name}"
47
+ end
48
+
49
+ def database_gem_name
50
+ config[:database_gem_name]
51
+ end
52
+
53
+ private
54
+ def config
55
+ config_from_config_file || travis_ci_config_from_environment || invalid_config_file!
56
+ end
57
+
58
+ def config_from_config_file
59
+ return nil unless File.exist?(config_file_path)
60
+ require config_file_path
61
+
62
+ return nil unless defined?(REQUIRED_SCOPES_SPEC_DATABASE_CONFIG)
63
+ return nil unless REQUIRED_SCOPES_SPEC_DATABASE_CONFIG.kind_of?(Hash)
64
+
65
+ return nil unless REQUIRED_SCOPES_SPEC_DATABASE_CONFIG[:require]
66
+ return nil unless REQUIRED_SCOPES_SPEC_DATABASE_CONFIG[:database_gem_name]
67
+
68
+ return nil unless REQUIRED_SCOPES_SPEC_DATABASE_CONFIG
69
+ REQUIRED_SCOPES_SPEC_DATABASE_CONFIG
70
+ end
71
+
72
+ def travis_ci_config_from_environment
73
+ dbtype = (ENV['REQUIRED_SCOPES_TRAVIS_CI_DATABASE_TYPE'] || '').strip.downcase
74
+ is_jruby = defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
75
+
76
+ if is_jruby
77
+ case dbtype
78
+ when 'mysql'
79
+ {
80
+ :require => 'activerecord-jdbcmysql-adapter',
81
+ :database_gem_name => 'activerecord-jdbcmysql-adapter',
82
+ :config => {
83
+ :adapter => 'jdbcmysql',
84
+ :database => 'myapp_test',
85
+ :username => 'travis',
86
+ :encoding => 'utf8'
87
+ }
88
+ }
89
+ when 'postgres', 'postgresql'
90
+ {
91
+ :require => 'activerecord-jdbcpostgresql-adapter',
92
+ :database_gem_name => 'activerecord-jdbcpostgresql-adapter',
93
+ :config => {
94
+ :adapter => 'jdbcpostgresql',
95
+ :database => 'myapp_test',
96
+ :username => 'postgres'
97
+ }
98
+ }
99
+ when 'sqlite'
100
+ {
101
+ :require => 'activerecord-jdbcsqlite3-adapter',
102
+ :database_gem_name => 'activerecord-jdbcsqlite3-adapter',
103
+ :config => {
104
+ :adapter => 'jdbcsqlite3',
105
+ :database => ':memory:'
106
+ }
107
+ }
108
+ when '', nil then nil
109
+ else
110
+ raise "Unknown Travis CI database type: #{dbtype.inspect}"
111
+ end
112
+ else
113
+ case dbtype
114
+ when 'postgres', 'postgresql'
115
+ {
116
+ :require => 'pg',
117
+ :database_gem_name => 'pg',
118
+ :config => {
119
+ :adapter => 'postgresql',
120
+ :database => 'myapp_test',
121
+ :username => 'postgres',
122
+ :min_messages => 'WARNING'
123
+ }
124
+ }
125
+ when 'mysql'
126
+ {
127
+ :require => 'mysql2',
128
+ :database_gem_name => 'mysql2',
129
+ :config => {
130
+ :adapter => 'mysql2',
131
+ :database => 'myapp_test',
132
+ :username => 'travis',
133
+ :encoding => 'utf8'
134
+ }
135
+ }
136
+ when 'sqlite'
137
+ {
138
+ :require => 'sqlite3',
139
+ :database_gem_name => 'sqlite3',
140
+ :config => {
141
+ :adapter => 'sqlite3',
142
+ :database => ':memory:',
143
+ :timeout => 500
144
+ }
145
+ }
146
+ when '', nil then nil
147
+ else
148
+ raise "Unknown Travis CI database type: #{dbtype.inspect}"
149
+ end
150
+ end
151
+ end
152
+
153
+ def config_file_path
154
+ @config_file_path ||= File.expand_path(File.join(File.dirname(__FILE__), '..', '..', '..', 'spec_database_config.rb'))
155
+ end
156
+
157
+ def invalid_config_file!
158
+ raise Errno::ENOENT, %{In order to run specs for required_scopes, you need to create a file at:
159
+
160
+ #{config_file_path}
161
+
162
+ ...that defines a top-level REQUIRED_SCOPES_SPEC_DATABASE_CONFIG hash, with members:
163
+
164
+ :require => 'name_of_adapter_to_require',
165
+ :database_gem_name => 'name_of_gem_for_adapter',
166
+ :config => { ...whatever ActiveRecord::Base.establish_connection should be passed... }
167
+
168
+ Alternatively, if you're running under Travis CI, you can set the environment variable
169
+ REQUIRED_SCOPES_TRAVIS_CI_DATABASE_TYPE to 'postgres', 'mysql', or 'sqlite', and it will
170
+ use the correct configuration for testing on Travis CI.}
171
+ end
172
+ end
173
+ end
174
+ end