puremvc 1.0.0 → 1.0.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 (51) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +11 -0
  3. data/LICENSE +28 -0
  4. data/README.md +88 -0
  5. data/lib/core/controller.rb +177 -0
  6. data/lib/core/model.rb +133 -0
  7. data/lib/core/view.rb +230 -0
  8. data/lib/patterns/command/macro_command.rb +81 -0
  9. data/lib/patterns/command/simple_command.rb +28 -0
  10. data/lib/patterns/facade/facade.rb +267 -0
  11. data/lib/patterns/mediator/mediator.rb +56 -0
  12. data/lib/patterns/observer/notification.rb +68 -0
  13. data/lib/patterns/observer/notifier.rb +87 -0
  14. data/lib/patterns/observer/observer.rb +55 -0
  15. data/lib/patterns/proxy/proxy.rb +50 -0
  16. data/lib/puremvc.rb +19 -0
  17. data/sig/lib/core/controller.rbs +40 -0
  18. data/sig/lib/core/model.rbs +38 -0
  19. data/sig/lib/core/view.rbs +42 -0
  20. data/sig/lib/interfaces/i_command.rbs +18 -0
  21. data/sig/lib/interfaces/i_controller.rbs +52 -0
  22. data/sig/lib/interfaces/i_facade.rbs +116 -0
  23. data/sig/lib/interfaces/i_mediator.rbs +60 -0
  24. data/sig/lib/interfaces/i_model.rbs +38 -0
  25. data/sig/lib/interfaces/i_notification.rbs +52 -0
  26. data/sig/lib/interfaces/i_notifier.rbs +48 -0
  27. data/sig/lib/interfaces/i_observer.rbs +56 -0
  28. data/sig/lib/interfaces/i_proxy.rbs +36 -0
  29. data/sig/lib/interfaces/i_view.rbs +71 -0
  30. data/sig/lib/patterns/command/macro_command.rbs +18 -0
  31. data/sig/lib/patterns/command/simple_command.rbs +11 -0
  32. data/sig/lib/patterns/facade/facade.rbs +48 -0
  33. data/sig/lib/patterns/mediator/mediator.rbs +18 -0
  34. data/sig/lib/patterns/observer/notification.rbs +18 -0
  35. data/sig/lib/patterns/observer/notifier.rbs +17 -0
  36. data/sig/lib/patterns/observer/observer.rbs +20 -0
  37. data/sig/lib/patterns/proxy/proxy.rbs +19 -0
  38. metadata +71 -56
  39. data/puremvc.rb +0 -13
  40. data/src/org/puremvc/ruby/core/controller.rb +0 -76
  41. data/src/org/puremvc/ruby/core/model.rb +0 -57
  42. data/src/org/puremvc/ruby/core/view.rb +0 -112
  43. data/src/org/puremvc/ruby/patterns/command/macro_command.rb +0 -59
  44. data/src/org/puremvc/ruby/patterns/command/simple_command.rb +0 -17
  45. data/src/org/puremvc/ruby/patterns/facade/facade.rb +0 -161
  46. data/src/org/puremvc/ruby/patterns/mediator/mediator.rb +0 -37
  47. data/src/org/puremvc/ruby/patterns/observer/notification.rb +0 -38
  48. data/src/org/puremvc/ruby/patterns/observer/notifier.rb +0 -27
  49. data/src/org/puremvc/ruby/patterns/observer/observer.rb +0 -31
  50. data/src/org/puremvc/ruby/patterns/proxy/proxy.rb +0 -32
  51. data/version.txt +0 -12
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 66bcb05b4710e85999ef1b4d7e223e3269a94b9acc9ef256a046069cf7bc6402
4
+ data.tar.gz: eac762df289443442fc67ac7a5d54992f5ea73406fb9916d2909a984f98e2c13
5
+ SHA512:
6
+ metadata.gz: 718373aa1bf508d2ebfd0bf575abb396741c30a4eebd62dfd87deef35cb2678f9fe460927019962e10f2872a3e672ce3469e124c038d71b829a17f295757c4df
7
+ data.tar.gz: 3e748babb7ac73b47b8e60f354300d11af2dac7ff4bf57c06f8598c0ff11111e1a156e53b8ec7d2374eb3d8b67dcfa807907278ac8ca04bfbed732cff4aaac26
data/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [1.0.1] - 2025-08-14
6
+ ### Changed
7
+ - Bumped version to 1.0.1 to allow Multicore override after initial 1.0.0 standard release on rubygems.org
8
+
9
+ ## [1.0.0] - 2025-08-14
10
+ ### Added
11
+ - Initial release of the framework
data/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2025, Saad Shams <saad.shams@puremvc.org>
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,88 @@
1
+ ## [PureMVC](http://puremvc.github.com/) Ruby MultiCore Framework [![Ruby](https://github.com/PureMVC/puremvc-ruby-multicore-framework/actions/workflows/ruby.yml/badge.svg)](https://github.com/PureMVC/puremvc-ruby-multicore-framework/actions/workflows/ruby.yml)
2
+
3
+ PureMVC is a lightweight framework for creating applications based upon the classic [Model-View-Controller](http://en.wikipedia.org/wiki/Model-view-controller) design meta-pattern. It supports [modular programming](http://en.wikipedia.org/wiki/Modular_programming) through the use of [Multiton](http://en.wikipedia.org/wiki/Multiton) Core actors instead of the [Singletons](http://en.wikipedia.org/wiki/Singleton_pattern) used in the [Standard](https://github.com/PureMVC/puremvc-ruby-standard-framework/wiki) Version.
4
+
5
+ * [Ruby Gem](https://rubygems.org/gems/puremvc)
6
+ * [API Docs](http://puremvc.org/pages/docs/Ruby/multicore/)
7
+
8
+ ## Installation
9
+ ```shell
10
+ gem install puremvc
11
+ ```
12
+
13
+ ## Platforms / Technologies
14
+ * [Ruby](https://en.wikipedia.org/wiki/Ruby_(programming_language))
15
+ * [Command-line interface](https://en.wikipedia.org/wiki/Command-line_interface)
16
+ * [Cross-platform software](https://en.wikipedia.org/wiki/Cross-platform_software)
17
+
18
+ ---
19
+ ## Development
20
+
21
+ ### Install Dependencies
22
+ ```shell
23
+ bundle install
24
+ ```
25
+
26
+ ### Lint Code
27
+ ```shell
28
+ bundle exec rubocop
29
+ ```
30
+
31
+ ### Generate RBS Signatures
32
+ ```shell
33
+ rbs prototype rb lib/**/*.rb --out-dir=sig
34
+ ```
35
+
36
+ ### Run Type Checking
37
+ ```shell
38
+ steep check
39
+ ```
40
+
41
+ ### Test
42
+ ```shell
43
+ ruby -Itest -e 'Dir["test/**/*_test.rb"].each { |file| require_relative file }'
44
+ RubyMine: Test file name mask: **/{*_test,test_*,*_spec}.rb
45
+ ```
46
+
47
+ ### Generate Documentation
48
+ ```shell
49
+ yard doc lib/**/*.rb --protected --private
50
+ open doc/index.html
51
+ ```
52
+
53
+ #### Publish
54
+ ##### 1. Setup RubyGems API Key
55
+ ```shell
56
+ mkdir -p ~/.gem
57
+ cat > ~/.gem/credentials <<EOF
58
+ ---
59
+ :rubygems_api_key: API_KEY
60
+ EOF
61
+ chmod 0600 ~/.gem/credentials
62
+ ```
63
+ ##### 2. Build the gem
64
+ ```shell
65
+ gem build puremvc.gemspec
66
+ gem push puremvc-1.0.0.gem
67
+ ```
68
+
69
+ ## Reference
70
+ * [Ruby](https://www.ruby-lang.org/en)
71
+ * [RBS](https://github.com/ruby/rbs)
72
+ * [YARD: Yay! A Ruby Documentation Tool](https://rubydoc.info/gems/yard)
73
+
74
+ ---
75
+
76
+ ## License
77
+ * PureMVC MultiCore Framework for Ruby
78
+ * PureMVC - Copyright © 2025 [Saad Shams](https://www.linkedin.com/in/muizz/)
79
+ * PureMVC - Copyright © 2025 [Futurescale, Inc.](http://futurescale.com/)
80
+ * All rights reserved.
81
+
82
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
83
+
84
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
85
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
86
+ * Neither the name of Futurescale, Inc., PureMVC.org, nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
87
+
88
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+
4
+ # controller.rb
5
+ # PureMVC Ruby Multicore
6
+ #
7
+ # Copyright(c) 2025 Saad Shams <saad.shams@puremvc.org>
8
+ # Your reuse is governed by the BSD 3-Clause License
9
+
10
+ module PureMVC
11
+ # A Multiton <code>IController</code> implementation.
12
+ #
13
+ # In PureMVC, the <code>Controller</code> class follows the
14
+ # 'Command and Controller' strategy and assumes these responsibilities:
15
+ #
16
+ # * Remembering which <code>ICommand</code>s are intended to handle which <code>INotifications</code>.
17
+ # * Registering itself as an <code>IObserver</code> with the <code>View</code> for each <code>INotification</code>
18
+ # that has an <code>ICommand</code> mapping.
19
+ # * Creating a new instance of the proper <code>ICommand</code> to handle a given <code>INotification</code>
20
+ # when notified by the <code>View</code>.
21
+ # * Calling the <code>ICommand</code>'s <code>execute</code> method, passing in the <code>INotification</code>.
22
+ #
23
+ # Your application must register <code>ICommands</code> with the <code>Controller</code>.
24
+ #
25
+ # The simplest way is to subclass <code>Facade</code>, and use its <code>initializeController</code> method.
26
+ # to add your registrations.
27
+ #
28
+ # @see View
29
+ # @see Observer
30
+ # @see Notification
31
+ # @see SimpleCommand
32
+ # @see MacroCommand
33
+ class Controller
34
+ # Message Constants
35
+ MULTITON_MSG = 'Controller instance for this Multiton key already constructed!'
36
+ private_constant :MULTITON_MSG
37
+
38
+ class << self
39
+ # The Multiton IController instanceMap.
40
+ # @return [Hash{String => IController}]
41
+ def instance_map = (@instance_map ||= {})
42
+
43
+ # Mutex used to synchronize access to the instance map for thread safety.
44
+ # @return [Mutex]
45
+ def mutex = (@mutex ||= Mutex.new)
46
+
47
+ # Gets an instance using the provided factory block
48
+ #
49
+ # @param key [String] the unique key identifying the Multiton instance
50
+ # @param factory [^(String) -> _IController] the unique key passed to the factory block
51
+ # @return [IController] The controller instance created by the factory
52
+ def get_instance(key, &factory)
53
+ mutex.synchronize do
54
+ instance_map[key] ||= factory.call(key)
55
+ end
56
+ end
57
+
58
+ # Remove an IController instance
59
+ #
60
+ # @param key [String] the multiton key of the IController instance to remove
61
+ def remove_controller(key)
62
+ mutex.synchronize do
63
+ instance_map.delete(key)
64
+ end
65
+ end
66
+ end
67
+
68
+ # Constructor.
69
+ #
70
+ # This IController implementation is a Multiton, so you should not call the constructor
71
+ # directly. Instead, call the static factory method, passing the unique key for this instance:
72
+ # <code>PureMVC::Controller.getInstance(key) { |key| PureMVC::Controller.new(key) }</code>
73
+ #
74
+ # @param key [String]
75
+ # @raise [RuntimeError] if an instance for this Multiton key has already been constructed
76
+ def initialize(key)
77
+ raise MULTITON_MSG if self.class.instance_map[key]
78
+
79
+ self.class.instance_map[key] = self
80
+ # The Multiton Key for this Core
81
+ # @type var multiton_key: String
82
+ @multiton_key = key
83
+ # Local reference to View
84
+ # @type var component: _IView?
85
+ @view = nil
86
+ # Mapping of Notification names to Command factories
87
+ # @type var command_map: Hash[String, ^() -> _ICommand]
88
+ @command_map = {}
89
+ # Mutex used to synchronize access to the @command_map
90
+ # @type var command_mutex: Mutex
91
+ @command_mutex = Mutex.new
92
+ initialize_controller
93
+ end
94
+
95
+ # Initialize the Multiton Controller instance.
96
+ #
97
+ # Called automatically by the constructor.
98
+ #
99
+ # Note that if you are using a subclass of View
100
+ # in your application, you should also subclass Controller
101
+ # and override the initialize_controller method in the
102
+ # following way:
103
+ #
104
+ # @example
105
+ # # ensure that the Controller is talking to my IView implementation
106
+ # def initialize_controller
107
+ # @view = MyView::get_instance(key) { |key| MyView.new(key) }
108
+ # end
109
+ #
110
+ def initialize_controller
111
+ @view = View.get_instance(@multiton_key) { |key| View.new(key) }
112
+ end
113
+
114
+ # Register a particular ICommand class as the handler for a particular INotification.
115
+ #
116
+ # If an ICommand has already been registered to handle INotifications with this name,
117
+ # it is replaced by the new ICommand.
118
+ #
119
+ # The Observer for the new ICommand is only created if this is the first time
120
+ # an ICommand has been registered for this notification name.
121
+ #
122
+ # @param notification_name [String] the name of the INotification
123
+ # @param factory [^<() -> ICommand>] the factory to produce an instance of the ICommand
124
+ def register_command(notification_name, &factory)
125
+ @command_mutex.synchronize do
126
+ if @command_map[notification_name].nil?
127
+ @view&.register_observer(notification_name, Observer.new(method(:execute_command), self))
128
+ end
129
+ @command_map[notification_name] = factory
130
+ end
131
+ end
132
+
133
+ # If an ICommand has previously been registered to handle the given INotification, then it is executed.
134
+ #
135
+ # @param notification [INotification] the notification to handle
136
+ def execute_command(notification)
137
+ @command_mutex.synchronize do
138
+ # @type factory: [^() -> ICommand]?
139
+ factory = @command_map[notification.name]
140
+ return if factory.nil?
141
+
142
+ # @type var command: _ICommand
143
+ command = factory.call
144
+ command.initialize_notifier(@multiton_key)
145
+ command.execute(notification)
146
+ end
147
+ end
148
+
149
+ # Check if a Command is registered for a given Notification.
150
+ #
151
+ # @param notification_name [String] the name of the Notification
152
+ # @return [Boolean] whether a Command is currently registered for the given notification_name
153
+ def has_command?(notification_name)
154
+ @command_mutex.synchronize do
155
+ @command_map.key?(notification_name)
156
+ end
157
+ end
158
+
159
+ # Remove a previously registered ICommand to INotification mapping.
160
+ #
161
+ # @param notification_name [String] the name of the INotification to remove the ICommand mapping for
162
+ def remove_command(notification_name)
163
+ @command_mutex.synchronize do
164
+ # @type var command: [^() -> ICommand]?
165
+ command = @command_map[notification_name]
166
+
167
+ # if the Command is registered...
168
+ if command
169
+ # remove the observer
170
+ @view&.remove_observer(notification_name, self)
171
+ # remove the command
172
+ @command_map.delete(notification_name)
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end
data/lib/core/model.rb ADDED
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+
4
+ # model.rb
5
+ # PureMVC Ruby Multicore
6
+ #
7
+ # Copyright(c) 2025 Saad Shams <saad.shams@puremvc.org>
8
+ # Your reuse is governed by the BSD 3-Clause License
9
+
10
+ module PureMVC
11
+ # A Multiton <code>IModel</code> implementation.
12
+ #
13
+ # In PureMVC, the <code>Model</code> class provides access to model objects (Proxies) by named lookup.
14
+ #
15
+ # The <code>Model</code> assumes these responsibilities:
16
+ #
17
+ # - Maintains a cache of <code>IProxy</code> instances.
18
+ # - Provides methods for registering, retrieving, and removing <code>IProxy</code> instances.
19
+ #
20
+ # Your application must register <code>IProxy</code> instances with the <code>Model</code>. Typically,
21
+ # an <code>ICommand</code> is used to create and register <code>IProxy</code> instances after the <code>Facade<code>
22
+ # has initialized the Core actors.
23
+ #
24
+ # @see Proxy
25
+ # @see IProxy
26
+ class Model
27
+ # Message Constants
28
+ MULTITON_MSG = 'Model instance for this Multiton key already constructed!'
29
+ private_constant :MULTITON_MSG
30
+
31
+ class << self
32
+ # The Multiton IModel instanceMap.
33
+ # @return [Hash{String => IModel}]
34
+ def instance_map = (@instance_map ||= {})
35
+
36
+ # Mutex used to synchronize access to the instance map for thread safety.
37
+ # @return [Mutex]
38
+ def mutex = (@mutex ||= Mutex.new)
39
+
40
+ # <code>Model</code> Multiton Factory method.
41
+ #
42
+ # @param key [String] the unique key identifying the Multiton instance
43
+ # @param factory [^(String) -> IModel] the unique key passed to the factory block
44
+ # @return [IModel] the instance for this Multiton key
45
+ def get_instance(key, &factory)
46
+ mutex.synchronize do
47
+ instance_map[key] ||= factory.call(key)
48
+ end
49
+ end
50
+
51
+ # Remove an IModel instance
52
+ #
53
+ # @param key [String] the multiton key of the IModel instance to remove
54
+ def remove_model(key)
55
+ mutex.synchronize do
56
+ instance_map.delete(key)
57
+ end
58
+ end
59
+ end
60
+
61
+ # Constructor.
62
+ #
63
+ # This <code>IModel</code> implementation is a Multiton,
64
+ # so you should not call the constructor directly, but instead call the static
65
+ # Multiton Factory method <code>PureMVC::Model.get_instance(key) { |key| PureMVC::Model.new(key) }</code>.
66
+ #
67
+ # @param key [String]
68
+ # @raise [RuntimeError] Error if an instance for this Multiton key has already been constructed.
69
+ def initialize(key)
70
+ raise MULTITON_MSG if self.class.instance_map[key]
71
+
72
+ self.class.instance_map[key] = self
73
+ @multiton_key = key
74
+ @proxy_map = {}
75
+ @proxy_mutex = Mutex.new
76
+ initialize_model
77
+ end
78
+
79
+ # Initialize the <code>Model</code> instance.
80
+ #
81
+ # Called automatically by the constructor, this
82
+ # is your opportunity to initialize the Multiton
83
+ # instance in your subclass without overriding the
84
+ # constructor.
85
+ def initialize_model; end
86
+
87
+ # Register an <code>IProxy</code> with the <code>Model</code>.
88
+ #
89
+ # @param proxy [_IProxy] an <code>IProxy</code> to be held by the <code>Model</code>.
90
+ def register_proxy(proxy)
91
+ proxy.initialize_notifier(@multiton_key)
92
+ @proxy_mutex.synchronize do
93
+ @proxy_map[proxy.name] = proxy
94
+ end
95
+ proxy.on_register
96
+ end
97
+
98
+ # Retrieve an <code>IProxy</code> from the <code>Model</code>.
99
+ #
100
+ # @param proxy_name [String] the name of the proxy to retrieve.
101
+ # @return [_IProxy, nil] the <code>IProxy</code> instance previously registered with the <code>proxy_name</code>,
102
+ # or nil if none found.
103
+ def retrieve_proxy(proxy_name)
104
+ @proxy_mutex.synchronize do
105
+ @proxy_map[proxy_name]
106
+ end
107
+ end
108
+
109
+ # Check if a Proxy is registered.
110
+ #
111
+ # @param proxy_name [String] the name of the proxy to check.
112
+ # @return [Boolean] whether a Proxy is currently registered with the given <code>proxy_name</code>.
113
+ def has_proxy?(proxy_name)
114
+ @proxy_mutex.synchronize do
115
+ @proxy_map.key?(proxy_name)
116
+ end
117
+ end
118
+
119
+ # Remove an <code>IProxy</code> from the <code>Model</code>.
120
+ #
121
+ # @param proxy_name [String] name of the <code>IProxy</code> instance to be removed.
122
+ # @return [_IProxy, nil] the <code>IProxy</code> that was removed from the <code>Model</code>, or nil if none found.
123
+ def remove_proxy(proxy_name)
124
+ # @type var proxy: _IProxy?
125
+ proxy = nil
126
+ @proxy_mutex.synchronize do
127
+ proxy = @proxy_map.delete(proxy_name)
128
+ end
129
+ proxy&.on_remove
130
+ proxy
131
+ end
132
+ end
133
+ end
data/lib/core/view.rb ADDED
@@ -0,0 +1,230 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+
4
+ # view.rb
5
+ # PureMVC Ruby Multicore
6
+ #
7
+ # Copyright(c) 2025 Saad Shams <saad.shams@puremvc.org>
8
+ # Your reuse is governed by the BSD 3-Clause License
9
+
10
+ module PureMVC
11
+ # A Multiton <code>IView</code> implementation.
12
+ #
13
+ # In PureMVC, the <code>IView</code> class assumes these responsibilities:
14
+ #
15
+ # - Maintains a cache of <code>IMediator</code>instances.
16
+ # - Provides methods for registering, retrieving, and removing <code>IMediators</code>.
17
+ # - Notifies <code>IMediators</code>when they are registered or removed.
18
+ # - Manages the observer lists for each <code>INotification</code>in the application.
19
+ # - Provides a method for attaching <code>IObservers</code>to an <code>INotification</code>'s observer list.
20
+ # - Provides a method for broadcasting an <code>INotification</code>.
21
+ # - Notifies the <code>IObservers</code>of a given <code>INotification</code>when it is broadcast.
22
+ #
23
+ # @see Mediator
24
+ # @see Observer
25
+ # @see Notification
26
+ class View
27
+ MULTITON_MSG = 'View instance for this Multiton key already constructed!'
28
+ private_constant :MULTITON_MSG
29
+
30
+ class << self
31
+ # The Multiton IModel instanceMap.
32
+ # @return [Hash{String => IView}]
33
+ def instance_map = (@instance_map ||= {})
34
+
35
+ # Mutex used to synchronize access to the instance map for thread safety.
36
+ # @return [Mutex]
37
+ def mutex = (@mutex ||= Mutex.new)
38
+
39
+ # View Multiton Factory method.
40
+ #
41
+ # @param key [String] the unique key identifying the Multiton instance
42
+ # @param factory [^(String) -> _IView] the unique key passed to the factory block
43
+ # @return [_IView] the Multiton instance of <code>View</code>
44
+ def get_instance(key, &factory)
45
+ mutex.synchronize do
46
+ instance_map[key] ||= factory.call(key)
47
+ end
48
+ end
49
+
50
+ # Remove an <code>IView</code> instance.
51
+ #
52
+ # @param key [String] the key of the <code>IView</code> instance to remove
53
+ def remove_view(key)
54
+ mutex.synchronize do
55
+ instance_map.delete(key)
56
+ end
57
+ end
58
+ end
59
+
60
+ # Constructor.
61
+ #
62
+ # This <code>IView</code> implementation is a Multiton,
63
+ # so you should not call the constructor directly. Instead, call the static
64
+ # Multiton factory method <code>View.get_instance(multiton_key) { |key| View.new(key) }</code>.
65
+ #
66
+ # @param key [String]
67
+ # @raise [RuntimeError] if an instance for this Multiton key has already been constructed.
68
+ def initialize(key)
69
+ raise MULTITON_MSG if self.class.instance_map[key]
70
+
71
+ self.class.instance_map[key] = self
72
+ # The Multiton Key for this Core
73
+ # @type var multiton_key: String
74
+ @multiton_key = key
75
+ # Mapping of Notification names to Observer lists
76
+ # @type var observer_map: Hash[String, Array[_IObserver]]
77
+ @observer_map = {}
78
+ # Mutex used to synchronize access to the observer_map
79
+ # @type var observer_mutex: Mutex
80
+ @observer_mutex = Mutex.new
81
+ # Mapping of Mediator names to Mediator instances
82
+ # @type var mediator_map: Hash[String, _IMediator]
83
+ @mediator_map = {}
84
+ # Mutex used to synchronize access to the mediator_map
85
+ # @type var mediator_mutex: Mutex
86
+ @mediator_mutex = Mutex.new
87
+ initialize_view
88
+ end
89
+
90
+ # Initialize the Multiton <code>View</code> instance.
91
+ #
92
+ # Called automatically by the constructor, this
93
+ # is your opportunity to initialize the Multiton
94
+ # instance in your subclass without overriding the
95
+ # constructor.
96
+ def initialize_view; end
97
+
98
+ # Register an <code>IObserver</code> to be notified
99
+ # of <code>INotifications</code> with a given name.
100
+ #
101
+ # @param notification_name [String] the name of the <code>INotifications</code> to notify <code>IObserver</code> of
102
+ # @param observer [IObserver] the <code>IObserver</code> to register
103
+ def register_observer(notification_name, observer)
104
+ @observer_mutex.synchronize do
105
+ observers = (@observer_map[notification_name] ||= [])
106
+ observers << observer
107
+ end
108
+ end
109
+
110
+ # Notify the <code>IObservers</code> for a particular <code>INotification</code>.
111
+ #
112
+ # All previously attached <code>IObservers</code> for this <code>INotification</code>'s
113
+ # list are notified and are passed a reference to the <code>INotification</code> in
114
+ # the order in which they were registered.
115
+ #
116
+ # @param notification [INotification] the <code>INotification</code> to notify <code>IObservers</code> of.
117
+ def notify_observers(notification)
118
+ # @type var observers: Array[_IObserver]?
119
+ observers = nil
120
+ @observer_mutex.synchronize do
121
+ # Get a reference to the observers list for this notification name
122
+ # Iteration safe, copy observers from reference array to working array,
123
+ # since the reference array may change during the notification loop
124
+ observers = @observer_map[notification.name].dup
125
+ end
126
+ # Notify Observers from the working array
127
+ observers&.each { |observer| observer.notify_observer(notification) }
128
+ end
129
+
130
+ # Remove the observer for a given notifyContext from an observer list for a given Notification name.
131
+ #
132
+ # @param notification_name [String] which observer list to remove from
133
+ # @param notify_context [Object] remove the observer with this object as its notifyContext
134
+ def remove_observer(notification_name, notify_context)
135
+ @observer_mutex.synchronize do
136
+ # the observer list for the notification under inspection
137
+ # @type var observers: Array[_IObserver]?
138
+ observers = @observer_map[notification_name]
139
+ # find and remove the sole Observer for the given notify_context
140
+ # there can only be one Observer for a given notify_context
141
+ # in any given Observer list, so remove it
142
+ observers&.reject! { |observer| observer.compare_notify_context?(notify_context) }
143
+ # Also, when a Notification's Observer list length falls to
144
+ # zero, delete the notification key from the observer map
145
+ @observer_map.delete(notification_name) if observers&.empty?
146
+ end
147
+ end
148
+
149
+ # Register an <code>IMediator</code> instance with the <code>View</code>.
150
+ #
151
+ # Registers the <code>IMediator</code> so that it can be retrieved by name,
152
+ # and further interrogates the <code>IMediator</code> for its
153
+ # <code>INotification</code> interests.
154
+ #
155
+ # If the <code>IMediator</code> returns any <code>INotification</code>
156
+ # names to be notified about, an <code>Observer</code> is created encapsulating
157
+ # the <code>IMediator</code> instance's <code>handleNotification</code> method
158
+ # and registering it as an <code>Observer</code> for all <code>INotifications</code> the
159
+ # <code>IMediator</code> is interested in.
160
+ #
161
+ # @param mediator [IMediator] a reference to the <code>IMediator</code> instance
162
+ def register_mediator(mediator)
163
+ @mediator_mutex.synchronize do
164
+ return if @mediator_map.key?(mediator.name)
165
+
166
+ @mediator_map[mediator.name] = mediator
167
+ end
168
+
169
+ mediator.initialize_notifier(@multiton_key)
170
+
171
+ # Create Observer referencing this mediator's handleNotification method
172
+ # @type observer [IObserver]
173
+ observer = Observer.new(mediator.method(:handle_notification), mediator)
174
+
175
+ # Get Notification interests, if any.
176
+ # @type interests [Array<String>]
177
+ interests = mediator.list_notification_interests
178
+ # Register Mediator as Observer for its list of Notification interests
179
+ interests.each { |interest| register_observer(interest, observer) }
180
+
181
+ # alert the mediator that it has been registered
182
+ mediator.on_register
183
+ end
184
+
185
+ # Retrieve an <code>IMediator</code> from the <code>View</code>.
186
+ #
187
+ # @param mediator_name [String] the name of the <code>IMediator</code> instance to retrieve.
188
+ # @return [IMediator, nil] <code>IMediator</code> previously registered with the given <code>mediatorName</code>.
189
+ def retrieve_mediator(mediator_name)
190
+ @mediator_mutex.synchronize do
191
+ @mediator_map[mediator_name]
192
+ end
193
+ end
194
+
195
+ # Check if a Mediator is registered or not.
196
+ #
197
+ # @param mediator_name [String] the name of the mediator to check.
198
+ # @return [Boolean] whether a Mediator is registered with the given <code>mediatorName</code>.
199
+ def has_mediator?(mediator_name)
200
+ @mediator_mutex.synchronize do
201
+ @mediator_map.key?(mediator_name)
202
+ end
203
+ end
204
+
205
+ # Remove an <code>IMediator</code> from the <code>View</code>.
206
+ #
207
+ # @param mediator_name [String] name of the <code>IMediator</code> instance to be removed.
208
+ # @return [IMediator, nil] <code>IMediator</code> that was removed from the <code>View</code>, or nil if none found.
209
+ def remove_mediator(mediator_name)
210
+ # @type var mediator: _IMediator?
211
+ mediator = nil
212
+ @mediator_mutex.synchronize do
213
+ # retrieve the named mediator and delete from the mediator map
214
+ mediator = @mediator_map.delete(mediator_name)
215
+ end
216
+ return unless mediator
217
+
218
+ # for every notification this mediator is interested in...
219
+ # @type var interests: Array[String]
220
+ interests = mediator.list_notification_interests
221
+ # remove the observer linking the mediator
222
+ # to the notification interest
223
+ interests.each { |interest| remove_observer(interest, mediator) }
224
+
225
+ # alert the mediator that it has been removed
226
+ mediator.on_remove
227
+ mediator
228
+ end
229
+ end
230
+ end