langis 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,57 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = 'langis'
8
+ gem.summary = %Q{
9
+ Langis is a Rack inspired publish-subscribe system for Ruby.}
10
+ gem.description = %Q{
11
+ Langis is a Rack inspired publish-subscribe system for Ruby.
12
+ It has flexible message routing and message handling using a custom
13
+ Domain Specific Language and a Rack-inspired message handler framework.
14
+ This can give Rails applications better (and decoupled) visibility
15
+ and management of the background processes it creates (or executes)
16
+ in response to its actions (in controllers or models).
17
+ }
18
+ gem.email = 'benjaminlyu@gmail.com'
19
+ gem.homepage = 'http://github.com/byu/langis'
20
+ gem.authors = ['Benjamin Yu']
21
+
22
+ # NOTE: Following development dependencies are commented out here
23
+ # because we include them in the Gemfile bundle. If we included them
24
+ # here, then they are required to be installed in the base rubygems
25
+ # repository instead of the Bundler's installation path.
26
+ #gem.add_development_dependency 'delayed_job'
27
+ #gem.add_development_dependency 'redis'
28
+ #gem.add_development_dependency 'resque'
29
+ gem.add_development_dependency 'rspec', '>= 1.2.9'
30
+ #gem.add_development_dependency 'sqlite3-ruby'
31
+ #gem.add_development_dependency 'temping'
32
+ gem.add_development_dependency 'yard'
33
+
34
+ gem.add_dependency 'blockenspiel'
35
+ gem.add_dependency 'eventmachine'
36
+
37
+ gem.files = FileList[
38
+ 'lib/**/*.rb',
39
+ 'bin/*',
40
+ '[A-Z]*',
41
+ 'spec/**/*',
42
+ 'features/**/*',
43
+ 'generators/**/*'].to_a
44
+ end
45
+ Jeweler::GemcutterTasks.new
46
+ rescue LoadError
47
+ puts 'Jeweler (or a dependency) not available. Install it with: gem install jeweler'
48
+ end
49
+
50
+ begin
51
+ require 'yard'
52
+ YARD::Rake::YardocTask.new
53
+ rescue LoadError
54
+ task :yardoc do
55
+ abort 'YARD is not available. In order to run yardoc, you must: sudo gem install yard'
56
+ end
57
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,7 @@
1
+ class LangisConfigGenerator < Rails::Generator::Base
2
+ def manifest
3
+ record do |m|
4
+ m.template 'langis_config.rb', 'config/initializers/langis_config.rb'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,44 @@
1
+ # This file requires modifications to be run in your rails app.
2
+
3
+ # Create the LangisEngine for the rails app to pump messages into.
4
+ LangisEngine = (lambda {
5
+ # Define the routes
6
+ config = Langis::Dsl.langis_plumbing do
7
+ intake :default do
8
+ flow_to :default
9
+ end
10
+
11
+ for_sink :default do
12
+ end
13
+
14
+ check_valve do
15
+ end
16
+ end
17
+
18
+ # Create an example success callback channel.
19
+ success_channel = EM::Channel.new
20
+ success_channel.subscribe(proc do |msg|
21
+ # TODO: Implement your own success handler.
22
+ # Rails.logger.info "Success: #{msg.inspect}"
23
+ end)
24
+
25
+ # Create an example error callback channel.
26
+ error_channel = EM::Channel.new
27
+ error_channel.subscribe(proc do |msg|
28
+ # TODO: Implement your own error handler.
29
+ # Rails.logger.warn "Error: #{msg.inspect}"
30
+ end)
31
+
32
+ # Create and return the actual EventMachine based Langis Engine.
33
+ return Langis::Engine::EventMachineEngine.new(
34
+ config.build_pipes,
35
+ :success_channel => success_channel,
36
+ :error_channel => error_channel)
37
+ }).call
38
+
39
+ # Start up the EventMachine reactor here if need be.
40
+ # Note that starting up the EventMachine is different depending on what
41
+ # web server you are running in.
42
+ #Thread.new do
43
+ # EM.run
44
+ #end
@@ -0,0 +1,60 @@
1
+ ##
2
+ # The base Langis module namespace is used to define global constants for
3
+ # this library.
4
+ module Langis
5
+
6
+ ##
7
+ # The key in the Rack-like environment whose value is the intake
8
+ # that received the message.
9
+ INTAKE_KEY = 'langis.intake'
10
+
11
+ ##
12
+ # The key in the Rack-like environment whose value is the message
13
+ # that is "pumped" into a Langis engine.
14
+ MESSAGE_KEY = 'langis.message'
15
+ ##
16
+ # The key in the Rack-like environment whose value is the message
17
+ # type of the message that is pumped into the Langis engine. This is
18
+ # set if the message's responds to the #message_type method.
19
+ MESSAGE_TYPE_KEY = 'langis.message_type'
20
+
21
+ ##
22
+ # The key in the Rack-like return headers whose value is the
23
+ # caught exception raised by any middleware or application.
24
+ X_EXCEPTION = 'X-Langis-Exception'
25
+ ##
26
+ # The key in the Rack-like return headers whose value is the name of the
27
+ # middleware that prevented the message from propagating further in the
28
+ # application stack.
29
+ X_FILTERED_BY = 'X-Langis-Filtered-By'
30
+ ##
31
+ # The key in the Rack-like return headers whose value is the message type
32
+ # that was filtered. This is
33
+ X_FILTERED_TYPE = 'X-Langis-Filtered-Type'
34
+
35
+ ##
36
+ # The application stack has completed its function successfully. This
37
+ # return result doesn't signify that the end application of the Rack-like
38
+ # stack was called; middleware may have completed its defined function.
39
+ OK = 200
40
+ ##
41
+ # The status message that states that there was an internal error.
42
+ SERVER_ERROR = 500
43
+
44
+ ##
45
+ # The base exception class for all errors that Langis will raise.
46
+ class LangisError < RuntimeError
47
+ end
48
+ end
49
+
50
+ # We include the Langis Library's hard dependencies.
51
+ require 'set'
52
+ require 'blockenspiel'
53
+ require 'eventmachine'
54
+
55
+ # Now we require the library modules itself.
56
+ require 'langis/middleware'
57
+ require 'langis/dsl'
58
+ require 'langis/engine'
59
+ require 'langis/sinks'
60
+ require 'langis/rackish'
@@ -0,0 +1,346 @@
1
+ module Langis
2
+
3
+ ##
4
+ # Module that implements the Domain Specific Language which is used
5
+ # to define Langis publish-subscribe routes.
6
+ module Dsl
7
+
8
+ ##
9
+ # Error that is raised when there is an error in the definition of
10
+ # a set of routes in the Dsl.
11
+ class PipingConfigError < LangisError
12
+ end
13
+
14
+ ##
15
+ # Method to parse the configuration of the routes.
16
+ #
17
+ # config = Langis::Dsl.langis_plumbing do
18
+ # intake :inbound_name do
19
+ # flow_to :sink_name, :when => [:message_type1, :message_type2]
20
+ # flow_to :catch_all
21
+ # end
22
+ # intake :inbound_name do
23
+ # # NOTE that we have a second intake block with the same
24
+ # # :inbound_name name. The configuration in this block
25
+ # # will be merged with the previously declared block.
26
+ # # NOTE that the above :catch_all flow to sink can be overwritten
27
+ # # if we declare a `flow_to :catch_all, :when => [...]`. The
28
+ # # catch all will be restricted to the types declared in this
29
+ # # second flow_to.
30
+ # # NOTE that if we add a `flow_to :sink_name, :when => :type_3`,
31
+ # # it will add :type_3 to the list of message types already
32
+ # # declared in the prior block.
33
+ # end
34
+ # for_sink :sink_name do
35
+ # use SinkSpecificMiddlewareApp, :argument1, :argument2
36
+ # run MyRealApp.new(:argument1, :argument2)
37
+ # end
38
+ # for_sink :catch_all do
39
+ # run lambda { |env| puts Rails.logger.info(env.inspect) }
40
+ # end
41
+ # check_valve do
42
+ # use GlobalMiddlewareApp, :argument1, :argument2
43
+ # end
44
+ # end
45
+ # engine = Langis::Engine::EventMachineEngine.new config.build_pipes
46
+ # engine.pump MyMessage.new(:someinfo), :inbound_name
47
+ #
48
+ # @see RackishConfig
49
+ # @param &block for the dsl configuration
50
+ # @return [PipesConfig] the parsed configuration of the defined routes.
51
+ def langis_plumbing(&block)
52
+ config = PipesConfig.new
53
+ Blockenspiel.invoke block, config
54
+ return config
55
+ end
56
+ module_function :langis_plumbing
57
+
58
+ ##
59
+ # This represents the configuration of the overall Langis piping.
60
+ # It is the configuration of the message intakes and their corresponding
61
+ # Rackish applications.
62
+ #
63
+ # @see Langis::Dsl#langis_plumbing
64
+ class PipesConfig
65
+ include Blockenspiel::DSL
66
+
67
+ ##
68
+ #
69
+ def initialize
70
+ @intakes = {}
71
+ @sinks = {}
72
+ @check_valve = nil
73
+ end
74
+
75
+ ##
76
+ # Dsl only method that parses a sub-block that defines an "intake".
77
+ # An intake is the "queue" name that a message is sent to, and whose
78
+ # defined "sinks" (application stacks) are executed in turn.
79
+ #
80
+ # @param [#to_s] name The name of the intake to define.
81
+ # @param [#to_s] *args Additional named aliases of this intake.
82
+ # @param [Block] &block The dsl configuration block for this intake.
83
+ # @return [IntakeConfig] The configuration of this intake block.
84
+ def intake(name, *args, &block)
85
+ # We require at least one intake to be defined here, and merge
86
+ # it into a list where other intake names have been defined.
87
+ intake_names = args.clone
88
+ intake_names.unshift name
89
+ intake_names.map! { |n| n.to_s }
90
+
91
+ # Here we launch the blockenspiel dsl processing for the intake block.
92
+ config = IntakeConfig.new
93
+ Blockenspiel.invoke block, config
94
+ sink_type_links = config.sink_type_links
95
+
96
+ # Iterate over the returned pipes, then only create intakes that have
97
+ # actual sinks. We don't want to have intakes without sinks.
98
+ # This also is set up so that we can use multiple intake dsl blocks
99
+ # to define a single intake (i.e.- intakes with identical names
100
+ # are only thought of as a single intake.)
101
+ sink_type_links.each do |sink_name, message_types|
102
+ intake_names.each do |intake_name|
103
+ @intakes[intake_name] ||= {}
104
+ @intakes[intake_name][sink_name] ||= Set.new
105
+ @intakes[intake_name][sink_name].merge message_types
106
+ end
107
+ end
108
+ end
109
+
110
+ ##
111
+ # Dsl only method to define a "sink", a Rackish application stack.
112
+ # Subsequent sinks of the same name will overwrite the configuration
113
+ # of a previously defined sink of that name.
114
+ #
115
+ # @param [#to_s] name The name of the sink to define.
116
+ # @param [Block] &block The dsl configuration block for this sink.
117
+ # @return [RackishConfig] The Rackish application stack for this sink.
118
+ def for_sink(name, &block)
119
+ config = RackishConfig.new
120
+ Blockenspiel.invoke block, config
121
+ @sinks[name.to_s] = config
122
+ end
123
+
124
+ ##
125
+ # Dsl only method to define a Rackish application stack that is
126
+ # prepended to all sinks defined in the Langis config. This is
127
+ # so one can define a global intercept patch for functionality like
128
+ # global custom error handling. Note that one SHOULD only declare
129
+ # `use Middleware` type statements since this stack will be prepended
130
+ # to the other sinks. A `run` declaration in here will
131
+ # terminate the execution, or even more likely fail to wire up correctly.
132
+ #
133
+ # @param [#to_s] name The name of the sink to define.
134
+ # @param [Block] &block The dsl configuration block for this sink.
135
+ # @return [RackishConfig] The Rackish application stack.
136
+ def check_valve(&block)
137
+ config = RackishConfig.new
138
+ Blockenspiel.invoke block, config
139
+ @check_valve = config
140
+ end
141
+
142
+ dsl_methods false
143
+
144
+ ##
145
+ # Creates the sinks (application stacks) and references them in their
146
+ # assigned intakes. A create sink can be referenced by multiple intakes.
147
+ #
148
+ # @return [{String => Array<#call>}] The intake name to list of
149
+ # created sinks.
150
+ # @raise [PipingConfigError] Error raised when an intake references
151
+ # a non-existent sink; we are unable to wire up an app.
152
+ def build_pipes
153
+ # Build the main sinks, which may be added to the end of other
154
+ # defined middleware.
155
+ built_sinks = {}
156
+ @sinks.each do |key, value|
157
+ built_sinks[key] = value.to_app
158
+ end
159
+
160
+ # Full pipes is the final return hash to the caller. Its keys are the
161
+ # intake names, and its values are Arrays of the sinks. Each sink
162
+ # is a Rackish application stack.
163
+ full_pipes = {}
164
+ @intakes.each do |intake_name, sink_type_links|
165
+ # Right now, each intake's value is a list of pairs. Each
166
+ # pair is a sink name and the set of message types it should watch
167
+ # for. Empty sets mean to do a catch all.
168
+ sink_type_links.each do |sink_name, message_types|
169
+ built_sink = built_sinks[sink_name]
170
+ # We want to confirm that the sink the intake is referencing
171
+ # actually exists.
172
+ unless built_sink
173
+ raise PipingConfigError.new "Sink not found: #{sink_name}"
174
+ end
175
+
176
+ # If any message type was defined to filter in the intaked block,
177
+ # then we want to create a middleware to filter out all messages
178
+ # whose type is not in that list. Otherwise we'll just flow
179
+ # all messages to this defined sink.
180
+ if message_types.empty?
181
+ half_pipe = built_sink
182
+ else
183
+ half_pipe = ::Langis::Middleware::MessageTypeFilter.new(
184
+ built_sink, *message_types)
185
+ end
186
+
187
+ # If we have a check_valve defined in the configuration, then
188
+ # we want to prepend it to the sink.
189
+ if @check_valve
190
+ full_pipe = @check_valve.to_app half_pipe
191
+ else
192
+ full_pipe = half_pipe
193
+ end
194
+
195
+ # Now add the wired up sink to the list of sinks to be handled
196
+ # by given intake.
197
+ full_pipes[intake_name] ||= []
198
+ full_pipes[intake_name] << full_pipe
199
+ end
200
+ end
201
+ return full_pipes
202
+ end
203
+ end
204
+
205
+ ##
206
+ # Dsl config class used to define an intake.
207
+ #
208
+ # @see Langis::Dsl#langis_plumbing
209
+ class IntakeConfig
210
+ include Blockenspiel::DSL
211
+
212
+ dsl_methods false
213
+
214
+ ##
215
+ # Returns the intake's configuration mapping between its sinks (names)
216
+ # and the list of types to filter for.
217
+ #
218
+ # @return [Hash{String => Set<String>}] The sink name to the set of
219
+ # message types to filter for.
220
+ attr_reader :sink_type_links
221
+
222
+ ##
223
+ #
224
+ def initialize
225
+ @sink_type_links = {}
226
+ end
227
+
228
+ dsl_methods true
229
+
230
+ ##
231
+ # Dsl only method to define which sinks to propagate a message to.
232
+ #
233
+ # @overload flow_to(...)
234
+ # Flow all messages for the intake to the given sink names.
235
+ # @param [Array<#to_s>] ... The list of sink names to push messages to.
236
+ # @overload flow_to(..., options={})
237
+ # Flow all messages for the intake to the given sink names, but
238
+ # may be restricted by type if that option is set.
239
+ # @param [Array<#to_s>] ... The list of sink names to push messages to.
240
+ # @option options [Array<#to_s>] :when ([]) The list of message types
241
+ # that should be sent to the sink, all unlisted types are filtered
242
+ # out. If a sink has zero listed types at the end of the Dsl config,
243
+ # then ALL messages will be sent to that sink.
244
+ def flow_to(*args)
245
+ # Check to see if we have an options hash. Properly pull out the
246
+ # options, and then make the list of sinks to push to.
247
+ case args[-1]
248
+ when Hash
249
+ options = args[-1]
250
+ sink_names = args[0...-1].map! { |name| name.to_s }
251
+ else
252
+ options = {}
253
+ sink_names = args.clone.map! { |name| name.to_s }
254
+ end
255
+
256
+ # Coerce the message types to handle into an Array of strings
257
+ case options[:when]
258
+ when Array
259
+ message_types = options[:when].map { |item| item.to_s }
260
+ when nil
261
+ # For the nil case, we have an empty array.
262
+ # The build_pipes method will interpret a sink with an empty set
263
+ # of types to actually handle all types.
264
+ message_types = []
265
+ else
266
+ message_types = [ options[:when].to_s ]
267
+ end
268
+
269
+ # We add the types to a Set, one set per sink name. This is for the
270
+ # multiple intake definitions.
271
+ sink_names.each do |name|
272
+ @sink_type_links[name] ||= Set.new
273
+ @sink_type_links[name].merge message_types
274
+ end
275
+ end
276
+ end
277
+
278
+ ##
279
+ # A Dsl class used to define Rackish application stacks. This classes
280
+ # implementation is heavily inspired by Rack itself.
281
+ #
282
+ # Example:
283
+ # block = proc do
284
+ # use Middleware, arg1, arg2
285
+ # run lambda { |env| return [200, {}, env[:input1]] }
286
+ # end
287
+ # config = Langis::RackishConfig.new
288
+ # Blockenspiel.invoke block, config
289
+ # my_app = config.to_app
290
+ #
291
+ # env = {
292
+ # :input1 => 'Hello World'
293
+ # }
294
+ # results = my_app.call env
295
+ #
296
+ # @see Langis::Dsl#langis_plumbing
297
+ class RackishConfig
298
+ include Blockenspiel::DSL
299
+
300
+ dsl_methods false
301
+
302
+ ##
303
+ # @param [#call] app Optional endpoint to declare up front.
304
+ # If nil, then a "no-op" end-point is used with a very basic return.
305
+ def initialize(app=nil)
306
+ @ins = []
307
+ @app = app ? app : lambda { |env| [OK, {}, [""]] }
308
+ end
309
+
310
+ ##
311
+ # The method that actually wires up each middleware and the end point
312
+ # into a real Rack stack.
313
+ #
314
+ # @param [#call] app Optional endpoint to use instead of the one
315
+ # previously defined by a `run`.
316
+ # @return [#call] The Rackish application.
317
+ def to_app(app=nil)
318
+ app ||= @app
319
+ @ins.reverse.inject(app) { |a, e| e.call(a) }
320
+ end
321
+
322
+ dsl_methods true
323
+
324
+ ##
325
+ # Dsl only method that defines a piece of Middleware to run, in order,
326
+ # in this Rack-lik application.
327
+ #
328
+ # @param [Class] middleware The middleware class to instantiate.
329
+ # @param *args The arguments to pass to the initialize method for
330
+ # the middleware class instantiation.
331
+ # @param &block A code block to pass to the initialize method for
332
+ # the middleware class instantiation.
333
+ def use(middleware, *args, &block)
334
+ @ins << lambda { |app| middleware.new(app, *args, &block) }
335
+ end
336
+
337
+ ##
338
+ # Dsl only method that defines the end point Rack app handler.
339
+ #
340
+ # @param [#call] app The Rackish endpoint for this app.
341
+ def run(app)
342
+ @app = app
343
+ end
344
+ end
345
+ end
346
+ end