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 +4 -4
- data/README.md +87 -9
- data/lib/bali.rb +8 -0
- data/lib/bali/dsl/map_rules_dsl.rb +27 -0
- data/lib/bali/exceptions/bali_error.rb +2 -0
- data/lib/bali/exceptions/dsl_error.rb +2 -1
- data/lib/bali/exceptions/objection_error.rb +3 -0
- data/lib/bali/objector.rb +140 -15
- data/lib/bali/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bfdb9274e88935206ba786522f65dba0cb1ddfb4
|
4
|
+
data.tar.gz: f3dc6a29a69a526540bfd1fab705acdade594773
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
225
|
+
== Version 1.0.0beta1
|
226
|
+
|
157
227
|
1. Initial version
|
158
228
|
|
159
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
249
|
+
== Version 1.0.0
|
250
|
+
|
177
251
|
1. Released the stable version of this gem
|
178
252
|
|
179
|
-
|
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
|
-
|
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
|
-
|
190
|
-
|
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
|
data/lib/bali.rb
CHANGED
@@ -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
|
data/lib/bali/objector.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
-
|
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
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
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?(
|
129
|
-
subs = (
|
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?(
|
139
|
-
subs =
|
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
|
data/lib/bali/version.rb
CHANGED
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.
|
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-
|
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
|