serf 0.9.0 → 0.10.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.
data/lib/serf/builder.rb CHANGED
@@ -1,6 +1,5 @@
1
- require 'serf/routing/endpoint'
2
- require 'serf/routing/registry'
3
- require 'serf/runners/direct'
1
+ require 'serf/routing/route'
2
+ require 'serf/routing/route_set'
4
3
  require 'serf/serfer'
5
4
  require 'serf/util/null_object'
6
5
  require 'serf/util/options_extraction'
@@ -9,7 +8,7 @@ module Serf
9
8
 
10
9
  ##
11
10
  # A Serf Builder that processes the SerfUp DSL to build a rack-like
12
- # app to endpoint and process received messages. This builder is
11
+ # app to handlers that process received messages. This builder is
13
12
  # implemented based on code from Rack::Builder.
14
13
  #
15
14
  # builder = Serf::Builder.parse_file 'examples/config.su'
@@ -25,47 +24,80 @@ module Serf
25
24
  class Builder
26
25
  include Serf::Util::OptionsExtraction
27
26
 
27
+ attr_reader :serfer_factory
28
+ attr_reader :route_set_factory
29
+ attr_reader :route_factory
30
+
28
31
  def self.parse_file(config)
29
32
  cfgfile = ::File.read(config)
30
- builder = eval "::Serf::Builder.new {\n" + cfgfile + "\n}",
33
+ builder = eval "Serf::Builder.new {\n" + cfgfile + "\n}",
31
34
  TOPLEVEL_BINDING, config
32
35
  return builder
33
36
  end
34
37
 
38
+ def self.app(*args, &block)
39
+ new(*args, &block).to_app
40
+ end
41
+
35
42
  def initialize(*args, &block)
36
43
  extract_options! args
37
44
 
38
- # Configuration about the endpoints and apps to run.
45
+ # Our factories
46
+ @serfer_factory = opts :serfer_factory, Serf::Serfer
47
+ @route_set_factory = opts :route_set_factory, Serf::Routing::RouteSet
48
+ @route_factory = opts :route_factory, Serf::Routing::Route
49
+
50
+ # List of middleware to be executed (non-built form)
39
51
  @use = []
40
- @not_found = opts :not_found, lambda { |env|
41
- raise ArgumentError, 'Endpoints Not Found'
42
- }
43
52
 
44
- # Utility and messaging channels that get passed as options
45
- # when building our Runners and Handlers.
46
- @response_channel = ::Serf::Util::NullObject.new
47
- @error_channel = ::Serf::Util::NullObject.new
48
- @logger = ::Serf::Util::NullObject.new
53
+ # A list of "mounted", non-built, command handlers with their
54
+ # matcher and policies.
55
+ @runs = []
56
+
57
+ # List of default policies to be run (non-built form)
58
+ @default_policies = []
49
59
 
50
- # Set up the starting state for our DSL calls.
51
- @runner_matcher_endpoint_map = {}
52
- @runner_params = {}
53
- runner :direct
60
+ # The current matcher
54
61
  @matcher = nil
55
62
 
63
+ # Current policies to be run (PRE-built)
64
+ @policies = []
65
+
56
66
  # configure based on a given block.
57
67
  instance_eval(&block) if block_given?
58
68
  end
59
69
 
60
- def self.app(default_app=nil, &block)
61
- self.new(default_app, &block).to_app
70
+ ##
71
+ # Append a policy to default policy chain. The default
72
+ # policy chain is used by any route that does not define
73
+ # at least one of its own policies.
74
+ #
75
+ # @param policy the policy factory to append
76
+ # @param *args the arguments to pass to the factory
77
+ # @param &block the block to pass to the factory
78
+ def default_policy(policy, *args, &block)
79
+ @default_policies << proc { policy.build(*args, &block) }
62
80
  end
63
81
 
82
+ ##
83
+ # Append a rack-like middleware
84
+ #
85
+ # @param the middleware class
86
+ # @param *args the arguments to pass to middleware.new
87
+ # @param &block the block to pass to middleware.new
64
88
  def use(middleware, *args, &block)
65
89
  @use << proc { |app| middleware.new(app, *args, &block) }
66
90
  end
67
91
 
68
- def not_found(app); @not_found = app; end
92
+ ##
93
+ # Append a policy to the current match's policy chain.
94
+ #
95
+ # @param policy the policy factory to append
96
+ # @param *args the arguments to pass to the factory
97
+ # @param &block the block to pass to the factory
98
+ def policy(policy, *args, &block)
99
+ @policies << proc { policy.build(*args, &block) }
100
+ end
69
101
 
70
102
  def response_channel(channel); @response_channel = channel; end
71
103
  def error_channel(channel); @error_channel = channel; end
@@ -74,74 +106,34 @@ module Serf
74
106
  ##
75
107
  # DSL Method to change our current context to use the given matcher.
76
108
  #
77
- def match(matcher); @matcher = matcher; end
78
-
79
- ##
80
- # Mount and endpoint to the current context's Runner and Matcher.
81
- # Connected so the endpoint will pass serf_options to the handler's build.
82
- def run(*args, &block); mount true, *args, &block; end
83
-
84
- ##
85
- # Mount and endpoint to the current context's Runner and Matcher.
86
- # Unconnected so the endpoint will omit serf_options to the handler's build.
87
- def run_unconnected(*args, &block); mount false, *args, &block; end
88
-
89
- ##
90
- # The generic mount method used by run & run_unconnected to create an
91
- # endpoint to be associated with the current context's Runner and Matcher.
92
- def mount(connected, handler_factory, *args, &block)
93
- raise 'No matcher defined yet' unless @matcher
94
- @runner_matcher_endpoint_map[@runner_factory] ||= {}
95
- @runner_matcher_endpoint_map[@runner_factory][@matcher] ||= []
96
- @runner_matcher_endpoint_map[@runner_factory][@matcher] <<
97
- Serf::Routing::Endpoint.new(
98
- connected,
99
- handler_factory,
100
- *args,
101
- &block)
102
- end
103
-
104
- ##
105
- # DSL Method to change our current context to use the given runner.
106
- #
107
- def runner(type)
108
- @runner_factory = case type
109
- when :direct
110
- ::Serf::Runners::Direct
111
- when :event_machine
112
- begin
113
- require 'serf/runners/event_machine'
114
- Serf::Runners::EventMachine
115
- rescue NameError => e
116
- e.extend Serf::Error
117
- raise e
118
- end
119
- when :girl_friday
120
- begin
121
- require 'serf/runners/girl_friday'
122
- Serf::Runners::GirlFriday
123
- rescue NameError => e
124
- e.extend Serf::Error
125
- raise e
126
- end
127
- else
128
- raise 'No callable runner' unless type.respond_to? :build
129
- type
130
- end
131
- end
132
-
133
- def params(*args)
134
- @runner_params[@runner_factory] = args
109
+ def match(matcher)
110
+ @matcher = matcher
111
+ @policies = []
135
112
  end
136
113
 
137
114
  ##
138
- # Returns a hash of our current serf infrastructure options
139
- # to be passed to Endpoint#build methods.
140
- def serf_options
141
- {
142
- response_channel: @response_channel,
143
- error_channel: @error_channel,
144
- logger: @logger
115
+ # @param command_factory the factory to invoke (in #to_app)
116
+ # @param *args the rest of the args to pass to command_factory#build method
117
+ # @param &block the block to pass to command_factory#build method
118
+ def run(command_factory, *args, &block)
119
+ raise 'No matcher defined yet' unless @matcher
120
+ # Create a local duplicate of the matcher and policies "snapshotted"
121
+ # at the time this method is called... so that snapshot is consistent
122
+ # for when the proc is called.
123
+ matcher = @matcher.dup
124
+ policies = @policies.dup
125
+
126
+ # This proc will be called in to_app when we actually go ahead and
127
+ # instantiate all the objects. By this point, route_set and
128
+ # default_policies passed to this proc will be ready, built.
129
+ @runs << proc { |route_set, default_policies|
130
+ route_set.add(
131
+ matcher,
132
+ route_factory.build(
133
+ command: command_factory.build(*args, &block),
134
+ policies: (policies.size > 0 ?
135
+ policies.map{ |p| p.call } :
136
+ default_policies)))
145
137
  }
146
138
  end
147
139
 
@@ -149,54 +141,26 @@ module Serf
149
141
  # Create our app.
150
142
  #
151
143
  def to_app
152
- # By default, we go to the not_found app.
153
- app = @not_found
154
-
155
- registries = {}
156
-
157
- # Set additional options for all the mounts
158
- @runner_matcher_endpoint_map.each do |runner_factory, matcher_endpoints|
159
-
160
- # 1. Create a registry for our given hash of matchers to endpoints.
161
- # 2. Convert the hash of matcher to endpoints into a useable registry
162
- # for lookups.
163
- registry = ::Serf::Routing::Registry.new
164
- matcher_endpoints.each do |matcher, endpoints|
165
- registry.add matcher, endpoints
166
- end
167
-
168
- # Ok, we'll create the runner and add it to our registries hash
169
- # if we actually have endpoints here.
170
- if registry.size > 0
171
- runner_params = @runner_params[runner_factory] ?
172
- @runner_params[runner_factory] :
173
- []
174
- runner_params << (runner_params.last.is_a?(Hash) ?
175
- runner_params.pop.merge(serf_options) :
176
- serf_options)
177
- runner = runner_factory.build *runner_params
178
- registries[runner] = registry
179
- end
180
- end
181
-
182
- if registries.size > 0
183
- app = Serf::Serfer.build(
184
- # The registries to match, and their runners to execute.
185
- registries: registries,
186
- # App if no endpoints were found.
187
- not_found: app,
188
- # Serf infrastructure options to pass to 'connected' Endpoints
189
- # to build a handler instance for each env hash received.
190
- serf_options: serf_options,
191
- # Options to use by serfer because it includes ErrorHandling.
192
- error_channel: @error_channel,
193
- logger: @logger)
144
+ # Create the route_set to resolve routes
145
+ route_set = route_set_factory.build
146
+ # Build the default policies to be used if routes did not specify any.
147
+ default_policies = @default_policies.map{ |p| p.call }
148
+ # Add each route to the route_set
149
+ for run in @runs
150
+ run.call route_set, default_policies
194
151
  end
152
+ # Create our serfer class
153
+ app = serfer_factory.build(
154
+ route_set: route_set,
155
+ response_channel: (@response_channel || Serf::Util::NullObject.new),
156
+ error_channel: (@error_channel || Serf::Util::NullObject.new),
157
+ logger: (@logger || Serf::Util::NullObject.new))
195
158
 
196
159
  # We're going to inject middleware here.
197
160
  app = @use.reverse.inject(app) { |a,e| e[a] } if @use.size > 0
198
161
 
199
162
  return app
200
163
  end
164
+
201
165
  end
202
166
  end
data/lib/serf/command.rb CHANGED
@@ -1,8 +1,10 @@
1
1
  require 'active_support/concern'
2
2
  require 'active_support/core_ext/class/attribute'
3
3
 
4
- require 'serf/util/mash_factory'
4
+ require 'serf/util/error_handling'
5
5
  require 'serf/util/options_extraction'
6
+ require 'serf/util/protected_call'
7
+ require 'serf/util/uuidable'
6
8
 
7
9
  module Serf
8
10
 
@@ -12,103 +14,64 @@ module Serf
12
14
  # class MyCommand
13
15
  # include Serf::Command
14
16
  #
15
- # # Set a default Message Parser for the class.
16
- # self.request_factory = MySerfRequestMessage
17
- #
18
- # # Validate the request, like using JSON-Schema for hash
19
- # # (or Hashie's Mash) requests.
20
- # self.request_validator = MySerfRequestValidator
21
- #
22
- # def initialize(*args, &block)
17
+ # def initialize(*contructor_params, &block)
23
18
  # # Do some validation here, or extra parameter setting with the args
19
+ # @model = opts :model, MyModel
24
20
  # end
25
21
  #
26
- # def call
27
- # # Do something w/ @request and @opts
28
- # return nil # e.g. MySerfMessage
22
+ # def call(request, context)
23
+ # # Do something w/ request, opts and context.
24
+ # item = @model.find request.model_id
25
+ # # create a new hashie of UUIDs, which we will use as the base
26
+ # # hash of our response
27
+ # response = create_uuids request
28
+ # response.kind = 'my_command/events/did_something'
29
+ # response.item = item
30
+ # return response
29
31
  # end
30
32
  # end
31
33
  #
32
- # MyCommand.call(REQUEST_ENV, some, extra, params, options_hash, &block)
33
- #
34
- # # Built in lambda wrapping to use the MyCommand with GirlFriday.
35
- # worker = MyCommand.worker some, extra, params, options_hash, &block
36
- # work_queue = GirlFriday::WorkQueue.new &worker
37
- # work_queue.push REQUEST_ENV
34
+ # constructor_params = [1, 2, 3, 4, etc]
35
+ # block = Proc.new {}
36
+ # request = ::Hashie::Mash.new
37
+ # context = ::Hashie::Mash.new user: current_user
38
+ # MyCommand.call(request, context, *contructor_params, &block)
38
39
  #
39
40
  module Command
40
41
  extend ActiveSupport::Concern
41
- include Serf::Util::OptionsExtraction
42
42
 
43
- included do
44
- class_attribute :request_factory
45
- __send__ 'request_factory=', Serf::Util::MashFactory
46
- class_attribute :request_validator
47
- attr_reader :request
48
- end
43
+ # Including Serf::Util::*... Order matters, kind of, here.
44
+ include Serf::Util::Uuidable
45
+ include Serf::Util::OptionsExtraction
46
+ include Serf::Util::ProtectedCall
47
+ include Serf::Util::ErrorHandling
49
48
 
50
- def call
49
+ def call(request, context=nil *args, &block)
51
50
  raise NotImplementedError
52
51
  end
53
52
 
54
- def validate_request!
55
- request_validator.validate! request if request_validator
56
- end
57
-
58
53
  module ClassMethods
59
54
 
60
55
  ##
61
56
  # Class method that both builds then executes the unit of work.
62
57
  #
63
- def call(*args, &block)
64
- self.build(*args, &block).call
58
+ # @param request the request
59
+ # @param context the context about the request. Things like the
60
+ # requesting :user for ACL.
61
+ # @param *args remaining contructor arguments
62
+ # @param &block the block to pass to constructor
63
+ #
64
+ def call(request, context=::Hashie::Mash.new, *args, &block)
65
+ self.build(*args, &block).call(request, context)
65
66
  end
66
67
 
67
68
  ##
68
69
  # Factory build method that creates an object of the implementing
69
- # class' unit of work with the given parameters.
70
+ # class' unit of work with the given parameters. By default,
71
+ # This just calls the class new method.
70
72
  #
71
73
  def build(*args, &block)
72
- # The very first argument is the Request, we shift it off the args var.
73
- req = args.shift
74
- req = {} if req.nil?
75
-
76
- # Now we allocate the object, and do some options extraction that may
77
- # modify the args array by popping off the last element if it is a hash.
78
- obj = allocate
79
- obj.__send__ :extract_options!, args
80
-
81
- # If the request was a hash, we MAY be able to convert it into a
82
- # request object. We only do this if a request_factory was set either
83
- # in the options, or if the request_factory class attribute is set.
84
- # Otherwise, just give the command the hash, and it is up to them
85
- # to understand what was given to it.
86
- factory = obj.opts :request_factory, self.request_factory
87
- request = (req.is_a?(Hash) && factory ? factory.build(req) : req)
88
-
89
- # Set the request instance variable to whatever type of request we got.
90
- obj.instance_variable_set :@request, request
91
-
92
- # Finalize the object's construction with the rest of the args & block.
93
- obj.__send__ :initialize, *args, &block
94
-
95
- # Now validate that the request is ok.
96
- # Implementing classes MAY override this method to do different
97
- # kind of request validation.
98
- obj.validate_request!
99
-
100
- return obj
101
- end
102
-
103
- ##
104
- # Generates a curried function to execute a Command's call class method.
105
- #
106
- # @returns lambda block to execute a call.
107
- #
108
- def worker(*args, &block)
109
- lambda { |message|
110
- self.call message, *args, &block
111
- }
74
+ new *args, &block
112
75
  end
113
76
 
114
77
  end
@@ -0,0 +1,8 @@
1
+ module Serf
2
+ module Errors
3
+
4
+ class NotFound < RuntimeError
5
+ end
6
+
7
+ end
8
+ end
@@ -0,0 +1,39 @@
1
+ require 'girl_friday'
2
+
3
+ require 'serf/util/options_extraction'
4
+ require 'serf/util/uuidable'
5
+
6
+ module Serf
7
+ module Middleware
8
+
9
+ class GirlFridayAsync
10
+ include Serf::Util::OptionsExtraction
11
+
12
+ attr_reader :uuidable
13
+ attr_reader :queue
14
+
15
+ def initialize(app, *args)
16
+ extract_options! args
17
+
18
+ @uuidable = opts :uuidable, Serf::Util::Uuidable
19
+
20
+ @queue = ::GirlFriday::WorkQueue.new(
21
+ opts(:name, :serf_runner),
22
+ :size => opts(:workers, 1)) do |env|
23
+ app.call env
24
+ end
25
+ end
26
+
27
+ def call(env)
28
+ queue.push env
29
+ response = uuidable.create_uuids env.message
30
+ response.kind = 'serf/messages/message_accepted_event'
31
+ response.message = env.message
32
+ response.context = env.context
33
+ return response
34
+ end
35
+
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,25 @@
1
+ require 'hashie'
2
+
3
+ module Serf
4
+ module Middleware
5
+
6
+ ##
7
+ # Middleware to turn an env into a Hashie::Mash.
8
+ #
9
+ class Masherize
10
+
11
+ ##
12
+ # @param app the app
13
+ #
14
+ def initialize(app)
15
+ @app = app
16
+ end
17
+
18
+ def call(env)
19
+ @app.call Hashie::Mash.new(env)
20
+ end
21
+
22
+ end
23
+
24
+ end
25
+ end
@@ -4,26 +4,32 @@ module Serf
4
4
  module Middleware
5
5
 
6
6
  ##
7
- # Middleware to add a request uuid the ENV Hash to uniquely identify
8
- # the handling of this input. But it won't overwrite the uuid field
7
+ # Middleware to add a request uuid to both the message and context
8
+ # of the env hash. But it won't overwrite the uuid field
9
9
  # if the incoming request already has it.
10
10
  #
11
11
  class UuidTagger
12
+ include Serf::Util::OptionsExtraction
13
+
14
+ attr_reader :uuidable
12
15
 
13
16
  ##
14
17
  # @param app the app
15
18
  # @options opts [String] :field the ENV field to set with a UUID.
16
19
  #
17
- def initialize(app, options={})
20
+ def initialize(app, *args)
21
+ extract_options! args
18
22
  @app = app
19
- @field = options.fetch(:field) { 'uuid' }
23
+ @uuidable = opts :uuidable, Serf::Util::Uuidable
20
24
  end
21
25
 
22
26
  def call(env)
23
- env = env.dup
24
- unless env[@field.to_sym] || env[@field.to_s]
25
- env[@field] = Serf::Util::Uuidable.create_coded_uuid
26
- end
27
+ message = env[:message]
28
+ message[:uuid] = uuidable.create_coded_uuid if message && !message[:uuid]
29
+
30
+ context = env[:context]
31
+ context[:uuid] = uuidable.create_coded_uuid if context && !context[:uuid]
32
+
27
33
  @app.call env
28
34
  end
29
35
 
@@ -1,5 +1,7 @@
1
+ require 'serf/util/options_extraction'
2
+
1
3
  module Serf
2
- module Util
4
+ module Routing
3
5
 
4
6
  ##
5
7
  # A matcher that does a regexp match on a specific field
@@ -7,20 +9,24 @@ module Util
7
9
  # on message kinds for routing.
8
10
  #
9
11
  class RegexpMatcher
12
+ include Serf::Util::OptionsExtraction
13
+
10
14
  attr_reader :regexp
11
15
  attr_reader :field
12
16
 
13
- def initialize(regexp, options={})
17
+ def initialize(regexp, *args)
18
+ extract_options! args
19
+
14
20
  @regexp = regexp
15
- @field = options.fetch(:field) { :kind }
21
+ @field = opts :field, :kind
16
22
  end
17
23
 
18
24
  def ===(env)
19
- return @regexp === env[@field]
25
+ return regexp === env[field]
20
26
  end
21
27
 
22
- def self.build(regexp)
23
- return self.new regexp
28
+ def self.build(*args, &block)
29
+ new *args, &block
24
30
  end
25
31
 
26
32
  end
@@ -0,0 +1,35 @@
1
+ require 'serf/util/options_extraction'
2
+
3
+ module Serf
4
+ module Routing
5
+
6
+ class Route
7
+ include Serf::Util::OptionsExtraction
8
+
9
+ attr_reader :policies
10
+ attr_reader :command
11
+
12
+ def initialize(*args, &block)
13
+ extract_options! args
14
+ @policies = opts :policies, []
15
+ @command = opts! :command
16
+ end
17
+
18
+ def check_policies!(request, context)
19
+ for policy in policies do
20
+ policy.check! request, context
21
+ end
22
+ end
23
+
24
+ def execute!(*args, &block)
25
+ command.call *args, &block
26
+ end
27
+
28
+ def self.build(*args, &block)
29
+ new *args, &block
30
+ end
31
+
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,64 @@
1
+ require 'serf/routing/regexp_matcher'
2
+ require 'serf/util/options_extraction'
3
+
4
+ module Serf
5
+ module Routing
6
+
7
+ ##
8
+ # RouteSet resolves a list of matched routes to execute based on
9
+ # criteria from associated 'matcher' objects.
10
+ #
11
+ class RouteSet
12
+ include Serf::Util::OptionsExtraction
13
+
14
+ attr_reader :routes
15
+ attr_reader :matchers
16
+ attr_reader :regexp_matcher_factory
17
+
18
+ def initialize(*args, &block)
19
+ extract_options! args
20
+ @routes = {}
21
+ @matchers = []
22
+ @regexp_matcher_factory = opts(
23
+ :regexp_matcher_factory,
24
+ ::Serf::Routing::RegexpMatcher)
25
+ end
26
+
27
+ ##
28
+ # Connects a matcher (String or an Object implementing ===) to routes.
29
+ #
30
+ def add(matcher, route)
31
+ # Maybe we have an non-String matcher. Handle the Regexp case.
32
+ # We only keep track of matchers if it isn't a string because
33
+ # string matchers are just pulled out of routes by key lookup.
34
+ matcher = regexp_matcher_factory.build matcher if matcher.kind_of? Regexp
35
+ matchers << matcher unless matcher.is_a? String
36
+
37
+ # We add the (matcher+routes) into our routes
38
+ routes[matcher] ||= []
39
+ routes[matcher].push route
40
+ end
41
+
42
+ ##
43
+ # @param [Hash] request The input message to match for routes.
44
+ # @return [Array] List of routes that matched.
45
+ #
46
+ def resolve(request, context)
47
+ resolved_routes = []
48
+ resolved_routes.concat routes.fetch(request[:kind]) { [] }
49
+ matchers.each do |matcher|
50
+ resolved_routes.concat routes[matcher] if matcher === request
51
+ end
52
+ return resolved_routes
53
+ end
54
+
55
+ ##
56
+ # Default factory method.
57
+ #
58
+ def self.build(*args, &block)
59
+ new *args, &block
60
+ end
61
+ end
62
+
63
+ end
64
+ end