rflow 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +6 -0
  3. data/.rvmrc +1 -0
  4. data/Gemfile +5 -0
  5. data/NOTES +187 -0
  6. data/README +0 -0
  7. data/Rakefile +16 -0
  8. data/bin/rflow +215 -0
  9. data/example/basic_config.rb +49 -0
  10. data/example/basic_extensions.rb +142 -0
  11. data/example/http_config.rb +21 -0
  12. data/example/http_extensions.rb +262 -0
  13. data/lib/rflow.rb +440 -0
  14. data/lib/rflow/component.rb +192 -0
  15. data/lib/rflow/component/port.rb +150 -0
  16. data/lib/rflow/components.rb +10 -0
  17. data/lib/rflow/components/raw.rb +26 -0
  18. data/lib/rflow/components/raw/extensions.rb +18 -0
  19. data/lib/rflow/configuration.rb +290 -0
  20. data/lib/rflow/configuration/component.rb +27 -0
  21. data/lib/rflow/configuration/connection.rb +98 -0
  22. data/lib/rflow/configuration/migrations/20010101000001_create_settings.rb +14 -0
  23. data/lib/rflow/configuration/migrations/20010101000002_create_components.rb +19 -0
  24. data/lib/rflow/configuration/migrations/20010101000003_create_ports.rb +24 -0
  25. data/lib/rflow/configuration/migrations/20010101000004_create_connections.rb +27 -0
  26. data/lib/rflow/configuration/port.rb +30 -0
  27. data/lib/rflow/configuration/ruby_dsl.rb +183 -0
  28. data/lib/rflow/configuration/setting.rb +67 -0
  29. data/lib/rflow/configuration/uuid_keyed.rb +18 -0
  30. data/lib/rflow/connection.rb +59 -0
  31. data/lib/rflow/connections.rb +2 -0
  32. data/lib/rflow/connections/zmq_connection.rb +101 -0
  33. data/lib/rflow/message.rb +191 -0
  34. data/lib/rflow/port.rb +4 -0
  35. data/lib/rflow/util.rb +19 -0
  36. data/lib/rflow/version.rb +3 -0
  37. data/rflow.gemspec +42 -0
  38. data/schema/message.avsc +36 -0
  39. data/schema/raw.avsc +9 -0
  40. data/spec/fixtures/config_ints.rb +61 -0
  41. data/spec/fixtures/extensions_ints.rb +141 -0
  42. data/spec/rflow_configuration_spec.rb +73 -0
  43. data/spec/rflow_message_data_raw.rb +26 -0
  44. data/spec/rflow_message_data_spec.rb +60 -0
  45. data/spec/rflow_message_spec.rb +182 -0
  46. data/spec/rflow_spec.rb +100 -0
  47. data/spec/schema_spec.rb +28 -0
  48. data/spec/spec_helper.rb +37 -0
  49. data/temp.rb +295 -0
  50. metadata +270 -0
@@ -0,0 +1,150 @@
1
+ class RFlow
2
+ class Component
3
+
4
+ # TODO: make this into a class to limit the amount of extensions
5
+ # that we have to do when operating on these "Arrays", i.e. when
6
+ # adding two together
7
+ module ConnectionCollection
8
+ def send_message(message)
9
+ each do |connection|
10
+ connection.send_message(message)
11
+ end
12
+ end
13
+ end
14
+
15
+ # Collection class to make it easier to index by both names,
16
+ # UUIDs, and types.
17
+ class PortCollection
18
+ attr_reader :ports, :by_uuid, :by_name, :by_type
19
+
20
+ def initialize
21
+ @ports = Array.new
22
+ @by_uuid = Hash.new
23
+ @by_name = Hash.new
24
+ @by_type = Hash.new {|hash, key| hash[key.to_s] = []}
25
+ end
26
+
27
+ def <<(port)
28
+ by_uuid[port.instance_uuid.to_s] = port
29
+ by_name[port.name.to_s] = port
30
+ by_type[port.class.to_s] << port
31
+ ports << port
32
+ self
33
+ end
34
+
35
+
36
+ # Enumerate through each connected (or disconnected but
37
+ # referenced) port
38
+ # TODO: simplify with enumerators and procs
39
+ def each
40
+ ports.each do |port|
41
+ yield port
42
+ end
43
+ end
44
+ end
45
+
46
+
47
+ # Bare superclass for (potential) later methods. Currently empty
48
+ class Port; end
49
+
50
+
51
+ # Allows for a list of connections to be assigned to each port/key
52
+ # combination. Note that binding an input port to an un-indexed
53
+ # output port will result in messages from all indexed connections
54
+ # being received. Similarly, sending to an unindexed port will
55
+ # result in the same message being sent to all indexed
56
+ # connections.
57
+ class HashPort < Port
58
+ attr_reader :name, :instance_uuid, :options, :connections_for
59
+
60
+ def initialize(name, instance_uuid, options={})
61
+ @name = name
62
+ @instance_uuid = instance_uuid
63
+ @connections_for = Hash.new {|hash, key| hash[key] = Array.new.extend(ConnectionCollection)}
64
+ end
65
+
66
+ # Returns an extended Array of all the connections that should
67
+ # be sent/received on this port. Merges the nil-keyed port
68
+ # (i.e. any connections for a port without a key) to those
69
+ # specific for the key, so should only be used to read a list of
70
+ # connections, not to add new ones. Use add_connection to add a
71
+ # new connection for a given key.
72
+ def [](key)
73
+ (connections_for[key] + connections_for[nil]).extend(ConnectionCollection)
74
+ end
75
+
76
+
77
+ # Adds a connection for a given key
78
+ def add_connection(key, connection)
79
+ connections_for[key] << connection
80
+ end
81
+
82
+
83
+ # Return a list of connected keys
84
+ def keys
85
+ connections_for.keys
86
+ end
87
+
88
+
89
+ # Enumerate through all the ConnectionCollections
90
+ # TODO: simplify with enumerators and procs
91
+ def each
92
+ connections_for.values.each do |connections|
93
+ yield connections
94
+ end
95
+ end
96
+
97
+
98
+ # Send a message to all connections on all keys for this port,
99
+ # but only once per connection.
100
+ def send_message(message)
101
+ all_connections.send_message(message)
102
+ end
103
+
104
+
105
+ # Should be overridden. Called when it is time to actually
106
+ # establish the connection
107
+ def connect!; raise NotImplementedError, "Raw ports do not know which direction to connect"; end
108
+
109
+ private
110
+
111
+ def all_connections
112
+ @all_connections ||= connections_for.map do |port_key, connections|
113
+ connections
114
+ end.flatten.uniq.extend(ConnectionCollection)
115
+ end
116
+
117
+ end
118
+
119
+
120
+ class InputPort < HashPort
121
+ def connect!
122
+ connections_for.each do |port_key, connections|
123
+ connections.each do |connection|
124
+ connection.connect_input!
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+
131
+ class OutputPort < HashPort
132
+ def connect!
133
+ connections_for.each do |port_key, keyed_connections|
134
+ keyed_connections.each do |connection|
135
+ connection.connect_output!
136
+ end
137
+ end
138
+ end
139
+ end
140
+
141
+ class DisconnectedPort < HashPort; end
142
+
143
+ end
144
+ end
145
+
146
+ __END__
147
+
148
+ out[even] -> a
149
+ out[odd] -> b
150
+ out[nil] -> c
@@ -0,0 +1,10 @@
1
+ require 'rflow/component'
2
+
3
+ class RFlow
4
+ module Components
5
+ end
6
+ end
7
+
8
+ require 'rflow/components/raw'
9
+
10
+
@@ -0,0 +1,26 @@
1
+ require 'rflow/components/raw/extensions'
2
+
3
+ class RFlow
4
+ module Components
5
+ module Raw
6
+
7
+ # Load the schemas
8
+ SCHEMA_DIRECTORY = ::File.expand_path(::File.join(::File.dirname(__FILE__), '..', '..', '..', 'schema'))
9
+
10
+ SCHEMA_FILES = {
11
+ 'raw.avsc' => 'RFlow::Message::Data::Raw',
12
+ }
13
+
14
+ SCHEMA_FILES.each do |file_name, data_type_name|
15
+ schema_string = ::File.read(::File.join(SCHEMA_DIRECTORY, file_name))
16
+ RFlow::Configuration.add_available_data_type data_type_name, 'avro', schema_string
17
+ end
18
+
19
+ # Load the data extensions
20
+ RFlow::Configuration.add_available_data_extension('RFlow::Message::Data::Raw',
21
+ RFlow::Components::Raw::Extensions::RawExtension)
22
+
23
+
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,18 @@
1
+ class RFlow
2
+ module Components
3
+ module Raw
4
+
5
+ module Extensions
6
+
7
+ module RawExtension
8
+ def self.extended(base_data)
9
+ base_data.data_object ||= {'raw' => ''}
10
+ end
11
+
12
+ def raw; data_object['raw']; end
13
+ def raw=(new_raw); data_object['raw'] = new_raw; end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,290 @@
1
+ require 'rflow/util'
2
+
3
+ class RFlow
4
+
5
+ # Contains all the configuration data and methods for RFlow.
6
+ # Interacts directly with underlying sqlite database, and keeps a
7
+ # registry of available data types, extensions, and components.
8
+ # Also includes an external DSL, RubyDSL, that can be used in
9
+ # crafting config-like files that load the database.
10
+ #
11
+ # Configuration provides a MVC-like framework for config files,
12
+ # where the models are the Setting, Component, Port, and Connection
13
+ # subclasses, the controllers are things like RubyDSL, and the views
14
+ # are defined relative to the controllers
15
+ class Configuration
16
+
17
+ # An exception class
18
+ class ConfigurationInvalid < StandardError; end
19
+
20
+
21
+ # A class to hold DB config and connection information
22
+ class ConfigDB < ActiveRecord::Base
23
+ self.abstract_class = true
24
+ end
25
+
26
+
27
+ # A collection class for data extensions that supports a naive
28
+ # prefix-based 'inheritance' on lookup. When looking up a key
29
+ # with [] all existing keys will be examined to determine if the
30
+ # existing key is a string prefix of the lookup key. All the
31
+ # results are consolidated into a single, flattened array.
32
+ class DataExtensionCollection
33
+
34
+ def initialize
35
+ # TODO: choose a different data structure ...
36
+ @hash = Hash.new {|hash, key| hash[key] = Array.new}
37
+ end
38
+
39
+ # Return an array of all of the values that have keys that are
40
+ # prefixes of the lookup key.
41
+ def [](key)
42
+ key_string = key.to_s
43
+ @hash.map do |data_type, extensions|
44
+ key_string.start_with?(data_type) ? extensions : nil
45
+ end.flatten.compact
46
+ end
47
+
48
+ # Adds a data extension for a given data type to the collection
49
+ def add(data_type, extension)
50
+ @hash[data_type.to_s] << extension
51
+ end
52
+
53
+ # Remove all elements from the collection. Useful for testing,
54
+ # not much else
55
+ def clear
56
+ @hash.clear
57
+ end
58
+
59
+ end
60
+
61
+
62
+ class << self
63
+
64
+ # A collection of data types (schemas) indexed by their name and
65
+ # their schema type ('avro').
66
+ def available_data_types
67
+ @available_data_types ||= Hash.new {|hash, key| hash[key] = Hash.new}
68
+ end
69
+
70
+ # A DataExtensionCollection to hold available extensions that
71
+ # will be applied to the de-serialized data types
72
+ def available_data_extensions
73
+ @available_data_extensions ||= DataExtensionCollection.new
74
+ end
75
+
76
+ # A Hash of defined components, usually automatically populated
77
+ # when a component subclasses RFlow::Component
78
+ def available_components
79
+ @available_components ||= Hash.new
80
+ end
81
+ end
82
+
83
+ # TODO: refactor each of these add_available_* into collections to
84
+ # make DRYer. Also figure out what to do with all to to_syms
85
+
86
+ # Add a schema to the available_data_types class attribute.
87
+ # Schema is indexed by data_type_name and schema/serialization
88
+ # type. 'avro' is currently the only supported
89
+ # data_serialization_type.
90
+ def self.add_available_data_type(data_type_name, data_serialization_type, data_schema)
91
+ unless data_serialization_type == 'avro'
92
+ error_message = "Data serialization_type must be 'avro' for '#{data_type_name}'"
93
+ RFlow.logger.error error_message
94
+ raise ArgumentError, error_message
95
+ end
96
+
97
+ if available_data_types[data_type_name.to_s].include? data_serialization_type.to_s
98
+ error_message = "Data type '#{data_type_name}' already defined for serialization_type '#{data_serialization_type}'"
99
+ RFlow.logger.error error_message
100
+ raise ArgumentError, error_message
101
+ end
102
+
103
+ available_data_types[data_type_name.to_s][data_serialization_type.to_s] = data_schema
104
+ end
105
+
106
+ # Add a data extension to the available_data_extensions class
107
+ # attributes. The data_extension parameter should be the name of
108
+ # a ruby module that will extend RFlow::Message::Data object to
109
+ # provide additional methods/capability. Naive, prefix-based
110
+ # inheritance is possible, see available_data_extensions or the
111
+ # DataExtensionCollection class
112
+ def self.add_available_data_extension(data_type_name, data_extension)
113
+ unless data_extension.is_a? Module
114
+ error_message = "Invalid data extension #{data_extension} for #{data_type_name}. Only Ruby Modules allowed"
115
+ RFlow.logger.error error_message
116
+ raise ArgumentError, error_message
117
+ end
118
+
119
+ available_data_extensions.add data_type_name, data_extension
120
+ end
121
+
122
+
123
+ # Used when RFlow::Component is subclassed to add another
124
+ # available component to the list.
125
+ def self.add_available_component(component)
126
+ if available_components.include?(component.name)
127
+ error_message = "Component already '#{component.name}' already defined"
128
+ RFlow.logger.error error_message
129
+ raise ArgumentError, error_message
130
+ end
131
+ available_components[component.name] = component
132
+ end
133
+
134
+
135
+ # Connect to the configuration sqlite database, but use the
136
+ # ConfigDB subclass to protect the connection information from
137
+ # other ActiveRecord apps (i.e. Rails)
138
+ def self.establish_config_database_connection(config_database_path)
139
+ RFlow.logger.debug "Establishing connection to config database (#{Dir.getwd}) '#{config_database_path}'"
140
+ ActiveRecord::Base.logger = RFlow.logger
141
+ ConfigDB.establish_connection(:adapter => "sqlite3",
142
+ :database => config_database_path)
143
+ end
144
+
145
+
146
+ # Using default ActiveRecord migrations, attempt to migrate the
147
+ # database to the latest version.
148
+ def self.migrate_database
149
+ RFlow.logger.debug "Applying default migrations to config database"
150
+ migrations_directory_path = File.join(File.dirname(__FILE__), 'configuration', 'migrations')
151
+ # ActiveRecord::Migration.verbose = RFlow.logger
152
+ ActiveRecord::Migrator.migrate migrations_directory_path
153
+ end
154
+
155
+
156
+ # Load the config file, which should load/process/store all the
157
+ # elements. Only run this after the database has been setup
158
+ def self.process_config_file(config_file_path)
159
+ RFlow.logger.info "Processing config file (#{Dir.getwd}) '#{config_file_path}'"
160
+ load config_file_path
161
+ end
162
+
163
+
164
+ # Connect to the configuration database, migrate it to the latest
165
+ # version, and process a config file if provided.
166
+ def self.initialize_database(config_database_path, config_file_path=nil)
167
+ RFlow.logger.debug "Initializing config database (#{Dir.getwd}) '#{config_database_path}'"
168
+
169
+ RFlow.logger.debug "Establishing connection to config database (#{Dir.getwd}) '#{config_database_path}'"
170
+ ActiveRecord::Base.logger = RFlow.logger
171
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3",
172
+ :database => config_database_path)
173
+
174
+ migrate_database
175
+
176
+ expanded_config_file_path = File.expand_path config_file_path if config_file_path
177
+
178
+ working_dir = Dir.getwd
179
+ Dir.chdir File.dirname(config_database_path)
180
+
181
+ if config_file_path
182
+ process_config_file(expanded_config_file_path)
183
+ end
184
+
185
+ RFlow.logger.debug "Defaulting non-existing config values"
186
+ merge_defaults!
187
+
188
+ Dir.chdir working_dir
189
+ end
190
+
191
+
192
+ # Make sure that the configuration has all the necessary values set.
193
+ def self.merge_defaults!
194
+ Setting::DEFAULTS.each do |name, default_value_or_proc|
195
+ setting = Setting.find_or_create_by_name(:name => name,
196
+ :value => default_value_or_proc.is_a?(Proc) ? default_value_or_proc.call() : default_value_or_proc)
197
+ unless setting.valid?
198
+ error_message = setting.errors.map do |attribute, error_string|
199
+ error_string
200
+ end.join ', '
201
+ raise RuntimeError, error_message
202
+ end
203
+ end
204
+ end
205
+
206
+
207
+ attr_accessor :config_database_path
208
+ attr_accessor :cached_settings
209
+ attr_accessor :cached_components
210
+ attr_accessor :cached_ports
211
+ attr_accessor :cached_connections
212
+
213
+
214
+ def initialize(config_database_path)
215
+ @cached_settings = Hash.new
216
+ @cached_components = Hash.new
217
+ @cached_ports = []
218
+ @cached_connections = []
219
+
220
+ @config_database_path = config_database_path
221
+ self.class.establish_config_database_connection(config_database_path)
222
+
223
+ # Validate the connected database. TODO: make this more
224
+ # complete, i.e. validate the various columns
225
+ begin
226
+ Setting.first
227
+ Component.first
228
+ Port.first
229
+ Connection.first
230
+ rescue ActiveRecord::StatementInvalid => e
231
+ error_message = "Invalid schema in configuration database: #{e.message}"
232
+ RFlow.logger.error error_message
233
+ raise ArgumentError, error_message
234
+ end
235
+ end
236
+
237
+
238
+ def to_s
239
+ string = "Configuration:\n"
240
+ settings.each do |setting|
241
+ string << "Setting: '#{setting.name}' = '#{setting.value}'\n"
242
+ end
243
+ components.each do |component|
244
+ string << "Component '#{component.name}' as #{component.specification} (#{component.uuid})\n"
245
+ component.output_ports.each do |output_port|
246
+ output_port.output_connections.each do |output_connection|
247
+ input_port = output_connection.input_port
248
+ string << "\tOutputPort '#{output_port.name}' key '#{output_connection.output_port_key}' (#{output_port.uuid}) =>\n"
249
+ string << "\t\tConnection '#{output_connection.name}' as #{output_connection.type} (#{output_connection.uuid}) =>\n"
250
+ string << "\t\tInputPort '#{input_port.name}' key '#{output_connection.input_port_key}' (#{input_port.uuid}) Component '#{input_port.component.name}' (#{input_port.component.uuid})\n"
251
+ end
252
+ end
253
+ end
254
+ string
255
+ end
256
+
257
+ # Helper method to access settings with minimal syntax
258
+ def [](setting_name)
259
+ Setting.find_by_name(setting_name).value rescue nil
260
+ end
261
+
262
+
263
+ def components
264
+ Component.all
265
+ end
266
+
267
+
268
+ def component(component_instance_uuid)
269
+ Component.find_by_uuid component_instance_uuid
270
+ end
271
+
272
+
273
+ def settings
274
+ Setting.all
275
+ end
276
+
277
+ def available_components
278
+ self.class.available_components
279
+ end
280
+ end
281
+ end
282
+
283
+ # Load the models
284
+ require 'rflow/configuration/setting'
285
+ require 'rflow/configuration/component'
286
+ require 'rflow/configuration/port'
287
+ require 'rflow/configuration/connection'
288
+
289
+ # Incorporate various config file processors
290
+ require 'rflow/configuration/ruby_dsl'