surrounded 0.9.0 → 0.9.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -0
- data/README.md +187 -2
- data/examples/rails.rb +10 -0
- data/lib/surrounded/context.rb +2 -1
- data/lib/surrounded/context/forwarding.rb +27 -0
- data/lib/surrounded/context/initializing.rb +1 -8
- data/lib/surrounded/context/negotiator.rb +2 -0
- data/lib/surrounded/context/role_builders.rb +20 -10
- data/lib/surrounded/context/trigger_controls.rb +3 -6
- data/lib/surrounded/version.rb +1 -1
- data/test/context_forwarding_test.rb +59 -0
- data/test/example_delegate_class_test.rb +29 -0
- data/test/example_proxy_test.rb +17 -1
- data/test/override_methods_test.rb +55 -33
- metadata +8 -18
- 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 +0 -544
- data/javascripts/main.js +0 -1
- data/javascripts/scale.fix.js +0 -17
- data/params.json +0 -1
- data/stylesheets/print.css +0 -226
- data/stylesheets/pygment_trac.css +0 -69
- data/stylesheets/styles.css +0 -255
- data/stylesheets/stylesheet.css +0 -371
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c8490f4520253094598c520cdc7b3a73186d48d5
|
4
|
+
data.tar.gz: 31d8f5baebf2e1f42b761207972de0523ac41885
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 161b86c91796ead35d60f817308a252585544b3690055039c026bf2ad97bf0f3ed5573709fd341673681399dcb8687113102cf698e2c5f255856d22e47471e2a
|
7
|
+
data.tar.gz: 975026239c9e4e2796d8f66f73c062c7907159beeaf30a8f0dfea8ee69960d970155bfdcdc1ec69b81a6c13c5ed9ad8d862cfdcfb159108758da8293db4cb270
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -136,6 +136,36 @@ end
|
|
136
136
|
Trigger methods are different from regular instance methods in that they apply behaviors from the roles to the role players.
|
137
137
|
A regular instance method just does what you define. But a trigger will make your role players come alive with their behaviors.
|
138
138
|
|
139
|
+
You may find that the code for your triggers is extremely simple and is merely creating a method to tell a role player what to do. If you find you have many methods like this:
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
def plan_weekend_work
|
143
|
+
employee.work_weekend
|
144
|
+
end
|
145
|
+
trigger :plan_weekend_work
|
146
|
+
```
|
147
|
+
|
148
|
+
You can shorten it to:
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
trigger :plan_weekend_work do
|
152
|
+
employee.work_weekend
|
153
|
+
end
|
154
|
+
```
|
155
|
+
|
156
|
+
But it can be even simpler and follows the same pattern provided by Ruby's standard library Forwardable:
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
# The first argument is the role to receive the messaged defined in the second argument.
|
160
|
+
# The third argument is optional and if provided will be the name of the trigger method on your context instance.
|
161
|
+
forward_trigger :employee, :work_weekend, :plan_weekend_work
|
162
|
+
|
163
|
+
# Alternatively, you can use an API similar to that of the `delegate` method from Forwardable
|
164
|
+
forwarding [:work_weekend] => :employee
|
165
|
+
```
|
166
|
+
|
167
|
+
The difference between `forward_trigger` and `forwarding` is that the first accepts an alternative method name for the context instance method. There's more on this below in the "Overview in code" section, or see `lib/surrounded/context/forwarding.rb`.
|
168
|
+
|
139
169
|
There's one last thing to make this work.
|
140
170
|
|
141
171
|
## Getting your role players ready
|
@@ -451,11 +481,11 @@ Alternatively, if you just want to define your own methods without the DSL using
|
|
451
481
|
|
452
482
|
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.
|
453
483
|
|
454
|
-
If you call the disallowed trigger directly, you'll raise
|
484
|
+
If you call the disallowed trigger directly, you'll raise an `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.
|
455
485
|
|
456
486
|
## Restricting return values
|
457
487
|
|
458
|
-
_Tell, Don't Ask_ style programming can better be enforced by following East-oriented Code principles. This means that the
|
488
|
+
_Tell, Don't Ask_ style programming can better be enforced by following East-oriented Code principles. This means that the return values from methods on your objects should not provide information about their internal state. Instead of returning values, you can enforce that triggers return the context object. This forces you to place context responsiblities inside the context and prevents leaking the details and responsiblities outside of the system.
|
459
489
|
|
460
490
|
Here's how you enforce it:
|
461
491
|
|
@@ -598,6 +628,10 @@ class ActiviatingAccount
|
|
598
628
|
# this must be done to handle the mapping of roles to objects
|
599
629
|
# pass an array of arrays with role name symbol and the object for that role
|
600
630
|
map_roles([[:activator, activator],[:account, account]])
|
631
|
+
|
632
|
+
# or load extra objects, perform other functions, etc. if you need and then use super
|
633
|
+
account.perform_some_funtion
|
634
|
+
super
|
601
635
|
end
|
602
636
|
# these also must be done if you create your own initialize method.
|
603
637
|
# this is a shortcut for using attr_reader and private
|
@@ -633,9 +667,13 @@ class ActiviatingAccount
|
|
633
667
|
def regular_method
|
634
668
|
apply_behaviors # handles the adding of all the roles and behaviors
|
635
669
|
activator.some_behavior # behavior not available unless you apply roles on initialize
|
670
|
+
ensure
|
671
|
+
# Use ensure to enforce the removal of behaviors in case of exceptions.
|
672
|
+
# This also does not affect the return value of this method.
|
636
673
|
remove_behaviors # handles the removal of all roles and behaviors
|
637
674
|
end
|
638
675
|
|
676
|
+
# This trigger or the forward* methods are preferred for creating triggers.
|
639
677
|
trigger :some_trigger_method do
|
640
678
|
activator.some_behavior # behavior always available
|
641
679
|
end
|
@@ -659,6 +697,8 @@ class ActiviatingAccount
|
|
659
697
|
def disallow_some_trigger_method?
|
660
698
|
# whatever conditional code for the instance of the context
|
661
699
|
end
|
700
|
+
# Prefer using `disallow` because it will wrap role players in their roles for you;
|
701
|
+
# the `disallow_some_trigger_method?` defined above, does not.
|
662
702
|
|
663
703
|
# Create shortcuts for triggers as class methods
|
664
704
|
# so you can do ActiviatingAccount.some_trigger_method(activator, account)
|
@@ -674,6 +714,11 @@ class ActiviatingAccount
|
|
674
714
|
# so you can enforce East-oriented style or Tell, Don't Ask
|
675
715
|
east_oriented_triggers
|
676
716
|
|
717
|
+
# Forward context instance methods as triggers to role players
|
718
|
+
forward_trigger :role_name, :method_name
|
719
|
+
forward_trigger :role_name, :method_name, :alternative_trigger_name_for_method_name
|
720
|
+
forward_triggers :role_name, :list, :of, :methods, :to, :forward
|
721
|
+
forwarding [:list, :of, :methods, :to, :forward] => :role_name
|
677
722
|
end
|
678
723
|
```
|
679
724
|
|
@@ -683,6 +728,146 @@ The dependencies are minimal. The plan is to keep it that way but allow you to c
|
|
683
728
|
|
684
729
|
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.
|
685
730
|
|
731
|
+
## Support for other ways to apply behavior
|
732
|
+
|
733
|
+
Surrounded is designed to be flexible for you. If you have your own code to manage applying behaviors, you can setup your context class to use it.
|
734
|
+
|
735
|
+
### Additional libraries
|
736
|
+
|
737
|
+
Here's an example using [Behavioral](https://github.com/saturnflyer/behavioral)
|
738
|
+
|
739
|
+
```ruby
|
740
|
+
class MyCustomContext
|
741
|
+
extend Surrounded::Context
|
742
|
+
|
743
|
+
initialize :employee, :boss
|
744
|
+
|
745
|
+
def module_extension_methods
|
746
|
+
[:with_behaviors].concat(super)
|
747
|
+
end
|
748
|
+
|
749
|
+
def module_removal_methods
|
750
|
+
[:without_behaviors].concat(super)
|
751
|
+
end
|
752
|
+
end
|
753
|
+
```
|
754
|
+
|
755
|
+
If you're using your own non-SimpleDelegator wrapper you can conform to that; whatever it may be.
|
756
|
+
|
757
|
+
```ruby
|
758
|
+
class MyCustomContext
|
759
|
+
extend Surrounded::Context
|
760
|
+
|
761
|
+
initialize :employee, :boss
|
762
|
+
|
763
|
+
class Employee < SuperWrapper
|
764
|
+
include Surrounded
|
765
|
+
|
766
|
+
# defined behaviors here...
|
767
|
+
|
768
|
+
def wrapped_object
|
769
|
+
# return the object that is wrapped
|
770
|
+
end
|
771
|
+
|
772
|
+
end
|
773
|
+
|
774
|
+
def unwrap_methods
|
775
|
+
[:wrapped_object]
|
776
|
+
end
|
777
|
+
end
|
778
|
+
```
|
779
|
+
|
780
|
+
### Applying individual roles
|
781
|
+
|
782
|
+
If you'd like to use a special approach for just a single role, you may do that too.
|
783
|
+
|
784
|
+
When applying behaviors from a role to your role players, your Surrounded context will first look for a method named `"apply_behavior_#{role}"`. Define your own method and set it to accept 2 arguments: the role constant and the role player.
|
785
|
+
|
786
|
+
```ruby
|
787
|
+
class MyCustomContext
|
788
|
+
extend Surrounded::Context
|
789
|
+
|
790
|
+
initialize :employee, :boss
|
791
|
+
|
792
|
+
def apply_behavior_employee(behavior_constant, role_player)
|
793
|
+
behavior_constant.build(role_player).apply # or whatever your need to do with your constant and object.
|
794
|
+
end
|
795
|
+
end
|
796
|
+
```
|
797
|
+
|
798
|
+
You can also plan for special ways to remove behavior as well.
|
799
|
+
|
800
|
+
```ruby
|
801
|
+
class MyCustomContext
|
802
|
+
extend Surrounded::Context
|
803
|
+
|
804
|
+
initialize :employee, :boss
|
805
|
+
|
806
|
+
def remove_behavior_employee(behavior_constant, role_player)
|
807
|
+
role_player.cleanup # or whatever your need to do with your constant and object.
|
808
|
+
end
|
809
|
+
end
|
810
|
+
```
|
811
|
+
|
812
|
+
You can remember the method name by the convention that `remove` or `apply` describes it's function, `behavior` refers to the first argument (thet contsant holding the behaviors), and then the name of the role which refers to the role playing object: `remove_behavior_role`.
|
813
|
+
|
814
|
+
## How to read this code
|
815
|
+
|
816
|
+
If you use this library, it's important to understand it.
|
817
|
+
|
818
|
+
As much as possible, when you use the Surrounded DSL for creating triggers, roles, initialize methods, and others you'll likely fine the actual method definitions created in a module and then find that module included in your class.
|
819
|
+
|
820
|
+
This is a design choice which allows you to override any standard behavior more easily.
|
821
|
+
|
822
|
+
### Where methods exist and why
|
823
|
+
|
824
|
+
When you define an initialize method for a Context class, Surrounded _could_ define the method on your class like this:
|
825
|
+
|
826
|
+
```ruby
|
827
|
+
def initialize(*roles)
|
828
|
+
self.class_eval do # <=== this evaluates on _your_ class and defines it there.
|
829
|
+
# code...
|
830
|
+
end
|
831
|
+
end
|
832
|
+
```
|
833
|
+
|
834
|
+
If we used the above approach, you'd need to redefine initialize in its entirety:
|
835
|
+
|
836
|
+
```ruby
|
837
|
+
initialize(:role1, role2)
|
838
|
+
|
839
|
+
def initialize(role1, role2) # <=== this will completely redefine initialize on _this class_
|
840
|
+
super # <=== this will NOT be the initialize method as provided to the Surrounded initialize above.
|
841
|
+
end
|
842
|
+
```
|
843
|
+
|
844
|
+
Surrounded uses a more flexible approach for you:
|
845
|
+
|
846
|
+
```ruby
|
847
|
+
def initialize(*roles)
|
848
|
+
mod = Module.new
|
849
|
+
mod.class_eval do # <=== this evaluates on the module and defines it there.
|
850
|
+
# code...
|
851
|
+
end
|
852
|
+
include mod # <=== this adds it to the class ancestors
|
853
|
+
end
|
854
|
+
```
|
855
|
+
|
856
|
+
With this approach you can use the way Surrounded is setup, but make changes if you need.
|
857
|
+
|
858
|
+
```ruby
|
859
|
+
initialize(:role1, :role2) # <=== defined in a module in the class ancestors
|
860
|
+
|
861
|
+
def initialize(role1, role2)
|
862
|
+
super # <=== run the method as defined above in the Surrounded DSL
|
863
|
+
# ... then do additional work
|
864
|
+
end
|
865
|
+
```
|
866
|
+
|
867
|
+
### Read methods, expect modules
|
868
|
+
|
869
|
+
When you go to read the code, expect to find behavior defined in modules.
|
870
|
+
|
686
871
|
## Installation
|
687
872
|
|
688
873
|
Add this line to your application's Gemfile:
|
data/examples/rails.rb
CHANGED
@@ -34,6 +34,16 @@ class SomeUseCase
|
|
34
34
|
listener.redirect_to('/')
|
35
35
|
end
|
36
36
|
end
|
37
|
+
|
38
|
+
class OtherUser < ::User
|
39
|
+
def special_feature
|
40
|
+
#....
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def apply_behavior_other_user(role, behavior, role_player)
|
45
|
+
role_player.becomes(behavior)
|
46
|
+
end
|
37
47
|
end
|
38
48
|
|
39
49
|
class SomethingController < ApplicationController
|
data/lib/surrounded/context.rb
CHANGED
@@ -2,6 +2,7 @@ require 'set'
|
|
2
2
|
require 'surrounded/context/role_map'
|
3
3
|
require 'surrounded/context/role_builders'
|
4
4
|
require 'surrounded/context/initializing'
|
5
|
+
require 'surrounded/context/forwarding'
|
5
6
|
require 'surrounded/context/trigger_controls'
|
6
7
|
require 'surrounded/access_control'
|
7
8
|
require 'surrounded/shortcuts'
|
@@ -17,7 +18,7 @@ module Surrounded
|
|
17
18
|
module Context
|
18
19
|
def self.extended(base)
|
19
20
|
base.class_eval {
|
20
|
-
extend RoleBuilders, Initializing
|
21
|
+
extend RoleBuilders, Initializing, Forwarding
|
21
22
|
|
22
23
|
@triggers = Set.new
|
23
24
|
include InstanceMethods
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Surrounded
|
2
|
+
module Context
|
3
|
+
module Forwarding
|
4
|
+
def forward_trigger(receiver, message, alternate=message)
|
5
|
+
raise(ArgumentError, %{you may not forward '%{m}`} % {m: message}) if ['__id__','__send__'].include?(message.to_s)
|
6
|
+
trigger alternate do
|
7
|
+
self.send(receiver).public_send(message)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def forward_triggers(receiver, *messages)
|
12
|
+
messages.each do |message|
|
13
|
+
forward_trigger(receiver, message)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def forwarding(hash)
|
18
|
+
hash.each { |messages, receiver|
|
19
|
+
forward_triggers(receiver, *messages)
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
alias forward forward_trigger
|
24
|
+
alias forwards forward_triggers
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -1,12 +1,6 @@
|
|
1
1
|
module Surrounded
|
2
2
|
module Context
|
3
3
|
module Initializing
|
4
|
-
def new(*args, &block)
|
5
|
-
instance = allocate
|
6
|
-
instance.send(:initialize, *args, &block)
|
7
|
-
instance
|
8
|
-
end
|
9
|
-
|
10
4
|
# Shorthand for creating an instance level initialize method which
|
11
5
|
# handles the mapping of the given arguments to their named role.
|
12
6
|
def initialize(*setup_args)
|
@@ -16,9 +10,8 @@ module Surrounded
|
|
16
10
|
line = __LINE__
|
17
11
|
mod.class_eval "
|
18
12
|
def initialize(#{setup_args.join(',')})
|
19
|
-
arguments = method(__method__).parameters.map{|arg| eval(arg[1].to_s) }
|
20
13
|
@role_map = RoleMap.new
|
21
|
-
map_roles(#{setup_args}.zip(
|
14
|
+
map_roles(#{setup_args.to_s}.zip([#{setup_args.join(',')}]))
|
22
15
|
end
|
23
16
|
", __FILE__, line
|
24
17
|
const_set("ContextInitializer", mod)
|
@@ -3,15 +3,13 @@ module Surrounded
|
|
3
3
|
module RoleBuilders
|
4
4
|
|
5
5
|
# Define behaviors for your role players
|
6
|
-
def role(name, type=
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
mod = Module.new(&block)
|
11
|
-
mod.send(:include, ::Surrounded)
|
6
|
+
def role(name, type=default_role_type, &block)
|
7
|
+
if type == :module
|
8
|
+
mod_name = RoleBuilders.mod_name(name)
|
9
|
+
mod = Module.new(&block).send(:include, ::Surrounded)
|
12
10
|
private_const_set(mod_name, mod)
|
13
11
|
else
|
14
|
-
meth = method(
|
12
|
+
meth = method(type)
|
15
13
|
meth.call(name, &block)
|
16
14
|
end
|
17
15
|
rescue NameError => e
|
@@ -22,17 +20,25 @@ module Surrounded
|
|
22
20
|
# Create a named behavior for a role using the standard library SimpleDelegator.
|
23
21
|
def wrap(name, &block)
|
24
22
|
require 'delegate'
|
25
|
-
wrapper_name =
|
23
|
+
wrapper_name = RoleBuilders.mod_name(name)
|
26
24
|
klass = private_const_set(wrapper_name, Class.new(SimpleDelegator, &block))
|
27
25
|
klass.send(:include, Surrounded)
|
28
26
|
end
|
29
27
|
alias_method :wrapper, :wrap
|
30
28
|
|
29
|
+
# Create a named behavior for a role using the standard library DelegateClass.
|
30
|
+
# This ties the implementation of the role to a specific class or module API.
|
31
|
+
def delegate_class(name, class_name, &block)
|
32
|
+
require 'delegate'
|
33
|
+
wrapper_name = RoleBuilders.mod_name(name)
|
34
|
+
klass = private_const_set(wrapper_name, DelegateClass(Object.const_get(class_name.to_s)))
|
35
|
+
klass.class_eval(&block)
|
36
|
+
klass.send(:include, Surrounded)
|
37
|
+
end
|
31
38
|
|
32
39
|
# Create an object which will bind methods to the role player
|
33
40
|
def interface(name, &block)
|
34
|
-
|
35
|
-
interface_name = class_basename + 'Interface'
|
41
|
+
interface_name = RoleBuilders.mod_name(name) + 'Interface'
|
36
42
|
|
37
43
|
behavior = private_const_set(interface_name, Module.new(&block))
|
38
44
|
|
@@ -43,6 +49,10 @@ module Surrounded
|
|
43
49
|
end
|
44
50
|
end
|
45
51
|
|
52
|
+
def self.mod_name(name)
|
53
|
+
name.to_s.gsub(/(?:^|_)([a-z])/){ $1.upcase }
|
54
|
+
end
|
55
|
+
|
46
56
|
end
|
47
57
|
end
|
48
58
|
end
|
@@ -37,7 +37,7 @@ module Surrounded
|
|
37
37
|
else
|
38
38
|
name = names.first
|
39
39
|
define_trigger_action(*names, &block)
|
40
|
-
define_trigger(name
|
40
|
+
define_trigger(name)
|
41
41
|
store_trigger(name)
|
42
42
|
end
|
43
43
|
end
|
@@ -53,8 +53,7 @@ module Surrounded
|
|
53
53
|
end
|
54
54
|
|
55
55
|
def define_trigger(name)
|
56
|
-
line = __LINE__
|
57
|
-
self.class_eval %{
|
56
|
+
line = __LINE__; self.class_eval %{
|
58
57
|
def #{name}(*args, &block)
|
59
58
|
begin
|
60
59
|
apply_behaviors
|
@@ -78,9 +77,7 @@ module Surrounded
|
|
78
77
|
|
79
78
|
|
80
79
|
def define_trigger_action(*name_and_args, &block)
|
81
|
-
trigger_action_module.
|
82
|
-
define_method(*name_and_args, &block)
|
83
|
-
end
|
80
|
+
trigger_action_module.send(:define_method, *name_and_args, &block)
|
84
81
|
end
|
85
82
|
|
86
83
|
def trigger_action_module
|