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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: cb66bc4320f6971198085580154f6ca683dcc952
4
- data.tar.gz: cfe6c6e94af4ff0fa78922cd1df3b74666abb58a
3
+ metadata.gz: c8490f4520253094598c520cdc7b3a73186d48d5
4
+ data.tar.gz: 31d8f5baebf2e1f42b761207972de0523ac41885
5
5
  SHA512:
6
- metadata.gz: 5e9e696a657da51806a9929adfec0c0ef6f838dae69ebe2acf9c10a547998428a935d021118b7550eff2033316b1b177906749e93e50f3150c62abc60d44d0e1
7
- data.tar.gz: 36395abadb826b0542061e3d51e0831f533a8c4321d4c4b113469dbfae4520f1ae15916919f4f1bf0006ff8ab84956fce227be65fd1e6b822156324aec840af9
6
+ metadata.gz: 161b86c91796ead35d60f817308a252585544b3690055039c026bf2ad97bf0f3ed5573709fd341673681399dcb8687113102cf698e2c5f255856d22e47471e2a
7
+ data.tar.gz: 975026239c9e4e2796d8f66f73c062c7907159beeaf30a8f0dfea8ee69960d970155bfdcdc1ec69b81a6c13c5ed9ad8d862cfdcfb159108758da8293db4cb270
data/.travis.yml CHANGED
@@ -2,6 +2,7 @@ language: ruby
2
2
  rvm:
3
3
  - 2.0.0
4
4
  - 2.1.1
5
+ - 2.2.0
5
6
  - ruby-head
6
7
  - jruby-head
7
8
  - rbx
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 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.
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 returns 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.
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
@@ -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(arguments))
14
+ map_roles(#{setup_args.to_s}.zip([#{setup_args.join(',')}]))
22
15
  end
23
16
  ", __FILE__, line
24
17
  const_set("ContextInitializer", mod)
@@ -24,5 +24,7 @@ module Surrounded
24
24
  end
25
25
  end
26
26
  end
27
+
28
+ Negotiator.send(:prepend, Surrounded)
27
29
  end
28
30
  end
@@ -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=nil, &block)
7
- role_type = type || default_role_type
8
- if role_type == :module
9
- mod_name = name.to_s.gsub(/(?:^|_)([a-z])/){ $1.upcase }
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(role_type)
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 = name.to_s.gsub(/(?:^|_)([a-z])/){ $1.upcase }
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
- class_basename = name.to_s.gsub(/(?:^|_)([a-z])/){ $1.upcase }
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, &block)
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.module_eval do
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