platanus 0.0.32 → 0.0.49

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.
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: