langis 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,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