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 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
- ## Create encapsulated environments for your objects.
9
+ # Surrounded aims to make things simple and get out of your way.
9
10
 
10
- Keep the distraction of other features out of your way. Write use cases and focus on just the business logic
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
- ## Usage
14
+ There are two main parts to this library.
13
15
 
14
- Add `Surrounded` to your objects to give them awareness of other objects.
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 User
18
- include Surrounded
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
- Now your user instances will be able to get objects in their environment.
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
- I didn't explain that yet.
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
- You can make an object which contains other objects. It acts as an environment
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
- First, you extend a class with the appropriate module to turn it into an object environment:
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
- Typical initialization of this environment has a lot of code. For example:
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
- attr_reader :employee, :boss
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
- _WTF was all that!?_
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
- Relax. I'll explain.
83
+ But the syntax can be even simpler than that if you want.
62
84
 
63
- When you create an instance of `MyEnvironment` it has certain objects inside.
64
- Here we see that it has an `employee` and a `boss`. Inside the methods of the environment it's simpler and easier to write `employee` instead of `@employee` so we make them `attr_reader`s. But we don't need these methods to be externally accessible so we set them to private.
85
+ ```ruby
86
+ class MyEnvironment
87
+ extend Surrounded::Context
88
+
89
+ initialize(:employee, :boss)
65
90
 
66
- Next, we want to add environment-specific behavior to the `employee` so we extend the object with the module `Employee`.
91
+ role :employee do
92
+ # extra behavior here...
93
+ end
94
+ end
95
+ ```
67
96
 
68
- If you're going to be doing this a lot, it's painful. Here's what `Surrounded` does for you:
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
- module Employee
119
+ role :employee, :wrapper do
77
120
  # extra behavior here...
78
121
  end
79
122
  end
80
123
  ```
81
124
 
82
- There! All that boilerplate code is cleaned up.
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
- Notice that there's no `Boss` module. If a module of that name does not exist, the object passed into initialize simply won't gain any new behavior.
127
+ These are minor little changes which highlight how simple it is to use Surrounded.
85
128
 
86
- _OK. I think I get it, but what about the objects? How are they aware of their environment? Isn't that what this is supposed to do?_
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. Ruby doesn't have a notion of a local environment, so we lean on `method_missing` to do the work for us.
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
- With that, all instances of `User` have implicit access to their surroundings.
147
+ Now the `User` instances will be able to implicitly access objects in their environment.
97
148
 
98
- _Yeah... How?_
149
+ Via `method_missing` those `User` instances can access a `context` object it stores in an internal collection.
99
150
 
100
- Via `method_missing` those `User` instances can access a `context` object it stores in a `@__surroundings__` collection. I didn't mention how the context is set, however.
151
+ Inside of the `MyEnvironment` context we saw above, the `employee` and `boss` objects are instances of `User` for this example.
101
152
 
102
- Your environment will have methods of it's own that will trigger actions on the objects inside, but we need those trigger methods to set the environment instance as the current context so that the objects it contains can access them.
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
- Here's an example of what we want:
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
- module Employee
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, the current environment object is stored as the context.
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
- _WTF!? That's insane!_
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 thought so too, at first. But continually passing references assumes there's no relationship between objects in that method. What `Surrounded` does for us is to make the relationship between objects and gives them the ability to access each other.
182
+ I didn't mention how the context is set, however.
134
183
 
135
- This simple example may seem trivial, but the more contextual code you have the more cumbersome passing references becomes. By moving knowledge to the local environment, you're free to make changes to the procedures without the need to alter method signatures with new refrences or the removal of unused ones.
184
+ ## Tying objects together
136
185
 
137
- By using `Surrounded::Context` you are declaring a relationship between the objects inside.
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
- Because all the behavior is defined internally and only relevant internally, those relationships don't exist outside of the environment.
188
+ Here's an example of what we want:
140
189
 
141
- _OK. I think I understand. So I can change business logic just by changing the procedures and the objects. I don't need to adjust arguments for a new requirement. That's kind of cool!_
190
+ ```ruby
191
+ class MyEnvironment
192
+ # other stuff from above is still here...
142
193
 
143
- Damn right.
194
+ def shove_it
195
+ employee.store_context(self)
196
+ employee.quit
197
+ employee.remove_context
198
+ end
144
199
 
145
- But you don't want to continually set those context details, do you?
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
- _No. That's annoying._
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
- Yeah. Instead, it would be easier to have this library do the work for us.
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
- # the other code from above...
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 written to set the `@__surroundings__` collection.
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
- Me either. I'd rather just use `def` but getting automatic code for setting the context is really convenient.
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
- module Activator
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
- ## How's the performance?
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
- There are a few defaults built in.
414
+ Here's a view of the possibilities in code.
236
415
 
237
- 1. If you define modules for the added behavior, the code will run `object.extend(RoleInterface)`
238
- 2. If you are using [casting](http://github.com/saturnflyer/casting), the code will run `object.cast_as(RoleInterface)`
239
- 3. If you would rather use wrappers you can define classes and the code will run `RoleInterface.new(object)` and assumes that the `new` method takes 1 argument. You'll need to remember to `include Surrounded` in your classes, however.
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
- initialize(:admin, :user)
247
-
248
- wrap :admin do
249
- # special methods defined here
250
- end
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
- Lastly, there's a 5th option if you're using Ruby 2.x: `interface`.
431
+ initialize(:activator, :account)
256
432
 
257
- The `interface` method acts similarly to the `wrap` method in that it returns an object that is not actually the object you want. But an `interface` is different in that it will apply methods from a module instead of using methods defined in a SimpleDelegator subclass. How is that important? Well you are free to use things like instance variables in your methods because they will be executed in the context of the object. This is unlike methods in a SimpleDelegator where the wrapper maintains its own instance variables.
433
+ role :activator do # module by default
434
+ def some_behavior; end
435
+ end
258
436
 
259
- _Which should I use?_
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
- Start with the default and see how it goes, then try another approach and measure the changes.
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
 
@@ -16,16 +16,31 @@ end
16
16
  module Surrounded
17
17
  module Context
18
18
  def self.extended(base)
19
- base.send(:include, InstanceMethods)
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
- def self.default_role_type=(type)
28
- @default_role_type = type
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(:"trigger_#{name}", *args, &block)
123
-
124
- private :"trigger_#{name}"
138
+ define_method(:"__trigger_#{name}", *args, &block)
125
139
 
126
- define_method(name, *args){
127
- begin
128
- apply_roles if __apply_role_policy == :trigger
140
+ private :"__trigger_#{name}"
129
141
 
130
- self.send("trigger_#{name}", *args)
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)
@@ -2,6 +2,6 @@ require 'triad'
2
2
  module Surrounded
3
3
  module Context
4
4
  class InvalidRole < ::Triad::KeyNotPresent; end
5
- class InvalidRoleType < StandardError; end
5
+ module InvalidRoleType; end
6
6
  end
7
7
  end
@@ -1,3 +1,3 @@
1
1
  module Surrounded
2
- VERSION = "0.4.1"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -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
@@ -20,7 +20,7 @@ end
20
20
  class TestContext
21
21
  extend Surrounded::Context
22
22
 
23
- setup(:user, :other_user)
23
+ initialize(:user, :other_user)
24
24
 
25
25
  trigger :access_other_object do
26
26
  user.other_user.name
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.1
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-02 00:00:00.000000000 Z
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
@@ -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
- ```