octiron 0.1.0

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.
@@ -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