platanus 0.0.32 → 0.0.49

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -1,5 +1,3 @@
1
1
  source 'https://rubygems.org'
2
2
  gemspec
3
- gem 'rspec'
4
- gem 'rspec-expectations'
5
3
 
@@ -47,6 +47,8 @@ module Platanus
47
47
  self.transaction do
48
48
  run_callbacks :remove do
49
49
 
50
+ # TODO: disable update callbacks and validations!
51
+
50
52
  # Retrieve dependant properties and remove them.
51
53
  self.class.reflect_on_all_associations.select do |assoc|
52
54
  if assoc.options[:dependent] == :destroy
@@ -94,78 +94,19 @@ module Platanus
94
94
  # TODO: example
95
95
  class Profile
96
96
 
97
- def is_avaliable()
98
- return @shared.all? do |rule|
99
- rule_ctx = RuleContext.new _ctx, _tests, @def_matcher, @def_resource
100
- rule_ctx.instance_eval(&rule)
101
- rule_ctx.passed?
102
- end
103
- end
104
-
105
- def is_forbidden()
106
- # TODO: local forbids
107
- end
108
-
109
- def is_allowed()
110
- end
111
-
112
- def
113
-
114
- def allowance_for(_ctx, _action, _tests)
115
-
116
- # all shared rules must pass
117
- return :not_applicable unless @shared.all? do |rule|
118
- rule_ctx = RuleContext.new _ctx, _tests, @def_matcher, @def_resource
119
- rule_ctx.instance_eval(&rule)
120
- rule_ctx.passed?
121
- end
122
-
123
- # TODO: local forbids
124
-
125
- # process base profile
126
- unless @base.nil?
127
- result = @base.process _ctx, _action, _tests
128
- return result if result == :not_applicable or result == :forbidden
129
- matches << resu
130
- end
131
-
132
- # process subprofiles
133
- if @isolated.each do |profile|
134
- result = profile.process _ctx, _action, _tests
135
- return :forbidden if result == :forbidden
136
- result = :allowed if result == :allowed
137
- end
138
-
139
- # see if any of the registered rules pass
140
- return :allowed if @rules[_action].any? do |rule|
141
- rule_ctx = RuleContext.new _ctx, _tests, @def_matcher, @def_resource
142
- rule_ctx.instance_eval(&rule)
143
- rule_ctx.passed?
144
- end
145
-
146
- return :not_allowed
147
- end
97
+ attr_reader :rules
98
+ attr_reader :def_matcher
99
+ attr_reader :def_resource
148
100
 
149
101
  # The initializer takes another profile as rules base.
150
- def initialize(_def_matcher, _def_resource, _base=nil)
151
- @base = _base
102
+ def initialize(_base, _def_matcher, _def_resource)
152
103
  @rules = Hash.new { |h, k| h[k] = [] }
153
- @isolated = []
154
- @shared = []
155
-
104
+ _base.rules.each { |k, tests| @rules[k] = tests.clone } unless _base.nil?
156
105
  raise Error.new 'Must provide a default test' if _def_matcher.nil?
157
106
  @def_matcher = _def_matcher
158
107
  @def_resource = _def_resource
159
108
  end
160
109
 
161
- def always(_upon=nil, &_block)
162
- @shared << (_upon || _block)
163
- end
164
-
165
- def isolate(_options={}, &_block)
166
- @isolated << Profile.new _options.fetch(, @def_matcher), _options.fetch(, @def_resource)
167
- end
168
-
169
110
  ## Adds an "allowance" rule
170
111
  def allow(_action, _upon=nil, &_block)
171
112
  @rules[_action] << (_upon || _block)
@@ -176,6 +117,11 @@ module Platanus
176
117
  # TODO
177
118
  end
178
119
 
120
+ ## Clear all rules related to an action
121
+ def clear(_action)
122
+ @rules[_action] = []
123
+ end
124
+
179
125
  ## SHORT HAND METHODS
180
126
 
181
127
  def upon(_expr=nil, &_block)
@@ -6,7 +6,7 @@ module Platanus
6
6
 
7
7
  ## Adds the has_stacked association to an ActiveRecord model.
8
8
  #
9
- # TODO
9
+ # TODO: Investigate how to turn this into an authentic association.
10
10
  #
11
11
  module StackedAttr2
12
12
 
@@ -28,49 +28,56 @@ module Platanus
28
28
  # prepare names
29
29
  tname = _name.to_s
30
30
  tname_single = tname.singularize
31
- tname_class = _options.fetch(:class_name, tname_single.camelize)
31
+ tname_class = _options.fetch :class_name, tname_single.camelize
32
+ stacked_model = tname_class.constantize
33
+ prefix = if _options[:cache_prf].nil? then 'last_' else _options.delete(:cache_prf) end # TODO: deprecate?
32
34
 
33
- # generate top_value property
35
+ # Generate top_value property
36
+ #
37
+ # How this property is generated can vary depending on given parameters or table structure:
38
+ # * If a top_value_key is provided in options, then a belongs_to association is created using it as foreign key.
39
+ # * If a top_xxx_id column is present, then a belongs_to association is created using if as foreign key.
40
+ # * If no key is provided, then a shorcut method that retrieves the stack's top is generated
41
+ #
34
42
  top_value_prop = "top_#{tname_single}"
35
- if _options.has_key? :top_value_key
36
- belongs_to top_value_prop.to_sym, class_name: tname_class, foreign_key: _options.delete(:top_value_key)
43
+ top_value_key = if _options.has_key? :top_value_key
44
+ belongs_to top_value_prop.to_sym, class_name: tname_class, foreign_key: _options[:top_value_key], autosave: true
45
+ _options.delete(:top_value_key)
37
46
  elsif self.column_names.include? "#{top_value_prop}_id"
38
- belongs_to top_value_prop.to_sym, class_name: tname_class
47
+ belongs_to top_value_prop.to_sym, class_name: tname_class, autosave: true
48
+ "#{top_value_prop}_id"
39
49
  else
40
- instance_var = "@_last_#{tname_single}".to_sym
50
+ top_value_var = "@_stacked_#{tname}_top".to_sym
41
51
  send :define_method, top_value_prop do
42
52
  # Storing the last stacked value will not prevent race conditions
43
53
  # when simultaneous updates occur.
44
- last = instance_variable_get instance_var
45
- return last unless last.nil?
46
- instance_variable_set(instance_var, self.send(tname).first)
47
- end
48
- send :define_method, "#{top_value_prop}=" do |_top|
49
- instance_variable_set(instance_var, _top)
54
+ last = instance_variable_get top_value_var
55
+ return last unless last.nil? or !last.persisted?
56
+ instance_variable_set(top_value_var, self.send(tname).all.first)
50
57
  end
58
+ nil
51
59
  end
52
- send :private, "#{top_value_prop}="
53
60
 
54
- prefix = if _options[:cache_prf].nil? then 'last_' else _options.delete(:cache_prf) end # TODO: deprecate
61
+ # When called inside callbacks, returns the new value being put at top of the stack.
62
+ new_value_var = "@_stacked_#{tname}_new"
63
+ send :define_method, "#{tname_single}_will" do
64
+ instance_variable_get(new_value_var)
65
+ end
55
66
 
56
- # generate virtual attributes
57
- changes = {}
58
- stacked_model = tname_class.constantize
59
- stacked_model.accessible_attributes.each do |attr_name|
60
- send :define_method, "#{attr_name}=" do |value| changes[attr_name] = value end
61
- send :define_method, "#{attr_name}" do
62
- return changes[attr_name] if changes.has_key? attr_name
63
- return self.send(prefix + attr_name) if self.respond_to? prefix + attr_name # Return cached value if avaliable
64
- top = self.send top_value_prop
65
- return nil if top.nil?
66
- return top.send attr_name
67
- end
68
- attr_accessible attr_name
67
+ # When called inside callbacks, will return the top value unless a new value is
68
+ # being pushed, in that case it returns the new value
69
+ last_value_var = "@_stacked_#{tname}_last"
70
+ send :define_method, "#{tname_single}_is" do
71
+ instance_variable_get(last_value_var)
69
72
  end
70
73
 
71
- # prepare cached attributes
74
+ # Prepare cached attributes
75
+ #
76
+ # Attribute caching allows the parent model to store the top value for
77
+ # some of the stacked model attributes (defined in options using the cached key)
78
+ #
72
79
  to_cache = _options.delete(:cached)
73
- unless to_cache.nil?
80
+ if to_cache
74
81
  to_cache = to_cache.map do |cache_attr|
75
82
  unless cache_attr.is_a? Hash
76
83
  name = cache_attr.to_s
@@ -83,61 +90,137 @@ module Platanus
83
90
  end
84
91
  end
85
92
 
86
- # callbacks
87
- on_stack = _options.delete(:on_stack)
93
+ # register callbacks
94
+ define_callbacks "stack_#{tname_single}"
88
95
 
89
- # limits and ordering
90
- # TODO: Support other kind of ordering, this would require to reevaluate top on every push
91
- _options[:order] = 'created_at DESC, id DESC'
92
- _options[:limit] = 10 if _options[:limit].nil?
96
+ # push logic
97
+ __update_stack = ->(_ctx, _top, _new_top, _save_quiet, &_block) do
98
+ begin
99
+ # make xx_top_value avaliable for event handlers
100
+ _ctx.instance_variable_set(new_value_var, _top) if _new_top
101
+ _ctx.instance_variable_set(last_value_var, _top)
93
102
 
94
- # setup main association
95
- has_many _name, _options
103
+ _ctx.run_callbacks "stack_#{tname_single}" do
104
+
105
+ # cache required fields
106
+ # TODO: improve cache: convention over configuration!
107
+ # cache should be automatic given certain column names and should include aliased attribues and virtual attributes.
108
+ # has_stacked :things, cache: { prefix: '', aliases: { xx => xx }, exclude: [], virtual: { xx => xx } }
109
+ if to_cache
110
+ to_cache.each do |cache_attr|
111
+ value = if cache_attr.has_key? :from
112
+ _top.nil? ? nil : _top.send(cache_attr[:from])
113
+ else
114
+ _ctx.send(cache_attr[:virtual])
115
+ end
116
+ _ctx.send(cache_attr[:to].to_s + '=', value)
117
+ end
118
+ end
119
+
120
+ _block.call if _block
121
+
122
+ if _new_top
123
+ # TODO: this leaves the invalid record on top of the stack and invalid cached values,
124
+ # maybe validation should ocurr before caching...
125
+ raise ActiveRecord::RecordInvalid.new(_top) unless _ctx.send(tname) << _top
126
+ end
127
+
128
+ # reset top_value_prop to top
129
+ if top_value_key
130
+ if _save_quiet
131
+ top_id = if _top.nil? then nil else _top.id end
132
+ if _ctx.send(top_value_key) != top_id
133
+ _ctx.update_column(top_value_key, top_id)
134
+ _ctx.send(top_value_prop, false) # reset belongs_to cache
135
+ end
136
+ else
137
+ _ctx.send("#{top_value_prop}=", _top)
138
+ end
139
+ else
140
+ _ctx.instance_variable_set(top_value_var, _top)
141
+ end
142
+ end
143
+ ensure
144
+ _ctx.instance_variable_set(new_value_var, nil)
145
+ _ctx.instance_variable_set(last_value_var, nil)
146
+ end
147
+ end
148
+
149
+ # Attribute mirroring
150
+ #
151
+ # Mirroring allows using the top value attributes in the parent model,
152
+ # it also allows modifying the attributes in the parent model, if the model is
153
+ # then saved, the modified attributes are wrapped in a new stack model object and put
154
+ # on top.
155
+ #
156
+ mirror_cache_var = "@_stacked_#{tname}_mirror".to_sym
157
+ if _options.delete(:mirroring)
158
+ stacked_model.accessible_attributes.each do |attr_name|
159
+
160
+ unless self.method_defined? "#{attr_name}="
161
+ send :define_method, "#{attr_name}=" do |value|
162
+ mirror = instance_variable_get(mirror_cache_var)
163
+ mirror = instance_variable_set(mirror_cache_var, {}) if mirror.nil?
164
+ mirror[attr_name] = value
165
+ end
166
+ else
167
+ Rails.logger.warn "stacked: failed to mirror setter for #{attr_name} in #{self.to_s}"
168
+ end
169
+
170
+ unless self.method_defined? attr_name
171
+ send :define_method, attr_name do
172
+ mirror = instance_variable_get(mirror_cache_var)
173
+ return mirror[attr_name] if !mirror.nil? and mirror.has_key? attr_name
174
+
175
+ return self.send(prefix + attr_name) if self.respond_to? prefix + attr_name # return cached value if avaliable
176
+ top = self.send top_value_prop
177
+ return nil if top.nil?
178
+ return top.send attr_name
179
+ end
96
180
 
97
- cache_step = ->(_ctx, _top, _top_is_new) {
98
- # cache required fields
99
- return if to_cache.nil?
100
- to_cache.each do |cache_attr|
101
- value = if cache_attr.has_key? :from
102
- _top.nil? ? _top : _top.send(cache_attr[:from])
181
+ send :define_method, "#{attr_name}_changed?" do
182
+ mirror = instance_variable_get(mirror_cache_var)
183
+ return true if !mirror.nil? and mirror.has_key? attr_name
184
+ return self.send(prefix + attr_name + '_changed?') if self.respond_to? prefix + attr_name + '_changed?' # return cached value if avaliable
185
+ return true # for now just return true for non cached attributes
186
+ end
187
+
188
+ attr_accessible attr_name
103
189
  else
104
- _ctx.send(cache_attr[:virtual], _top, _top_is_new)
190
+ Rails.logger.warn "stacked: failed to mirror getter for #{attr_name} in #{self.to_s}"
105
191
  end
106
- _ctx.send(cache_attr[:to].to_s + '=', value)
107
192
  end
108
- }
109
-
110
- after_step = ->(_ctx, _top) {
111
- # update top value property
112
- _ctx.send("#{top_value_prop}=", _top)
113
-
114
- # execute after callback
115
- _ctx.send(on_stack, _top) unless on_stack.nil?
116
- }
117
-
118
- # before saving model, load changes from virtual attributes.
119
- before_save do
120
- if changes.count > 0
121
- obj = stacked_model.new(changes)
122
- changes = {} # since push sometimes saves, then we must prevent a stack overflow by unsetting changes here.
123
-
124
- cache_step.call(self, obj, true)
125
- self.save! if self.new_record? # make sure there is an id BEFORE pushing
126
- raise ActiveRecord::RecordInvalid.new(obj) unless send(tname).send('<<', obj)
127
- after_step.call(self, obj)
193
+
194
+ # before saving model, load changes from virtual attributes.
195
+ set_callback :save, :around do |&_block|
196
+
197
+ mirror = instance_variable_get(mirror_cache_var)
198
+ if !mirror.nil? and mirror.count > 0
199
+
200
+ # propagate non cached attributes (only if record is not new and there is a top state)
201
+ unless self.new_record?
202
+ top = self.send top_value_prop
203
+ unless top.nil?
204
+ stacked_model.accessible_attributes.each do |attr_name|
205
+ mirror[attr_name] = top.send(attr_name) unless mirror.has_key? attr_name
206
+ end
207
+ end
208
+ end
209
+
210
+ obj = stacked_model.new(mirror)
211
+ instance_variable_set(mirror_cache_var, {}) # reset mirror changes
212
+ __update_stack.call(self, obj, true, true, &_block)
213
+
214
+ else _block.call end
128
215
  end
129
216
  end
130
217
 
218
+ # Push methods
219
+
131
220
  send :define_method, "push_#{tname_single}!" do |obj|
132
221
  self.class.transaction do
133
-
134
- # cache, then save if new, then push and finally process state
135
- cache_step.call(self, obj, true)
136
- self.save! if self.new_record? # make sure there is an id BEFORE pushing
137
- raise ActiveRecord::RecordInvalid.new(obj) unless send(tname).send('<<', obj)
138
- after_step.call(self, obj)
139
-
140
- self.save! if self.changed? # Must save again, no other way...
222
+ __update_stack.call(self, obj, true, false) { self.save! if self.new_record? }
223
+ self.save! if self.changed?
141
224
  end
142
225
  end
143
226
 
@@ -149,15 +232,13 @@ module Platanus
149
232
  end
150
233
  end
151
234
 
235
+ # Restore methods
236
+
152
237
  send :define_method, "restore_#{tname}!" do
153
238
  self.class.transaction do
154
-
155
- # find current top, then restore stack state
156
- top = self.send(_name).first
157
- cache_step.call(self, top, false)
158
- after_step.call(self, top)
159
-
160
- self.save! if self.changed?
239
+ top = self.send(tname).all.first
240
+ __update_stack.call(self, top, false, false)
241
+ self.save! if self.changed?
161
242
  end
162
243
  end
163
244
 
@@ -168,6 +249,13 @@ module Platanus
168
249
  return false
169
250
  end
170
251
  end
252
+
253
+ # setup main association
254
+ # TODO: Support other kind of ordering, this would require to reevaluate top on every push
255
+ _options[:order] = 'created_at DESC, id DESC'
256
+ _options[:limit] = 1 if _options[:limit].nil?
257
+ _options.delete(:limit) if _options[:limit] == :no_limit
258
+ has_many _name, _options
171
259
  end
172
260
  end
173
261
  end
@@ -1,3 +1,3 @@
1
1
  module Platanus
2
- VERSION = "0.0.32" # 0.1 will come with tests!
2
+ VERSION = "0.0.49" # 0.1 will come with tests!
3
3
  end
data/platanus.gemspec CHANGED
@@ -12,8 +12,9 @@ Gem::Specification.new do |gem|
12
12
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
13
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
14
  gem.name = "platanus"
15
- gem.require_paths = ["lib","lib/platanus"]
15
+ gem.require_paths = ["lib"]
16
16
  gem.version = Platanus::VERSION
17
17
 
18
18
  gem.add_runtime_dependency "multi_json", [">= 1.3.2"]
19
+ gem.add_development_dependency "rspec"
19
20
  end
data/spec/canned2_spec.rb CHANGED
@@ -17,12 +17,10 @@ describe Platanus::Canned2 do
17
17
  class DummyCtx
18
18
 
19
19
  attr_reader :params
20
- attr_reader :action_name
21
20
  attr_reader :current_user
22
21
 
23
- def initialize(_user, _action_name=nil, _params={})
22
+ def initialize(_user, _params={})
24
23
  @current_user = _user
25
- @action_name = _action_name
26
24
  @params = _params
27
25
  end
28
26
  end
@@ -42,7 +40,6 @@ describe Platanus::Canned2 do
42
40
  allow 'rute1#action3', upon { same(:char1, key: "current_user.char1") }
43
41
  allow 'rute1#action4', upon(:current_user) { same(:param2, key: "char2") and checks(:test1) }
44
42
  allow 'rute1#action5', upon(:current_user) { passes { current_user.char2 == params[:param2] } }
45
- allow 'rute2', upon(:current_user) { same(:char1) and action_is_not(:create) }
46
43
 
47
44
  # Complex routes
48
45
  allow 'rute1#action5' do
@@ -52,8 +49,8 @@ describe Platanus::Canned2 do
52
49
  end
53
50
  end
54
51
 
55
- let(:good_ctx) { DummyCtx.new(DummyUsr.new(10, "200"), 'create', char1: '10', param2: '200') }
56
- let(:bad_ctx) { DummyCtx.new(DummyUsr.new(10, 30), 'create', char1: '10', param2: '200') }
52
+ let(:good_ctx) { DummyCtx.new(DummyUsr.new(10, "200"), char1: '10', param2: '200') }
53
+ let(:bad_ctx) { DummyCtx.new(DummyUsr.new(10, 30), char1: '10', param2: '200') }
57
54
 
58
55
  describe "._run" do
59
56
  context 'when using single context rules' do
@@ -76,9 +73,6 @@ describe Platanus::Canned2 do
76
73
  it "does authorize on rute with context and inline test" do
77
74
  Roles.can?(good_ctx, :user, 'rute1#action5').should be_true
78
75
  end
79
- it "does not authorize on rute with context, match and action_is_not" do
80
- Roles.can?(good_ctx, :user, 'rute2').should be_false
81
- end
82
76
  end
83
77
 
84
78
  context 'when using multiple context rules' do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: platanus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.32
4
+ version: 0.0.49
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-11-07 00:00:00.000000000 Z
12
+ date: 2012-12-26 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: multi_json
@@ -27,6 +27,22 @@ dependencies:
27
27
  - - ! '>='
28
28
  - !ruby/object:Gem::Version
29
29
  version: 1.3.2
30
+ - !ruby/object:Gem::Dependency
31
+ name: rspec
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
30
46
  description: Platan.us utility gem
31
47
  email:
32
48
  - ignacio@platan.us
@@ -68,7 +84,6 @@ post_install_message:
68
84
  rdoc_options: []
69
85
  require_paths:
70
86
  - lib
71
- - lib/platanus
72
87
  required_ruby_version: !ruby/object:Gem::Requirement
73
88
  none: false
74
89
  requirements: