surrounded 0.9.11 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/examples/bottles.rb CHANGED
@@ -1,145 +1,135 @@
1
- require 'surrounded'
1
+ require "surrounded"
2
+ class CountdownSong
3
+ def initialize(max:, min:)
4
+ @max = max
5
+ @min = min
6
+ end
7
+ attr_reader :max, :min
8
+
9
+ def sing_with(verse_template)
10
+ max.downto(min).map { |num| verse(num, verse_template) }
11
+ end
12
+
13
+ def verse(number, verse_template)
14
+ verse_template.lyrics(number)
15
+ end
16
+ end
17
+
18
+ class BottleVerse
19
+ def self.lyrics(number)
20
+ new(bottle_number: number).lyrics
21
+ end
2
22
 
3
- class Countdown
4
23
  extend Surrounded::Context
5
-
6
- initialize :singer, :number, :location
7
-
8
- trigger :start do
9
- singer.start
10
- end
11
-
12
- trigger :continue do
13
- singer.continue
14
- end
15
-
16
- trigger :finish do
17
- singer.announce_status
18
- end
19
-
20
- role :singer do
21
- def start
22
- announce_full_status
23
- take_action
24
- end
25
-
26
- def continue
27
- announce_status
28
- pause
29
- start
30
- end
31
-
32
- def announce_full_status
33
- output %{#{location.status}, #{location.inventory}}.capitalize
34
- end
35
-
36
- def announce_status
37
- output %{#{location.status}}.capitalize
24
+
25
+ initialize :bottle_number
26
+
27
+ trigger :lyrics do
28
+ "#{bottle_number} of beer on the wall, ".capitalize +
29
+ "#{bottle_number} of beer.\n" +
30
+ "#{bottle_number.action}, " +
31
+ "#{bottle_number.successor} of beer on the wall.\n"
32
+ end
33
+
34
+ role :bottle_number, :wrapper do
35
+ def self.handles(number)
36
+ BottleVerse.register_bottle_role(number, self)
38
37
  end
39
-
40
- def take_action
41
- if location.empty?
42
- output %{Go to the store and get some more}
43
- next_part.finish
44
- else
45
- output %{#{location.subtraction}, pass it around}.capitalize
46
- next_part.continue
47
- end
38
+
39
+ def to_s
40
+ "#{quantity} #{container}"
48
41
  end
49
-
50
- def pause
51
- output ""
42
+
43
+ def quantity
44
+ __getobj__.to_s
52
45
  end
53
-
54
- def next_part
55
- context.class.new(singer, number.pred, location)
46
+
47
+ def container
48
+ "bottles"
56
49
  end
57
- end
58
-
59
- role :number, :wrap do
60
- def name
61
- self.zero? ? 'no more' : to_i
50
+
51
+ def action
52
+ "Take #{pronoun} down and pass it around"
62
53
  end
63
-
54
+
64
55
  def pronoun
65
- self == 1 ? 'it' : 'one'
66
- end
67
-
68
- def container
69
- self == 1 ? 'bottle' : 'bottles'
56
+ "one"
70
57
  end
71
-
72
- def pred
73
- self.zero? ? 99 : super
58
+
59
+ def successor
60
+ context.bottle_role_player(pred)
74
61
  end
75
62
  end
76
-
77
- role :location do
78
- def empty?
79
- number.zero?
80
- end
81
-
82
- def inventory
83
- %{#{number.name} #{number.container} of beer}
84
- end
85
-
86
- def status
87
- %{#{inventory} #{placement} #{name}}
88
- end
89
-
90
- def subtraction
91
- %{take #{number.pronoun} #{removal_strategy}}
92
- end
63
+
64
+ # Inherit from existing role
65
+ def self.bottle_role(name, &block)
66
+ mod_name = RoleName(name)
67
+ klass = Class.new(BottleNumber, &block)
68
+ const_set(mod_name, klass)
93
69
  end
94
- end
95
70
 
96
- class Location
97
- include Surrounded
98
-
99
- def placement
100
- 'on'
71
+ def self.register_bottle_role(number, klass)
72
+ @@registry ||= Hash.new { BottleNumber }
73
+ @@registry[number] = klass
101
74
  end
102
-
103
- def removal_strategy
104
- 'off'
75
+
76
+ def bottle_role_for(number)
77
+ @@registry[number]
105
78
  end
106
-
107
- def name
108
- "the " + self.class.to_s.downcase
79
+
80
+ def map_role_bottle_number(num)
81
+ map_role(:bottle_number, bottle_role_for(num), num)
109
82
  end
110
- end
111
83
 
112
- class Wall < Location
113
- def removal_strategy
114
- 'down'
84
+ def bottle_role_player(number)
85
+ bottle_role_for(number).new(number)
115
86
  end
116
- end
117
87
 
118
- class Box < Location
119
- def placement
120
- 'in'
88
+ role :bottle_number_0, :bottle_role do
89
+ handles 0
90
+
91
+ def quantity
92
+ "no more"
93
+ end
94
+
95
+ def action
96
+ "Go to the store and buy some more"
97
+ end
98
+
99
+ def successor
100
+ context.bottle_role_player(99)
101
+ end
121
102
  end
122
-
123
- def removal_strategy
124
- 'out'
103
+
104
+ role :bottle_number_1, :bottle_role do
105
+ handles 1
106
+
107
+ def container
108
+ "bottle"
109
+ end
110
+
111
+ def pronoun
112
+ "it"
113
+ end
125
114
  end
126
115
  end
127
116
 
128
- class Chorus
129
- include Surrounded
130
- def output(value)
131
- STDOUT.puts(value)
117
+ class Bottles
118
+ def song_template(upper: 99, lower: 0)
119
+ CountdownSong.new(max: upper, min: lower)
132
120
  end
133
- end
134
121
 
135
- class Sheet
136
- include Surrounded
137
- def output(value)
138
- File.open('bottles.txt', 'a') do |f|
139
- f.puts(value)
140
- end
122
+ def song
123
+ song_template.sing_with(BottleVerse)
124
+ end
125
+
126
+ def verses(upper, lower)
127
+ song_template(upper: upper, lower: lower).sing_with(BottleVerse)
128
+ end
129
+
130
+ def verse(number)
131
+ song_template.verse(number, BottleVerse)
141
132
  end
142
133
  end
143
134
 
144
- Countdown.new(Chorus.new, 3, Wall.new).start
145
- # Countdown.new(Sheet.new, 3, Box.new).start
135
+ puts Bottles.new.song
@@ -33,12 +33,10 @@ module Surrounded
33
33
  mod = Module.new
34
34
  mod.class_eval {
35
35
  define_method "disallow_#{name}?" do
36
- begin
37
- apply_behaviors
38
- instance_exec(&block)
39
- ensure
40
- remove_behaviors
41
- end
36
+ apply_behaviors
37
+ instance_exec(&block)
38
+ ensure
39
+ remove_behaviors
42
40
  end
43
41
  }
44
42
  const_set("SurroundedAccess#{name}", mod)
@@ -55,22 +53,16 @@ module Surrounded
55
53
  # in disallow blocks.
56
54
  def triggers
57
55
  all_triggers.select {|name|
58
- method_restrictor = "disallow_#{name}?"
59
- !self.respond_to?(method_restrictor, true) || !self.send(method_restrictor)
56
+ allow?(name)
60
57
  }.to_set
61
58
  end
62
59
 
63
60
  # Ask if the context will allow access to a trigger given the current players.
64
61
  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
62
+ raise NoMethodError, %{undefined method `#{name}' for #{self.inspect}} unless self.respond_to?(name)
63
+ predicate = "disallow_#{name}?"
64
+ !self.respond_to?(predicate) || !self.public_send(predicate)
73
65
  end
74
66
  end
75
67
  end
76
- end
68
+ end
@@ -1,22 +1,19 @@
1
1
  module Surrounded
2
2
  module Context
3
3
  module Initializing
4
+ extend Seclusion
4
5
  # Shorthand for creating an instance level initialize method which
5
6
  # handles the mapping of the given arguments to their named role.
6
7
  def initialize_without_keywords(*setup_args, &block)
7
8
  parameters = setup_args.join(',')
8
9
  default_initializer(parameters, setup_args, &block)
9
10
  end
10
- def initialize(*setup_args, &block)
11
- warn "Deprecated: The behavior of 'initialize' will require keywords in the future
12
- Consider using keyword arguments or switching to 'initialize_without_keywords'\n\n"
13
- initialize_without_keywords(*setup_args, &block)
14
- end
15
11
 
16
- def keyword_initialize(*setup_args, &block)
12
+ def initialize(*setup_args, &block)
17
13
  parameters = setup_args.map{|a| "#{a}:"}.join(',')
18
14
  default_initializer(parameters, setup_args, &block)
19
15
  end
16
+ alias keyword_initialize initialize
20
17
  alias initialize_with_keywords keyword_initialize
21
18
 
22
19
  def initializer_block
@@ -45,4 +42,4 @@ module Surrounded
45
42
  end
46
43
  end
47
44
  end
48
- end
45
+ end
@@ -15,8 +15,9 @@ module Surrounded
15
15
  # circumvent method_missing
16
16
  mod.instance_methods(false).each do |meth|
17
17
  num = __LINE__; klass.class_eval %{
18
- def #{meth}(*args, &block)
19
- __behaviors__.instance_method(:#{meth}).bind(@object).call(*args, &block)
18
+ def #{meth}(...)
19
+ @#{meth}_method ||= __behaviors__.instance_method(:#{meth}).bind(@object)
20
+ @#{meth}_method.call(...)
20
21
  end
21
22
  }, __FILE__, num
22
23
  end
@@ -57,8 +58,8 @@ module Surrounded
57
58
  @object = object
58
59
  end
59
60
 
60
- def method_missing(meth, *args, &block)
61
- @object.send(meth, *args, &block)
61
+ def method_missing(meth, ...)
62
+ @object.send(meth, ...)
62
63
  end
63
64
 
64
65
  def respond_to_missing?(meth, include_private=false)
@@ -66,4 +67,4 @@ module Surrounded
66
67
  end
67
68
  end
68
69
  end
69
- end
70
+ end
@@ -1,16 +1,9 @@
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
3
+ class InvalidRoleType < ::StandardError; end
12
4
 
13
5
  module RoleBuilders
6
+ extend Seclusion
14
7
 
15
8
  def self.extended(base)
16
9
  Surrounded::Exceptions.define(base, exceptions: :InvalidRoleType)
@@ -67,7 +60,7 @@ module Surrounded
67
60
  # Admin
68
61
  private_const_set(RoleName(name), Negotiator.for_role(behavior))
69
62
  end
70
-
63
+
71
64
  private
72
65
  def RoleName(text, suffix=nil)
73
66
  RoleName.new(text, suffix)
@@ -75,4 +68,4 @@ module Surrounded
75
68
 
76
69
  end
77
70
  end
78
- end
71
+ end
@@ -6,12 +6,16 @@ module Surrounded
6
6
  extend Forwardable
7
7
 
8
8
  class << self
9
+ # Get the role map container and provide an alternative if desired
10
+ # Ex: RoleMap.from_base(SomeCustomContainer)
9
11
  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
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
21
  def container_class=(klass)
@@ -19,23 +23,28 @@ module Surrounded
19
23
  end
20
24
  end
21
25
 
26
+ def_delegators :container, :update, :each, :values, :keys
27
+
22
28
  def container
23
29
  @container ||= self.class.instance_variable_get(:@container_class).new
24
30
  end
25
31
 
32
+ # Check if a role exists in the map
26
33
  def role?(role)
27
34
  keys.include?(role)
28
35
  end
29
36
 
37
+ # Check if an object is playing a role in this map
30
38
  def role_player?(object)
31
39
  !values(object).empty?
32
- rescue ::StandardError
40
+ rescue self.container.class::ItemNotPresent
33
41
  false
34
42
  end
35
43
 
44
+ # Get the object playing the given role
36
45
  def assigned_player(role)
37
46
  values(role).first
38
47
  end
39
48
  end
40
49
  end
41
- end
50
+ 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
@@ -54,7 +54,7 @@ module Surrounded
54
54
 
55
55
  def define_trigger(name)
56
56
  line = __LINE__; self.class_eval %{
57
- def #{name}(*args, &block)
57
+ def #{name}(...)
58
58
  begin
59
59
  apply_behaviors
60
60
 
@@ -71,7 +71,7 @@ module Surrounded
71
71
  if method_defined?(name)
72
72
  %{super}
73
73
  else
74
- %{self.send("__trigger_#{name}", *args, &block)}
74
+ %{self.send("__trigger_#{name}", ...)}
75
75
  end
76
76
  end
77
77
 
@@ -85,4 +85,4 @@ module Surrounded
85
85
  end
86
86
  end
87
87
  end
88
- end
88
+ end
@@ -1,6 +1,7 @@
1
1
  require 'set'
2
2
  require 'surrounded/exceptions'
3
3
  require 'surrounded/context/role_map'
4
+ require 'surrounded/context/seclusion'
4
5
  require 'surrounded/context/role_builders'
5
6
  require 'surrounded/context/initializing'
6
7
  require 'surrounded/context/forwarding'
@@ -20,7 +21,7 @@ module Surrounded
20
21
  module Context
21
22
  def self.extended(base)
22
23
  base.class_eval {
23
- extend RoleBuilders, Initializing, Forwarding, NameCollisionDetector
24
+ extend Seclusion, RoleBuilders, Initializing, Forwarding, NameCollisionDetector
24
25
 
25
26
  @triggers = Set.new
26
27
  include InstanceMethods
@@ -53,7 +54,7 @@ module Surrounded
53
54
  def default_role_type=(type)
54
55
  @default_role_type = type
55
56
  end
56
-
57
+
57
58
  # Provide the ability to create access control methods for your triggers.
58
59
  def protect_triggers; self.extend(::Surrounded::AccessControl); end
59
60
 
@@ -68,21 +69,6 @@ module Surrounded
68
69
  def role_const_defined?(name)
69
70
  const_defined?(name, false)
70
71
  end
71
-
72
- # Set a named constant and make it private
73
- def private_const_set(name, const)
74
- unless role_const_defined?(name)
75
- const = const_set(name, const)
76
- private_constant name.to_sym
77
- end
78
- const
79
- end
80
-
81
- # Create attr_reader for the named methods and make them private
82
- def private_attr_reader(*method_names)
83
- attr_reader(*method_names)
84
- private(*method_names)
85
- end
86
72
 
87
73
  # Conditional const_get for a named role behavior
88
74
  def role_const(name)
@@ -119,11 +105,14 @@ module Surrounded
119
105
  self.class.triggers
120
106
  end
121
107
 
122
- def rebind(options_hash)
108
+ # Reuse the same context object but pass new values
109
+ def rebind(**options_hash)
123
110
  clear_instance_variables
124
- initialize(**options_hash)
125
- rescue ArgumentError
126
- initialize(*options_hash.values)
111
+ begin
112
+ initialize(**options_hash)
113
+ rescue ArgumentError
114
+ initialize(*options_hash.values)
115
+ end
127
116
  self
128
117
  end
129
118
 
@@ -193,7 +182,7 @@ module Surrounded
193
182
  return obj if !wrapper_name
194
183
  klass.method(wrapper_name).call(obj)
195
184
  end
196
-
185
+
197
186
  def remove_behavior(role, behavior, object)
198
187
  if behavior && role_const_defined?(behavior)
199
188
  remover_name = (module_removal_methods + unwrap_methods).find do |meth|
@@ -306,4 +295,4 @@ module Surrounded
306
295
  end
307
296
  end
308
297
  end
309
- end
298
+ end
@@ -3,9 +3,21 @@ module Surrounded
3
3
  private
4
4
 
5
5
  def define_shortcut(name)
6
- singleton_class.send(:define_method, name) do |*args|
7
- instance = self.new(*args)
8
- instance.public_send(name)
6
+ # if keyword initialize
7
+ if instance_method(:initialize).parameters.dig(0,0) == :keyreq
8
+ singleton_class.send(:define_method, name) do |**args|
9
+ instance = begin
10
+ self.new(**args)
11
+ end
12
+ instance.public_send(name)
13
+ end
14
+ else # non-keyword initialize
15
+ singleton_class.send(:define_method, name) do |*args|
16
+ instance = begin
17
+ self.new(*args)
18
+ end
19
+ instance.public_send(name)
20
+ end
9
21
  end
10
22
  end
11
23
 
@@ -16,4 +28,4 @@ module Surrounded
16
28
  super
17
29
  end
18
30
  end
19
- end
31
+ end
@@ -1,6 +1,6 @@
1
1
  module Surrounded
2
- VERSION = "0.9.11"
3
-
2
+ VERSION = "1.1.0"
3
+
4
4
  def self.version
5
5
  VERSION
6
6
  end
data/surrounded.gemspec CHANGED
@@ -20,6 +20,5 @@ Gem::Specification.new do |spec|
20
20
 
21
21
  spec.add_dependency "triad", "~> 0.3.0"
22
22
 
23
- spec.add_development_dependency "bundler", "~> 1.12"
24
23
  spec.add_development_dependency "rake"
25
24
  end
@@ -39,7 +39,7 @@ describe Surrounded::Context, 'auto-assigning roles for collections' do
39
39
  let(:other_two){ User.new('Jason') }
40
40
  let(:others){ [other_one, other_two] }
41
41
 
42
- let(:context){ CollectionContext.new(members, others) }
42
+ let(:context){ CollectionContext.new(members: members, others: others) }
43
43
 
44
44
  it 'assigns the collection role to collections' do
45
45
  assert_equal members.size, context.get_members_count
@@ -37,7 +37,7 @@ end
37
37
  describe Surrounded::Context, 'access control' do
38
38
  let(:user){ User.new("Jim") }
39
39
  let(:other_user){ User.new("Guille") }
40
- let(:context){ FilteredContext.new(user, other_user) }
40
+ let(:context){ FilteredContext.new(user: user, other_user: other_user) }
41
41
 
42
42
  it 'includes triggers when allowed' do
43
43
  context.stub(:disallow_if_ready?, false) do
@@ -38,7 +38,7 @@ end
38
38
  describe Surrounded::Context, 'forwarding triggers' do
39
39
  let(:user){ User.new("Jim") }
40
40
  let(:other_user){ User.new("Guille") }
41
- let(:context){ Sending.new(user, other_user) }
41
+ let(:context){ Sending.new(one: user, two: other_user) }
42
42
 
43
43
  it 'forwards multiple configured instance methods as triggers' do
44
44
  assert_equal 'hello', context.hello
@@ -3,7 +3,7 @@ require 'test_helper'
3
3
  describe Surrounded::Context, 'reusing context object' do
4
4
  let(:user){ User.new("Jim") }
5
5
  let(:other_user){ User.new("Guille") }
6
- let(:context){ TestContext.new(user, other_user) }
6
+ let(:context){ TestContext.new(user: user, other_user: other_user) }
7
7
 
8
8
  it 'allows rebinding new players' do
9
9
  expect(context.access_other_object).must_equal 'Guille'