surrounded 0.9.9 → 0.9.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 676d57f548d17fbf0d9112208472938b7c5a2e82
4
- data.tar.gz: 830bf96b8114f786840315b24fba5f90450be813
3
+ metadata.gz: 2236ff5ee05ddc4b3c857e3700ca2205fa966e62
4
+ data.tar.gz: 7e47950aaac3dd0dc4b74f18409ef5590226b5ef
5
5
  SHA512:
6
- metadata.gz: 1bc24e149d0fefd9961ea08c92a01390cbdc24d94a42fa6a06e542a219838572f3c75fa8e65ec6e54324236a53950461e4b81afd7e732a0a6765220e4c5a27d6
7
- data.tar.gz: dd4ee61d04717d8418e011e6421031196bada55ae25d6fb04e24c9913fc06ccc2c199a443c4f554618f3298ef6e7319d81467ee69e43b135cf7da6aa2214f0cd
6
+ metadata.gz: 1b6e97b48a2c4cda950b2507bff57bcb36bad18171774474405d01030f546e43b2df6adb4e8809477517953ee330df5ffde71c8d0e19af1f469103db5eece897
7
+ data.tar.gz: 606cb9ba7d77942a770956eb73581bd4f0fe08caf31bd8305ad404dcea7dd2dbe558521fc908160ba91c2b70bf36c644fc4aef65d940efb6353cf502be24ae3b
@@ -0,0 +1,4 @@
1
+ ---
2
+ notifications:
3
+ pullrequest:
4
+ comment: verbose
@@ -1,17 +1,16 @@
1
+ before_install:
2
+ - gem install bundler
1
3
  language: ruby
2
4
  cache: bundler
3
5
  rvm:
4
- - 2.0.0
5
- - 2.1.5
6
- - 2.2.2
6
+ - 2.3.1
7
+ - 2.2.5
8
+ - 2.1.10
7
9
  - ruby-head
8
10
  - jruby-head
9
- - rbx
10
11
  env:
11
12
  - COVERALLS=true
12
13
  matrix:
13
14
  allow_failures:
14
- - rvm: 2.0.0
15
15
  - rvm: ruby-head
16
16
  - rvm: jruby-head
17
- - rvm: rbx
@@ -0,0 +1,8 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [0.9.10]
6
+
7
+ - Do something with name collisions when a role player has an existing method of another role in the context.
8
+ - Move InvalidRoleType exception under the host context class namespace. This allows you to rescue from your own namespace.
@@ -1,4 +1,4 @@
1
- Copyright (c) 2013-2015 'Jim Gay'
1
+ Copyright (c) 2013-2016 'Jim Gay'
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -722,6 +722,15 @@ class ActiviatingAccount
722
722
  # ...
723
723
  end
724
724
 
725
+ # Handle method name collisions on role players against role names in the context
726
+ on_name_collision :raise # will raise your context namespaced error: ActiviatingAccount::NameCollisionError
727
+ on_name_collision :warn
728
+ on_name_collision ->(message){ puts "Here's the message! #{message}" }
729
+ on_name_collision :my_custom_handler
730
+ def my_custom_handler(message)
731
+ # do something with the message here
732
+ end
733
+
725
734
  role :activator do # module by default
726
735
  def some_behavior; end
727
736
  end
@@ -910,6 +919,87 @@ end
910
919
 
911
920
  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`.
912
921
 
922
+ ##Name collisions between methods and roles
923
+
924
+ Lets say that you wish to create a context as below, intending to use instances of the following two classes as role players:
925
+
926
+ ```ruby
927
+ class Postcode
928
+ # other methods...
929
+ def code
930
+ @code
931
+ end
932
+
933
+ def country
934
+ @country
935
+ end
936
+ end
937
+
938
+ class Country
939
+ # other methods...
940
+ def country_code
941
+ @code
942
+ end
943
+ end
944
+
945
+ class SendAParcel
946
+ extend Surrounded::Context
947
+
948
+ keyword_initialize :postcode, :country
949
+
950
+ trigger :send do
951
+ postcode.send
952
+ end
953
+
954
+ end
955
+ role :postcode do
956
+ def send
957
+ # do things...
958
+ country_code = country.country_code # name collision...probably raises an exception!
959
+ end
960
+ end
961
+ end
962
+ ```
963
+ When you call the `:send` trigger you are likely to be greeted with an `NoMethodError` exception. The reason for this is that there is a name collision between `Postcode#country`, and the `:country` role in the `SendAParcel` context. Where a name collision exists, the method in the role player overrides that of the calling class and you get unexpected results.
964
+
965
+ To address this issue, use `on_name_collision` to specify the name of a method to use when collisions are found:
966
+
967
+ ```ruby
968
+
969
+ class SendAParcel
970
+ extend Surrounded::Context
971
+
972
+ on_name_collision :raise
973
+ end
974
+
975
+ ```
976
+
977
+ This option will raise an exception (obviously). You may use any method which is available to the context but it must accept a single message as the argument.
978
+
979
+ You can also use a lambda:
980
+
981
+ ```ruby
982
+
983
+ class SendAParcel
984
+ extend Surrounded::Context
985
+
986
+ on_name_collision ->(message){ puts "Here's the message: #{message}"}
987
+ end
988
+
989
+ ```
990
+
991
+ You may also user a class method:
992
+
993
+ ```ruby
994
+ class SendAParcel
995
+ extend Surrounded::Context
996
+
997
+ def self.handle_collisions(message)
998
+ Logger.debug "#{Time.now}: #{message}"
999
+ end
1000
+ end
1001
+ ```
1002
+
913
1003
  ## How to read this code
914
1004
 
915
1005
  If you use this library, it's important to understand it.
@@ -1,4 +1,7 @@
1
1
  module Surrounded
2
+ module Context
3
+ class AccessError < ::StandardError; end
4
+ end
2
5
  module AccessControl
3
6
  def self.extended(base)
4
7
  base.send(:include, AccessMethods)
@@ -7,6 +7,7 @@ require 'surrounded/context/trigger_controls'
7
7
  require 'surrounded/access_control'
8
8
  require 'surrounded/shortcuts'
9
9
  require 'surrounded/east_oriented'
10
+ require 'surrounded/context/name_collision_detector'
10
11
 
11
12
  # Extend your classes with Surrounded::Context to handle their
12
13
  # initialization and application of behaviors to the role players
@@ -18,7 +19,7 @@ module Surrounded
18
19
  module Context
19
20
  def self.extended(base)
20
21
  base.class_eval {
21
- extend RoleBuilders, Initializing, Forwarding
22
+ extend RoleBuilders, Initializing, Forwarding, NameCollisionDetector
22
23
 
23
24
  @triggers = Set.new
24
25
  include InstanceMethods
@@ -28,11 +29,23 @@ module Surrounded
28
29
  include trigger_mod
29
30
 
30
31
  extend TriggerControls
32
+
31
33
  }
34
+ define_exceptions(base)
32
35
  end
33
36
 
34
37
  private
35
38
 
39
+ def self.define_exceptions(klass)
40
+ self.constants.select{|const|
41
+ self.const_get(const) < ::StandardError
42
+ }.map{|exception|
43
+ unless klass.const_defined?(exception)
44
+ klass.const_set(exception, Class.new(self.const_get(exception)))
45
+ end
46
+ }
47
+ end
48
+
36
49
  # Set the default type of implementation for role methods for all contexts.
37
50
  def self.default_role_type
38
51
  @default_role_type ||= :module
@@ -135,6 +148,7 @@ module Surrounded
135
148
  end
136
149
 
137
150
  def map_roles(role_object_array)
151
+ detect_collisions role_object_array
138
152
  role_object_array.to_a.each do |role, object|
139
153
  if self.respond_to?("map_role_#{role}")
140
154
  self.send("map_role_#{role}", object)
@@ -29,8 +29,6 @@ module Surrounded
29
29
 
30
30
  def default_initializer(params, setup_args, &block)
31
31
  private_attr_reader(*setup_args)
32
-
33
- parameters = setup_args.map{|a| "#{a}:"}.join(',')
34
32
  @initializer_block = block || nil
35
33
  mod = Module.new
36
34
  line = __LINE__; mod.class_eval %{
@@ -0,0 +1,70 @@
1
+ module Surrounded
2
+ module Context
3
+ class NameCollisionError <::StandardError; end
4
+ module NameCollisionDetector
5
+
6
+ attr_reader :handler
7
+
8
+ def self.extended(base)
9
+ base.send :include, NameCollisionHandler
10
+ unless defined?(base::NameCollisionError)
11
+ base.const_set(:NameCollinionError, Class.new(::Surrounded::Context::NameCollisionError))
12
+ end
13
+ end
14
+
15
+ def on_name_collision(method_name)
16
+ @handler = method_name
17
+ end
18
+
19
+ module NameCollisionHandler
20
+
21
+ private
22
+
23
+ def detect_collisions(role_object_map)
24
+ if handler
25
+ handle_collisions(collision_warnings(role_object_map))
26
+ end
27
+ end
28
+
29
+ def collision_warnings(role_object_map)
30
+ role_object_map.select{|role, object|
31
+ ![object.methods & role_object_map.keys].flatten.empty?
32
+ }.map{|role, object|
33
+ role_collision_message(role,(object.methods & role_object_map.keys).sort)
34
+ }.join("\n")
35
+ end
36
+
37
+ def handle_collisions(collisions)
38
+ handler_args = [collisions]
39
+ if handler == :raise
40
+ handler_args.unshift self.class::NameCollisionError
41
+ end
42
+
43
+ handler_method.call(*handler_args)
44
+ end
45
+
46
+ def role_collision_message(role, colliding_method_names)
47
+ "#{role} has name collisions with #{colliding_method_names}"
48
+ end
49
+
50
+ def nothing(*); end
51
+
52
+ def handler
53
+ self.class.handler
54
+ end
55
+
56
+ def handler_method
57
+ if handler.respond_to?(:call)
58
+ handler
59
+ elsif respond_to?(handler, true)
60
+ method(handler)
61
+ elsif self.class.respond_to?(handler, true)
62
+ self.class.method(handler)
63
+ else
64
+ method(:nothing)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -1,5 +1,15 @@
1
1
  module Surrounded
2
2
  module Context
3
+ class InvalidRoleType < ::StandardError
4
+ unless method_defined?(:cause)
5
+ def initialize(msg=nil)
6
+ super
7
+ @cause = $!
8
+ end
9
+ attr_reader :cause
10
+ end
11
+ end
12
+
3
13
  module RoleBuilders
4
14
 
5
15
  # Define behaviors for your role players
@@ -13,7 +23,7 @@ module Surrounded
13
23
  meth.call(name, &block)
14
24
  end
15
25
  rescue NameError => e
16
- raise InvalidRoleType, e.message
26
+ raise self::InvalidRoleType, e.message
17
27
  end
18
28
  alias_method :role_methods, :role
19
29
 
@@ -2,6 +2,8 @@ require 'triad'
2
2
  require 'surrounded/context_errors'
3
3
  module Surrounded
4
4
  module Context
5
+ class InvalidRole < ::Triad::ItemNotPresent; end
6
+
5
7
  class RoleMap
6
8
 
7
9
  class << self
@@ -1,16 +1,5 @@
1
1
  require 'triad'
2
2
  module Surrounded
3
3
  module Context
4
- class InvalidRole < ::Triad::ItemNotPresent; end
5
- class InvalidRoleType < ::StandardError
6
- unless method_defined?(:cause)
7
- def initialize(msg=nil)
8
- super
9
- @cause = $!
10
- end
11
- attr_reader :cause
12
- end
13
- end
14
- class AccessError < ::StandardError; end
15
4
  end
16
- end
5
+ end
@@ -8,6 +8,7 @@ module Surrounded
8
8
  instance.public_send(name)
9
9
  end
10
10
  end
11
+
11
12
  def store_trigger(*names)
12
13
  names.each do |name|
13
14
  define_shortcut(name)
@@ -1,5 +1,5 @@
1
1
  module Surrounded
2
- VERSION = "0.9.9"
2
+ VERSION = "0.9.10"
3
3
 
4
4
  def self.version
5
5
  VERSION
@@ -20,6 +20,6 @@ Gem::Specification.new do |spec|
20
20
 
21
21
  spec.add_dependency "triad", "~> 0.2.2"
22
22
 
23
- spec.add_development_dependency "bundler", "~> 1.3"
23
+ spec.add_development_dependency "bundler", "~> 1.12"
24
24
  spec.add_development_dependency "rake"
25
25
  end
@@ -13,6 +13,10 @@ class ProxyContext
13
13
  def talking_to_others
14
14
  task.name
15
15
  end
16
+
17
+ def combined_methods
18
+ some_admin_method
19
+ end
16
20
  end
17
21
 
18
22
  wrap :task do
@@ -41,6 +45,10 @@ class ProxyContext
41
45
  trigger :get_admin_method do
42
46
  admin.method(:talking_to_others)
43
47
  end
48
+
49
+ trigger :combined_interface_methods do
50
+ admin.combined_methods
51
+ end
44
52
  end
45
53
 
46
54
  ProxyUser = Class.new do
@@ -83,6 +91,15 @@ describe ProxyContext do
83
91
  assert context.admin_responds?
84
92
  end
85
93
 
94
+ # A Negotiator object merely applies methods to another object
95
+ # so that once the method is called, the object has no knowledge
96
+ # of the module from which the method was applied.
97
+ it 'does not find other interface methods' do
98
+ assert_raises(NameError){
99
+ context.combined_interface_methods
100
+ }
101
+ end
102
+
86
103
  it 'is able to grab methods from the object' do
87
104
  assert_equal :talking_to_others, context.get_admin_method.name
88
105
  end
@@ -0,0 +1,162 @@
1
+ require 'test_helper'
2
+ ##
3
+ # The problem is as follows: When an object already contains a method
4
+ # that has the same name as an actor in the context, then the object
5
+ # defaults to its own method rather than calling the actor.
6
+ # This is simulated in the two classes below, where HasNameCollision has
7
+ # an empty attribute called will_collide (which is always nil). In the following
8
+ # context, when the method collide is called on the actor will_collide, the
9
+ # actor is ignored and the HasNameCollision#will_collide is called instead,
10
+ # returning nil.
11
+ ##
12
+
13
+ class HasNameCollision
14
+ include Surrounded
15
+
16
+ attr_accessor :will_collide # should always return nil
17
+
18
+ def assert_has_role
19
+ true
20
+ end
21
+
22
+ def will_collide=(_); end
23
+ end
24
+
25
+ class ShouldCollide
26
+ include Surrounded
27
+ def collide
28
+ return 'Method called in ShouldCollide'
29
+ end
30
+ end
31
+
32
+ class ContextOverridesName
33
+ extend Surrounded::Context
34
+
35
+ # The specific behaviour we want to test is that when a role has a method that
36
+ # has the same name as another role, then when that method is called strange
37
+ # things happen.
38
+ keyword_initialize :base, :will_collide
39
+ on_name_collision :nothing
40
+
41
+ trigger :check_setup do
42
+ base.assert_has_role
43
+ end
44
+
45
+ trigger :induce_collision do
46
+ base.name_collision
47
+ end
48
+
49
+ role :base do
50
+ def name_collision
51
+ will_collide.collide
52
+ end
53
+
54
+ def assert_has_role
55
+ true
56
+ end
57
+ end
58
+ end
59
+
60
+ class ContextWithMultipleCollisions
61
+ extend Surrounded::Context
62
+
63
+ on_name_collision :warn
64
+
65
+ keyword_initialize :first, :second, :third
66
+ end
67
+
68
+ class First
69
+
70
+ def second;end
71
+ def third;end
72
+ end
73
+
74
+ class Second
75
+ def first;end
76
+
77
+ def third;end
78
+ end
79
+
80
+ class Third
81
+ def first;end
82
+ def second;end
83
+
84
+ end
85
+
86
+ describe 'handling name collisions' do
87
+
88
+ let(:new_context_with_collision){
89
+ ContextOverridesName.new(base: HasNameCollision.new, will_collide: ShouldCollide.new)
90
+ }
91
+
92
+ after do
93
+ ContextOverridesName.instance_eval{
94
+ remove_instance_variable :@handler if defined?(@handler)
95
+ }
96
+ end
97
+
98
+ def set_handler(handler)
99
+ ContextOverridesName.instance_eval{
100
+ on_name_collision handler
101
+ }
102
+ end
103
+
104
+ it 'is works properly without handling collisions' do
105
+ assert new_context_with_collision.check_setup
106
+ end
107
+
108
+ it 'allows a name collision' do
109
+ err = assert_raises(NoMethodError){
110
+ new_context_with_collision.induce_collision
111
+ }
112
+ assert_match(/undefined method \`collide' for nil:NilClass/, err.message)
113
+ end
114
+
115
+ it 'can raise an exception' do
116
+ set_handler :raise
117
+ assert_raises(ContextOverridesName::NameCollisionError){
118
+ new_context_with_collision
119
+ }
120
+ end
121
+
122
+ it 'can print a warning' do
123
+ set_handler :warn
124
+ assert_output(nil, "base has name collisions with [:will_collide]\n") {
125
+ new_context_with_collision
126
+ }
127
+ end
128
+
129
+ let(:create_context_with_multiple_collisions){
130
+ ContextWithMultipleCollisions.new(first: First.new, second: Second.new, third: Third.new)
131
+ }
132
+
133
+ it 'can handle multiple collisions' do
134
+ expected_message = <<ERR
135
+ first has name collisions with [:second, :third]
136
+ second has name collisions with [:first, :third]
137
+ third has name collisions with [:first, :second]
138
+ ERR
139
+ assert_output(nil, expected_message){
140
+ create_context_with_multiple_collisions
141
+ }
142
+ end
143
+
144
+ it 'can use a class method' do
145
+ class ContextOverridesName
146
+ def class_method_handler(message)
147
+ puts message
148
+ end
149
+ end
150
+ set_handler :class_method_handler
151
+ assert_output("base has name collisions with [:will_collide]\n"){
152
+ new_context_with_collision
153
+ }
154
+ end
155
+
156
+ it 'can use a proc' do
157
+ set_handler ->(message){ puts "message from a proc: #{message}"}
158
+ assert_output("message from a proc: base has name collisions with [:will_collide]\n"){
159
+ new_context_with_collision
160
+ }
161
+ end
162
+ end
@@ -89,14 +89,16 @@ describe Surrounded::Context, '.role' do
89
89
 
90
90
  describe 'unknown' do
91
91
  it 'raises an error' do
92
+ class UnknownRole
93
+ extend Surrounded::Context
94
+ end
95
+
92
96
  err = _{
93
- class UnknownRole
94
- extend Surrounded::Context
95
-
97
+ UnknownRole.instance_eval do
96
98
  role :admin, :unknown do
97
99
  end
98
100
  end
99
- }.must_raise Surrounded::Context::InvalidRoleType
101
+ }.must_raise UnknownRole::InvalidRoleType
100
102
  _(err.cause).must_be_kind_of NameError
101
103
  end
102
104
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: surrounded
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.9
4
+ version: 0.9.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - "'Jim Gay'"
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-06-18 00:00:00.000000000 Z
11
+ date: 2016-05-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: triad
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '1.3'
33
+ version: '1.12'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '1.3'
40
+ version: '1.12'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rake
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -61,8 +61,10 @@ extra_rdoc_files: []
61
61
  files:
62
62
  - ".codeclimate.yml"
63
63
  - ".gitignore"
64
+ - ".pullreview.yml"
64
65
  - ".simplecov"
65
66
  - ".travis.yml"
67
+ - Changelog.md
66
68
  - Gemfile
67
69
  - LICENSE.txt
68
70
  - README.md
@@ -74,6 +76,7 @@ files:
74
76
  - lib/surrounded/context.rb
75
77
  - lib/surrounded/context/forwarding.rb
76
78
  - lib/surrounded/context/initializing.rb
79
+ - lib/surrounded/context/name_collision_detector.rb
77
80
  - lib/surrounded/context/negotiator.rb
78
81
  - lib/surrounded/context/role_builders.rb
79
82
  - lib/surrounded/context/role_map.rb
@@ -95,6 +98,7 @@ files:
95
98
  - test/example_threaded_test.rb
96
99
  - test/example_wrapper_test.rb
97
100
  - test/initialization_test.rb
101
+ - test/name_collisions_test.rb
98
102
  - test/non_surrounded_role_player_test.rb
99
103
  - test/override_methods_test.rb
100
104
  - test/role_context_method_test.rb
@@ -121,7 +125,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
121
125
  version: '0'
122
126
  requirements: []
123
127
  rubyforge_project:
124
- rubygems_version: 2.4.8
128
+ rubygems_version: 2.5.1
125
129
  signing_key:
126
130
  specification_version: 4
127
131
  summary: Create encapsulated environments for your objects.
@@ -138,6 +142,7 @@ test_files:
138
142
  - test/example_threaded_test.rb
139
143
  - test/example_wrapper_test.rb
140
144
  - test/initialization_test.rb
145
+ - test/name_collisions_test.rb
141
146
  - test/non_surrounded_role_player_test.rb
142
147
  - test/override_methods_test.rb
143
148
  - test/role_context_method_test.rb