surrounded 1.0.0 → 1.1.1

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.
Files changed (50) hide show
  1. checksums.yaml +5 -5
  2. data/Changelog.md +11 -11
  3. data/LICENSE.txt +1 -1
  4. data/README.md +39 -9
  5. data/Rakefile +12 -5
  6. data/lib/surrounded/access_control.rb +19 -26
  7. data/lib/surrounded/context/forwarding.rb +10 -10
  8. data/lib/surrounded/context/initializing.rb +10 -7
  9. data/lib/surrounded/context/name_collision_detector.rb +17 -17
  10. data/lib/surrounded/context/negotiator.rb +13 -13
  11. data/lib/surrounded/context/role_builders.rb +8 -7
  12. data/lib/surrounded/context/role_map.rb +20 -13
  13. data/lib/surrounded/context/seclusion.rb +20 -0
  14. data/lib/surrounded/context/trigger_controls.rb +14 -16
  15. data/lib/surrounded/context.rb +64 -72
  16. data/lib/surrounded/east_oriented.rb +4 -4
  17. data/lib/surrounded/exceptions.rb +1 -1
  18. data/lib/surrounded/shortcuts.rb +15 -5
  19. data/lib/surrounded/version.rb +2 -2
  20. data/lib/surrounded.rb +7 -7
  21. data/surrounded.gemspec +21 -16
  22. data/test/{casting_role_player_test.rb → casting_test_helper.rb} +4 -3
  23. data/test/collection_role_players_test.rb +16 -16
  24. data/test/context_access_test.rb +31 -30
  25. data/test/context_forwarding_test.rb +30 -30
  26. data/test/context_reuse_test.rb +14 -14
  27. data/test/context_shortcuts_test.rb +38 -11
  28. data/test/east_oriented_triggers_test.rb +14 -13
  29. data/test/example_delegate_class_test.rb +8 -8
  30. data/test/example_proxy_test.rb +25 -23
  31. data/test/example_threaded_test.rb +13 -13
  32. data/test/example_wrapper_test.rb +7 -7
  33. data/test/initialization_test.rb +25 -26
  34. data/test/name_collisions_test.rb +48 -42
  35. data/test/non_surrounded_role_player_test.rb +8 -8
  36. data/test/override_methods_test.rb +9 -9
  37. data/test/role_context_method_test.rb +144 -98
  38. data/test/surrounded_context_test.rb +71 -62
  39. data/test/surrounded_test.rb +13 -15
  40. data/test/test_helper.rb +8 -5
  41. data/test/threaded_context_test.rb +70 -0
  42. metadata +9 -52
  43. data/.codeclimate.yml +0 -4
  44. data/.gitignore +0 -19
  45. data/.pullreview.yml +0 -4
  46. data/.simplecov +0 -3
  47. data/.travis.yml +0 -19
  48. data/Gemfile +0 -10
  49. data/examples/bottles.rb +0 -145
  50. data/examples/rails.rb +0 -57
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 32e91c5123e8db80ef2b15289144c2cec4f5a774
4
- data.tar.gz: 8ffdd46f4497eee0fc0c96f29de1efb0631879d6
2
+ SHA256:
3
+ metadata.gz: e6cd923340dae45ac571dd98abf41be26d111f4569c59d98c27cd4e71f6204d1
4
+ data.tar.gz: a5d11c76eccb01b30fdec8aaced90571449d96c21fe4eced13e55dc1603241da
5
5
  SHA512:
6
- metadata.gz: a2c8b8e5b137b7b8b74c2642cdc490bbcab860dcd85ca0a08e9b67490ce0f1ff4c5b405821bb3daa1e3a643514e75023265cc902b0617a3cff1bee90a61a2480
7
- data.tar.gz: a25fd8d9c292be440405e732ce59f5087a0100a628a0f424ad95a918f72e924377c8485eeeec4a70fa1d01dd900ad5828530ee693ab252d1b56ebfa569aa16ad
6
+ metadata.gz: 90659aa84b6565797f5b45fb4b637d0dfa21190dfed5366c20b586c4351ca1d0f02fdc5f8d6c45620ac6c9b36dccac28a842ce74b93a2d0a9258297db3d9e0e4
7
+ data.tar.gz: 4831ec1fe6a7dda8a9578776ecb0b2e2030ff37490d15f7e7f35351253bfe7f2c52c9ae1aa8fb7172cb471236b3d5c639f7eae8ef8c5510668a56542ba0fe014
data/Changelog.md CHANGED
@@ -1,19 +1,19 @@
1
- # Change Log
1
+ # Changelog
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
- ## [1.0.0]
5
+ The format is based on [Keep a Changelog](http://keepachangelog.com/)
6
+ and this project adheres to [Semantic Versioning](http://semver.org/).
6
7
 
7
- - Drop deprecations around Context initialize method. It now requires keyword arguments. Non-keyword argumennts may be used with initialize_without_keywords
8
- - Remove code supporting exception cause it InvalidRoleType prior to ruby 2.1
8
+ ## [1.1.1] - 2025-10-17
9
9
 
10
- ## [0.9.11]
10
+ ### Changed
11
11
 
12
- - Rely on the standard library Forwardable to setup how the RoleMap forwards messages to the container.
13
- - Update RoleMap role_player? method to rescue from StandardError, a non-implementation-specific exception.
14
- - Move to using triad 0.3.0 which relies on concurrent-ruby 0.9+ and moves off of thread_safe 0.3.5
12
+ - Version and changelog management with Reissue. (c919a3a)
13
+ - Set the gemspec to use a file list rather than depending on git (69b9a43)
14
+ - Update singularize_name to duplicate a string before altering the value. (3487d0d)
15
+ - Alter the list of identity methods in Negotiator to include __id__. (b98fb5a)
15
16
 
16
- ## [0.9.10]
17
+ ### Removed
17
18
 
18
- - Do something with name collisions when a role player has an existing method of another role in the context.
19
- - Move InvalidRoleType exception under the host context class namespace. This allows you to rescue from your own namespace.
19
+ - Code Climate and Pull Review configuration. (46f0057)
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2013-2016 'Jim Gay'
1
+ Copyright (c) 2013-2025 'Jim Gay'
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # ![Surrounded](http://saturnflyer.github.io/surrounded/images/surrounded.png "Surrounded")
2
+
2
3
  ## Be in control of business logic.
3
4
 
4
- [![Build Status](https://travis-ci.org/saturnflyer/surrounded.png?branch=master)](https://travis-ci.org/saturnflyer/surrounded)
5
- [![Code Climate](https://codeclimate.com/github/saturnflyer/surrounded.png)](https://codeclimate.com/github/saturnflyer/surrounded)
5
+ [![Build Status](https://github.com/saturnflyer/surrounded/actions/workflows/test.yml/badge.svg)](https://github.com/saturnflyer/surrounded/actions)
6
6
 
7
7
  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.
8
8
 
@@ -53,13 +53,13 @@ There are 2 things left to do:
53
53
 
54
54
  Initializing contexts does not require the use of keyword arguments, but you may opt out.
55
55
 
56
- You should consider using explicit names when initialize now by using `initialize_without_keywords`:
56
+ You should consider using explicit names when initializing now by using `initialize_without_keywords`:
57
57
 
58
58
  ```ruby
59
59
  class Employment
60
60
  extend Surrounded::Context
61
61
 
62
- initialize_withou_keywords :employee, :boss
62
+ initialize_without_keywords :employee, :boss
63
63
  end
64
64
 
65
65
  user1 = User.find(1)
@@ -636,6 +636,22 @@ class Organization
636
636
  end
637
637
  ```
638
638
 
639
+ If you want to change the way the singular verson of a role is used, override `singularize_name`:
640
+
641
+ ```ruby
642
+ class Organization
643
+ extend Surrounded::Context
644
+
645
+ def singularize_name(name)
646
+ if name == "my special rule"
647
+ # do your thing
648
+ else
649
+ super # use the default
650
+ end
651
+ end
652
+ end
653
+ ```
654
+
639
655
  ## Reusing context objects
640
656
 
641
657
  If you create a context object and need to use the same type of object with new role players, you may use the `rebind` method. It will clear any instance_variables from your context object and map the given objects to their names:
@@ -824,7 +840,7 @@ context = ActiviatingAccount.new(some_object, some_account)
824
840
  context.triggers # => lists a Set of triggers
825
841
  # when using protect_triggers
826
842
  context.triggers # => lists a Set of triggers which may currently be called
827
- context.triggers # => lists a Set of all triggers (the same as if protect_triggers was _not_ used)
843
+ context.all_triggers # => lists a Set of all triggers (the same as if protect_triggers was _not_ used)
828
844
  context.allow?(:trigger_name) # => returns a boolean if the trigger may be run
829
845
 
830
846
  # reuse the context object with new role players
@@ -835,6 +851,20 @@ context.rebind(activator: another_object, account: another_account)
835
851
 
836
852
  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.
837
853
 
854
+ If you want to override the class used for mapping roles to behaviors, override the `role_map` method.
855
+
856
+ ```ruby
857
+ class MyContext
858
+ extend Surrounded::Context
859
+
860
+ def role_map
861
+ @container ||= role_mapper_class.new(base: MySpecialDataContainer)
862
+ end
863
+ end
864
+ ```
865
+
866
+ The class you provide will be initialized with `new` and is expected to implement the methods: `:update`, `:each`, `:values`, and `:keys`.
867
+
838
868
  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.
839
869
 
840
870
  ## Support for other ways to apply behavior
@@ -890,7 +920,7 @@ end
890
920
 
891
921
  If you'd like to use a special approach for just a single role, you may do that too.
892
922
 
893
- 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.
923
+ 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.
894
924
 
895
925
  ```ruby
896
926
  class MyCustomContext
@@ -918,9 +948,9 @@ class MyCustomContext
918
948
  end
919
949
  ```
920
950
 
921
- 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`.
951
+ You can remember the method name by the convention that `remove` or `apply` describes it's function, `behavior` refers to the first argument (the constant holding the behaviors), and then the name of the role which refers to the role playing object: `remove_behavior_role`.
922
952
 
923
- ##Name collisions between methods and roles
953
+ ## Name collisions between methods and roles
924
954
 
925
955
  Lets say that you wish to create a context as below, intending to use instances of the following two classes as role players:
926
956
 
@@ -952,7 +982,6 @@ Lets say that you wish to create a context as below, intending to use instances
952
982
  postcode.send
953
983
  end
954
984
 
955
- end
956
985
  role :postcode do
957
986
  def send
958
987
  # do things...
@@ -961,6 +990,7 @@ Lets say that you wish to create a context as below, intending to use instances
961
990
  end
962
991
  end
963
992
  ```
993
+
964
994
  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.
965
995
 
966
996
  To address this issue, use `on_name_collision` to specify the name of a method to use when collisions are found:
data/Rakefile CHANGED
@@ -1,17 +1,24 @@
1
1
  require "bundler/gem_tasks"
2
2
 
3
- require 'rake/testtask'
3
+ require "rake/testtask"
4
4
 
5
5
  Rake::TestTask.new do |t|
6
6
  t.libs << "test"
7
- t.test_files = FileList['test/*_test.rb']
8
- t.ruby_opts = ["-w"]
7
+ t.test_files = FileList["test/*_test.rb"]
9
8
  t.verbose = true
9
+ t.warning = true
10
10
  end
11
- task :default => :test
12
11
 
12
+ task default: :test
13
+
14
+ require "reissue/gem"
15
+
16
+ Reissue::Task.create :reissue do |task|
17
+ task.version_file = "lib/surrounded/version.rb"
18
+ task.fragment = :git
19
+ end
13
20
 
14
21
  # task :mutant, [:class] do |task, args|
15
22
  # klass = args[:class] || 'Surrounded'
16
23
  # sh "bundle exec mutant --include lib --require surrounded --use minitest #{klass}"
17
- # end
24
+ # end
@@ -2,75 +2,68 @@ module Surrounded
2
2
  module Context
3
3
  class AccessError < ::StandardError; end
4
4
  end
5
+
5
6
  module AccessControl
6
7
  def self.extended(base)
7
8
  base.send(:include, AccessMethods)
8
9
  Surrounded::Exceptions.define(base, exceptions: :AccessError)
9
10
  end
10
-
11
+
11
12
  private
12
-
13
+
13
14
  def disallow(*names, &block)
14
15
  names.map do |name|
15
16
  define_access_method(name, &block)
16
17
  end
17
18
  end
18
- alias guard disallow
19
-
19
+ alias_method :guard, :disallow
20
+
20
21
  def trigger_return_content(name, *args, &block)
21
22
  %{
22
23
 
23
24
  method_restrictor = "disallow_#{name}?"
24
25
  if self.respond_to?(method_restrictor, true) && self.send(method_restrictor)
25
- raise ::#{self.to_s}::AccessError.new("access to #{self.name}##{name} is not allowed")
26
+ raise ::#{self}::AccessError.new("access to #{self.name}##{name} is not allowed")
26
27
  end
27
28
 
28
29
  #{super}
29
30
  }
30
31
  end
31
-
32
+
32
33
  def define_access_method(name, &block)
33
34
  mod = Module.new
34
35
  mod.class_eval {
35
36
  define_method "disallow_#{name}?" do
36
- begin
37
- apply_behaviors
38
- instance_exec(&block)
39
- ensure
40
- remove_behaviors
41
- end
37
+ apply_behaviors
38
+ instance_exec(&block)
39
+ ensure
40
+ remove_behaviors
42
41
  end
43
42
  }
44
43
  const_set("SurroundedAccess#{name}", mod)
45
44
  include mod
46
45
  end
47
-
46
+
48
47
  module AccessMethods
49
48
  # Return a Set of all defined triggers regardless of any disallow blocks
50
49
  def all_triggers
51
50
  self.class.triggers
52
51
  end
53
-
52
+
54
53
  # Return a Set of triggers which may be run according to any restrictions defined
55
54
  # in disallow blocks.
56
55
  def triggers
57
- all_triggers.select {|name|
58
- method_restrictor = "disallow_#{name}?"
59
- !self.respond_to?(method_restrictor, true) || !self.send(method_restrictor)
56
+ all_triggers.select { |name|
57
+ allow?(name)
60
58
  }.to_set
61
59
  end
62
60
 
63
61
  # Ask if the context will allow access to a trigger given the current players.
64
62
  def allow?(name)
65
- unless self.respond_to?(name)
66
- raise NoMethodError, %{undefined method `#{name}' for #{self.inspect}}
67
- end
68
- if self.respond_to?("disallow_#{name}?")
69
- !self.public_send("disallow_#{name}?")
70
- else
71
- true
72
- end
63
+ raise NoMethodError, %(undefined method `#{name}' for #{inspect}) unless respond_to?(name)
64
+ predicate = "disallow_#{name}?"
65
+ !respond_to?(predicate) || !public_send(predicate)
73
66
  end
74
67
  end
75
68
  end
76
- end
69
+ end
@@ -1,27 +1,27 @@
1
1
  module Surrounded
2
2
  module Context
3
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 |*args, &block|
7
- self.send(receiver).public_send(message,*args, &block)
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 |*args, **kwargs, &block|
7
+ send(receiver).public_send(message, *args, **kwargs, &block)
8
8
  end
9
9
  end
10
-
10
+
11
11
  def forward_triggers(receiver, *messages)
12
12
  messages.each do |message|
13
13
  forward_trigger(receiver, message)
14
14
  end
15
15
  end
16
-
16
+
17
17
  def forwarding(hash)
18
18
  hash.each { |messages, receiver|
19
19
  forward_triggers(receiver, *messages)
20
20
  }
21
21
  end
22
-
23
- alias forward forward_trigger
24
- alias forwards forward_triggers
22
+
23
+ alias_method :forward, :forward_trigger
24
+ alias_method :forwards, :forward_triggers
25
25
  end
26
26
  end
27
- end
27
+ end
@@ -1,19 +1,21 @@
1
1
  module Surrounded
2
2
  module Context
3
3
  module Initializing
4
+ extend Seclusion
5
+
4
6
  # Shorthand for creating an instance level initialize method which
5
7
  # handles the mapping of the given arguments to their named role.
6
8
  def initialize_without_keywords(*setup_args, &block)
7
- parameters = setup_args.join(',')
9
+ parameters = setup_args.join(",")
8
10
  default_initializer(parameters, setup_args, &block)
9
11
  end
10
12
 
11
13
  def initialize(*setup_args, &block)
12
- parameters = setup_args.map{|a| "#{a}:"}.join(',')
14
+ parameters = setup_args.map { |a| "#{a}:" }.join(",")
13
15
  default_initializer(parameters, setup_args, &block)
14
16
  end
15
- alias keyword_initialize initialize
16
- alias initialize_with_keywords keyword_initialize
17
+ alias_method :keyword_initialize, :initialize
18
+ alias_method :initialize_with_keywords, :keyword_initialize
17
19
 
18
20
  def initializer_block
19
21
  @initializer_block
@@ -30,15 +32,16 @@ module Surrounded
30
32
  line = __LINE__; mod.class_eval %{
31
33
  def initialize(#{params})
32
34
  @role_map = role_mapper_class.new
33
- @initializer_arguments = Hash[#{setup_args.to_s}.zip([#{setup_args.join(',')}])]
35
+ @initializer_arguments = Hash[#{setup_args}.zip([#{setup_args.join(",")}])]
34
36
  map_roles(@initializer_arguments)
35
37
  self.class.apply_initializer_block(self)
36
38
  end
37
39
  }, __FILE__, line
38
- const_set("ContextInitializer", mod)
40
+ const_set(:ContextInitializer, mod)
39
41
  include mod
42
+
40
43
  private_attr_reader :initializer_arguments
41
44
  end
42
45
  end
43
46
  end
44
- end
47
+ end
@@ -1,56 +1,56 @@
1
1
  module Surrounded
2
2
  module Context
3
- class NameCollisionError <::StandardError; end
4
- module NameCollisionDetector
3
+ class NameCollisionError < ::StandardError; end
5
4
 
5
+ module NameCollisionDetector
6
6
  attr_reader :handler
7
7
 
8
8
  def self.extended(base)
9
9
  base.send :include, NameCollisionHandler
10
10
  Surrounded::Exceptions.define(base, exceptions: :NameCollisionError)
11
11
  end
12
-
12
+
13
13
  def on_name_collision(method_name)
14
14
  @handler = method_name
15
15
  end
16
16
 
17
17
  module NameCollisionHandler
18
-
19
18
  private
20
-
19
+
21
20
  def detect_collisions(role_object_map)
22
21
  if handler
23
22
  handle_collisions(collision_warnings(role_object_map))
24
23
  end
25
24
  end
26
-
25
+
27
26
  def collision_warnings(role_object_map)
28
- role_object_map.select{|role, object|
27
+ role_object_map.select { |role, object|
29
28
  ![object.methods & role_object_map.keys].flatten.empty?
30
- }.map{|role, object|
31
- role_collision_message(role,(object.methods & role_object_map.keys).sort)
29
+ }.map { |role, object|
30
+ role_collision_message(role, (object.methods & role_object_map.keys).sort)
32
31
  }.join("\n")
33
32
  end
34
-
33
+
35
34
  def handle_collisions(collisions)
36
35
  handler_args = [collisions]
37
36
  if handler == :raise
38
37
  handler_args.unshift self.class::NameCollisionError
39
38
  end
40
-
39
+
41
40
  handler_method.call(*handler_args)
42
41
  end
43
-
42
+
44
43
  def role_collision_message(role, colliding_method_names)
45
44
  "#{role} has name collisions with #{colliding_method_names}"
46
45
  end
47
-
48
- def nothing(*); end
46
+
47
+ def nothing(*)
48
+ end
49
49
 
50
50
  def handler
51
51
  self.class.handler
52
52
  end
53
-
53
+
54
54
  def handler_method
55
55
  if handler.respond_to?(:call)
56
56
  handler
@@ -59,10 +59,10 @@ module Surrounded
59
59
  elsif self.class.respond_to?(handler, true)
60
60
  self.class.method(handler)
61
61
  else
62
- raise ArgumentError, %{your name collision handler was set to `#{handler}' but there is no instance nor class method of that name}
62
+ raise ArgumentError, %(your name collision handler was set to `#{handler}' but there is no instance nor class method of that name)
63
63
  end
64
64
  end
65
65
  end
66
66
  end
67
67
  end
68
- end
68
+ end
@@ -14,24 +14,24 @@ module Surrounded
14
14
  # For each method in the module, directly forward to the wrapped object to
15
15
  # circumvent method_missing
16
16
  mod.instance_methods(false).each do |meth|
17
- num = __LINE__; klass.class_eval %{
18
- def #{meth}(*args, &block)
19
- __behaviors__.instance_method(:#{meth}).bind(@object).call(*args, &block)
17
+ klass.class_eval <<~MOD, __FILE__, __LINE__ + 1
18
+ def #{meth}(...)
19
+ @#{meth}_method ||= __behaviors__.instance_method(:#{meth}).bind(@object)
20
+ @#{meth}_method.call(...)
20
21
  end
21
- }, __FILE__, num
22
+ MOD
22
23
  end
23
24
  klass
24
25
  end
25
26
  end
26
27
 
27
-
28
- identity = %w[__send__ object_id equal?]
28
+ identity = %w[__send__ __id__ object_id equal?]
29
29
  method_access = %w[respond_to? method __behaviors__]
30
30
 
31
- reserved_methods = (identity + method_access).join('|')
31
+ reserved_methods = (identity + method_access).join("|")
32
32
 
33
33
  # Remove all methods except the reserved methods
34
- instance_methods.reject{ |m|
34
+ instance_methods.reject { |m|
35
35
  m.to_s =~ /#{reserved_methods}/
36
36
  }.each do |meth|
37
37
  undef_method meth
@@ -51,19 +51,19 @@ module Surrounded
51
51
  self
52
52
  end
53
53
  # These only differ in the message they send
54
- alias remove_context store_context
54
+ alias_method :remove_context, :store_context
55
55
 
56
56
  def initialize(object)
57
57
  @object = object
58
58
  end
59
59
 
60
- def method_missing(meth, *args, &block)
61
- @object.send(meth, *args, &block)
60
+ def method_missing(meth, ...)
61
+ @object.send(meth, ...)
62
62
  end
63
63
 
64
- def respond_to_missing?(meth, include_private=false)
64
+ def respond_to_missing?(meth, include_private = false)
65
65
  @object.respond_to?(meth, include_private)
66
66
  end
67
67
  end
68
68
  end
69
- end
69
+ end
@@ -3,13 +3,14 @@ module Surrounded
3
3
  class InvalidRoleType < ::StandardError; end
4
4
 
5
5
  module RoleBuilders
6
+ extend Seclusion
6
7
 
7
8
  def self.extended(base)
8
9
  Surrounded::Exceptions.define(base, exceptions: :InvalidRoleType)
9
10
  end
10
11
 
11
12
  # Define behaviors for your role players
12
- def role(name, type=default_role_type, &block)
13
+ def role(name, type = default_role_type, &block)
13
14
  if type == :module
14
15
  mod_name = RoleName(name)
15
16
  mod = Module.new(&block).send(:include, ::Surrounded)
@@ -25,7 +26,7 @@ module Surrounded
25
26
 
26
27
  # Create a named behavior for a role using the standard library SimpleDelegator.
27
28
  def wrap(name, &block)
28
- require 'delegate'
29
+ require "delegate"
29
30
  wrapper_name = RoleName(name)
30
31
  klass = private_const_set(wrapper_name, Class.new(SimpleDelegator, &block))
31
32
  klass.send(:include, Surrounded)
@@ -35,7 +36,7 @@ module Surrounded
35
36
  # Create a named behavior for a role using the standard library DelegateClass.
36
37
  # This ties the implementation of the role to a specific class or module API.
37
38
  def delegate_class(name, class_name, &block)
38
- require 'delegate'
39
+ require "delegate"
39
40
  wrapper_name = RoleName(name)
40
41
  klass = private_const_set(wrapper_name, DelegateClass(Object.const_get(class_name.to_s)))
41
42
  klass.class_eval(&block)
@@ -52,19 +53,19 @@ module Surrounded
52
53
  # access them from any of its methods.
53
54
  def interface(name, &block)
54
55
  # AdminInterface
55
- interface_name = RoleName(name, 'Interface')
56
+ interface_name = RoleName(name, "Interface")
56
57
  behavior = private_const_set(interface_name, Module.new(&block))
57
58
 
58
- require 'surrounded/context/negotiator'
59
+ require "surrounded/context/negotiator"
59
60
  # Admin
60
61
  private_const_set(RoleName(name), Negotiator.for_role(behavior))
61
62
  end
62
63
 
63
64
  private
64
- def RoleName(text, suffix=nil)
65
+
66
+ def RoleName(text, suffix = nil)
65
67
  RoleName.new(text, suffix)
66
68
  end
67
-
68
69
  end
69
70
  end
70
71
  end
@@ -1,41 +1,48 @@
1
- require 'triad'
2
- require 'forwardable'
1
+ require "triad"
2
+ require "forwardable"
3
3
  module Surrounded
4
4
  module Context
5
5
  class RoleMap
6
6
  extend Forwardable
7
7
 
8
8
  class << self
9
- def from_base(klass=::Triad)
10
- role_mapper = Class.new(self)
11
- Surrounded::Exceptions.define(role_mapper, exceptions: :ItemNotPresent, namespace: klass)
12
- role_mapper.container_class=(klass)
13
- role_mapper.def_delegators :container, :update, :each, :values, :keys
14
- role_mapper
9
+ # Get the role map container and provide an alternative if desired
10
+ # Ex: RoleMap.from_base(SomeCustomContainer)
11
+ def from_base(klass = ::Triad)
12
+ unless const_defined?(:Container)
13
+ role_mapper = Class.new(self)
14
+ role_mapper.container_class = (klass)
15
+ Surrounded::Exceptions.define(role_mapper, exceptions: :ItemNotPresent, namespace: klass)
16
+ const_set(:Container, role_mapper)
17
+ end
18
+ const_get(:Container)
15
19
  end
16
20
 
17
- def container_class=(klass)
18
- @container_class = klass
19
- end
21
+ attr_writer :container_class
20
22
  end
21
23
 
24
+ def_delegators :container, :update, :each, :values, :keys
25
+
22
26
  def container
23
27
  @container ||= self.class.instance_variable_get(:@container_class).new
24
28
  end
25
29
 
30
+ # Check if a role exists in the map
26
31
  def role?(role)
27
32
  keys.include?(role)
28
33
  end
29
34
 
35
+ # Check if an object is playing a role in this map
30
36
  def role_player?(object)
31
37
  !values(object).empty?
32
- rescue ::StandardError
38
+ rescue container.class::ItemNotPresent
33
39
  false
34
40
  end
35
41
 
42
+ # Get the object playing the given role
36
43
  def assigned_player(role)
37
44
  values(role).first
38
45
  end
39
46
  end
40
47
  end
41
- end
48
+ end
@@ -0,0 +1,20 @@
1
+ module Surrounded
2
+ module Context
3
+ module Seclusion
4
+ # Set a named constant and make it private
5
+ def private_const_set(name, const)
6
+ unless role_const_defined?(name)
7
+ const = const_set(name, const)
8
+ private_constant name.to_sym
9
+ end
10
+ const
11
+ end
12
+
13
+ # Create attr_reader for the named methods and make them private
14
+ def private_attr_reader(*method_names)
15
+ attr_reader(*method_names)
16
+ private(*method_names)
17
+ end
18
+ end
19
+ end
20
+ end