surrounded 0.4.1 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +294 -84
- data/lib/surrounded/context.rb +66 -31
- data/lib/surrounded/context_errors.rb +1 -1
- data/lib/surrounded/version.rb +1 -1
- data/test/surrounded_context_test.rb +10 -1
- data/test/test_helper.rb +1 -1
- metadata +2 -3
- data/lib/surrounded/README.md +0 -209
data/README.md
CHANGED
@@ -1,91 +1,142 @@
|
|
1
1
|
# Surrounded
|
2
|
+
## Bring your own complexity
|
2
3
|
|
3
4
|
[![Build Status](https://travis-ci.org/saturnflyer/surrounded.png?branch=master)](https://travis-ci.org/saturnflyer/surrounded)
|
4
5
|
[![Code Climate](https://codeclimate.com/github/saturnflyer/surrounded.png)](https://codeclimate.com/github/saturnflyer/surrounded)
|
5
6
|
[![Coverage Status](https://coveralls.io/repos/saturnflyer/surrounded/badge.png)](https://coveralls.io/r/saturnflyer/surrounded)
|
6
7
|
[![Gem Version](https://badge.fury.io/rb/surrounded.png)](http://badge.fury.io/rb/surrounded)
|
7
8
|
|
8
|
-
|
9
|
+
# Surrounded aims to make things simple and get out of your way.
|
9
10
|
|
10
|
-
|
11
|
+
Most of what you care about is defining the behavior of objects. How they interact is important.
|
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
13
|
|
12
|
-
|
14
|
+
There are two main parts to this library.
|
13
15
|
|
14
|
-
|
16
|
+
1. `Surrounded` gives objects an implicit awareness of other objects in their environments.
|
17
|
+
2. `Surrounded::Context` helps you create objects which encapsulate other objects. These *are* the environments.
|
18
|
+
|
19
|
+
First, take a look at creating contexts. This is where you'll spend most of your time.
|
20
|
+
|
21
|
+
## Easily create encapsulated environments for your objects.
|
22
|
+
|
23
|
+
Typical initialization of an environment, or a Context in DCI, has a lot of code. For example:
|
15
24
|
|
16
25
|
```ruby
|
17
|
-
class
|
18
|
-
|
26
|
+
class MyEnvironment
|
27
|
+
|
28
|
+
attr_reader :employee, :boss
|
29
|
+
private :employee, :boss
|
30
|
+
def initialize(employee, boss)
|
31
|
+
@employee = employee.extend(Employee)
|
32
|
+
@boss = boss
|
33
|
+
end
|
34
|
+
|
35
|
+
module Employee
|
36
|
+
# extra behavior here...
|
37
|
+
end
|
19
38
|
end
|
20
39
|
```
|
21
40
|
|
22
|
-
|
23
|
-
|
24
|
-
_What environment!? I don't get it._
|
41
|
+
This code allows the MyEnvironment 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.
|
25
42
|
|
26
|
-
|
43
|
+
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.
|
27
44
|
|
28
|
-
|
29
|
-
and objects inside should have knowledge of the other objects in the environment.
|
30
|
-
Take a breath, because there's a lot going on.
|
45
|
+
Most of the time you'll follow a pattern like this. Some objects will get extra behavior and some won't. The modules that you use to provide the behavior will match the names you use for the roles to which you assign objects.
|
31
46
|
|
32
|
-
|
47
|
+
By adding `Surrounded::Context` you can shortcut all this work.
|
33
48
|
|
34
49
|
```ruby
|
35
50
|
class MyEnvironment
|
36
51
|
extend Surrounded::Context
|
52
|
+
|
53
|
+
initialize(:employee, :boss)
|
54
|
+
|
55
|
+
module Employee
|
56
|
+
# extra behavior here...
|
57
|
+
end
|
37
58
|
end
|
38
59
|
```
|
39
60
|
|
40
|
-
|
61
|
+
Surrounded gives you an `initialize` class method which does all the setup work for you.
|
62
|
+
|
63
|
+
## Managing Roles
|
64
|
+
|
65
|
+
_I don't want to use modules. Can't I use something like SimpleDelegator?_
|
66
|
+
|
67
|
+
Well, it just so happens that you can. This code will work just fine:
|
41
68
|
|
42
69
|
```ruby
|
43
70
|
class MyEnvironment
|
44
71
|
extend Surrounded::Context
|
72
|
+
|
73
|
+
initialize(:employee, :boss)
|
45
74
|
|
46
|
-
|
47
|
-
private :employee, :boss
|
48
|
-
def initialize(employee, boss)
|
49
|
-
@employee = employee.extend(Employee)
|
50
|
-
@boss = boss
|
51
|
-
end
|
52
|
-
|
53
|
-
module Employee
|
75
|
+
class Employee < SimpleDelegator
|
54
76
|
# extra behavior here...
|
55
77
|
end
|
56
78
|
end
|
57
79
|
```
|
58
80
|
|
59
|
-
|
81
|
+
Instead of extending the `employee` object, Surrounded will run `Employee.new(employee)` to create the wrapper for you. You'll need to include the `Surrounded` module in your wrapper, but we'll get to that.
|
60
82
|
|
61
|
-
|
83
|
+
But the syntax can be even simpler than that if you want.
|
62
84
|
|
63
|
-
|
64
|
-
|
85
|
+
```ruby
|
86
|
+
class MyEnvironment
|
87
|
+
extend Surrounded::Context
|
88
|
+
|
89
|
+
initialize(:employee, :boss)
|
65
90
|
|
66
|
-
|
91
|
+
role :employee do
|
92
|
+
# extra behavior here...
|
93
|
+
end
|
94
|
+
end
|
95
|
+
```
|
67
96
|
|
68
|
-
|
97
|
+
By default, this code will create a module for you named `Employee`. If you want to use a wrapper, you can do this:
|
69
98
|
|
70
99
|
```ruby
|
71
100
|
class MyEnvironment
|
72
101
|
extend Surrounded::Context
|
102
|
+
|
103
|
+
initialize(:employee, :boss)
|
104
|
+
|
105
|
+
wrap :employee do
|
106
|
+
# extra behavior here...
|
107
|
+
end
|
108
|
+
end
|
109
|
+
```
|
73
110
|
|
111
|
+
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
|
+
|
113
|
+
```ruby
|
114
|
+
class MyEnvironment
|
115
|
+
extend Surrounded::Context
|
116
|
+
|
74
117
|
initialize(:employee, :boss)
|
75
118
|
|
76
|
-
|
119
|
+
role :employee, :wrapper do
|
77
120
|
# extra behavior here...
|
78
121
|
end
|
79
122
|
end
|
80
123
|
```
|
81
124
|
|
82
|
-
|
125
|
+
The default available types are `:module`, `:wrap` or `:wrapper`, and `:interface`. We'll get to `interface` below. The `:wrap` and `:wrapper` types are the same and they'll both create classes which inherit from SimpleDelegator _and_ include Surrounded for you.
|
83
126
|
|
84
|
-
|
127
|
+
These are minor little changes which highlight how simple it is to use Surrounded.
|
85
128
|
|
86
|
-
|
129
|
+
_Well... I want to use [Casting](https://github.com/saturnflyer/casting) so I get the benefit of modules without extending objects. Can I do that?_
|
87
130
|
|
88
|
-
Yup.
|
131
|
+
Yup. The ability to use Casting is built-in. If the objects you provide to your context respond to `cast_as` then Surrounded will use that.
|
132
|
+
|
133
|
+
_Ok. So is that it?_
|
134
|
+
|
135
|
+
There's a lot more. Let's look at the individual objects and what they need for this to be valuable...
|
136
|
+
|
137
|
+
## Objects' access to their environments
|
138
|
+
|
139
|
+
Add `Surrounded` to your objects to give them awareness of other objects.
|
89
140
|
|
90
141
|
```ruby
|
91
142
|
class User
|
@@ -93,27 +144,25 @@ class User
|
|
93
144
|
end
|
94
145
|
```
|
95
146
|
|
96
|
-
|
147
|
+
Now the `User` instances will be able to implicitly access objects in their environment.
|
97
148
|
|
98
|
-
|
149
|
+
Via `method_missing` those `User` instances can access a `context` object it stores in an internal collection.
|
99
150
|
|
100
|
-
|
151
|
+
Inside of the `MyEnvironment` context we saw above, the `employee` and `boss` objects are instances of `User` for this example.
|
101
152
|
|
102
|
-
|
153
|
+
Because the `User` class includes `Surrounded`, the instances of that class will be able to access other objects in the same context implicitly.
|
103
154
|
|
104
|
-
|
155
|
+
Let's make our context look like this:
|
105
156
|
|
106
157
|
```ruby
|
107
158
|
class MyEnvironment
|
108
159
|
# other stuff from above is still here...
|
109
160
|
|
110
161
|
def shove_it
|
111
|
-
employee.store_context(self)
|
112
162
|
employee.quit
|
113
|
-
employee.remove_context
|
114
163
|
end
|
115
164
|
|
116
|
-
|
165
|
+
role :employee do
|
117
166
|
def quit
|
118
167
|
say("I'm sick of this place, #{boss.name}!")
|
119
168
|
stomp
|
@@ -124,47 +173,67 @@ class MyEnvironment
|
|
124
173
|
end
|
125
174
|
```
|
126
175
|
|
127
|
-
What's happening in there is that when the `shove_it` method is called
|
176
|
+
What's happening in there is that when the `shove_it` method is called on the instance of `MyEnvironment`, the `employee` has the ability to refer to `boss` because it is in the same context, e.g. the same environment.
|
128
177
|
|
129
178
|
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.
|
130
179
|
|
131
|
-
|
180
|
+
What `Surrounded` does for us is to make the relationship between objects and gives them the ability to access each other. Adding new or different roles to the context now only requires that we add them to the context and nothing else. No explicit references must be passed to each individual method. The objects are aware of the other objects around them and can refer to them by their role name.
|
132
181
|
|
133
|
-
I
|
182
|
+
I didn't mention how the context is set, however.
|
134
183
|
|
135
|
-
|
184
|
+
## Tying objects together
|
136
185
|
|
137
|
-
|
186
|
+
Your context will have methods of it's own which will trigger actions on the objects inside, but we need those trigger methods to set the accessible context for each of the contained objects.
|
138
187
|
|
139
|
-
|
188
|
+
Here's an example of what we want:
|
140
189
|
|
141
|
-
|
190
|
+
```ruby
|
191
|
+
class MyEnvironment
|
192
|
+
# other stuff from above is still here...
|
142
193
|
|
143
|
-
|
194
|
+
def shove_it
|
195
|
+
employee.store_context(self)
|
196
|
+
employee.quit
|
197
|
+
employee.remove_context
|
198
|
+
end
|
144
199
|
|
145
|
-
|
200
|
+
role :employee do
|
201
|
+
def quit
|
202
|
+
say("I'm sick of this place, #{boss.name}!")
|
203
|
+
stomp
|
204
|
+
throw_papers
|
205
|
+
say("I quit!")
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
```
|
146
210
|
|
147
|
-
|
211
|
+
Now that the `employee` has a reference to the context, it won't blow up when it hits `boss` inside that `quit` method.
|
148
212
|
|
149
|
-
|
150
|
-
Here's what you can do:
|
213
|
+
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:
|
151
214
|
|
152
215
|
```ruby
|
153
216
|
class MyEnvironment
|
154
|
-
#
|
217
|
+
# other stuff from above is still here...
|
155
218
|
|
156
219
|
trigger :shove_it do
|
157
220
|
employee.quit
|
158
221
|
end
|
222
|
+
|
223
|
+
role :employee do
|
224
|
+
def quit
|
225
|
+
say("I'm sick of this place, #{boss.name}!")
|
226
|
+
stomp
|
227
|
+
throw_papers
|
228
|
+
say("I quit!")
|
229
|
+
end
|
230
|
+
end
|
159
231
|
end
|
160
232
|
```
|
161
233
|
|
162
|
-
By using this `trigger` keyword, our block is the code we care about, but internally the method is
|
163
|
-
|
164
|
-
_Hmm. I don't like having to do that._
|
234
|
+
By using this `trigger` keyword, our block is the code we care about, but internally the method is created to first set all the objects' current contexts.
|
165
235
|
|
166
|
-
|
167
|
-
It also allows us to store the triggers so that you can, for example, provide details outside of the environment about what triggers exist.
|
236
|
+
The context will also store the triggers so that you can, for example, provide details outside of the environment about what triggers exist.
|
168
237
|
|
169
238
|
```ruby
|
170
239
|
context = MyEnvironment.new(current_user, the_boss)
|
@@ -173,6 +242,118 @@ context.triggers #=> [:shove_it]
|
|
173
242
|
|
174
243
|
You might find that useful for dynamically defining user interfaces.
|
175
244
|
|
245
|
+
Sometimes I'd rather not use this DSL, however. I want to just write regular methods.
|
246
|
+
|
247
|
+
We can do that too. You'll need to opt in to this by specifying `set_methods_as_triggers` for the context class.
|
248
|
+
|
249
|
+
```ruby
|
250
|
+
class MyEnvironment
|
251
|
+
# other stuff from above is still here...
|
252
|
+
|
253
|
+
set_methods_as_triggers
|
254
|
+
|
255
|
+
def shove_it
|
256
|
+
employee.quit
|
257
|
+
end
|
258
|
+
|
259
|
+
role :employee do
|
260
|
+
def quit
|
261
|
+
say("I'm sick of this place, #{boss.name}!")
|
262
|
+
stomp
|
263
|
+
throw_papers
|
264
|
+
say("I quit!")
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
```
|
269
|
+
|
270
|
+
This will allow you to write methods like you normally would. They are aliased internally with a prefix and the method name that you use is rewritten to add and remove the context for the objects in this context. The public API of your class remains the same, but the extra feature of wrapping your method is handled for you.
|
271
|
+
|
272
|
+
This will treat all instance methods defined on your context the same way, so be aware of that.
|
273
|
+
|
274
|
+
## Where roles exist
|
275
|
+
|
276
|
+
By using `Surrounded::Context` you are declaring a relationship between the objects inside playing your defined roles.
|
277
|
+
|
278
|
+
Because all the behavior is defined internally and only relevant internally, those relationships don't exist outside of the environment.
|
279
|
+
|
280
|
+
Surrounded makes all of your role modules and classes private constants. It's not a good idea to try to reuse behavior defined for one context in another area.
|
281
|
+
|
282
|
+
## The role DSL
|
283
|
+
|
284
|
+
Using the `role` method to define modules and classes takes care of the setup for you. This way you can swap between implementations:
|
285
|
+
|
286
|
+
```ruby
|
287
|
+
|
288
|
+
# this uses modules
|
289
|
+
role :source do
|
290
|
+
def transfer
|
291
|
+
self.balance -= amount
|
292
|
+
destination.balance += amount
|
293
|
+
self
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
# this uses SimpleDelegator and Surrounded
|
298
|
+
role :source, :wrap do
|
299
|
+
def transfer
|
300
|
+
self.balance -= amount
|
301
|
+
destination.balance += amount
|
302
|
+
__getobj__
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
# this uses a special interface object which pulls
|
307
|
+
# methods from a module and applies them to your object.
|
308
|
+
role :source, :interface do
|
309
|
+
def transfer
|
310
|
+
self.balance -= amount
|
311
|
+
destination.balance += amount
|
312
|
+
self
|
313
|
+
end
|
314
|
+
end
|
315
|
+
```
|
316
|
+
|
317
|
+
The `:interface` option is a special object which has all of its methods removed (excepting `__send__` and `object_id`) so that other methods will be pulled from the ones that you define, or from the object it attempts to proxy.
|
318
|
+
|
319
|
+
Notice that the `:interface` allows you to return `self` whereas the `:wrap` acts more like a wrapper and forces you to deal with that shortcoming by using it's wrapped-object-accessor method: `__getobj__`.
|
320
|
+
|
321
|
+
If you'd like to choose one and use it all the time, you can set the default:
|
322
|
+
|
323
|
+
```ruby
|
324
|
+
class MoneyTransfer
|
325
|
+
extend Surrounded::Context
|
326
|
+
|
327
|
+
self.default_role_type = :interface # also :wrap, :wrapper, or :module
|
328
|
+
|
329
|
+
role :source do
|
330
|
+
def transfer
|
331
|
+
self.balance -= amount
|
332
|
+
destination.balance += amount
|
333
|
+
self
|
334
|
+
end
|
335
|
+
end
|
336
|
+
end
|
337
|
+
```
|
338
|
+
|
339
|
+
Or, if you like, you can choose the default for your entire project:
|
340
|
+
|
341
|
+
```ruby
|
342
|
+
Surrounded::Context.default_role_type = :interface
|
343
|
+
|
344
|
+
class MoneyTransfer
|
345
|
+
extend Surrounded::Context
|
346
|
+
|
347
|
+
role :source do
|
348
|
+
def transfer
|
349
|
+
self.balance -= amount
|
350
|
+
destination.balance += amount
|
351
|
+
self
|
352
|
+
end
|
353
|
+
end
|
354
|
+
end
|
355
|
+
```
|
356
|
+
|
176
357
|
## Policies for the application of role methods
|
177
358
|
|
178
359
|
There are 2 approaches to applying new behavior to your objects.
|
@@ -194,7 +375,7 @@ class ActiviatingAccount
|
|
194
375
|
|
195
376
|
initialize(:activator, :account)
|
196
377
|
|
197
|
-
|
378
|
+
role :activator do
|
198
379
|
def some_behavior; end
|
199
380
|
end
|
200
381
|
|
@@ -228,41 +409,66 @@ context.do_something
|
|
228
409
|
current_user.some_behavior # NoMethodError
|
229
410
|
```
|
230
411
|
|
231
|
-
##
|
232
|
-
|
233
|
-
I haven't really tested yet, but there are several ways you can add behavior to your objects.
|
412
|
+
## Overview in code
|
234
413
|
|
235
|
-
|
414
|
+
Here's a view of the possibilities in code.
|
236
415
|
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
4. If you want to use wrappers but would rather not muck about with including modules and whatnot, you can define them like this:
|
416
|
+
```ruby
|
417
|
+
# set default role type for *all* contexts in your program
|
418
|
+
Surrounded::Context.default_role_type = :module # also :wrap, :wrapper, or :interface
|
241
419
|
|
242
|
-
|
243
|
-
class SomeContext
|
420
|
+
class ActiviatingAccount
|
244
421
|
extend Surrounded::Context
|
245
422
|
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
The `wrap` method will create a class of the given name (`Admin` in this case) and will inherit from `SimpleDelegator` from the Ruby standard library _and_ will `include Surrounded`.
|
423
|
+
apply_roles_on(:trigger) # this is the default
|
424
|
+
# apply_roles_on(:initialize) # set this to apply behavior from the start
|
425
|
+
|
426
|
+
set_methods_as_triggers # allows you to skip the 'trigger' dsl
|
427
|
+
|
428
|
+
# set the default role type only for this class
|
429
|
+
self.default_role_type = :module # also :wrap, :wrapper, or :interface
|
254
430
|
|
255
|
-
|
431
|
+
initialize(:activator, :account)
|
256
432
|
|
257
|
-
|
433
|
+
role :activator do # module by default
|
434
|
+
def some_behavior; end
|
435
|
+
end
|
258
436
|
|
259
|
-
|
437
|
+
# role :activator, :module do
|
438
|
+
# def some_behavior; end
|
439
|
+
# end
|
440
|
+
#
|
441
|
+
# role :activator, :wrap do
|
442
|
+
# def some_behavior; end
|
443
|
+
# end
|
444
|
+
#
|
445
|
+
# role :activator, :interface do
|
446
|
+
# def some_behavior; end
|
447
|
+
# end
|
448
|
+
#
|
449
|
+
# use your own classes if you don't want SimpleDelegator
|
450
|
+
# class MySpecialClass
|
451
|
+
# include Surrounded # you must remember this
|
452
|
+
# # Surrounded assumes MySpecialClass.new(the_role_player_here)
|
453
|
+
# def initialize(...);
|
454
|
+
# # ... your code here
|
455
|
+
# end
|
456
|
+
# end
|
457
|
+
|
458
|
+
# works as a trigger (assigning the current context) only if set_methods_as_triggers is set
|
459
|
+
def regular_method
|
460
|
+
activator.some_behavior # behavior not available unless you apply roles on initialize
|
461
|
+
end
|
260
462
|
|
261
|
-
|
463
|
+
trigger :some_trigger_method do
|
464
|
+
activator.some_behavior # behavior always available
|
465
|
+
end
|
466
|
+
end
|
467
|
+
```
|
262
468
|
|
263
469
|
## Dependencies
|
264
470
|
|
265
|
-
The dependencies are minimal. The plan is to keep it that way but allow you to configure things as you need.
|
471
|
+
The dependencies are minimal. The plan is to keep it that way but allow you to configure things as you need. The [Triad](http://github.com/saturnflyer/triad) project was written specifically to manage the mapping of roles and objects to the modules which contain the behaviors.
|
266
472
|
|
267
473
|
If you're using [Casting](http://github.com/saturnflyer/casting), for example, Surrounded will attempt to use that before extending an object, but it will still work without it.
|
268
474
|
|
@@ -281,6 +487,10 @@ And then execute:
|
|
281
487
|
Or install it yourself as:
|
282
488
|
|
283
489
|
$ gem install surrounded
|
490
|
+
|
491
|
+
## Installation for Rails
|
492
|
+
|
493
|
+
See [surrounded-rails](https://github.com/saturnflyer/surrounded-rails)
|
284
494
|
|
285
495
|
## Contributing
|
286
496
|
|
data/lib/surrounded/context.rb
CHANGED
@@ -16,16 +16,31 @@ end
|
|
16
16
|
module Surrounded
|
17
17
|
module Context
|
18
18
|
def self.extended(base)
|
19
|
-
base.
|
19
|
+
base.class_eval {
|
20
|
+
@triggers = Set.new
|
21
|
+
@methods_as_triggers = Surrounded::Context.methods_as_triggers
|
22
|
+
include InstanceMethods
|
23
|
+
}
|
20
24
|
base.singleton_class.send(:alias_method, :setup, :initialize)
|
21
25
|
end
|
22
26
|
|
23
27
|
def self.default_role_type
|
24
28
|
@default_role_type ||= :module
|
25
29
|
end
|
26
|
-
|
27
|
-
|
28
|
-
|
30
|
+
|
31
|
+
class << self
|
32
|
+
attr_writer :default_role_type, :methods_as_triggers
|
33
|
+
end
|
34
|
+
|
35
|
+
attr_reader :methods_as_triggers
|
36
|
+
|
37
|
+
def self.methods_as_triggers
|
38
|
+
return @methods_as_triggers if defined?(@methods_as_triggers)
|
39
|
+
@methods_as_triggers = false
|
40
|
+
end
|
41
|
+
|
42
|
+
def set_methods_as_triggers
|
43
|
+
@methods_as_triggers = true
|
29
44
|
end
|
30
45
|
|
31
46
|
def new(*args, &block)
|
@@ -56,25 +71,13 @@ module Surrounded
|
|
56
71
|
@default_role_type = type
|
57
72
|
end
|
58
73
|
|
59
|
-
def role(name, type=nil, &block)
|
60
|
-
role_type = type || default_role_type
|
61
|
-
case role_type
|
62
|
-
when :wrap, :wrapper then wrap(name, &block)
|
63
|
-
when :interface then interface(name, &block)
|
64
|
-
when :module then
|
65
|
-
mod_name = name.to_s.gsub(/(?:^|_)([a-z])/){ $1.upcase }
|
66
|
-
private_const_set(mod_name, Module.new(&block))
|
67
|
-
else
|
68
|
-
raise InvalidRoleType.new
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
74
|
def wrap(name, &block)
|
73
75
|
require 'delegate'
|
74
76
|
wrapper_name = name.to_s.gsub(/(?:^|_)([a-z])/){ $1.upcase }
|
75
77
|
klass = private_const_set(wrapper_name, Class.new(SimpleDelegator, &block))
|
76
78
|
klass.send(:include, Surrounded)
|
77
79
|
end
|
80
|
+
alias_method :wrapper, :wrap
|
78
81
|
|
79
82
|
if module_method_rebinding?
|
80
83
|
def interface(name, &block)
|
@@ -89,7 +92,20 @@ module Surrounded
|
|
89
92
|
end
|
90
93
|
end
|
91
94
|
end
|
92
|
-
|
95
|
+
|
96
|
+
def role(name, type=nil, &block)
|
97
|
+
role_type = type || default_role_type
|
98
|
+
if role_type == :module
|
99
|
+
mod_name = name.to_s.gsub(/(?:^|_)([a-z])/){ $1.upcase }
|
100
|
+
private_const_set(mod_name, Module.new(&block))
|
101
|
+
else
|
102
|
+
meth = method(role_type)
|
103
|
+
meth.call(name, &block)
|
104
|
+
end
|
105
|
+
rescue NameError => e
|
106
|
+
raise e.extend(InvalidRoleType)
|
107
|
+
end
|
108
|
+
|
93
109
|
def apply_roles_on(which)
|
94
110
|
@__apply_role_policy = which
|
95
111
|
end
|
@@ -119,24 +135,14 @@ module Surrounded
|
|
119
135
|
def trigger(name, *args, &block)
|
120
136
|
store_trigger(name)
|
121
137
|
|
122
|
-
define_method(:"
|
123
|
-
|
124
|
-
private :"trigger_#{name}"
|
138
|
+
define_method(:"__trigger_#{name}", *args, &block)
|
125
139
|
|
126
|
-
|
127
|
-
begin
|
128
|
-
apply_roles if __apply_role_policy == :trigger
|
140
|
+
private :"__trigger_#{name}"
|
129
141
|
|
130
|
-
|
131
|
-
|
132
|
-
ensure
|
133
|
-
remove_roles if __apply_role_policy == :trigger
|
134
|
-
end
|
135
|
-
}
|
142
|
+
redo_method(name, args)
|
136
143
|
end
|
137
144
|
|
138
145
|
def store_trigger(name)
|
139
|
-
@triggers ||= Set.new
|
140
146
|
@triggers << name
|
141
147
|
end
|
142
148
|
|
@@ -145,6 +151,35 @@ module Surrounded
|
|
145
151
|
const_get(name)
|
146
152
|
end
|
147
153
|
end
|
154
|
+
|
155
|
+
def redo_method(name, args)
|
156
|
+
class_eval %{
|
157
|
+
def #{name}(#{args.join(', ')})
|
158
|
+
begin
|
159
|
+
apply_roles if __apply_role_policy == :trigger
|
160
|
+
|
161
|
+
self.send("__trigger_#{name}", #{args.join(', ')})
|
162
|
+
|
163
|
+
ensure
|
164
|
+
remove_roles if __apply_role_policy == :trigger
|
165
|
+
end
|
166
|
+
end
|
167
|
+
}
|
168
|
+
end
|
169
|
+
|
170
|
+
def method_added(name)
|
171
|
+
if methods_as_triggers
|
172
|
+
unless name.to_s.match(/^__trigger|initialize/) || (@triggers && triggers.include?(name))
|
173
|
+
store_trigger(name)
|
174
|
+
args = self.instance_method(name).parameters.map{|p| p.last }
|
175
|
+
alias_method :"__trigger_#{name}", :"#{name}"
|
176
|
+
private :"__trigger_#{name}"
|
177
|
+
remove_method :"#{name}"
|
178
|
+
redo_method(name, args)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
super
|
182
|
+
end
|
148
183
|
|
149
184
|
module InstanceMethods
|
150
185
|
def role?(name, &block)
|
data/lib/surrounded/version.rb
CHANGED
@@ -90,6 +90,7 @@ end
|
|
90
90
|
|
91
91
|
class RoleAssignmentContext
|
92
92
|
extend Surrounded::Context
|
93
|
+
set_methods_as_triggers
|
93
94
|
|
94
95
|
initialize(:user, :other_user)
|
95
96
|
|
@@ -108,6 +109,10 @@ class RoleAssignmentContext
|
|
108
109
|
trigger :check_other_user_response do
|
109
110
|
user.respond_to?(:a_method!)
|
110
111
|
end
|
112
|
+
|
113
|
+
def regular_method_trigger
|
114
|
+
user.respond_to?(:a_method!)
|
115
|
+
end
|
111
116
|
|
112
117
|
module User
|
113
118
|
def a_method!; end
|
@@ -164,6 +169,10 @@ describe Surrounded::Context, 'assigning roles' do
|
|
164
169
|
|
165
170
|
context = ClassRoleAssignmentContext.new(user, self)
|
166
171
|
|
167
|
-
context.check_user_response
|
172
|
+
assert context.check_user_response
|
173
|
+
end
|
174
|
+
|
175
|
+
it 'allows usage of regular methods for triggers' do
|
176
|
+
assert context.regular_method_trigger
|
168
177
|
end
|
169
178
|
end
|
data/test/test_helper.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: surrounded
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
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: 2013-10-
|
12
|
+
date: 2013-10-27 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: triad
|
@@ -75,7 +75,6 @@ files:
|
|
75
75
|
- Rakefile
|
76
76
|
- examples/rails.rb
|
77
77
|
- lib/surrounded.rb
|
78
|
-
- lib/surrounded/README.md
|
79
78
|
- lib/surrounded/context.rb
|
80
79
|
- lib/surrounded/context/negotiator.rb
|
81
80
|
- lib/surrounded/context/role_map.rb
|
data/lib/surrounded/README.md
DELETED
@@ -1,209 +0,0 @@
|
|
1
|
-
# Surrounded aims to make things simple and get out of your way.
|
2
|
-
|
3
|
-
Most of what you care about is defining the behavior of objects. How they interact is important.
|
4
|
-
The purpose of this library is to clear away the details of getting things setup.
|
5
|
-
|
6
|
-
You should read the [main README](../../README.md) to get the gist of what's going on, if you haven't.
|
7
|
-
|
8
|
-
When you get started, you'll probably be specifying things exactly how you want them. But you can easily make changes.
|
9
|
-
|
10
|
-
```ruby
|
11
|
-
class MoneyTransfer
|
12
|
-
extend Surrounded::Context
|
13
|
-
|
14
|
-
def initialize(source, destination, amount)
|
15
|
-
@source = source.extend(Source)
|
16
|
-
@destination = destination
|
17
|
-
@amount = amount
|
18
|
-
end
|
19
|
-
|
20
|
-
attr_reader :source, :destination, :amount
|
21
|
-
private :source, :destination, :amount
|
22
|
-
|
23
|
-
def execute
|
24
|
-
source.transfer
|
25
|
-
end
|
26
|
-
|
27
|
-
module Source
|
28
|
-
def transfer
|
29
|
-
self.balance -= amount
|
30
|
-
destination.balance += amount
|
31
|
-
self
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
```
|
36
|
-
|
37
|
-
That's a lot of setup just to create the `execute` and `transfer` methods.
|
38
|
-
|
39
|
-
Here's a shortened version:
|
40
|
-
|
41
|
-
```ruby
|
42
|
-
class MoneyTransfer
|
43
|
-
extend Surrounded::Context
|
44
|
-
|
45
|
-
initialize(:source, :destination, :amount)
|
46
|
-
|
47
|
-
trigger :execute do
|
48
|
-
source.transfer
|
49
|
-
end
|
50
|
-
|
51
|
-
module Source
|
52
|
-
def transfer
|
53
|
-
self.balance -= amount
|
54
|
-
destination.balance += amount
|
55
|
-
self
|
56
|
-
end
|
57
|
-
end
|
58
|
-
end
|
59
|
-
```
|
60
|
-
|
61
|
-
Now it's cleaned up a bit and is much easier to see what's important.
|
62
|
-
If you decide you want to try out wrappers, you can start making changes to use `SimpleDelegator` from the standard library, but you'll have to remember to 1) `include Surrounded` 2) to do the initialize yourself again and 3) return the correct object from your role method.
|
63
|
-
|
64
|
-
Simply by changing your mind to use `SimpleDelegator`, here's what you'll need to do:
|
65
|
-
|
66
|
-
```ruby
|
67
|
-
class MoneyTransfer
|
68
|
-
extend Surrounded::Context
|
69
|
-
|
70
|
-
def initialize(source, destination, amount)
|
71
|
-
@source = Source.new(source)
|
72
|
-
@destination = destination
|
73
|
-
@amount = amount
|
74
|
-
end
|
75
|
-
|
76
|
-
attr_reader :source, :destination, :amount
|
77
|
-
private :source, :destination, :amount
|
78
|
-
|
79
|
-
def execute
|
80
|
-
source.transfer
|
81
|
-
end
|
82
|
-
|
83
|
-
class Source < SimpleDelegator
|
84
|
-
include Surrounded
|
85
|
-
|
86
|
-
def transfer
|
87
|
-
self.balance -= amount
|
88
|
-
destination.balance += amount
|
89
|
-
__getobj__
|
90
|
-
end
|
91
|
-
end
|
92
|
-
end
|
93
|
-
```
|
94
|
-
Once again, it's big and ugly. But surrounded has your back; you can istead do this:
|
95
|
-
|
96
|
-
```ruby
|
97
|
-
class MoneyTransfer
|
98
|
-
extend Surrounded::Context
|
99
|
-
|
100
|
-
initialize(:source, :destination, :amount)
|
101
|
-
|
102
|
-
trigger :execute do
|
103
|
-
source.transfer
|
104
|
-
end
|
105
|
-
|
106
|
-
wrap :source do
|
107
|
-
def transfer
|
108
|
-
self.balance -= amount
|
109
|
-
destination.balance += amount
|
110
|
-
__getobj__
|
111
|
-
end
|
112
|
-
end
|
113
|
-
end
|
114
|
-
```
|
115
|
-
|
116
|
-
That's not much different from the module and it takes care of using `SimpleDelegator` and including `Surrounded` for you. If you want to make changing your approach even easier, you can use the `role` method instead.
|
117
|
-
|
118
|
-
```ruby
|
119
|
-
class MoneyTransfer
|
120
|
-
extend Surrounded::Context
|
121
|
-
|
122
|
-
initialize(:source, :destination, :amount)
|
123
|
-
|
124
|
-
trigger :execute do
|
125
|
-
source.transfer
|
126
|
-
end
|
127
|
-
|
128
|
-
role :source, :wrap do
|
129
|
-
def transfer
|
130
|
-
self.balance -= amount
|
131
|
-
destination.balance += amount
|
132
|
-
__getobj__
|
133
|
-
end
|
134
|
-
end
|
135
|
-
end
|
136
|
-
```
|
137
|
-
|
138
|
-
This way you can swap between implementations:
|
139
|
-
|
140
|
-
```ruby
|
141
|
-
|
142
|
-
# this uses modules
|
143
|
-
role :source do
|
144
|
-
def transfer
|
145
|
-
self.balance -= amount
|
146
|
-
destination.balance += amount
|
147
|
-
self
|
148
|
-
end
|
149
|
-
end
|
150
|
-
|
151
|
-
# this uses SimpleDelegator and Surrounded
|
152
|
-
role :source, :wrap do
|
153
|
-
def transfer
|
154
|
-
self.balance -= amount
|
155
|
-
destination.balance += amount
|
156
|
-
__getobj__
|
157
|
-
end
|
158
|
-
end
|
159
|
-
|
160
|
-
# this uses a special interface object which pulls
|
161
|
-
# methods from a module and applies them to your object.
|
162
|
-
role :source, :interface do
|
163
|
-
def transfer
|
164
|
-
self.balance -= amount
|
165
|
-
destination.balance += amount
|
166
|
-
self
|
167
|
-
end
|
168
|
-
end
|
169
|
-
```
|
170
|
-
|
171
|
-
The `:interface` option is a special object which has all of its methods removed (excepting `__send__` and `object_id`) so that other methods will be pulled from the ones that you define, or from the object it attempts to proxy.
|
172
|
-
|
173
|
-
Notice that the `:interface` allows you to return `self` whereas the `:wrap` acts more like a wrapper and forces you to deal with that shortcoming by using it's wrapped-object-accessor method: `__getobj__`.
|
174
|
-
|
175
|
-
If you'd like to choose one and use it all the time, you can set the default:
|
176
|
-
|
177
|
-
```ruby
|
178
|
-
class MoneyTransfer
|
179
|
-
extend Surrounded::Context
|
180
|
-
|
181
|
-
self.default_role_type = :interface # also :wrap, :wrapper, or :module
|
182
|
-
|
183
|
-
role :source do
|
184
|
-
def transfer
|
185
|
-
self.balance -= amount
|
186
|
-
destination.balance += amount
|
187
|
-
self
|
188
|
-
end
|
189
|
-
end
|
190
|
-
end
|
191
|
-
```
|
192
|
-
|
193
|
-
Or, if you like, you can choose the default for your entire project:
|
194
|
-
|
195
|
-
```ruby
|
196
|
-
Surrounded::Context.default_role_type = :interface
|
197
|
-
|
198
|
-
class MoneyTransfer
|
199
|
-
extend Surrounded::Context
|
200
|
-
|
201
|
-
role :source do
|
202
|
-
def transfer
|
203
|
-
self.balance -= amount
|
204
|
-
destination.balance += amount
|
205
|
-
self
|
206
|
-
end
|
207
|
-
end
|
208
|
-
end
|
209
|
-
```
|