protector 0.1.1 → 0.2.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 +4 -4
- data/Appraisals +8 -0
- data/Gemfile +5 -1
- data/README.md +16 -7
- data/gemfiles/AR_3.2.gemfile +3 -1
- data/gemfiles/AR_3.2.gemfile.lock +33 -2
- data/gemfiles/AR_4.gemfile +3 -1
- data/gemfiles/AR_4.gemfile.lock +18 -1
- data/gemfiles/Mongoid.gemfile +17 -0
- data/gemfiles/Mongoid.gemfile.lock +112 -0
- data/gemfiles/Sequel.gemfile +18 -0
- data/gemfiles/Sequel.gemfile.lock +103 -0
- data/lib/protector/adapters/active_record/base.rb +15 -17
- data/lib/protector/adapters/active_record/relation.rb +20 -61
- data/lib/protector/adapters/sequel/dataset.rb +66 -0
- data/lib/protector/adapters/sequel/eager_graph_loader.rb +24 -0
- data/lib/protector/adapters/sequel/model.rb +99 -0
- data/lib/protector/adapters/sequel.rb +17 -0
- data/lib/protector/dsl.rb +5 -2
- data/lib/protector/version.rb +1 -1
- data/lib/protector.rb +3 -1
- data/migrations/active_record.rb +0 -1
- data/migrations/sequel.rb +49 -0
- data/perf/{active_record.rb → active_record_perf.rb} +2 -0
- data/perf/perf_helpers/boot.rb +20 -2
- data/perf/sequel_perf.rb +84 -0
- data/spec/lib/adapters/active_record_spec.rb +17 -4
- data/spec/lib/adapters/sequel_spec.rb +156 -0
- data/spec/spec_helpers/adapters/active_record.rb +34 -0
- data/spec/spec_helpers/adapters/sequel.rb +64 -0
- data/spec/spec_helpers/boot.rb +3 -13
- data/spec/spec_helpers/examples/model.rb +20 -20
- metadata +17 -3
@@ -1,8 +1,7 @@
|
|
1
1
|
module Protector
|
2
2
|
module Adapters
|
3
3
|
module ActiveRecord
|
4
|
-
|
5
|
-
# Pathces `ActiveRecord::Relation`
|
4
|
+
# Patches `ActiveRecord::Relation`
|
6
5
|
module Relation
|
7
6
|
extend ActiveSupport::Concern
|
8
7
|
|
@@ -10,9 +9,6 @@ module Protector
|
|
10
9
|
include Protector::DSL::Base
|
11
10
|
|
12
11
|
alias_method_chain :exec_queries, :protector
|
13
|
-
alias_method_chain :eager_loading?, :protector
|
14
|
-
|
15
|
-
attr_accessor :eager_loadable_when_protected
|
16
12
|
|
17
13
|
# AR 3.2 workaround. Come on, guys... SQL parsing :(
|
18
14
|
unless method_defined?(:references_values)
|
@@ -67,86 +63,49 @@ module Protector
|
|
67
63
|
#
|
68
64
|
# Patching includes:
|
69
65
|
#
|
70
|
-
# * turning `includes` into `preload`
|
66
|
+
# * turning `includes` (that are not referenced for eager loading) into `preload`
|
71
67
|
# * delaying built-in preloading to the stage where selection is restricted
|
72
|
-
# * merging current relation with restriction
|
68
|
+
# * merging current relation with restriction (of self and every eager association)
|
73
69
|
def exec_queries_with_protector(*args)
|
70
|
+
return @records if loaded?
|
74
71
|
return exec_queries_without_protector unless @protector_subject
|
75
72
|
|
76
73
|
subject = @protector_subject
|
77
74
|
relation = merge(protector_meta.relation).unrestrict!
|
78
|
-
|
79
75
|
relation = protector_substitute_includes(relation)
|
80
76
|
|
81
|
-
# We should explicitly allow/deny eager loading now that we know
|
82
|
-
# if we can use it
|
83
|
-
relation.eager_loadable_when_protected = relation.includes_values.any?
|
84
|
-
|
85
77
|
# Preserve associations from internal loading. We are going to handle that
|
86
78
|
# ourselves respecting security scopes FTW!
|
87
79
|
associations, relation.preload_values = relation.preload_values, []
|
88
80
|
|
89
|
-
@records = relation.send(:exec_queries).each{|
|
81
|
+
@records = relation.send(:exec_queries).each{|record| record.restrict!(subject)}
|
90
82
|
|
91
83
|
# Now we have @records restricted properly so let's preload associations!
|
92
|
-
associations.each do |
|
93
|
-
::ActiveRecord::Associations::Preloader.new(@records,
|
84
|
+
associations.each do |association|
|
85
|
+
::ActiveRecord::Associations::Preloader.new(@records, association).run
|
94
86
|
end
|
95
87
|
|
96
88
|
@loaded = true
|
97
89
|
@records
|
98
90
|
end
|
99
91
|
|
100
|
-
# Swaps `includes` with `preload`
|
101
|
-
#
|
92
|
+
# Swaps `includes` with `preload` whether it's not referenced or merges
|
93
|
+
# security scope of proper class otherwise
|
102
94
|
def protector_substitute_includes(relation)
|
103
95
|
subject = @protector_subject
|
104
96
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
# We can not allow join-based eager loading for scoped associations
|
110
|
-
# since actual filtering can differ for host model and joined relation.
|
111
|
-
# Therefore we turn all `includes` into `preloads`.
|
112
|
-
includes.each do |iv|
|
113
|
-
protector_expand_include(iv).each do |ive|
|
114
|
-
# First-level associations can stay JOINed if restriction scope
|
115
|
-
# is absent. Checking deep associations would make us to check
|
116
|
-
# every parent. This should probably be done sometimes :\
|
117
|
-
meta = ive[0].protector_meta.evaluate(ive[0], subject) unless ive[1].is_a?(Hash)
|
118
|
-
|
119
|
-
# We leave unscoped restrictions as `includes`
|
120
|
-
# but turn scoped ones into `preloads`
|
121
|
-
if meta && !meta.scoped?
|
122
|
-
relation.includes!(ive[1])
|
123
|
-
else
|
124
|
-
if relation.references_values.include?(ive[0].table_name)
|
125
|
-
if relation.respond_to?(:joins!)
|
126
|
-
relation.joins!(ive[1])
|
127
|
-
else
|
128
|
-
relation = relation.joins(ive[1])
|
129
|
-
end
|
130
|
-
end
|
131
|
-
|
132
|
-
# AR 3.2 Y U NO HAVE BANG RELATION MODIFIERS
|
133
|
-
relation.preload_values << ive[1]
|
134
|
-
false
|
135
|
-
end
|
97
|
+
if eager_loading?
|
98
|
+
protector_expand_inclusion(includes_values + eager_load_values).each do |klass, path|
|
99
|
+
relation = relation.merge(klass.protector_meta.evaluate(klass, subject).relation)
|
136
100
|
end
|
101
|
+
else
|
102
|
+
relation.preload_values += includes_values
|
103
|
+
relation.includes_values = []
|
137
104
|
end
|
138
105
|
|
139
106
|
relation
|
140
107
|
end
|
141
108
|
|
142
|
-
# Checks whether current object can be eager loaded respecting
|
143
|
-
# protector flags
|
144
|
-
def eager_loading_with_protector?
|
145
|
-
flag = eager_loading_without_protector?
|
146
|
-
flag &&= !!@eager_loadable_when_protected unless @eager_loadable_when_protected.nil?
|
147
|
-
flag
|
148
|
-
end
|
149
|
-
|
150
109
|
# Indexes `includes` format by actual entity class
|
151
110
|
#
|
152
111
|
# Turns `{foo: :bar}` into `[[Foo, :foo], [Bar, {foo: :bar}]`
|
@@ -155,13 +114,13 @@ module Protector
|
|
155
114
|
# @param [Array] results Resulting set
|
156
115
|
# @param [Array] base Association path ([:foo, :bar])
|
157
116
|
# @param [Class] klass Base class
|
158
|
-
def
|
117
|
+
def protector_expand_inclusion(inclusion, results=[], base=[], klass=@klass)
|
159
118
|
if inclusion.is_a?(Hash)
|
160
|
-
|
119
|
+
protector_expand_inclusion_hash(inclusion, results, base, klass)
|
161
120
|
else
|
162
121
|
Array(inclusion).each do |i|
|
163
122
|
if i.is_a?(Hash)
|
164
|
-
|
123
|
+
protector_expand_inclusion_hash(i, results, base, klass)
|
165
124
|
else
|
166
125
|
results << [
|
167
126
|
klass.reflect_on_association(i.to_sym).klass,
|
@@ -176,7 +135,7 @@ module Protector
|
|
176
135
|
|
177
136
|
private
|
178
137
|
|
179
|
-
def
|
138
|
+
def protector_expand_inclusion_hash(inclusion, results=[], base=[], klass=@klass)
|
180
139
|
inclusion.each do |key, value|
|
181
140
|
model = klass.reflect_on_association(key.to_sym).klass
|
182
141
|
value = [value] unless value.is_a?(Array)
|
@@ -184,7 +143,7 @@ module Protector
|
|
184
143
|
|
185
144
|
value.each do |v|
|
186
145
|
if v.is_a?(Hash)
|
187
|
-
|
146
|
+
protector_expand_inclusion_hash(v, results, nest)
|
188
147
|
else
|
189
148
|
results << [
|
190
149
|
model.reflect_on_association(v.to_sym).klass,
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Protector
|
2
|
+
module Adapters
|
3
|
+
module Sequel
|
4
|
+
# Patches `Sequel::Dataset`
|
5
|
+
module Dataset extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
# Wrapper for the Dataset `row_proc` adding restriction function
|
8
|
+
class Restrictor
|
9
|
+
attr_accessor :subject
|
10
|
+
attr_accessor :mutator
|
11
|
+
|
12
|
+
def initialize(subject, mutator)
|
13
|
+
@subject = subject
|
14
|
+
@mutator = mutator
|
15
|
+
end
|
16
|
+
|
17
|
+
# Mutate entity through `row_proc` if available and then protect
|
18
|
+
#
|
19
|
+
# @param entity [Object] Entity coming from Dataset
|
20
|
+
def call(entity)
|
21
|
+
entity = mutator.call(entity) if mutator
|
22
|
+
return entity if !entity.respond_to?(:restrict!)
|
23
|
+
entity.restrict!(@subject)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
included do |klass|
|
28
|
+
include Protector::DSL::Base
|
29
|
+
|
30
|
+
alias_method_chain :each, :protector
|
31
|
+
end
|
32
|
+
|
33
|
+
# Gets {Protector::DSL::Meta::Box} of this dataset
|
34
|
+
def protector_meta
|
35
|
+
model.protector_meta.evaluate(model, @protector_subject)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Substitutes `row_proc` with {Protector} and injects protection scope
|
39
|
+
def each_with_protector(*args, &block)
|
40
|
+
return each_without_protector(*args, &block) if !@protector_subject
|
41
|
+
|
42
|
+
relation = protector_defend_graph(clone, @protector_subject)
|
43
|
+
relation = relation.instance_eval(&protector_meta.scope_proc) if protector_meta.scoped?
|
44
|
+
|
45
|
+
relation.row_proc = Restrictor.new(@protector_subject, relation.row_proc)
|
46
|
+
relation.each_without_protector(*args, &block)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Injects protection scope for every joined graph association
|
50
|
+
def protector_defend_graph(relation, subject)
|
51
|
+
return relation unless @opts[:eager_graph]
|
52
|
+
|
53
|
+
@opts[:eager_graph][:reflections].each do |association, reflection|
|
54
|
+
model = reflection[:cache][:class] if reflection[:cache].is_a?(Hash) && reflection[:cache][:class]
|
55
|
+
model = reflection[:class_name].constantize unless model
|
56
|
+
meta = model.protector_meta.evaluate(model, subject)
|
57
|
+
|
58
|
+
relation = relation.instance_eval(&meta.scope_proc) if meta.scoped?
|
59
|
+
end
|
60
|
+
|
61
|
+
relation
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Protector
|
2
|
+
module Adapters
|
3
|
+
module Sequel
|
4
|
+
# Patches `Sequel::Model::Associations::EagerGraphLoader`
|
5
|
+
module EagerGraphLoader extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
alias_method_chain :initialize, :protector
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize_with_protector(dataset)
|
12
|
+
initialize_without_protector(dataset)
|
13
|
+
|
14
|
+
if dataset.protector_subject
|
15
|
+
@row_procs.each do |k,v|
|
16
|
+
@row_procs[k] = Dataset::Restrictor.new(dataset.protector_subject, v)
|
17
|
+
@ta_map[k][1] = @row_procs[k] if @ta_map.has_key?(k)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Protector
|
2
|
+
module Adapters
|
3
|
+
module Sequel
|
4
|
+
# Patches `Sequel::Model`
|
5
|
+
module Model extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
include Protector::DSL::Base
|
9
|
+
include Protector::DSL::Entry
|
10
|
+
|
11
|
+
# Drops {Protector::DSL::Meta::Box} cache when subject changes
|
12
|
+
def restrict!(*args)
|
13
|
+
@protector_meta = nil
|
14
|
+
super
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
# Gets default restricted `Dataset`
|
20
|
+
def restrict!(subject)
|
21
|
+
dataset.clone.restrict! subject
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Storage for {Protector::DSL::Meta::Box}
|
26
|
+
def protector_meta
|
27
|
+
@protector_meta ||= self.class.protector_meta.evaluate(
|
28
|
+
self.class,
|
29
|
+
@protector_subject,
|
30
|
+
self.class.columns,
|
31
|
+
self
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Checks if current model can be selected in the context of current subject
|
36
|
+
def visible?
|
37
|
+
protector_meta.relation.where(pk_hash).any?
|
38
|
+
end
|
39
|
+
|
40
|
+
# Checks if current model can be created in the context of current subject
|
41
|
+
def creatable?
|
42
|
+
fields = HashWithIndifferentAccess[keys.map{|x| [x.to_s, @values[x]]}]
|
43
|
+
protector_meta.creatable?(fields)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Checks if current model can be updated in the context of current subject
|
47
|
+
def updatable?
|
48
|
+
fields = HashWithIndifferentAccess[changed_columns.map{|x| [x.to_s, @values[x]]}]
|
49
|
+
protector_meta.updatable?(fields)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Checks if current model can be destroyed in the context of current subject
|
53
|
+
def destroyable?
|
54
|
+
protector_meta.destroyable?
|
55
|
+
end
|
56
|
+
|
57
|
+
# Basic security validations
|
58
|
+
def validate
|
59
|
+
super
|
60
|
+
return unless @protector_subject
|
61
|
+
method = new? ? :creatable? : :updatable?
|
62
|
+
errors.add(:base, I18n.t('protector.invalid')) unless __send__(method)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Destroy availability check
|
66
|
+
def before_destroy
|
67
|
+
return false if @protector_subject && !destroyable?
|
68
|
+
super
|
69
|
+
end
|
70
|
+
|
71
|
+
# Security-checking attributes reader
|
72
|
+
#
|
73
|
+
# @param name [Symbol] Name of attribute to read
|
74
|
+
def [](name)
|
75
|
+
if (
|
76
|
+
!@protector_subject ||
|
77
|
+
name == self.class.primary_key ||
|
78
|
+
(self.class.primary_key.is_a?(Array) && self.class.primary_key.include?(name)) ||
|
79
|
+
protector_meta.readable?(name.to_s)
|
80
|
+
)
|
81
|
+
@values[name]
|
82
|
+
else
|
83
|
+
nil
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# This is used whenever we fetch data
|
88
|
+
def _associated_dataset(*args)
|
89
|
+
super.restrict!(@protector_subject)
|
90
|
+
end
|
91
|
+
|
92
|
+
# This is used whenever we call counters and existance checkers
|
93
|
+
def _dataset(*args)
|
94
|
+
super.restrict!(@protector_subject)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'protector/adapters/sequel/model'
|
2
|
+
require 'protector/adapters/sequel/dataset'
|
3
|
+
require 'protector/adapters/sequel/eager_graph_loader'
|
4
|
+
|
5
|
+
module Protector
|
6
|
+
module Adapters
|
7
|
+
# Sequel adapter
|
8
|
+
module Sequel
|
9
|
+
# YIP YIP! Monkey-Patch the Sequel.
|
10
|
+
def self.activate!
|
11
|
+
::Sequel::Model.send :include, Protector::Adapters::Sequel::Model
|
12
|
+
::Sequel::Dataset.send :include, Protector::Adapters::Sequel::Dataset
|
13
|
+
::Sequel::Model::Associations::EagerGraphLoader.send :include, Protector::Adapters::Sequel::EagerGraphLoader
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/protector/dsl.rb
CHANGED
@@ -5,7 +5,7 @@ module Protector
|
|
5
5
|
|
6
6
|
# Single DSL evaluation result
|
7
7
|
class Box
|
8
|
-
attr_accessor :access, :relation, :destroyable
|
8
|
+
attr_accessor :access, :scope_proc, :relation, :destroyable
|
9
9
|
|
10
10
|
# @param model [Class] The class of protected entity
|
11
11
|
# @param fields [Array<String>] All the fields the model has
|
@@ -50,7 +50,8 @@ module Protector
|
|
50
50
|
# scope { none }
|
51
51
|
# end
|
52
52
|
def scope(&block)
|
53
|
-
@
|
53
|
+
@scope_proc = block
|
54
|
+
@relation = @model.instance_eval(&block)
|
54
55
|
end
|
55
56
|
|
56
57
|
# Enables action for given fields.
|
@@ -198,6 +199,8 @@ module Protector
|
|
198
199
|
# @param fields [Array<String>] All the fields the model has
|
199
200
|
# @param entry [Object] An instance of the model
|
200
201
|
def evaluate(model, subject, fields=[], entry=nil)
|
202
|
+
raise "Unprotected entity detected: use `restrict` method to protect it." unless subject
|
203
|
+
|
201
204
|
Box.new(model, fields, subject, entry, blocks)
|
202
205
|
end
|
203
206
|
end
|
data/lib/protector/version.rb
CHANGED
data/lib/protector.rb
CHANGED
@@ -4,7 +4,9 @@ require "i18n"
|
|
4
4
|
require "protector/version"
|
5
5
|
require "protector/dsl"
|
6
6
|
require "protector/adapters/active_record"
|
7
|
+
require "protector/adapters/sequel"
|
7
8
|
|
8
9
|
I18n.load_path << Dir[File.join File.expand_path(File.dirname(__FILE__)), '..', 'locales', '*.yml']
|
9
10
|
|
10
|
-
Protector::Adapters::ActiveRecord.activate! if defined?(ActiveRecord)
|
11
|
+
Protector::Adapters::ActiveRecord.activate! if defined?(ActiveRecord)
|
12
|
+
Protector::Adapters::Sequel.activate! if defined?(Sequel)
|
data/migrations/active_record.rb
CHANGED
@@ -0,0 +1,49 @@
|
|
1
|
+
### Connection
|
2
|
+
|
3
|
+
DB = if RUBY_PLATFORM == 'java'
|
4
|
+
Jdbc::SQLite3.load_driver
|
5
|
+
Sequel.connect('jdbc:sqlite::memory:')
|
6
|
+
else
|
7
|
+
Sequel.sqlite
|
8
|
+
end
|
9
|
+
|
10
|
+
Sequel::Model.instance_eval do
|
11
|
+
def none
|
12
|
+
where('1 = 0')
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
### Tables
|
17
|
+
|
18
|
+
[:dummies, :fluffies, :bobbies].each do |m|
|
19
|
+
DB.create_table m do
|
20
|
+
primary_key :id
|
21
|
+
String :string
|
22
|
+
Integer :number
|
23
|
+
Text :text
|
24
|
+
Integer :dummy_id
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
DB.create_table :loonies do
|
29
|
+
Integer :fluffy_id
|
30
|
+
String :string
|
31
|
+
end
|
32
|
+
|
33
|
+
### Classes
|
34
|
+
|
35
|
+
class Dummy < Sequel::Model
|
36
|
+
one_to_many :fluffies
|
37
|
+
one_to_many :bobbies
|
38
|
+
end
|
39
|
+
|
40
|
+
class Fluffy < Sequel::Model
|
41
|
+
many_to_one :dummy
|
42
|
+
one_to_one :loony
|
43
|
+
end
|
44
|
+
|
45
|
+
class Bobby < Sequel::Model
|
46
|
+
end
|
47
|
+
|
48
|
+
class Loony < Sequel::Model
|
49
|
+
end
|
data/perf/perf_helpers/boot.rb
CHANGED
@@ -2,7 +2,7 @@ class Perf
|
|
2
2
|
def self.load(adapter)
|
3
3
|
perf = Perf.new(adapter.camelize)
|
4
4
|
base = Pathname.new(File.expand_path '../..', __FILE__)
|
5
|
-
file = base.join(adapter+'.rb').to_s
|
5
|
+
file = base.join(adapter+'_perf.rb').to_s
|
6
6
|
perf.instance_eval File.read(file), file
|
7
7
|
perf.run!
|
8
8
|
end
|
@@ -11,6 +11,7 @@ class Perf
|
|
11
11
|
@blocks = {}
|
12
12
|
@adapter = adapter
|
13
13
|
@activated = false
|
14
|
+
@profiling = {}
|
14
15
|
end
|
15
16
|
|
16
17
|
def migrate
|
@@ -32,15 +33,22 @@ class Perf
|
|
32
33
|
@activation = block
|
33
34
|
end
|
34
35
|
|
35
|
-
def benchmark(subject, &block)
|
36
|
+
def benchmark(subject, options={}, &block)
|
36
37
|
@blocks[subject] = block
|
37
38
|
end
|
38
39
|
|
40
|
+
def benchmark!(subject, options={min_percent: 4}, &block)
|
41
|
+
@profiling[subject] = options
|
42
|
+
benchmark(subject, &block)
|
43
|
+
end
|
44
|
+
|
39
45
|
def activated?
|
40
46
|
@activated
|
41
47
|
end
|
42
48
|
|
43
49
|
def run!
|
50
|
+
require 'ruby-prof' if @profiling.any?
|
51
|
+
|
44
52
|
results = {}
|
45
53
|
|
46
54
|
results[:off] = run_state('disabled', :red)
|
@@ -65,11 +73,21 @@ class Perf
|
|
65
73
|
|
66
74
|
def run_state(state, color)
|
67
75
|
data = {}
|
76
|
+
prof = @profiling
|
68
77
|
|
69
78
|
print_block "Protector #{state.send color}" do
|
70
79
|
@blocks.each do |s, b|
|
80
|
+
RubyProf.start if prof.include?(s)
|
81
|
+
|
71
82
|
data[s] = Benchmark.realtime(&b)
|
72
83
|
print_result s, data[s].to_s
|
84
|
+
|
85
|
+
if prof.include?(s)
|
86
|
+
result = RubyProf.stop
|
87
|
+
|
88
|
+
printer = RubyProf::FlatPrinter.new(result)
|
89
|
+
printer.print(STDOUT, prof[s])
|
90
|
+
end
|
73
91
|
end
|
74
92
|
end
|
75
93
|
|
data/perf/sequel_perf.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
migrate
|
2
|
+
|
3
|
+
seed do
|
4
|
+
500.times do
|
5
|
+
d = Dummy.create string: 'zomgstring', number: [999,777].sample, text: 'zomgtext'
|
6
|
+
|
7
|
+
2.times do
|
8
|
+
f = Fluffy.create string: 'zomgstring', number: [999,777].sample, text: 'zomgtext', dummy_id: d.id
|
9
|
+
b = Bobby.create string: 'zomgstring', number: [999,777].sample, text: 'zomgtext', dummy_id: d.id
|
10
|
+
l = Loony.create string: 'zomgstring', fluffy_id: f.id
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
activate do
|
16
|
+
Dummy.instance_eval do
|
17
|
+
protect do
|
18
|
+
scope { where }
|
19
|
+
can :view, :string
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
Fluffy.instance_eval do
|
24
|
+
protect do
|
25
|
+
scope { where }
|
26
|
+
can :view
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Define attributes methods
|
31
|
+
Dummy.first
|
32
|
+
end
|
33
|
+
|
34
|
+
benchmark 'Read from unprotected model (100k)' do
|
35
|
+
d = Dummy.first
|
36
|
+
100_000.times { d.string }
|
37
|
+
end
|
38
|
+
|
39
|
+
benchmark 'Read open field (100k)' do
|
40
|
+
d = Dummy.first
|
41
|
+
d = d.restrict!('!') if activated?
|
42
|
+
100_000.times { d.string }
|
43
|
+
end
|
44
|
+
|
45
|
+
benchmark 'Read nil field (100k)' do
|
46
|
+
d = Dummy.first
|
47
|
+
d = d.restrict!('!') if activated?
|
48
|
+
100_000.times { d.text }
|
49
|
+
end
|
50
|
+
|
51
|
+
benchmark 'Check existance' do
|
52
|
+
scope = activated? ? Dummy.restrict!('!') : Dummy.where
|
53
|
+
1000.times { scope.any? }
|
54
|
+
end
|
55
|
+
|
56
|
+
benchmark 'Count' do
|
57
|
+
scope = Dummy.limit(1)
|
58
|
+
scope = scope.restrict!('!') if activated?
|
59
|
+
1000.times { scope.count }
|
60
|
+
end
|
61
|
+
|
62
|
+
benchmark 'Select one' do
|
63
|
+
scope = Dummy.limit(1)
|
64
|
+
scope = scope.restrict!('!') if activated?
|
65
|
+
1000.times { scope.to_a }
|
66
|
+
end
|
67
|
+
|
68
|
+
benchmark 'Select many' do
|
69
|
+
scope = Dummy.where
|
70
|
+
scope = scope.restrict!('!') if activated?
|
71
|
+
200.times { scope.to_a }
|
72
|
+
end
|
73
|
+
|
74
|
+
benchmark 'Select with eager loading' do
|
75
|
+
scope = Dummy.eager(:fluffies)
|
76
|
+
scope = scope.restrict!('!') if activated?
|
77
|
+
200.times { scope.to_a }
|
78
|
+
end
|
79
|
+
|
80
|
+
benchmark 'Select with filtered eager loading' do
|
81
|
+
scope = Dummy.eager_graph(fluffies: :loony)
|
82
|
+
scope = scope.restrict!('!') if activated?
|
83
|
+
200.times { scope.to_a }
|
84
|
+
end
|
@@ -7,6 +7,19 @@ if defined?(ActiveRecord)
|
|
7
7
|
before(:all) do
|
8
8
|
load 'migrations/active_record.rb'
|
9
9
|
|
10
|
+
module ProtectionCase
|
11
|
+
extend ActiveSupport::Concern
|
12
|
+
|
13
|
+
included do |klass|
|
14
|
+
protect do |x|
|
15
|
+
scope{ where('1=0') } if x == '-'
|
16
|
+
scope{ where(klass.table_name => {number: 999}) } if x == '+'
|
17
|
+
|
18
|
+
can :view, :dummy_id unless x == '-'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
10
23
|
[Dummy, Fluffy].each{|c| c.send :include, ProtectionCase}
|
11
24
|
|
12
25
|
Dummy.create! string: 'zomgstring', number: 999, text: 'zomgtext'
|
@@ -127,7 +140,7 @@ if defined?(ActiveRecord)
|
|
127
140
|
|
128
141
|
context "joined to filtered association" do
|
129
142
|
it "scopes" do
|
130
|
-
d = Dummy.restrict!('+').includes(:fluffies).where(fluffies: {
|
143
|
+
d = Dummy.restrict!('+').includes(:fluffies).where(fluffies: {string: 'zomgstring'})
|
131
144
|
d.length.should == 2
|
132
145
|
d.first.fluffies.length.should == 1
|
133
146
|
end
|
@@ -136,18 +149,18 @@ if defined?(ActiveRecord)
|
|
136
149
|
context "joined to plain association" do
|
137
150
|
it "scopes" do
|
138
151
|
d = Dummy.restrict!('+').includes(:bobbies, :fluffies).where(
|
139
|
-
bobbies: {
|
152
|
+
bobbies: {string: 'zomgstring'}, fluffies: {string: 'zomgstring'}
|
140
153
|
)
|
141
154
|
d.length.should == 2
|
142
155
|
d.first.fluffies.length.should == 1
|
143
|
-
d.first.bobbies.length.should ==
|
156
|
+
d.first.bobbies.length.should == 2
|
144
157
|
end
|
145
158
|
end
|
146
159
|
|
147
160
|
context "with complex include" do
|
148
161
|
it "scopes" do
|
149
162
|
d = Dummy.restrict!('+').includes(fluffies: :loony).where(
|
150
|
-
fluffies: {
|
163
|
+
fluffies: {string: 'zomgstring'},
|
151
164
|
loonies: {string: 'zomgstring'}
|
152
165
|
)
|
153
166
|
d.length.should == 2
|