octiron 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,11 @@
1
+ # coding: utf-8
2
+ #
3
+ # octiron
4
+ # https://github.com/jfinkhaeuser/octiron
5
+ #
6
+ # Copyright (c) 2016 Jens Finkhaeuser and other octiron contributors.
7
+ # All rights reserved.
8
+ #
9
+
10
+ require 'octiron/version'
11
+ require 'octiron/world'
@@ -0,0 +1,161 @@
1
+ # coding: utf-8
2
+ #
3
+ # octiron
4
+ # https://github.com/jfinkhaeuser/octiron
5
+ #
6
+ # Copyright (c) 2016 Jens Finkhaeuser and other octiron contributors.
7
+ # All rights reserved.
8
+ #
9
+
10
+ require 'octiron/support/identifiers'
11
+
12
+ require 'collapsium/recursive_sort'
13
+ require 'collapsium/prototype_match'
14
+
15
+ module Octiron::Events
16
+
17
+ ##
18
+ # Implements and in-process pub-sub events broadcaster allowing multiple
19
+ # observers to subscribe to different events.
20
+ class Bus
21
+ # @return (String) the default namespace to search for events
22
+ attr_reader :default_namespace
23
+
24
+ ##
25
+ # @param default_namespace (Symbol) The default namespace to look in for
26
+ # Event classes.
27
+ def initialize(default_namespace = ::Octiron::Events)
28
+ @default_namespace = default_namespace.to_s
29
+ @handlers = {}
30
+ end
31
+
32
+ ##
33
+ # Subscribe an event handler to an event.
34
+ # @param event_id (Class, String, other) A class or String naming an event
35
+ # class.
36
+ # @param handler_object (Object) Handler object that must implement a
37
+ # `#call` method accepting an instance of the event class provided in
38
+ # the first parameter. If nil, a block needs to be provided.
39
+ # @param handler_proc (Proc) Handler block that accepts an instance of the
40
+ # event class provided in the first parameter. If nil, a handler object
41
+ # must be provided.
42
+ # @return The class represented by the event_id, as a String name.
43
+ def subscribe(event_id, handler_object = nil, &handler_proc)
44
+ handler = resolve_handler(handler_object, &handler_proc)
45
+ event_name = identify(event_id)
46
+
47
+ handlers_for(event_name, true) << handler
48
+
49
+ return event_name
50
+ end
51
+ alias register subscribe
52
+
53
+ ##
54
+ # Unsubscribe an event handler from an event.
55
+ # @param event_id (Class, String, other) A class or String naming an event
56
+ # class.
57
+ # @param handler_object (Object) Handler object that must implement a
58
+ # `#call` method accepting an instance of the event class provided in
59
+ # the first parameter. If nil, a block needs to be provided.
60
+ # @param handler_proc (Proc) Handler block that accepts an instance of the
61
+ # event class provided in the first parameter. If nil, a handler object
62
+ # must be provided.
63
+ # @return The class represented by the event_id, as a String name.
64
+ def unsubscribe(event_id, handler_object = nil, &handler_proc)
65
+ handler = resolve_handler(handler_object, &handler_proc)
66
+ event_name = identify(event_id)
67
+
68
+ handlers_for(event_name, true).delete(handler)
69
+
70
+ return event_name
71
+ end
72
+
73
+ ##
74
+ # Broadcast an event. This is an instance of a class provided to #subscribe
75
+ # previously.
76
+ # @param event (Object) the event to publish
77
+ def publish(event)
78
+ event_name = event
79
+ if not event.is_a?(Hash)
80
+ event_name = event.class.to_s
81
+ end
82
+ handlers_for(event_name, false).each do |handler|
83
+ handler.call(event)
84
+ end
85
+ end
86
+ alias broadcast publish
87
+ alias notify publish
88
+
89
+ private
90
+
91
+ # The first parameters is the event class or event name. The second parameter
92
+ # is whether this is for write or read access. We don't want to pollute
93
+ # @handlers with data from the #publish call.
94
+ def handlers_for(name, write)
95
+ # Use prototype matching for Hash names
96
+ if name.is_a?(Hash)
97
+ name.extend(::Collapsium::PrototypeMatch)
98
+
99
+ # The prototype hash logic is a little complex. A hash event can match
100
+ # multiple prototypes, e.g. { a: 42 } should match { a: nil } as well as
101
+ # { a: 42 }.
102
+ # When writing, we want to be as precise as possible and find the best
103
+ # match. When reading, we want to merge all matches, ideally.
104
+ if write
105
+ best_score = -1
106
+ best_proto = nil
107
+
108
+ # Find the best matching prototype
109
+ @handlers.keys.each do |proto|
110
+ score = name.prototype_match_score(proto)
111
+ if score > best_score
112
+ best_score = score
113
+ best_proto = proto
114
+ end
115
+ end
116
+
117
+ if not best_proto.nil?
118
+ return @handlers[best_proto]
119
+ end
120
+ else
121
+ merged = []
122
+
123
+ @handlers.keys.each do |proto|
124
+ if name.prototype_match(proto)
125
+ merged += @handlers[proto]
126
+ end
127
+ end
128
+
129
+ if not merged.empty?
130
+ return merged
131
+ end
132
+ end
133
+
134
+ # No prototype matches. That means if write is true, we need to treat
135
+ # name as a new prototype to regiser. Otherwise, we need to return an
136
+ # empty list. That happens to be the same logic as for the simple key
137
+ # below.
138
+ end
139
+
140
+ # If we're in write access, make sure to store an empty list as well as
141
+ # returning one (if necessary).
142
+ if write
143
+ @handlers[name] ||= []
144
+ end
145
+
146
+ # In read access, want to either return an empty list, or the registered
147
+ # handlers, but not ovewrite the registered handlers.
148
+ return @handlers[name] || []
149
+ end
150
+
151
+ def resolve_handler(handler_object = nil, &handler_proc)
152
+ handler = handler_proc || handler_object
153
+ if not handler
154
+ raise ArgumentError, "Please pass either an object or a handler block"
155
+ end
156
+ return handler
157
+ end
158
+
159
+ include ::Octiron::Support::Identifiers
160
+ end # class Bus
161
+ end # module Octiron::Events
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ #
3
+ # octiron
4
+ # https://github.com/jfinkhaeuser/octiron
5
+ #
6
+ # Copyright (c) 2016 Jens Finkhaeuser and other octiron contributors.
7
+ # All rights reserved.
8
+ #
9
+
10
+ module Octiron::Support
11
+ ##
12
+ # @see CamelCase::camel_case
13
+ module CamelCase
14
+
15
+ ##
16
+ # Takes an underscored name and turns it into a CamelCase String
17
+ # @param underscored_name (String, Symbol) the underscored name to turn
18
+ # into camel case.
19
+ # @return (String) CamelCased version of the underscored_name
20
+ def camel_case(underscored_name)
21
+ return underscored_name.to_s.split("_").map do |word|
22
+ word.upcase[0] + word[1..-1]
23
+ end.join
24
+ end
25
+
26
+ end # module CamelCase
27
+ end # module Octiron::Support
@@ -0,0 +1,43 @@
1
+ # coding: utf-8
2
+ #
3
+ # octiron
4
+ # https://github.com/jfinkhaeuser/octiron
5
+ #
6
+ # Copyright (c) 2016 Jens Finkhaeuser and other octiron contributors.
7
+ # All rights reserved.
8
+ #
9
+
10
+ module Octiron::Support
11
+ ##
12
+ # @see Constantize::constantize
13
+ module Constantize
14
+
15
+ ##
16
+ # Takes a String containing a constant name, and returns the canonical path
17
+ # of the constant (i.e. where it's defined, even if it's accessed
18
+ # differently.). If the constant does not exist, a NameError is thrown.
19
+ # @param constant_name (String) the constant name
20
+ # @return (Object) the actual named constant.
21
+ def constantize(constant_name)
22
+ names = constant_name.split('::')
23
+
24
+ # Trigger a built-in NameError exception including the ill-formed
25
+ # constant in the message.
26
+ if names.empty?
27
+ Object.const_get(constant_name, false)
28
+ end
29
+
30
+ # Remove the first blank element in case of '::ClassName' notation.
31
+ if names.size > 1 && names.first.empty?
32
+ names.shift
33
+ end
34
+
35
+ # Note: this would be much more complex in Ruby < 1.9.3, so yay for not
36
+ # bothering to support these!
37
+ return names.inject(Object) do |constant, name|
38
+ next constant.const_get(name)
39
+ end
40
+ end
41
+
42
+ end # module Constantize
43
+ end # module Octiron::Support
@@ -0,0 +1,45 @@
1
+ # coding: utf-8
2
+ #
3
+ # octiron
4
+ # https://github.com/jfinkhaeuser/octiron
5
+ #
6
+ # Copyright (c) 2016 Jens Finkhaeuser and other octiron contributors.
7
+ # All rights reserved.
8
+ #
9
+
10
+ require_relative 'camel_case'
11
+ require_relative 'constantize'
12
+
13
+ module Octiron::Support
14
+ ##
15
+ # @see Identifiers::identify
16
+ module Identifiers
17
+ include ::Octiron::Support::CamelCase
18
+ include ::Octiron::Support::Constantize
19
+
20
+ ##
21
+ # This function "identifies" an object, i.e. returns a unique ID according
22
+ # to the octiron logic. That means that:
23
+ # - For classes, a stringified version is returned
24
+ # - For hashes, the hash itself is returned
25
+ # - Strings are interpreted as constant names, and are returned fully
26
+ # qualified.
27
+ # - Everything else is considered to be a constant name in the default
28
+ # namespace. This requires access to a @default_namespace variable in
29
+ # the including class.
30
+ def identify(name)
31
+ case name
32
+ when Class
33
+ return name.to_s
34
+ when Hash
35
+ return name
36
+ when String
37
+ return constantize(name).to_s
38
+ when nil
39
+ raise NameError, "Can't identify 'nil'!"
40
+ else
41
+ return constantize("#{@default_namespace}::#{camel_case(name)}").to_s
42
+ end
43
+ end
44
+ end # module Identifiers
45
+ end # module Octiron::Support
@@ -0,0 +1,201 @@
1
+ # coding: utf-8
2
+ #
3
+ # octiron
4
+ # https://github.com/jfinkhaeuser/octiron
5
+ #
6
+ # Copyright (c) 2016 Jens Finkhaeuser and other octiron contributors.
7
+ # All rights reserved.
8
+ #
9
+
10
+ require 'rgl/adjacency'
11
+ require 'rgl/dijkstra'
12
+
13
+ require 'octiron/support/identifiers'
14
+
15
+ # require 'collapsium/recursive_sort'
16
+ require 'collapsium/prototype_match'
17
+
18
+ module Octiron::Transmogrifiers
19
+
20
+ ##
21
+ # Registers transmogrifiers between one (event) class and another.
22
+ #
23
+ # A transmogrifier is an object with a call method or a block that accepts
24
+ # an instance of one (event) class and produces an instance of another
25
+ # (event) class.
26
+ #
27
+ # The registry also exposes a #transmogrify function which uses any
28
+ # registered transmogrifier, or raises an error if there is no
29
+ # transmogrification possible.
30
+ #
31
+ # One piece of magic makes this particularly powerful: the registry creates
32
+ # a graph of how to transmogrify an object to another by chaining
33
+ # transmogrifiers. That is, if there is a transmogrifier that turns A into B,
34
+ # and one that turns B into C, then by chaining both the registry can also
35
+ # turn A into C directly. (This is done via the 'rgl' gem).
36
+ class Registry
37
+ # @return (String) the default namespace to search for transmogrifiers
38
+ attr_reader :default_namespace
39
+
40
+ ##
41
+ # @param default_namespace (Symbol) The default namespace to look in for
42
+ # Transmogrifier classes.
43
+ def initialize(default_namespace = ::Octiron::Transmogrifiers)
44
+ @default_namespace = default_namespace.to_s
45
+ @graph = RGL::DirectedAdjacencyGraph.new
46
+ @visitor = RGL::DijkstraVisitor.new(@graph)
47
+ @map_data = {}
48
+ @map = nil
49
+ @transmogrifiers = {}
50
+ end
51
+
52
+ ##
53
+ # Register transmogrifier
54
+ #
55
+ # @param from (Class, String, other) A class or String nameing a
56
+ # transmogrifier source. With prototype Hash matching, this would be
57
+ # a prototype.
58
+ # @param to (Class, String, other) Transmogrifier target.
59
+ # @param overwrite (Boolean) The registry can only hold one transmogrifier
60
+ # per from -> to pair. If overwrite is true, registering a
61
+ # transmogrifier where one already exists overwrites the old one,
62
+ # otherwise an exception is raised.
63
+ # @param transmogrifier_object (Object) Transmogrifier object that must
64
+ # implement a `#call` method accepting an instance of the event class
65
+ # provided in the first parameter. If nil, a block needs to be provided.
66
+ # @param transmogrifier_proc (Proc) Transmogrifier block that accepts an
67
+ # instance of the event class provided in the first parameter. If nil, a
68
+ # transmogrifier object must be provided.
69
+ def register(from, to, overwrite = false, transmogrifier_object = nil,
70
+ &transmogrifier_proc)
71
+ transmogrifier = transmogrifier_proc || transmogrifier_object
72
+ if not transmogrifier
73
+ raise ArgumentError, "Please pass either an object or a transmogrifier "\
74
+ "block"
75
+ end
76
+
77
+ # Convert to canonical names
78
+ from_name = identify(from)
79
+ to_name = identify(to)
80
+ key = [from_name, to_name]
81
+
82
+ # We treat the graph as authoritative for what transmogrifiers exist.
83
+ if @graph.has_edge?(from_name, to_name)
84
+ if not overwrite
85
+ raise ArgumentError, "Registry already knows a transmogrifier for "\
86
+ "#{key}, aborting!"
87
+ end
88
+ end
89
+
90
+ # Add edges and map data for the shortest path search. We treat all paths
91
+ # as equally weighted.
92
+ @graph.add_edge(from_name, to_name)
93
+ @map_data[key] = 1
94
+ @map = RGL::EdgePropertiesMap.new(@map_data, true)
95
+
96
+ # Finally, register transmogrifier
97
+ @transmogrifiers[key] = transmogrifier
98
+ end
99
+
100
+ ##
101
+ # Deregister transmogrifier
102
+ #
103
+ # @param from (Class, String, other) A class or String nameing a
104
+ # transmogrifier source. With prototype Hash matching, this would be
105
+ # a prototype.
106
+ # @param to (Class, String, other) Transmogrifier target.
107
+ def deregister(from, to)
108
+ # Convert to canonical names
109
+ from_name = identify(from)
110
+ to_name = identify(to)
111
+ key = [from_name, to_name]
112
+
113
+ # Graph, map data and transmogrifiers need to be modified
114
+ @graph.remove_edge(from_name, to_name)
115
+ @map_data.delete(key)
116
+ @map = RGL::EdgePropertiesMap.new(@map_data, true)
117
+
118
+ @transmogrifiers.delete(key)
119
+ end
120
+
121
+ alias unregister deregister
122
+
123
+ ##
124
+ # Transmogrify an object of one class into another class.
125
+ def transmogrify(from, to)
126
+ # Get lookup keys
127
+ from_name = from.class.to_s
128
+ if from.is_a?(Hash)
129
+ # Finding the correct from_name is tricky, because from is not a
130
+ # prototype, but the graph and all intermediate
131
+ from_name = best_matching_hash_prototype(from)
132
+ end
133
+ to_name = identify(to)
134
+
135
+ # We'll ask the graph for the shortest path. If there is none, we can't
136
+ # transmogrify. (Note: the @map changes with each registration/
137
+ # deregistration, so we instanciate the algorithm here).
138
+ algo = RGL::DijkstraAlgorithm.new(@graph, @map, @visitor)
139
+ path = algo.shortest_path(from_name, to_name)
140
+
141
+ if path.nil?
142
+ raise ArgumentError, "No transmogrifiers for #{[from_name, to_name]} "\
143
+ "found, aborting!"
144
+ end
145
+
146
+ # Transmogrify for each part of the path
147
+ input = from
148
+ result = nil
149
+ path.inject do |step_from, step_to|
150
+ # Call transmogrifier
151
+ key = [step_from, step_to]
152
+ result = @transmogrifiers[key].call(input)
153
+
154
+ # Verify result
155
+ if step_to.is_a?(Hash)
156
+ result.extend(::Collapsium::PrototypeMatch)
157
+ if not result.prototype_match(step_to)
158
+ raise "Transmogrifier returned Hash that did not match prototype "\
159
+ "#{step_to}, aborting!"
160
+ end
161
+ elsif result.class.to_s != step_to
162
+ raise "Transmogrifier returned result of invalid class "\
163
+ "#{result.class}, aborting!"
164
+ end
165
+
166
+ # Result is input for the next transmogrifier in the chain
167
+ input = result
168
+
169
+ # Make step_to the next step_from
170
+ next step_to
171
+ end
172
+ return result
173
+ end
174
+
175
+ private
176
+
177
+ include ::Octiron::Support::Identifiers
178
+
179
+ def best_matching_hash_prototype(value)
180
+ value.extend(::Collapsium::PrototypeMatch)
181
+ best_score = -1
182
+ best_proto = nil
183
+
184
+ @transmogrifiers.each do |key, _|
185
+ proto = key[0]
186
+
187
+ if not proto.is_a?(Hash)
188
+ next
189
+ end
190
+
191
+ score = value.prototype_match_score(proto)
192
+ if score > best_score
193
+ best_score = score
194
+ best_proto = proto
195
+ end
196
+ end
197
+
198
+ return best_proto
199
+ end
200
+ end # class Registry
201
+ end # module Octiron::Transmogrifiers