bali 1.1.0 → 1.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ace804a23b863182a963445bfa6c155019eb63fb
4
- data.tar.gz: 35aec1c6457269fac4f5e145b7714fa1509ffa09
3
+ metadata.gz: bfdb9274e88935206ba786522f65dba0cb1ddfb4
4
+ data.tar.gz: f3dc6a29a69a526540bfd1fab705acdade594773
5
5
  SHA512:
6
- metadata.gz: 8fdcf47c1d1222118e81bc425ec045729419e63ca834dced4ba9471102e29a420deb6618d111fd21324938e2c79ec3d079cc1d32161dbc52876959e608185140
7
- data.tar.gz: 0d0157c91b98396a0805742f619e124cce725bac14417af71afa2300fe234885e5c35dd5695fec498b07f80182b8d14ecaca9a01ca4a2663d4bdaddd251f16df
6
+ metadata.gz: 4801d9b6dc47bd70b75689663485487533c82d1de7ae2cb37c8657972837f5894d03cc7598fad9f87adea0312672ccd81ce00458012ae7aa157d95895bba17a0
7
+ data.tar.gz: 1a0597385c6bded708cba7d9bac719921706465df49d9a29b445910e086859e3da5b0b9e123da4ce38d63d11b1c3dae033854ca76c539d5b57077da57243c709
data/README.md CHANGED
@@ -85,6 +85,9 @@ class My::Employee
85
85
 
86
86
  # working experience in the company
87
87
  attr_accessor :exp_years
88
+
89
+ # role/roles of this employee
90
+ attr_accessor :roles
88
91
  end
89
92
  ```
90
93
 
@@ -143,6 +146,72 @@ Thus, if we have:
143
146
 
144
147
  That is, we can check `can?` and `cant?` with multiple roles by passing array of roles to it.
145
148
 
149
+ ### Using subtarget's instance for Can and Cant testing
150
+
151
+ You may pass in real subtarget instance rather than (1) a symbol, (2) string or (3) array of string/symbol for can/cant testing.
152
+
153
+ In order to do so, you need to specify the field/method in the subtarget that will be used to evaluating the subtarget's role(s). To do that, we define `roles_for` as follow inside `Bali.map_rules` block:
154
+
155
+ ```ruby
156
+ Bali.map_rules do
157
+ roles_for My::Employee, :roles
158
+ roles_for My::AdminUser, :admin_roles
159
+ roles_for My::BankUser, :roles
160
+
161
+ # rules definition
162
+ # may follow
163
+ end
164
+ ```
165
+
166
+ `roles_for` accept two parameters, namely the class of the subtarget, and the field/method that will be invoked on it to gain data about its role(s).
167
+
168
+ By doing so, we can now perform authorisation testing as follow:
169
+
170
+ ```ruby
171
+ txn = My::Transaction.new
172
+ current_employee = My::Employee.find_by_id(1)
173
+ txn.can?(current_employee, :print)
174
+ ```
175
+
176
+ ### Rule clause if/unless
177
+
178
+ Rule clause may contain `if` and `unless` (decider) proc as already seen before. This `if` and `unless` `proc` have three variants that can be used to express your rule in sufficient detail:
179
+
180
+ 1. Zero arity
181
+ 2. One arity
182
+ 3. Two arity
183
+
184
+ When rule is very brief, use zero-arity rule clause as below:
185
+
186
+ ```ruby
187
+ Bali.map_rules do
188
+ rules_for My::Transaction do
189
+ describe(:staff) { can :cancel }
190
+ describe(:finance) { can :cancel }
191
+ end
192
+ end
193
+ ```
194
+
195
+ Say that (for staff) to cancel a transaction, the transaction must have not been settled yet, you need to define the rule by using one-arity rule clause decider:
196
+
197
+ ```ruby
198
+ describe :staff do
199
+ can :cancel, if: { |txn| txn.is_settled? }
200
+ end
201
+ ```
202
+
203
+ Good, but, in addition to that, how to allow transaction cancellation only to staff who has 3 years or so experience in working with the company?
204
+
205
+ In order to do that, we need to resort to 2-arity decider, as follow:
206
+
207
+ ```ruby
208
+ describe :staff do
209
+ can :cancel, if: { |txn, usr| txn.is_settled? && usr.exp_years >= 3 }
210
+ end
211
+ ```
212
+
213
+ This way, we can keep our controller/logic/model clean from unnecessary and un-DRY role-testing logic.
214
+
146
215
  ## Contributing
147
216
 
148
217
  Bug reports and pull requests are welcome on GitHub at https://github.com/saveav/bali. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](contributor-covenant.org) code of conduct.
@@ -153,19 +222,23 @@ Bali is proudly available as open source under the terms of the [MIT License](ht
153
222
 
154
223
  ## Changelog
155
224
 
156
- ### Version 1.0.0beta1
225
+ == Version 1.0.0beta1
226
+
157
227
  1. Initial version
158
228
 
159
- ### Version 1.0.0rc1
229
+ == Version 1.0.0rc1
230
+
160
231
  1. Fix bug where user can't check on class
161
232
  2. Adding new clause: cant_all
162
233
 
163
- ### Version 1.0.0rc2
234
+ == Version 1.0.0rc2
235
+
164
236
  1. [Fix bug when class's name, as a constant, is reloaded](http://stackoverflow.com/questions/2509350/rails-class-object-id-changes-after-i-make-a-request) (re-allocated to different address in the memory)
165
237
  2. Allow describing rule for `nil`, useful if user is not authenticated thus role is probably `nil`
166
238
  3. Remove pry from development dependency
167
239
 
168
- ### Version 1.0.0rc3
240
+ == Version 1.0.0rc3
241
+
169
242
  1. Each target class should includes `Bali::Objector`, for the following reasons:
170
243
  - Makes it clear that class do want to include the Bali::Objector
171
244
  - Transparant, and thus less confusing as to where "can?" and "cant" come from
@@ -173,18 +246,23 @@ Bali is proudly available as open source under the terms of the [MIT License](ht
173
246
  2. Return `true` to any `can?` for undefined target/subtarget alike
174
247
  3. Return `false` to any `cant?` for undefined target/subtarget alike
175
248
 
176
- ### Version 1.0.0
249
+ == Version 1.0.0
250
+
177
251
  1. Released the stable version of this gem
178
252
 
179
- ### Version 1.1.0rc1
253
+ == Version 1.1.0rc1
254
+
180
255
  1. Ability for rule class to be parsed later by passing `later: true` to rule class definition
181
256
  2. Add `Bali.parse` and `Bali.parse!` (`Bali.parse!` executes "later"-tagged rule class, Bali.parse executes automatically after all rules are defined)
182
257
  3. Added more thorough testing specs
183
258
  4. Proc can be served under `unless` for defining the rule's decider
184
259
 
185
- ### Version 1.1.0rc2
260
+ == Version 1.1.0rc2
261
+
186
262
  1. Ability to check `can?` and `cant?` for subtarget with multiple roles
187
263
  2. Describe multiple rules at once for multiple subtarget
188
264
 
189
- ### Version 1.1.0
190
- 1. Respecting precedence; rule that is defined first can be replaced with later-defined rule clause for the same operation
265
+ == Version 1.2.0
266
+
267
+ 1. Passing real object as subtarget's role, instead of symbol or array of symbol
268
+ 2. Clause can also yielding user, along with the object in question
@@ -13,7 +13,9 @@ require_relative "bali/dsl/rules_for_dsl.rb"
13
13
  require_relative "bali/objector"
14
14
 
15
15
  # exception classes
16
+ require_relative "bali/exceptions/bali_error"
16
17
  require_relative "bali/exceptions/dsl_error"
18
+ require_relative "bali/exceptions/objection_error"
17
19
 
18
20
  module Bali
19
21
  # mapping class to a RuleClass
@@ -24,6 +26,12 @@ module Bali
24
26
 
25
27
  # from full class name to symbol
26
28
  REVERSE_ALIASED_RULE_CLASS_MAP = {}
29
+
30
+ # {
31
+ # User: :roles,
32
+ # AdminUser: :admin_roles
33
+ # }
34
+ TRANSLATED_SUBTARGET_ROLES = {}
27
35
  end
28
36
 
29
37
  module Bali
@@ -6,6 +6,7 @@ class Bali::MapRulesDsl
6
6
  @@lock = Mutex.new
7
7
  end
8
8
 
9
+ # defining rules
9
10
  def rules_for(target_class, target_alias_hash = {}, &block)
10
11
  @@lock.synchronize do
11
12
  self.current_rule_class = Bali::RuleClass.new(target_class)
@@ -18,7 +19,33 @@ class Bali::MapRulesDsl
18
19
  end
19
20
  end
20
21
 
22
+ # subtarget_class is the subtarget's class definition
23
+ # field_name is the field that will be consulted when instantiated object of this class is passed in can? or cant?
24
+ def roles_for(subtarget_class, field_name)
25
+ raise Bali::DslError, "Subtarget must be a class" unless subtarget_class.is_a?(Class)
26
+ raise Bali::DslError, "Field name must be a symbol/string" if !(field_name.is_a?(Symbol) || field_name.is_a?(String))
27
+
28
+ Bali::TRANSLATED_SUBTARGET_ROLES[subtarget_class.to_s] = field_name
29
+ nil
30
+ end
31
+
21
32
  def describe(*params)
22
33
  raise Bali::DslError, "describe block must be within rules_for block"
23
34
  end
35
+
36
+ def can(*params)
37
+ raise Bali::DslError, "can block must be within describe block"
38
+ end
39
+
40
+ def cant(*params)
41
+ raise Bali::DslError, "cant block must be within describe block"
42
+ end
43
+
44
+ def can_all(*params)
45
+ raise Bali::DslError, "can_all block must be within describe block"
46
+ end
47
+
48
+ def cant_all(*params)
49
+ raise Bali::DslError, "cant_all block must be within describe block"
50
+ end
24
51
  end
@@ -0,0 +1,2 @@
1
+ class Bali::Error < StandardError
2
+ end
@@ -1,2 +1,3 @@
1
- class Bali::DslError < StandardError
1
+ # this error is thrown when there is incorrect syntax in the DSL
2
+ class Bali::DslError < Bali::Error
2
3
  end
@@ -0,0 +1,3 @@
1
+ # this error is thrown when objection is cannot be executed
2
+ class Bali::ObjectionError < Bali::Error
3
+ end
@@ -16,10 +16,46 @@ end
16
16
 
17
17
  module Bali::Objector::Statics
18
18
 
19
+ # will return array
20
+ def __translate_subtarget_roles__(_subtarget_roles)
21
+ if _subtarget_roles.is_a?(String) || _subtarget_roles.is_a?(Symbol) || _subtarget_roles.is_a?(NilClass)
22
+ return [_subtarget_roles]
23
+ elsif _subtarget_roles.is_a?(Array)
24
+ return _subtarget_roles
25
+ else
26
+ # this case, _subtarget_roles is an object but not a symbol or a string
27
+ # let's try to deduct subtarget's roles
28
+
29
+ _subtarget = _subtarget_roles
30
+ _subtarget_class = _subtarget.class.to_s
31
+
32
+ # variable to hold deducted role of the passed object
33
+ deducted_roles = nil
34
+
35
+ Bali::TRANSLATED_SUBTARGET_ROLES.each do |subtarget_class, roles_field_name|
36
+ if _subtarget_class == subtarget_class
37
+ deducted_roles = _subtarget.send(roles_field_name)
38
+ if deducted_roles.is_a?(String) || deducted_roles.is_a?(Symbol)
39
+ deducted_roles = [deducted_roles]
40
+ break
41
+ elsif deducted_roles.is_a?(Array)
42
+ break
43
+ else
44
+ # keep it nil if _subtarget_roles is not either String, Symbol or Array
45
+ deducted_roles = nil
46
+ end
47
+ end # if matching class
48
+ end # each TRANSLATED_SUBTARGET_ROLES
49
+
50
+ return deducted_roles
51
+ end # if
52
+ end
53
+
19
54
  ### options passable to __can__? and __cant__? are:
20
55
  ### cross_action: if set to true wouldn't call its counterpart so as to prevent
21
56
  ### overflowing stack
22
- ###
57
+ ### original_subtarget: the original passed to can? and cant? before
58
+ ### processed by __translate_subtarget_roles__
23
59
 
24
60
  def __can__?(subtarget, operation, record = self, options = {})
25
61
  # if performed on a class-level, don't call its class or it will return
@@ -58,15 +94,56 @@ module Bali::Objector::Statics
58
94
  # default if can? for undefined rule is false, after related clause
59
95
  # cannot be found in cant?
60
96
  return false if options[:cross_check]
61
- return !self.cant?(subtarget, operation, record, cross_check: true)
97
+ options[:cross_check] = true
98
+ return !self.cant?(subtarget, operation, record, options)
62
99
  else
63
100
  if rule.has_decider?
64
101
  # must test first
65
102
  decider = rule.decider
66
- if decider.arity == 0
67
- return (rule.decider_type == :if) ? decider.() == true : decider.() == false
68
- else
69
- return (rule.decider_type == :if) ? decider.(record) == true : decider.(record) == false
103
+ original_subtarget = options.fetch(:original_subtarget)
104
+ case decider.arity
105
+ when 0
106
+ if rule.decider_type == :if
107
+ if decider.()
108
+ return true
109
+ else
110
+ return false
111
+ end
112
+ elsif rule.decider_type == :unless
113
+ unless decider.()
114
+ return true
115
+ else
116
+ return false
117
+ end
118
+ end
119
+ when 1
120
+ if rule.decider_type == :if
121
+ if decider.(record)
122
+ return true
123
+ else
124
+ return false
125
+ end
126
+ elsif rule.decider_type == :unless
127
+ unless decider.(record)
128
+ return true
129
+ else
130
+ return false
131
+ end
132
+ end
133
+ when 2
134
+ if rule.decider_type == :if
135
+ if decider.(record, original_subtarget)
136
+ return true
137
+ else
138
+ return false
139
+ end
140
+ elsif rule.decider_type == :unless
141
+ unless decider.(record, original_subtarget)
142
+ return true
143
+ else
144
+ return false
145
+ end
146
+ end
70
147
  end
71
148
  else
72
149
  # rule is properly defined
@@ -110,14 +187,55 @@ module Bali::Objector::Statics
110
187
  # can? is defined exactly for the same target, and subtarget, and record (if given)
111
188
  if rule.nil?
112
189
  return true if options[:cross_check]
113
- return !self.can?(subtarget, operation, record, cross_check: true)
190
+ options[:cross_check] = true
191
+ return !self.can?(subtarget, operation, record, options)
114
192
  else
115
193
  if rule.has_decider?
116
194
  decider = rule.decider
117
- if decider.arity == 0
118
- return (rule.decider_type == :if) ? decider.() == true : decider.() == false
119
- else
120
- return (rule.decider_type == :if) ? decider.(record) == true : decider.(record) == false
195
+ original_subtarget = options.fetch(:original_subtarget)
196
+ case decider.arity
197
+ when 0
198
+ if rule.decider_type == :if
199
+ if decider.()
200
+ return true
201
+ else
202
+ return false
203
+ end
204
+ elsif rule.decider_type == :unless
205
+ unless decider.()
206
+ return true
207
+ else
208
+ return false
209
+ end
210
+ end
211
+ when 1
212
+ if rule.decider_type == :if
213
+ if decider.(record)
214
+ return true
215
+ else
216
+ return false
217
+ end
218
+ elsif rule.decider_type == :unless
219
+ unless decider.(record)
220
+ return true
221
+ else
222
+ return false
223
+ end
224
+ end
225
+ when 2
226
+ if rule.decider_type == :if
227
+ if decider.(record, original_subtarget)
228
+ return true
229
+ else
230
+ return false
231
+ end
232
+ elsif rule.decider_type == :unless
233
+ unless decider.(record, original_subtarget)
234
+ return true
235
+ else
236
+ return false
237
+ end
238
+ end
121
239
  end
122
240
  else
123
241
  return true # rule is properly defined
@@ -125,23 +243,30 @@ module Bali::Objector::Statics
125
243
  end # if rule is nil
126
244
  end
127
245
 
128
- def can?(subtargets, operation, record = self, options = {})
129
- subs = (subtargets.is_a?(Array)) ? subtargets : [subtargets]
246
+ def can?(subtarget_roles, operation, record = self, options = {})
247
+ subs = __translate_subtarget_roles__(subtarget_roles)
248
+ # well, it is largely not used unless decider's is 2 arity
249
+ options[:original_subtarget] = options[:original_subtarget].nil? ? subtarget_roles : options[:original_subtarget]
130
250
 
131
251
  subs.each do |subtarget|
132
252
  can_value = __can__?(subtarget, operation, record, options)
133
253
  return true if can_value == true
134
254
  end
135
255
  false
256
+ rescue => e
257
+ raise Bali::ObjectionError, e.message
136
258
  end
137
259
 
138
- def cant?(subtargets, operation, record = self, options = {})
139
- subs = (subtargets.is_a?(Array)) ? subtargets : [subtargets]
260
+ def cant?(subtarget_roles, operation, record = self, options = {})
261
+ subs = __translate_subtarget_roles__ subtarget_roles
262
+ options[:original_subtarget] = options[:original_subtarget].nil? ? subtarget_roles : options[:original_subtarget]
140
263
 
141
264
  subs.each do |subtarget|
142
265
  cant_value = __cant__?(subtarget, operation, record, options)
143
266
  return false if cant_value == false
144
267
  end
145
268
  true
269
+ rescue => e
270
+ raise Bali::ObjectionError, e.message
146
271
  end
147
272
  end
@@ -1,3 +1,3 @@
1
1
  module Bali
2
- VERSION = "1.1.0"
2
+ VERSION = "1.2.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bali
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Pahlevi
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2015-08-24 00:00:00.000000000 Z
11
+ date: 2015-08-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -75,7 +75,9 @@ files:
75
75
  - lib/bali.rb
76
76
  - lib/bali/dsl/map_rules_dsl.rb
77
77
  - lib/bali/dsl/rules_for_dsl.rb
78
+ - lib/bali/exceptions/bali_error.rb
78
79
  - lib/bali/exceptions/dsl_error.rb
80
+ - lib/bali/exceptions/objection_error.rb
79
81
  - lib/bali/foundations/bali_statics.rb
80
82
  - lib/bali/foundations/rule.rb
81
83
  - lib/bali/foundations/rule_class.rb