surrounded 0.9.1 → 0.9.2

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: c8490f4520253094598c520cdc7b3a73186d48d5
4
- data.tar.gz: 31d8f5baebf2e1f42b761207972de0523ac41885
3
+ metadata.gz: f7ef86c7f64ee33fa79398d58bbdcb13ed3f8e4e
4
+ data.tar.gz: 9f65308ecb76695601b51fa5a7f745b21e7ca866
5
5
  SHA512:
6
- metadata.gz: 161b86c91796ead35d60f817308a252585544b3690055039c026bf2ad97bf0f3ed5573709fd341673681399dcb8687113102cf698e2c5f255856d22e47471e2a
7
- data.tar.gz: 975026239c9e4e2796d8f66f73c062c7907159beeaf30a8f0dfea8ee69960d970155bfdcdc1ec69b81a6c13c5ed9ad8d862cfdcfb159108758da8293db4cb270
6
+ metadata.gz: 2f83e74f34de171f5f66170b9f481624a4a148ce448fb880c59318dc9d2f9389e25321dc6552ba0c8058ec475a67be5bc297d82453f3a3f0e37cbe8a367e993d
7
+ data.tar.gz: 84e3aa18ddaf6b1b30c60897152e5dab28fe545df81566a3bd3c40cbbee3b7108e5a7bdfcb3c4a70520c9a913bac64ad27ee716d85b6d32cd696e0069ef87fb3
data/.travis.yml CHANGED
@@ -1,4 +1,5 @@
1
1
  language: ruby
2
+ cache: bundler
2
3
  rvm:
3
4
  - 2.0.0
4
5
  - 2.1.1
data/README.md CHANGED
@@ -6,9 +6,7 @@
6
6
  [![Coverage Status](https://coveralls.io/repos/saturnflyer/surrounded/badge.png)](https://coveralls.io/r/saturnflyer/surrounded)
7
7
  [![Gem Version](https://badge.fury.io/rb/surrounded.png)](http://badge.fury.io/rb/surrounded)
8
8
 
9
- # Get work done with only what you need and nothing more.
10
-
11
- Surrounded is designed to help you better manage your business logic.
9
+ Surrounded is designed to help you better manage your business logic by keeping cohesive behaviors together. Bring objects together to implement your use cases and gain behavior only when necessary.
12
10
 
13
11
  ## How to think about your objects
14
12
 
@@ -543,10 +541,12 @@ Using the `role` method to define modules and classes takes care of the setup fo
543
541
  end
544
542
  ```
545
543
 
546
- 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.
544
+ The `:interface` option is a special object which has all of the standard Object 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.
547
545
 
548
546
  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__`.
549
547
 
548
+ The downside of using an interface is that it is still a wrapper. All of your defined role methods are executed in the context of the object playing the role, but the interface has it's own identity.
549
+
550
550
  If you'd like to choose one and use it all the time, you can set the default:
551
551
 
552
552
  ```ruby
@@ -636,6 +636,13 @@ class ActiviatingAccount
636
636
  # these also must be done if you create your own initialize method.
637
637
  # this is a shortcut for using attr_reader and private
638
638
  private_attr_reader :activator, :account
639
+
640
+ # initialize with keyword arguments
641
+ keyword_initialize(:activator, :account)
642
+ # this makes the following instance method signature with required keyword arguments
643
+ def initialize(activator:, account:)
644
+ # ...
645
+ end
639
646
 
640
647
  role :activator do # module by default
641
648
  def some_behavior; end
@@ -724,7 +731,7 @@ end
724
731
 
725
732
  ## Dependencies
726
733
 
727
- 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.
734
+ 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. It is used in Surrounded to keep track of role player, roles, and role constant names but it is not a hard requirement. You may implement your own but presently you'll need to dive into the implementation to fully understand how. Future updates may provide better support and guidance.
728
735
 
729
736
  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.
730
737
 
@@ -30,7 +30,7 @@ module Surrounded
30
30
 
31
31
  def define_access_method(name, &block)
32
32
  mod = Module.new
33
- mod.class_eval {
33
+ num = __LINE__; mod.class_eval {
34
34
  define_method "disallow_#{name}?" do
35
35
  begin
36
36
  apply_behaviors
@@ -10,13 +10,32 @@ module Surrounded
10
10
  line = __LINE__
11
11
  mod.class_eval "
12
12
  def initialize(#{setup_args.join(',')})
13
- @role_map = RoleMap.new
13
+ @role_map = role_mapper_class.new
14
14
  map_roles(#{setup_args.to_s}.zip([#{setup_args.join(',')}]))
15
15
  end
16
16
  ", __FILE__, line
17
17
  const_set("ContextInitializer", mod)
18
18
  include mod
19
19
  end
20
+
21
+ def keyword_initialize(*setup_args)
22
+ private_attr_reader(*setup_args)
23
+
24
+ parameters = setup_args.map{|a| "#{a}:"}.join(',')
25
+ puts parameters
26
+
27
+ mod = Module.new
28
+ line = __LINE__
29
+ mod.class_eval %{
30
+ def initialize(#{parameters})
31
+ @role_map = role_mapper_class.new
32
+ map_roles(#{setup_args.to_s}.zip([#{setup_args.join(',')}]))
33
+ end
34
+ }, __FILE__, line
35
+ const_set("ContextInitializer", mod)
36
+ include mod
37
+ end
38
+ alias initialize_with_keywords keyword_initialize
20
39
  end
21
40
  end
22
41
  end
@@ -1,12 +1,31 @@
1
1
  module Surrounded
2
2
  module Context
3
3
  class Negotiator
4
+ class << self
5
+ # Return a class which has methods defined to forward the method to
6
+ # the wrapped object delegating to the behavior module.
7
+ # This prevents hits to method_missing.
8
+ def for_role(mod)
9
+ klass = Class.new(self)
10
+ mod.instance_methods(false).each do |meth|
11
+ num = __LINE__; klass.class_eval %{
12
+ def #{meth}(*args, &block)
13
+ @behaviors.instance_method(:#{meth}).bind(@object).call(*args, &block)
14
+ end
15
+ }, __FILE__, num
16
+ end
17
+ klass
18
+ end
19
+ end
20
+
21
+
4
22
  identity = "__send__|object_id"
5
23
 
6
- instance_methods.each do |meth|
7
- unless meth.to_s =~ /#{identity}/
8
- undef_method meth
9
- end
24
+ # Remove all methods except the identity methods
25
+ instance_methods.reject{ |m|
26
+ m.to_s =~ /#{identity}/
27
+ }.each do |meth|
28
+ undef_method meth
10
29
  end
11
30
 
12
31
  private
@@ -16,15 +35,17 @@ module Surrounded
16
35
  end
17
36
 
18
37
  def method_missing(meth, *args, &block)
19
- if @behaviors.instance_methods.include?(meth)
20
- the_method = @behaviors.instance_method(meth)
21
- the_method.bind(@object).call(*args, &block)
22
- else
23
- @object.send(meth, *args, &block)
24
- end
38
+ @object.send(meth, *args, &block)
39
+ end
40
+
41
+ def respond_to_missing?(meth, include_private=false)
42
+ @object.respond_to?(meth, include_private)
25
43
  end
26
44
  end
27
45
 
46
+ # The method_missing definition from Surrounded will apply
47
+ # before the one defined above. This allows the methods for
48
+ # the objects in the context to work properly
28
49
  Negotiator.send(:prepend, Surrounded)
29
50
  end
30
51
  end
@@ -5,7 +5,7 @@ module Surrounded
5
5
  # Define behaviors for your role players
6
6
  def role(name, type=default_role_type, &block)
7
7
  if type == :module
8
- mod_name = RoleBuilders.mod_name(name)
8
+ mod_name = RoleName(name)
9
9
  mod = Module.new(&block).send(:include, ::Surrounded)
10
10
  private_const_set(mod_name, mod)
11
11
  else
@@ -20,7 +20,7 @@ module Surrounded
20
20
  # Create a named behavior for a role using the standard library SimpleDelegator.
21
21
  def wrap(name, &block)
22
22
  require 'delegate'
23
- wrapper_name = RoleBuilders.mod_name(name)
23
+ wrapper_name = RoleName(name)
24
24
  klass = private_const_set(wrapper_name, Class.new(SimpleDelegator, &block))
25
25
  klass.send(:include, Surrounded)
26
26
  end
@@ -30,7 +30,7 @@ module Surrounded
30
30
  # This ties the implementation of the role to a specific class or module API.
31
31
  def delegate_class(name, class_name, &block)
32
32
  require 'delegate'
33
- wrapper_name = RoleBuilders.mod_name(name)
33
+ wrapper_name = RoleName(name)
34
34
  klass = private_const_set(wrapper_name, DelegateClass(Object.const_get(class_name.to_s)))
35
35
  klass.class_eval(&block)
36
36
  klass.send(:include, Surrounded)
@@ -38,19 +38,23 @@ module Surrounded
38
38
 
39
39
  # Create an object which will bind methods to the role player
40
40
  def interface(name, &block)
41
- interface_name = RoleBuilders.mod_name(name) + 'Interface'
42
-
41
+ # AdminInterface
42
+ interface_name = RoleName(name, 'Interface')
43
43
  behavior = private_const_set(interface_name, Module.new(&block))
44
44
 
45
45
  require 'surrounded/context/negotiator'
46
46
  undef_method(name)
47
+
48
+ # AdminInterfaceProxy
49
+ proxy = private_const_set(RoleName(interface_name, 'Proxy'), Negotiator.for_role(behavior))
47
50
  define_method(name) do
48
- instance_variable_set("@#{name}", Negotiator.new(role_map.assigned_player(name), behavior))
51
+ instance_variable_set("@#{name}", proxy.new(role_map.assigned_player(name), behavior))
49
52
  end
50
53
  end
51
54
 
52
- def self.mod_name(name)
53
- name.to_s.gsub(/(?:^|_)([a-z])/){ $1.upcase }
55
+ private
56
+ def RoleName(text, suffix=nil)
57
+ RoleName.new(text, suffix)
54
58
  end
55
59
 
56
60
  end
@@ -2,7 +2,27 @@ require 'triad'
2
2
  require 'surrounded/context_errors'
3
3
  module Surrounded
4
4
  module Context
5
- class RoleMap < Triad
5
+ class RoleMap
6
+
7
+ class << self
8
+ def from_base(klass=::Triad)
9
+ role_mapper = Class.new(::Surrounded::Context::RoleMap)
10
+ num = __LINE__; role_mapper.class_eval %{
11
+ def container
12
+ @container ||= #{klass}.new
13
+ end
14
+ }, __FILE__, num
15
+ %w{ update each values keys }.each do |meth|
16
+ num = __LINE__; role_mapper.class_eval %{
17
+ def #{meth}(*args, &block)
18
+ container.send(:#{meth}, *args, &block)
19
+ end
20
+ }, __FILE__, num
21
+ end
22
+ role_mapper
23
+ end
24
+ end
25
+
6
26
  def role?(role)
7
27
  keys.include?(role)
8
28
  end
@@ -88,6 +88,14 @@ module Surrounded
88
88
  end
89
89
  end
90
90
 
91
+ # Allow alternative implementations for the role map
92
+ # This requires that the specified mapper klass have an
93
+ # initializer method called 'from_base' which accepts a
94
+ # class name used to initialize the base object
95
+ def role_mapper_class(mapper: RoleMap, base: ::Triad)
96
+ @role_mapper_class ||= mapper.from_base(base)
97
+ end
98
+
91
99
  module InstanceMethods
92
100
  # Check whether a given name is a role inside the context.
93
101
  # The provided block is used to evaluate whether or not the caller
@@ -111,7 +119,7 @@ module Surrounded
111
119
  private
112
120
 
113
121
  def role_map
114
- @role_map ||= RoleMap.new
122
+ @role_map ||= role_mapper_class.new
115
123
  end
116
124
 
117
125
  def map_roles(role_object_array)
@@ -189,14 +197,14 @@ module Surrounded
189
197
  def apply_behaviors
190
198
  role_map.each do |role, mod_name, object|
191
199
  player = apply_behavior(role, mod_name, object)
192
- player.send(:store_context, self) do; end
200
+ player.__send__(:store_context) do; end
193
201
  end
194
202
  end
195
203
 
196
204
  def remove_behaviors
197
205
  role_map.each do |role, mod_name, player|
198
206
  if player.respond_to?(:remove_context, true)
199
- player.send(:remove_context) do; end
207
+ player.__send__(:remove_context) do; end
200
208
  end
201
209
  remove_behavior(role, mod_name, player)
202
210
  end
@@ -223,7 +231,7 @@ module Surrounded
223
231
  end
224
232
 
225
233
  def role_behavior_name(role)
226
- role.to_s.gsub(/(?:^|_)([a-z])/) { $1.upcase }.sub(/_\d+/,'')
234
+ RoleName.new(role)
227
235
  end
228
236
 
229
237
  def role_module_basename(mod)
@@ -238,6 +246,10 @@ module Surrounded
238
246
  self.class.send(:role_const_defined?, name)
239
247
  end
240
248
 
249
+ def role_mapper_class
250
+ self.class.send(:role_mapper_class)
251
+ end
252
+
241
253
  def singularize_name(name)
242
254
  if name.respond_to?(:singularize)
243
255
  name.singularize
@@ -253,5 +265,27 @@ module Surrounded
253
265
  end
254
266
  end
255
267
  end
268
+
269
+ class RoleName
270
+ def initialize(string, suffix=nil)
271
+ @string = string.
272
+ to_s.
273
+ split(/_/).
274
+ map{|part|
275
+ part.capitalize
276
+ }.
277
+ join.
278
+ sub(/_\d+/,'') + suffix.to_s
279
+ end
280
+
281
+ def to_str
282
+ @string
283
+ end
284
+ alias to_s to_str
285
+
286
+ def to_sym
287
+ @string.to_sym
288
+ end
289
+ end
256
290
  end
257
291
  end
@@ -1,5 +1,5 @@
1
1
  module Surrounded
2
- VERSION = "0.9.1"
2
+ VERSION = "0.9.2"
3
3
 
4
4
  def self.version
5
5
  VERSION
data/lib/surrounded.rb CHANGED
@@ -11,19 +11,15 @@ module Surrounded
11
11
 
12
12
  private
13
13
 
14
- def store_context(ctxt, &block)
14
+ def store_context(&block)
15
15
  accessor = block.binding.eval('self')
16
- if accessor.role_player?(self)
17
- surroundings.unshift(ctxt)
18
- end
16
+ surroundings.unshift(accessor)
19
17
  self
20
18
  end
21
19
 
22
20
  def remove_context(&block)
23
21
  accessor = block.binding.eval('self')
24
- if accessor.role_player?(self)
25
- surroundings.shift
26
- end
22
+ surroundings.shift if surroundings.include?(accessor)
27
23
  self
28
24
  end
29
25
 
@@ -36,8 +36,13 @@ class ProxyContext
36
36
  end
37
37
  end
38
38
 
39
- ProxyUser = Struct.new(:name)
40
- ProxyUser.send(:include, Surrounded)
39
+ ProxyUser = Class.new do
40
+ include Surrounded
41
+ def initialize(name)
42
+ @name = name
43
+ end
44
+ attr_reader :name
45
+ end
41
46
 
42
47
  describe ProxyContext do
43
48
  let(:user){
@@ -60,7 +65,7 @@ describe ProxyContext do
60
65
  it 'passes missing methods up the ancestry of the object' do
61
66
  err = ->{ context.admin_missing_method }.must_raise(NoMethodError)
62
67
 
63
- assert_match 'ProxyUser name="Jim"', err.message
68
+ assert_match /ProxyUser.*name="Jim"/, err.message
64
69
  end
65
70
 
66
71
  it 'allows access to other objects in the context' do
@@ -83,7 +83,7 @@ describe Surrounded::Context, '.role' do
83
83
  end
84
84
 
85
85
  it 'creates a private accessor method' do
86
- assert_respond_to(context, :admin)
86
+ assert context.respond_to?(:admin, true)
87
87
  end
88
88
  end
89
89
 
@@ -258,3 +258,22 @@ describe Surrounded::Context, 'auto-assigning roles for collections' do
258
258
  assert_equal "member show, member show", context.get_member_show
259
259
  end
260
260
  end
261
+
262
+ class Keyworder
263
+ extend Surrounded::Context
264
+
265
+ keyword_initialize :this, :that
266
+ end
267
+
268
+ describe Surrounded::Context, 'keyword initializers' do
269
+ it 'works with keyword arguments' do
270
+ assert context = Keyworder.new(this: User.new('Jim'), that: User.new('Guille'))
271
+ end
272
+
273
+ it 'raises errors with missing keywords' do
274
+ err = assert_raises(ArgumentError){
275
+ Keyworder.new(this: User.new('Amy'))
276
+ }
277
+ assert_match /missing keyword: that/, err.message
278
+ end
279
+ end
@@ -40,7 +40,7 @@ describe "Surrounded", "added to an existing object" do
40
40
  thing.name = 'Jim'
41
41
 
42
42
  assert_raises(NoMethodError){
43
- thing.send(:store_context)
43
+ thing.__send__(:store_context)
44
44
  }
45
45
  thing.extend(Surrounded)
46
46
 
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.1
4
+ version: 0.9.2
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-02-18 00:00:00.000000000 Z
11
+ date: 2015-05-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: triad