surrounded 0.8.4 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +0 -2
- data/README.md +185 -99
- data/examples/bottles.rb +145 -0
- data/images/body-bg.png +0 -0
- data/images/highlight-bg.jpg +0 -0
- data/images/hr.png +0 -0
- data/images/octocat-icon.png +0 -0
- data/images/surrounded.png +0 -0
- data/images/tar-gz-icon.png +0 -0
- data/images/zip-icon.png +0 -0
- data/index.html +544 -0
- data/javascripts/main.js +1 -0
- data/javascripts/scale.fix.js +17 -0
- data/lib/surrounded/access_control.rb +14 -2
- data/lib/surrounded/context.rb +47 -123
- data/lib/surrounded/context/initializing.rb +0 -4
- data/lib/surrounded/context/role_builders.rb +9 -25
- data/lib/surrounded/context/role_map.rb +1 -1
- data/lib/surrounded/context/trigger_controls.rb +91 -0
- data/lib/surrounded/context_errors.rb +1 -1
- data/lib/surrounded/shortcuts.rb +1 -2
- data/lib/surrounded/version.rb +1 -1
- data/params.json +1 -0
- data/stylesheets/print.css +226 -0
- data/stylesheets/pygment_trac.css +69 -0
- data/stylesheets/styles.css +255 -0
- data/stylesheets/stylesheet.css +371 -0
- data/surrounded.gemspec +1 -1
- data/test/context_access_test.rb +16 -0
- data/test/example_proxy_test.rb +0 -5
- data/test/override_methods_test.rb +40 -0
- data/test/role_context_method_test.rb +71 -65
- data/test/surrounded_context_test.rb +2 -1
- data/test/test_helper.rb +0 -13
- metadata +23 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cb66bc4320f6971198085580154f6ca683dcc952
|
4
|
+
data.tar.gz: cfe6c6e94af4ff0fa78922cd1df3b74666abb58a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5e9e696a657da51806a9929adfec0c0ef6f838dae69ebe2acf9c10a547998428a935d021118b7550eff2033316b1b177906749e93e50f3150c62abc60d44d0e1
|
7
|
+
data.tar.gz: 36395abadb826b0542061e3d51e0831f533a8c4321d4c4b113469dbfae4520f1ae15916919f4f1bf0006ff8ab84956fce227be65fd1e6b822156324aec840af9
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -1,29 +1,173 @@
|
|
1
1
|
# ![Surrounded](http://saturnflyer.github.io/surrounded/images/surrounded.png "Surrounded")
|
2
|
-
##
|
2
|
+
## Be in control of business logic.
|
3
3
|
|
4
4
|
[![Build Status](https://travis-ci.org/saturnflyer/surrounded.png?branch=master)](https://travis-ci.org/saturnflyer/surrounded)
|
5
5
|
[![Code Climate](https://codeclimate.com/github/saturnflyer/surrounded.png)](https://codeclimate.com/github/saturnflyer/surrounded)
|
6
6
|
[![Coverage Status](https://coveralls.io/repos/saturnflyer/surrounded/badge.png)](https://coveralls.io/r/saturnflyer/surrounded)
|
7
7
|
[![Gem Version](https://badge.fury.io/rb/surrounded.png)](http://badge.fury.io/rb/surrounded)
|
8
8
|
|
9
|
-
#
|
9
|
+
# Get work done with only what you need and nothing more.
|
10
10
|
|
11
|
-
|
12
|
-
The purpose of this library is to clear away the details of getting things setup and to allow you to make changes to the way you handle roles.
|
11
|
+
Surrounded is designed to help you better manage your business logic.
|
13
12
|
|
14
|
-
|
13
|
+
## How to think about your objects
|
15
14
|
|
16
|
-
|
17
|
-
2. `Surrounded::Context` helps you create objects which encapsulate other objects **and** their behavior. These *are* the environments.
|
15
|
+
First, name the problem you're solving. Then, break down your problem into responsible roles.
|
18
16
|
|
19
|
-
|
17
|
+
Use your problem name as a class and extend it with `Surrounded::Context`
|
20
18
|
|
21
|
-
|
19
|
+
It might look like this:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
class Employment
|
23
|
+
extend Surrounded::Context
|
24
|
+
|
25
|
+
role :boss
|
26
|
+
role :employee
|
27
|
+
end
|
28
|
+
```
|
29
|
+
|
30
|
+
In your application, you'll initialize this class with objects to play the roles that you've defined, so you'll need to specify which role players will use which role.
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
class Employment
|
34
|
+
extend Surrounded::Context
|
35
|
+
|
36
|
+
initialize :employee, :boss
|
37
|
+
|
38
|
+
role :boss
|
39
|
+
role :employee
|
40
|
+
end
|
41
|
+
```
|
42
|
+
|
43
|
+
Here, you've specified the order when initializing so you can use it like this:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
user1 = User.find(1)
|
47
|
+
user2 = User.find(2)
|
48
|
+
context = Employment.new(user1, user2)
|
49
|
+
```
|
50
|
+
|
51
|
+
That ensures that `user1` will become (and have all the features of) the `employee` and `user2` will become (and have all the features of) the `boss`.
|
52
|
+
|
53
|
+
There are 2 things left to do:
|
54
|
+
|
55
|
+
1. define behaviors for each role and
|
56
|
+
2. define how you can trigger their actions
|
57
|
+
|
58
|
+
## Defining behaviors for roles
|
59
|
+
|
60
|
+
Behaviors for your roles are easily defined just like you define a method. Provide your role a block and define methods there.
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
class Employment
|
64
|
+
extend Surrounded::Context
|
65
|
+
|
66
|
+
initialize :employee, :boss
|
67
|
+
|
68
|
+
role :boss
|
69
|
+
|
70
|
+
role :employee do
|
71
|
+
def work_weekend
|
72
|
+
if fed_up?
|
73
|
+
quit
|
74
|
+
else
|
75
|
+
schedule_weekend_work
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def quit
|
80
|
+
say("I'm sick of this place, #{boss.name}!")
|
81
|
+
stomp
|
82
|
+
throw_papers
|
83
|
+
say("I quit!")
|
84
|
+
end
|
85
|
+
|
86
|
+
def schedule_weekend_work
|
87
|
+
# ...
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
```
|
92
|
+
|
93
|
+
If any of your roles don't have special behaviors, like `boss`, you don't need to specify it. Your `initialize` setup will handle assiging who's who when this context is used.
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
class Employment
|
97
|
+
extend Surrounded::Context
|
98
|
+
|
99
|
+
initialize :employee, :boss
|
100
|
+
|
101
|
+
role :employee do
|
102
|
+
#...
|
103
|
+
end
|
104
|
+
end
|
105
|
+
```
|
106
|
+
|
107
|
+
## Triggering interactions
|
108
|
+
|
109
|
+
You'll need to define way to trigger these behaviors to occur so that you can use them.
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
context = Employment.new(user1, user2)
|
113
|
+
|
114
|
+
context.plan_weekend_work
|
115
|
+
```
|
116
|
+
|
117
|
+
The method you need is defined as an instance method in your context, but before that method will work as expected you'll need to mark it as a trigger.
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
class Employment
|
121
|
+
extend Surrounded::Context
|
122
|
+
|
123
|
+
initialize :employee, :boss
|
124
|
+
|
125
|
+
def plan_weekend_work
|
126
|
+
employee.work_weekend
|
127
|
+
end
|
128
|
+
trigger :plan_weekend_work
|
129
|
+
|
130
|
+
role :employee do
|
131
|
+
#...
|
132
|
+
end
|
133
|
+
end
|
134
|
+
```
|
135
|
+
|
136
|
+
Trigger methods are different from regular instance methods in that they apply behaviors from the roles to the role players.
|
137
|
+
A regular instance method just does what you define. But a trigger will make your role players come alive with their behaviors.
|
138
|
+
|
139
|
+
There's one last thing to make this work.
|
140
|
+
|
141
|
+
## Getting your role players ready
|
142
|
+
|
143
|
+
You'll need to include `Surrounded` in the classes of objects which will be role players in your context.
|
144
|
+
|
145
|
+
It's as easy as:
|
146
|
+
|
147
|
+
```ruby
|
148
|
+
class User
|
149
|
+
include Surrounded
|
150
|
+
|
151
|
+
# ...
|
152
|
+
end
|
153
|
+
```
|
154
|
+
|
155
|
+
This gives each of the objects the ability to understand its context and direct access to other objects in the context.
|
156
|
+
|
157
|
+
## Why is this valuable?
|
158
|
+
|
159
|
+
By creating environments which encapsulate roles and all necessary behaviors, you will be better able to isolate the logic of your system. A `user` in your system doesn't have all possible behaviors defined in its class, it gains the behaviors only when they are necessary.
|
160
|
+
|
161
|
+
The objects that interact have their behaviors defined and available right where they are needed. Implementation is in proximity to necessity. The behaviors you need for each role player are highly cohesive and are coupled to their use rather than being coupled to the class of an object which might use them at some point.
|
162
|
+
|
163
|
+
# Deeper Dive
|
164
|
+
|
165
|
+
## Create encapsulated environments for your objects.
|
22
166
|
|
23
167
|
Typical initialization of an environment, or a Context in DCI, has a lot of code. For example:
|
24
168
|
|
25
169
|
```ruby
|
26
|
-
class
|
170
|
+
class Employment
|
27
171
|
|
28
172
|
attr_reader :employee, :boss
|
29
173
|
private :employee, :boss
|
@@ -38,7 +182,7 @@ class MyEnvironment
|
|
38
182
|
end
|
39
183
|
```
|
40
184
|
|
41
|
-
This code allows the
|
185
|
+
This code allows the Employment class to create instances where it will have an `employee` and a `boss` role internally. These are set to `attr_reader`s and are made private.
|
42
186
|
|
43
187
|
The `employee` is extended with behaviors defined in the `Employee` module, and in this case there's no extra stuff for the `boss` so it doesn't get extended with anything.
|
44
188
|
|
@@ -47,7 +191,7 @@ Most of the time you'll follow a pattern like this. Some objects will get extra
|
|
47
191
|
By adding `Surrounded::Context` you can shortcut all this work.
|
48
192
|
|
49
193
|
```ruby
|
50
|
-
class
|
194
|
+
class Employment
|
51
195
|
extend Surrounded::Context
|
52
196
|
|
53
197
|
initialize(:employee, :boss)
|
@@ -67,7 +211,7 @@ _I don't want to use modules. Can't I use something like SimpleDelegator?_
|
|
67
211
|
Well, it just so happens that you can. This code will work just fine:
|
68
212
|
|
69
213
|
```ruby
|
70
|
-
class
|
214
|
+
class Employment
|
71
215
|
extend Surrounded::Context
|
72
216
|
|
73
217
|
initialize(:employee, :boss)
|
@@ -83,7 +227,7 @@ Instead of extending the `employee` object, Surrounded will run `Employee.new(em
|
|
83
227
|
But the syntax can be even simpler than that if you want.
|
84
228
|
|
85
229
|
```ruby
|
86
|
-
class
|
230
|
+
class Employment
|
87
231
|
extend Surrounded::Context
|
88
232
|
|
89
233
|
initialize(:employee, :boss)
|
@@ -97,7 +241,7 @@ end
|
|
97
241
|
By default, this code will create a module for you named `Employee`. If you want to use a wrapper, you can do this:
|
98
242
|
|
99
243
|
```ruby
|
100
|
-
class
|
244
|
+
class Employment
|
101
245
|
extend Surrounded::Context
|
102
246
|
|
103
247
|
initialize(:employee, :boss)
|
@@ -111,7 +255,7 @@ end
|
|
111
255
|
But if you're making changes and you decide to move from a module to a wrapper or from a wrapper to a module, you'll need to change that method call. Instead, you could just tell it which type of role to use:
|
112
256
|
|
113
257
|
```ruby
|
114
|
-
class
|
258
|
+
class Employment
|
115
259
|
extend Surrounded::Context
|
116
260
|
|
117
261
|
initialize(:employee, :boss)
|
@@ -148,17 +292,17 @@ Now the `User` instances will be able to implicitly access objects in their envi
|
|
148
292
|
|
149
293
|
Via `method_missing` those `User` instances can access a `context` object it stores in an internal collection.
|
150
294
|
|
151
|
-
Inside of the `
|
295
|
+
Inside of the `Employment` context we saw above, the `employee` and `boss` objects are instances of `User` for this example.
|
152
296
|
|
153
297
|
Because the `User` class includes `Surrounded`, the instances of that class will be able to access other objects in the same context implicitly.
|
154
298
|
|
155
299
|
Let's make our context look like this:
|
156
300
|
|
157
301
|
```ruby
|
158
|
-
class
|
302
|
+
class Employment
|
159
303
|
# other stuff from above is still here...
|
160
304
|
|
161
|
-
def
|
305
|
+
def plan_weekend_work
|
162
306
|
employee.quit
|
163
307
|
end
|
164
308
|
|
@@ -173,7 +317,7 @@ class MyEnvironment
|
|
173
317
|
end
|
174
318
|
```
|
175
319
|
|
176
|
-
What's happening in there is that when the `
|
320
|
+
What's happening in there is that when the `plan_weekend_work` method is called on the instance of `Employment`, the `employee` has the ability to refer to `boss` because it is in the same context, e.g. the same environment.
|
177
321
|
|
178
322
|
The behavior defined in the `Employee` module assumes that it may access other objects in it's local environment. The `boss` object, for example, is never explicitly passed in as an argument.
|
179
323
|
|
@@ -188,10 +332,10 @@ Your context will have methods of it's own which will trigger actions on the obj
|
|
188
332
|
Here's an example of what we want:
|
189
333
|
|
190
334
|
```ruby
|
191
|
-
class
|
335
|
+
class Employment
|
192
336
|
# other stuff from above is still here...
|
193
337
|
|
194
|
-
def
|
338
|
+
def plan_weekend_work
|
195
339
|
employee.store_context(self)
|
196
340
|
employee.quit
|
197
341
|
employee.remove_context
|
@@ -213,10 +357,10 @@ Now that the `employee` has a reference to the context, it won't blow up when it
|
|
213
357
|
We saw how we were able to clear up a lot of that repetitive work with the `initialize` method, so this is how we do it here:
|
214
358
|
|
215
359
|
```ruby
|
216
|
-
class
|
360
|
+
class Employment
|
217
361
|
# other stuff from above is still here...
|
218
362
|
|
219
|
-
trigger :
|
363
|
+
trigger :plan_weekend_work do
|
220
364
|
employee.quit
|
221
365
|
end
|
222
366
|
|
@@ -236,8 +380,8 @@ By using this `trigger` keyword, our block is the code we care about, but intern
|
|
236
380
|
The context will also store the triggers so that you can, for example, provide details outside of the environment about what triggers exist.
|
237
381
|
|
238
382
|
```ruby
|
239
|
-
context =
|
240
|
-
context.triggers #=> [:
|
383
|
+
context = Employment.new(current_user, the_boss)
|
384
|
+
context.triggers #=> [:plan_weekend_work]
|
241
385
|
```
|
242
386
|
|
243
387
|
You might find that useful for dynamically defining user interfaces.
|
@@ -247,16 +391,16 @@ Sometimes I'd rather not use this DSL, however. I want to just write regular met
|
|
247
391
|
We can do that too. You'll need to opt in to this by specifying `trigger :your_method_name` for the methods you want to use.
|
248
392
|
|
249
393
|
```ruby
|
250
|
-
class
|
394
|
+
class Employment
|
251
395
|
# other stuff from above is still here...
|
252
396
|
|
253
|
-
def
|
397
|
+
def plan_weekend_work
|
254
398
|
employee.quit
|
255
399
|
end
|
256
|
-
trigger :
|
400
|
+
trigger :plan_weekend_work
|
257
401
|
|
258
402
|
# or in Ruby 2.x
|
259
|
-
trigger def
|
403
|
+
trigger def plan_weekend_work
|
260
404
|
employee.quit
|
261
405
|
end
|
262
406
|
|
@@ -284,30 +428,30 @@ Fortunately, you can make it easy.
|
|
284
428
|
By running `protect_triggers` you'll be able to define when triggers may or may not be run. You can still run them, but they'll raise an error. Here's an example.
|
285
429
|
|
286
430
|
```ruby
|
287
|
-
class
|
431
|
+
class Employment
|
288
432
|
extend Surrounded::Context
|
289
433
|
protect_triggers
|
290
434
|
|
291
|
-
def
|
435
|
+
def plan_weekend_work
|
292
436
|
employee.quit
|
293
437
|
end
|
294
|
-
trigger :
|
438
|
+
trigger :plan_weekend_work
|
295
439
|
|
296
|
-
disallow :
|
297
|
-
employee.bank_balance
|
440
|
+
disallow :plan_weekend_work do
|
441
|
+
employee.bank_balance > 1000000
|
298
442
|
end
|
299
443
|
end
|
300
444
|
```
|
301
445
|
|
302
|
-
Then, when the employee role's `bank_balance` is
|
446
|
+
Then, when the employee role's `bank_balance` is greater than `1000000`, the available triggers won't include `:plan_weekend_work`.
|
303
447
|
|
304
448
|
You can compare the instance of the context by listing `all_triggers` and `triggers` to see what could be possible and what's currently possible.
|
305
449
|
|
306
450
|
Alternatively, if you just want to define your own methods without the DSL using `disallow`, you can just follow the pattern of `disallow_#{method_name}?` when creating your own protection.
|
307
451
|
|
308
|
-
In fact, that's exactly what happens with the `disallow` keyword. After using it here, we'd have a `
|
452
|
+
In fact, that's exactly what happens with the `disallow` keyword. After using it here, we'd have a `disallow_plan_weekend_work?` method defined.
|
309
453
|
|
310
|
-
If you call the disallowed trigger directly, you'll raise a `
|
454
|
+
If you call the disallowed trigger directly, you'll raise a `Employment::AccessError` exception and the code in your trigger will not be run. You may rescue from that or you may rescue from `Surrounded::Context::AccessError` although you should prefer to use the error name from your own class.
|
311
455
|
|
312
456
|
## Restricting return values
|
313
457
|
|
@@ -316,7 +460,7 @@ _Tell, Don't Ask_ style programming can better be enforced by following East-ori
|
|
316
460
|
Here's how you enforce it:
|
317
461
|
|
318
462
|
```ruby
|
319
|
-
class
|
463
|
+
class Employment
|
320
464
|
extend Surrounded::Context
|
321
465
|
east_oriented_triggers
|
322
466
|
end
|
@@ -433,61 +577,6 @@ class Organization
|
|
433
577
|
end
|
434
578
|
```
|
435
579
|
|
436
|
-
## Policies for the application of role methods
|
437
|
-
|
438
|
-
There are 2 approaches to applying new behavior to your objects.
|
439
|
-
|
440
|
-
By default your context will add methods to an object before a trigger is run
|
441
|
-
and behaviors will be removed after the trigger is run.
|
442
|
-
|
443
|
-
Alternatively you may set the behaviors to be added during the initialize method
|
444
|
-
of your context.
|
445
|
-
|
446
|
-
Here's how it works:
|
447
|
-
|
448
|
-
```ruby
|
449
|
-
class ActivatingAccount
|
450
|
-
extend Surrounded::Context
|
451
|
-
|
452
|
-
apply_roles_on(:trigger) # this is the default
|
453
|
-
# apply_roles_on(:initialize) # set this to apply behavior from the start
|
454
|
-
|
455
|
-
initialize(:activator, :account)
|
456
|
-
|
457
|
-
role :activator do
|
458
|
-
def some_behavior; end
|
459
|
-
end
|
460
|
-
|
461
|
-
def non_trigger_method
|
462
|
-
activator.some_behavior # not available unless you apply roles on initialize
|
463
|
-
end
|
464
|
-
|
465
|
-
trigger :some_trigger_method do
|
466
|
-
activator.some_behavior # always available
|
467
|
-
end
|
468
|
-
end
|
469
|
-
```
|
470
|
-
|
471
|
-
_Why are those options there?_
|
472
|
-
|
473
|
-
When you initialize a context and apply behavior at the same time, you'll need
|
474
|
-
to remove that behavior. For example, if you are using Casting AND you apply roles on initialize:
|
475
|
-
|
476
|
-
```ruby
|
477
|
-
context = ActiviatingAccount.new(current_user, Account.find(123))
|
478
|
-
context.do_something
|
479
|
-
current_user.some_behavior # this method is still available
|
480
|
-
current_user.uncast # you'll have to manually cleanup
|
481
|
-
```
|
482
|
-
|
483
|
-
But if you go with the default and apply behaviors on trigger, your roles will be cleaned up automatically:
|
484
|
-
|
485
|
-
```ruby
|
486
|
-
context = ActiviatingAccount.new(current_user, Account.find(123))
|
487
|
-
context.do_something
|
488
|
-
current_user.some_behavior # NoMethodError
|
489
|
-
```
|
490
|
-
|
491
580
|
## Overview in code
|
492
581
|
|
493
582
|
Here's a view of the possibilities in code.
|
@@ -498,9 +587,6 @@ Surrounded::Context.default_role_type = :module # also :wrap, :wrapper, or :inte
|
|
498
587
|
|
499
588
|
class ActiviatingAccount
|
500
589
|
extend Surrounded::Context
|
501
|
-
|
502
|
-
apply_roles_on(:trigger) # this is the default
|
503
|
-
# apply_roles_on(:initialize) # set this to apply behavior from the start
|
504
590
|
|
505
591
|
# set the default role type only for this class
|
506
592
|
self.default_role_type = :module # also :wrap, :wrapper, or :interface
|
@@ -545,9 +631,9 @@ class ActiviatingAccount
|
|
545
631
|
# if you use a regular method and want to use context-specific behavior,
|
546
632
|
# you must handle storing the context yourself:
|
547
633
|
def regular_method
|
548
|
-
|
634
|
+
apply_behaviors # handles the adding of all the roles and behaviors
|
549
635
|
activator.some_behavior # behavior not available unless you apply roles on initialize
|
550
|
-
|
636
|
+
remove_behaviors # handles the removal of all roles and behaviors
|
551
637
|
end
|
552
638
|
|
553
639
|
trigger :some_trigger_method do
|