adhearsion 0.7.7 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (179) hide show
  1. data/CHANGELOG +9 -42
  2. data/EVENTS +11 -0
  3. data/README.txt +5 -0
  4. data/Rakefile +94 -84
  5. data/adhearsion.gemspec +148 -0
  6. data/app_generators/ahn/USAGE +5 -0
  7. data/app_generators/ahn/ahn_generator.rb +87 -0
  8. data/app_generators/ahn/templates/.ahnrc +34 -0
  9. data/app_generators/ahn/templates/README +8 -0
  10. data/app_generators/ahn/templates/Rakefile +23 -0
  11. data/app_generators/ahn/templates/components/ami_remote/ami_remote.rb +15 -0
  12. data/app_generators/ahn/templates/components/disabled/HOW_TO_ENABLE +7 -0
  13. data/app_generators/ahn/templates/components/disabled/stomp_gateway/README.markdown +47 -0
  14. data/app_generators/ahn/templates/components/disabled/stomp_gateway/config.yml +12 -0
  15. data/app_generators/ahn/templates/components/disabled/stomp_gateway/stomp_gateway.rb +34 -0
  16. data/app_generators/ahn/templates/components/restful_rpc/README.markdown +11 -0
  17. data/app_generators/ahn/templates/components/restful_rpc/config.yml +34 -0
  18. data/app_generators/ahn/templates/components/restful_rpc/example-client.rb +48 -0
  19. data/app_generators/ahn/templates/components/restful_rpc/restful_rpc.rb +87 -0
  20. data/app_generators/ahn/templates/components/restful_rpc/spec/restful_rpc_spec.rb +263 -0
  21. data/app_generators/ahn/templates/components/simon_game/simon_game.rb +56 -0
  22. data/app_generators/ahn/templates/config/startup.rb +50 -0
  23. data/app_generators/ahn/templates/dialplan.rb +3 -0
  24. data/app_generators/ahn/templates/events.rb +32 -0
  25. data/bin/ahn +28 -0
  26. data/bin/ahnctl +68 -0
  27. data/bin/jahn +42 -0
  28. data/examples/asterisk_manager_interface/standalone.rb +51 -0
  29. data/lib/adhearsion.rb +35 -953
  30. data/lib/adhearsion/cli.rb +223 -0
  31. data/lib/adhearsion/component_manager.rb +208 -0
  32. data/lib/adhearsion/component_manager/component_tester.rb +55 -0
  33. data/lib/adhearsion/component_manager/spec_framework.rb +24 -0
  34. data/lib/adhearsion/events_support.rb +84 -0
  35. data/lib/adhearsion/foundation/all.rb +9 -0
  36. data/lib/adhearsion/foundation/blank_slate.rb +5 -0
  37. data/lib/adhearsion/foundation/custom_daemonizer.rb +45 -0
  38. data/lib/adhearsion/foundation/event_socket.rb +203 -0
  39. data/lib/adhearsion/foundation/future_resource.rb +36 -0
  40. data/lib/adhearsion/foundation/global.rb +1 -0
  41. data/lib/adhearsion/foundation/metaprogramming.rb +17 -0
  42. data/lib/adhearsion/foundation/numeric.rb +13 -0
  43. data/lib/adhearsion/foundation/pseudo_guid.rb +10 -0
  44. data/lib/adhearsion/foundation/relationship_properties.rb +42 -0
  45. data/lib/adhearsion/foundation/string.rb +26 -0
  46. data/lib/adhearsion/foundation/synchronized_hash.rb +96 -0
  47. data/lib/adhearsion/foundation/thread_safety.rb +7 -0
  48. data/lib/adhearsion/host_definitions.rb +67 -0
  49. data/lib/adhearsion/initializer.rb +373 -0
  50. data/lib/adhearsion/initializer/asterisk.rb +81 -0
  51. data/lib/adhearsion/initializer/configuration.rb +254 -0
  52. data/lib/adhearsion/initializer/database.rb +49 -0
  53. data/lib/adhearsion/initializer/drb.rb +31 -0
  54. data/lib/adhearsion/initializer/freeswitch.rb +22 -0
  55. data/lib/adhearsion/initializer/rails.rb +40 -0
  56. data/lib/adhearsion/logging.rb +92 -0
  57. data/lib/adhearsion/tasks.rb +16 -0
  58. data/lib/adhearsion/tasks/database.rb +5 -0
  59. data/lib/adhearsion/tasks/deprecations.rb +59 -0
  60. data/lib/adhearsion/tasks/generating.rb +20 -0
  61. data/lib/adhearsion/tasks/lint.rb +4 -0
  62. data/lib/adhearsion/tasks/testing.rb +37 -0
  63. data/lib/adhearsion/version.rb +9 -0
  64. data/lib/adhearsion/voip/asterisk.rb +4 -0
  65. data/lib/adhearsion/voip/asterisk/agi_server.rb +81 -0
  66. data/lib/adhearsion/voip/asterisk/commands.rb +1284 -0
  67. data/lib/adhearsion/voip/asterisk/config_generators/agents.conf.rb +140 -0
  68. data/lib/adhearsion/voip/asterisk/config_generators/config_generator.rb +101 -0
  69. data/lib/adhearsion/voip/asterisk/config_generators/queues.conf.rb +250 -0
  70. data/lib/adhearsion/voip/asterisk/config_generators/voicemail.conf.rb +240 -0
  71. data/lib/adhearsion/voip/asterisk/config_manager.rb +71 -0
  72. data/lib/adhearsion/voip/asterisk/manager_interface.rb +562 -0
  73. data/lib/adhearsion/voip/asterisk/manager_interface/ami_lexer.rb +1754 -0
  74. data/lib/adhearsion/voip/asterisk/manager_interface/ami_lexer.rl.rb +286 -0
  75. data/lib/adhearsion/voip/asterisk/manager_interface/ami_messages.rb +78 -0
  76. data/lib/adhearsion/voip/asterisk/manager_interface/ami_protocol_lexer_machine.rl +87 -0
  77. data/lib/adhearsion/voip/asterisk/special_dial_plan_managers.rb +80 -0
  78. data/lib/adhearsion/voip/asterisk/super_manager.rb +19 -0
  79. data/lib/adhearsion/voip/call.rb +440 -0
  80. data/lib/adhearsion/voip/call_routing.rb +64 -0
  81. data/lib/adhearsion/voip/commands.rb +9 -0
  82. data/lib/adhearsion/voip/constants.rb +39 -0
  83. data/lib/adhearsion/voip/conveniences.rb +18 -0
  84. data/lib/adhearsion/voip/dial_plan.rb +218 -0
  85. data/lib/adhearsion/voip/dsl/dialing_dsl.rb +151 -0
  86. data/lib/adhearsion/voip/dsl/dialing_dsl/dialing_dsl_monkey_patches.rb +37 -0
  87. data/lib/adhearsion/voip/dsl/dialplan/control_passing_exception.rb +27 -0
  88. data/lib/adhearsion/voip/dsl/dialplan/dispatcher.rb +124 -0
  89. data/lib/adhearsion/voip/dsl/dialplan/parser.rb +71 -0
  90. data/lib/adhearsion/voip/dsl/dialplan/thread_mixin.rb +16 -0
  91. data/lib/adhearsion/voip/dsl/numerical_string.rb +117 -0
  92. data/lib/adhearsion/voip/freeswitch/basic_connection_manager.rb +48 -0
  93. data/lib/adhearsion/voip/freeswitch/event_handler.rb +58 -0
  94. data/lib/adhearsion/voip/freeswitch/freeswitch_dialplan_command_factory.rb +129 -0
  95. data/lib/adhearsion/voip/freeswitch/inbound_connection_manager.rb +38 -0
  96. data/lib/adhearsion/voip/freeswitch/oes_server.rb +195 -0
  97. data/lib/adhearsion/voip/menu_state_machine/calculated_match.rb +80 -0
  98. data/lib/adhearsion/voip/menu_state_machine/matchers.rb +123 -0
  99. data/lib/adhearsion/voip/menu_state_machine/menu_builder.rb +58 -0
  100. data/lib/adhearsion/voip/menu_state_machine/menu_class.rb +149 -0
  101. data/lib/theatre.rb +151 -0
  102. data/lib/theatre/README.markdown +64 -0
  103. data/lib/theatre/callback_definition_loader.rb +84 -0
  104. data/lib/theatre/guid.rb +23 -0
  105. data/lib/theatre/invocation.rb +121 -0
  106. data/lib/theatre/namespace_manager.rb +153 -0
  107. data/lib/theatre/version.rb +2 -0
  108. metadata +160 -140
  109. data/.version +0 -1
  110. data/TODO +0 -71
  111. data/ahn +0 -223
  112. data/apps/default/Rakefile +0 -65
  113. data/apps/default/config/adhearsion.sqlite3 +0 -0
  114. data/apps/default/config/adhearsion.yml +0 -95
  115. data/apps/default/config/database.rb +0 -50
  116. data/apps/default/config/database.yml +0 -12
  117. data/apps/default/config/helpers/drb_server.yml +0 -43
  118. data/apps/default/config/helpers/factorial.alien.c.yml +0 -1
  119. data/apps/default/config/helpers/growler.yml +0 -21
  120. data/apps/default/config/helpers/lookup.yml +0 -1
  121. data/apps/default/config/helpers/manager_proxy.yml +0 -8
  122. data/apps/default/config/helpers/micromenus.yml +0 -1
  123. data/apps/default/config/helpers/micromenus/collab.rb +0 -60
  124. data/apps/default/config/helpers/micromenus/images/arrow-off.gif +0 -0
  125. data/apps/default/config/helpers/micromenus/images/arrow-on.gif +0 -0
  126. data/apps/default/config/helpers/micromenus/images/error.gif +0 -0
  127. data/apps/default/config/helpers/micromenus/images/folder-off.gif +0 -0
  128. data/apps/default/config/helpers/micromenus/images/folder-on.gif +0 -0
  129. data/apps/default/config/helpers/micromenus/images/folder.png +0 -0
  130. data/apps/default/config/helpers/micromenus/images/ggbridge.jpg +0 -0
  131. data/apps/default/config/helpers/micromenus/images/green.png +0 -0
  132. data/apps/default/config/helpers/micromenus/images/microbrowser.bg.gif +0 -0
  133. data/apps/default/config/helpers/micromenus/images/red.png +0 -0
  134. data/apps/default/config/helpers/micromenus/images/tux.bmp +0 -0
  135. data/apps/default/config/helpers/micromenus/images/url-off.gif +0 -0
  136. data/apps/default/config/helpers/micromenus/images/url-on.gif +0 -0
  137. data/apps/default/config/helpers/micromenus/images/yellow.png +0 -0
  138. data/apps/default/config/helpers/micromenus/javascripts/animation.js +0 -1341
  139. data/apps/default/config/helpers/micromenus/javascripts/carousel.js +0 -1238
  140. data/apps/default/config/helpers/micromenus/javascripts/columnav.js +0 -306
  141. data/apps/default/config/helpers/micromenus/javascripts/connection.js +0 -965
  142. data/apps/default/config/helpers/micromenus/javascripts/container.js +0 -4727
  143. data/apps/default/config/helpers/micromenus/javascripts/container_core.js +0 -2915
  144. data/apps/default/config/helpers/micromenus/javascripts/dom.js +0 -892
  145. data/apps/default/config/helpers/micromenus/javascripts/dragdrop.js +0 -2958
  146. data/apps/default/config/helpers/micromenus/javascripts/event.js +0 -1771
  147. data/apps/default/config/helpers/micromenus/javascripts/yahoo.js +0 -433
  148. data/apps/default/config/helpers/micromenus/stylesheets/carousel.css +0 -78
  149. data/apps/default/config/helpers/micromenus/stylesheets/columnav.css +0 -135
  150. data/apps/default/config/helpers/micromenus/stylesheets/microbrowsers.css +0 -42
  151. data/apps/default/config/helpers/multi_messenger.yml +0 -9
  152. data/apps/default/config/helpers/weather.yml +0 -1
  153. data/apps/default/config/helpers/xbmc.yml +0 -2
  154. data/apps/default/config/migration.rb +0 -59
  155. data/apps/default/extensions.rb +0 -41
  156. data/apps/default/helpers/factorial.alien.c +0 -32
  157. data/apps/default/helpers/growler.rb +0 -53
  158. data/apps/default/helpers/lookup.rb +0 -44
  159. data/apps/default/helpers/manager_proxy.rb +0 -112
  160. data/apps/default/helpers/micromenus.rb +0 -514
  161. data/apps/default/helpers/multi_messenger.rb +0 -53
  162. data/apps/default/helpers/oscar_wilde_quotes.rb +0 -197
  163. data/apps/default/helpers/weather.rb +0 -85
  164. data/apps/default/helpers/xbmc.rb +0 -39
  165. data/apps/default/logs/adhearsion.log +0 -0
  166. data/apps/default/logs/database.log +0 -0
  167. data/lib/constants.rb +0 -24
  168. data/lib/core_extensions.rb +0 -180
  169. data/lib/drb_server.rb +0 -101
  170. data/lib/logging.rb +0 -85
  171. data/lib/phone_number.rb +0 -85
  172. data/lib/rami.rb +0 -823
  173. data/lib/servlet_container.rb +0 -174
  174. data/lib/sexy_migrations.rb +0 -70
  175. data/test/asterisk_module_test.rb +0 -14
  176. data/test/core_extensions_test.rb +0 -26
  177. data/test/dial_test.rb +0 -43
  178. data/test/specs/numerical_string_spec.rb +0 -53
  179. data/test/test_micromenus.rb +0 -0
@@ -0,0 +1,64 @@
1
+ Theatre
2
+ =======
3
+
4
+ Present status: stable
5
+
6
+ A library for choreographing a dynamic pool of hierarchically organized actors. This was originally extracted from the [Adhearsion](http://adhearsion.com) framework by Jay Phillips.
7
+
8
+ In the Adhearsion framework, it was necessary to develop an internal message-passing system that could work either synchronously or asynchronously. This is used by the framework itself and for framework extensions (called _components_) to talk with each other. The source of the events is completely undefined -- events could originate from within the framework out outside the framework. For example, a Message Queue such as [Stomp](http://stomp.codehaus.org) can wire incoming events into Theatre and catch events going to a particular destination so it can proxy them out to the server.
9
+
10
+ Motivations and Design Decisions
11
+ --------------------------------
12
+
13
+ * Must maintain Ruby 1.8 and JRuby compatibility
14
+ * Must be Thread-safe
15
+ * Must provide some level of transparency into the events going through it
16
+ * Must be dynamic enough to reallocate the number of triggerrs based on load
17
+ * Must help facilitate test-driven development of Actor functionality
18
+ * Must allow external persistence in case of a crash
19
+
20
+ Example
21
+ -------
22
+
23
+ Below is an example taken from Adhearsion for executing framework-level callbacks. Note: the framework treats this callback synchronously.
24
+
25
+ events.framework.asterisk.before_call.each do |event|
26
+ # Pull headers from event and respond to it here.
27
+ end
28
+
29
+ Below is an example of integration with [Stomp](http://stomp.codehaus.org/), a simple yet robust open-protocol message queue.
30
+
31
+ events.stomp.new_call.each do |event|
32
+ # Handle all events from the Stomp MQ server whose name is "new_call" (the String)
33
+ end
34
+
35
+ This will filter all events whose name is "new_call" and yield the Stomp::Message to the block.
36
+
37
+ Framework terminology
38
+ --------------------
39
+
40
+ Below are definitions of terms I use in Theatre. See the respective links for more information.
41
+
42
+ * **callback**: This is the block given to the `each` method which triggers events coming in.
43
+ * **payload**: This is the "message" sent to the Theatre and is what will ultimately be yielded to the callback
44
+ * **[Actor](http://en.wikipedia.org/wiki/Actor_model)**: This refers to concurrent responders to events in a concurrent system.
45
+
46
+ Synchronous vs. Asynchronous
47
+ ----------------------------
48
+
49
+ With Theatre, all events are asynchronous with the optional ability to synchronously block until the event is scheduled, triggered, and has returned. If you wish to synchronously trigger the event, simple call `wait` on an `Invocation` object returned from `trigger` and then check the `Invocation#current_state` property for `:success` or `:error`. Optionally the `Invocation#success?` and `Invocation#error?` methods also provide more intuitive access to the finished state. If the event finished with `:success`, you may retrieve the returned value of the event Proc by calling `Invocation#returned_value`. If the event finished with `:error`, you may get the Exception with `Invocation#error`.
50
+
51
+ Because many callbacks can be registered for a particular namespace, each needing its own Invocation object, the `Theatre#trigger` method returns an Array of Invocation objects.
52
+
53
+ # We'll assume only one callback is registered and call #first on the Array of Invocations returned by #trigger
54
+ invocation = my_theatre.trigger("your/namespace/here", YourSpecialClass.new("this can be anything")).first
55
+ invocation.wait
56
+ raise invocation.error if invocation.error?
57
+ log "Actor finished with return value #{invocation.returned_value}"
58
+
59
+ Ruby 1.8 vs. Ruby 1.9 vs. JRuby
60
+ -------------------------------
61
+
62
+ Theatre was created for Ruby 1.8 because no good Actor system existed on Ruby 1.8 that met Adhearsion's needs (e.g. hierarchal with synchronous and asynchronous modes. If you wish to achieve real processor-level concurrency, use JRuby.
63
+
64
+ Presently Ruby 1.9 compatibility is not a priority but patches for compatibility will be accepted, as long as they preserve compatibility with both MRI and JRuby.
@@ -0,0 +1,84 @@
1
+ module Theatre
2
+
3
+ ##
4
+ # This class provides the a wrapper aroung which an events.rb file can be instance_eval'd.
5
+ #
6
+ class CallbackDefinitionLoader
7
+
8
+ attr_reader :theatre, :root_name
9
+ def initialize(theatre, root_name=:events)
10
+ @theatre = theatre
11
+ @root_name = root_name
12
+
13
+ create_recorder_method root_name
14
+ end
15
+
16
+ def anonymous_recorder
17
+ BlankSlateMessageRecorder.new(&method(:callback_registered))
18
+ end
19
+
20
+ ##
21
+ # Parses the given Ruby source code file and returns this object.
22
+ #
23
+ # @param [String, File] file The filename or File object for the Ruby source code file to parse.
24
+ #
25
+ def load_events_file(file)
26
+ file = File.open(file) if file.kind_of? String
27
+ instance_eval file.read, file.path
28
+ self
29
+ end
30
+
31
+ ##
32
+ # Parses the given Ruby source code and returns this object.
33
+ #
34
+ # NOTE: Only use this if you're generating the code yourself! If you're loading a file from the filesystem, you should
35
+ # use load_events_file() since load_events_file() will properly attribute errors in the code to the file from which the
36
+ # code was loaded.
37
+ #
38
+ # @param [String] code The Ruby source code to parse
39
+ #
40
+ def load_events_code(code)
41
+ instance_eval code
42
+ self
43
+ end
44
+
45
+ protected
46
+
47
+ ##
48
+ # Immediately register the namespace and callback with the Theatre instance given to the constructor. This method is only
49
+ # called when a new BlankSlateMessageRecorder is instantiated and receives #each().
50
+ #
51
+ def callback_registered(namespaces, callback)
52
+ # Get rid of all arguments passed to the namespaces. Will support arguments in the future.
53
+ namespaces = namespaces.map { |namespace| namespace.first }
54
+
55
+ theatre.namespace_manager.register_callback_at_namespace namespaces, callback
56
+ end
57
+
58
+ def create_recorder_method(record_method_name)
59
+ (class << self; self; end).send(:alias_method, record_method_name, :anonymous_recorder)
60
+ end
61
+
62
+ class BlankSlateMessageRecorder
63
+
64
+ (instance_methods - %w[__send__ __id__]).each { |m| undef_method m }
65
+
66
+ def initialize(&notify_on_completion)
67
+ @notify_on_completion = notify_on_completion
68
+ @namespaces = []
69
+ end
70
+
71
+ def method_missing(*method_name_and_args)
72
+ raise ArgumentError, "Supplying a block is not supported" if block_given?
73
+ @namespaces << method_name_and_args
74
+ self
75
+ end
76
+
77
+ def each(&callback)
78
+ @notify_on_completion.call(@namespaces, callback)
79
+ end
80
+
81
+ end
82
+
83
+ end
84
+ end
@@ -0,0 +1,23 @@
1
+ # Right now Adhearsion also defines this method. The eventual solution will be to extract the Adhearsion features on which
2
+ # Theatre depends and make that a dependent library.
3
+
4
+ unless respond_to? :new_guid
5
+
6
+ def random_character
7
+ case random_digit = rand(62)
8
+ when 0...10 : random_digit.to_s
9
+ when 10...36 : (random_digit + 55).chr
10
+ when 36...62 : (random_digit + 61).chr
11
+ end
12
+ end
13
+
14
+ def random_string(length_of_string=8)
15
+ Array.new(length_of_string) { random_character }.join
16
+ end
17
+
18
+ # This GUID implementation doesn't adhere to the RFC which wants to make certain segments based on the MAC address of a
19
+ # network interface card and other wackiness. It's sufficiently random for our needs.
20
+ def new_guid
21
+ [8,4,4,4,12].map { |segment_length| random_string(segment_length) }.join('-')
22
+ end
23
+ end
@@ -0,0 +1,121 @@
1
+ require 'theatre/guid'
2
+ require 'thread'
3
+ require 'monitor'
4
+
5
+ module Theatre
6
+
7
+ ##
8
+ # An Invocation is an object which Theatre generates and returns from Theatre#trigger.
9
+ #
10
+ class Invocation
11
+
12
+ attr_reader :queued_time, :started_time, :finished_time, :unique_id, :callback, :namespace, :error, :returned_value
13
+
14
+ class InvalidStateError < Exception; end
15
+
16
+ ##
17
+ # Create a new Invocation.
18
+ #
19
+ # @param [String] namespace The "/foo/bar/qaz" path to the namespace to which this Invocation belongs.
20
+ # @param [Proc] callback The block which should be executed by an Actor scheduler.
21
+ # @param [Object] payload The message that will be sent to the callback for processing.
22
+ #
23
+ def initialize(namespace, callback, payload=:theatre_no_payload)
24
+ raise ArgumentError, "Callback must be a Proc" unless callback.kind_of? Proc
25
+ @payload = payload
26
+ @unique_id = new_guid.freeze
27
+ @callback = callback
28
+ @current_state = :new
29
+ @state_lock = Mutex.new
30
+
31
+ # Used just to protect access to the @returned_value instance variable
32
+ @returned_value_lock = Monitor.new
33
+
34
+ # Used when wait() is called to notify all waiting threads by using a ConditionVariable
35
+ @returned_value_blocker = Monitor::ConditionVariable.new @returned_value_lock
36
+ end
37
+
38
+ def queued
39
+ with_state_lock do
40
+ raise InvalidStateError unless @current_state == :new
41
+ @current_state = :queued
42
+ @queued_time = Time.now.freeze
43
+ end
44
+ true
45
+ end
46
+
47
+ def current_state
48
+ with_state_lock { @current_state }
49
+ end
50
+
51
+ def start
52
+ with_state_lock do
53
+ raise InvalidStateError unless @current_state == :queued
54
+ @current_state = :running
55
+ end
56
+ @started_time = Time.now.freeze
57
+
58
+ begin
59
+ self.returned_value = if @payload.equal? :theatre_no_payload
60
+ @callback.call
61
+ else
62
+ @callback.call @payload
63
+ end
64
+ with_state_lock { @current_state = :success }
65
+ rescue => @error
66
+ with_state_lock { @current_state = :error }
67
+ ensure
68
+ @finished_time = Time.now.freeze
69
+ end
70
+ end
71
+
72
+ def execution_duration
73
+ return nil unless @finished_time
74
+ @finished_time - @started_time
75
+ end
76
+
77
+ def error?
78
+ current_state.equal? :error
79
+ end
80
+
81
+ def success?
82
+ current_state.equal? :success
83
+ end
84
+
85
+ ##
86
+ # When this Invocation has been queued, started, and entered either the :success or :error state, this method will
87
+ # finally return. Until then, it blocks the Thread.
88
+ #
89
+ # @return [Object] The result of invoking this Invocation's callback
90
+ #
91
+ def wait
92
+ with_returned_value_lock { return @returned_value if defined? @returned_value }
93
+ @returned_value_blocker.wait
94
+ # Return the returned_value
95
+ with_returned_value_lock { @returned_value }
96
+ end
97
+
98
+ protected
99
+
100
+ ##
101
+ # Protected setter which does some other housework when the returned value is found (such as notifying wait()ers)
102
+ #
103
+ # @param [returned_value] The value to set this returned value to.
104
+ #
105
+ def returned_value=(returned_value)
106
+ with_returned_value_lock do
107
+ @returned_value = returned_value
108
+ @returned_value_blocker.broadcast
109
+ end
110
+ end
111
+
112
+ def with_returned_value_lock(&block)
113
+ @returned_value_lock.synchronize(&block)
114
+ end
115
+
116
+ def with_state_lock(&block)
117
+ @state_lock.synchronize(&block)
118
+ end
119
+
120
+ end
121
+ end
@@ -0,0 +1,153 @@
1
+ module Theatre
2
+
3
+ ##
4
+ # Manages the hierarchial namespaces of a Theatre. This class is Thread-safe.
5
+ #
6
+ class ActorNamespaceManager
7
+
8
+ VALID_NAMESPACE = %r{^(/[\w_]+)+$}
9
+
10
+ class << self
11
+ def valid_namespace_path?(namespace_path)
12
+ namespace_path =~ VALID_NAMESPACE
13
+ end
14
+
15
+ ##
16
+ # Since there are a couple ways to represent namespaces, this is a helper method which will normalize
17
+ # them into the most practical: an Array of Symbols
18
+ # @param [String, Array] paths The namespace to register. Can be in "/foo/bar" or *[foo,bar] format
19
+ def normalize_path_to_array(paths)
20
+ paths = paths.is_a?(Array) ? paths.flatten : Array(paths)
21
+ paths.map! { |path_segment| path_segment.kind_of?(String) ? path_segment.split('/') : path_segment }
22
+ paths.flatten!
23
+ paths.reject! { |path| path.nil? || (path.kind_of?(String) && path.empty?) }
24
+ paths.map { |path| path.to_sym }
25
+ end
26
+
27
+ end
28
+
29
+ def initialize
30
+ @registry_lock = Mutex.new
31
+ @root = RootNamespaceNode.new
32
+ end
33
+
34
+ ##
35
+ # Have this registry recognize a new path and prepare it for callback registrations. All path segements will be created
36
+ # in order. For example, when registering "/foo/bar/qaz" when no namespaces at all have been registered, this method will
37
+ # first register "foo", then "bar", then "qaz". If the namespace was already registered, it will not be affected.
38
+ #
39
+ # @param [String, Array] paths The namespace to register. Can be in "/foo/bar" or *[foo,bar] format
40
+ # @return [NamespaceNode] The NamespaceNode representing the path given.
41
+ # @raise NamespaceNotFound if a segment has not been registered yet
42
+ #
43
+ def register_namespace_name(*paths)
44
+ paths = self.class.normalize_path_to_array paths
45
+
46
+ paths.inject(@root) do |node, name|
47
+ node.register_namespace_name name
48
+ end
49
+ end
50
+
51
+ ##
52
+ # Returns a Proc found after searching with the namespace you provide
53
+ #
54
+ # @raise NamespaceNotFound if a segment has not been registered yet
55
+ #
56
+ def callbacks_for_namespaces(*paths)
57
+ search_for_namespace(paths).callbacks
58
+ end
59
+
60
+ ##
61
+ # Find a namespace in the tree.
62
+ #
63
+ # @param [Array, String] paths Must be an Array of segments or a name like "/foo/bar/qaz"
64
+ # @raise NamespaceNotFound if a segment has not been registered yet
65
+ #
66
+ def search_for_namespace(paths)
67
+ paths = self.class.normalize_path_to_array paths
68
+ path_string = "/"
69
+
70
+ found_namespace = paths.inject(@root) do |last_node,this_node_name|
71
+ raise NamespaceNotFound.new(path_string) if last_node.nil?
72
+ path_string << this_node_name.to_s
73
+ last_node.child_named this_node_name
74
+ end
75
+ raise NamespaceNotFound.new("/#{paths.join('/')}") unless found_namespace
76
+ found_namespace
77
+ end
78
+
79
+ ##
80
+ # Registers the given callback at a namespace, assuming the namespace was already registered.
81
+ #
82
+ # @param [Array] paths Must be an Array of segments
83
+ # @param [Proc] callback
84
+ # @raise NamespaceNotFound if a segment has not been registered yet
85
+ #
86
+ def register_callback_at_namespace(paths, callback)
87
+ raise ArgumentError, "callback must be a Proc" unless callback.kind_of? Proc
88
+ search_for_namespace(paths).register_callback callback
89
+ end
90
+
91
+ protected
92
+
93
+ ##
94
+ # Used by NamespaceManager to build a tree of namespaces. Has a Hash of children which is not
95
+ # Thread-safe. For Thread-safety, all access should semaphore through the NamespaceManager.
96
+ class NamespaceNode
97
+
98
+ attr_reader :name
99
+ def initialize(name)
100
+ @name = name.freeze
101
+ @children = {}
102
+ @callbacks = []
103
+ end
104
+
105
+ def register_namespace_name(name)
106
+ @children[name] ||= NamespaceNode.new(name)
107
+ end
108
+
109
+ def register_callback(callback)
110
+ @callbacks << callback
111
+ callback
112
+ end
113
+
114
+ def callbacks
115
+ @callbacks.clone
116
+ end
117
+
118
+ def delete_callback(callback)
119
+ @callbacks.delete callback
120
+ end
121
+
122
+ def child_named(name)
123
+ @children[name]
124
+ end
125
+
126
+ def destroy_namespace(name)
127
+ @children.delete name
128
+ end
129
+
130
+ def root?
131
+ false
132
+ end
133
+
134
+ end
135
+
136
+ class RootNamespaceNode < NamespaceNode
137
+ def initialize
138
+ super :ROOT
139
+ end
140
+ def root?
141
+ true
142
+ end
143
+ end
144
+
145
+ end
146
+
147
+ class NamespaceNotFound < Exception
148
+ def initialize(full_path)
149
+ super "Could not find #{full_path.inspect} in the namespace registry. Did you register it yet?"
150
+ end
151
+ end
152
+
153
+ end