serf 0.11.0 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/Gemfile +1 -0
  2. data/Guardfile +1 -2
  3. data/README.md +131 -15
  4. data/example/components/logger.serf +7 -0
  5. data/example/serfs/create_widget.serf +24 -0
  6. data/lib/serf/builder.rb +7 -6
  7. data/lib/serf/loader.rb +16 -0
  8. data/lib/serf/loader/loader.rb +103 -0
  9. data/lib/serf/loader/registry.rb +86 -0
  10. data/lib/serf/middleware/error_handler.rb +4 -4
  11. data/lib/serf/middleware/parcel_freezer.rb +3 -6
  12. data/lib/serf/middleware/parcel_masher.rb +3 -6
  13. data/lib/serf/middleware/policy_checker.rb +3 -5
  14. data/lib/serf/middleware/request_timer.rb +47 -0
  15. data/lib/serf/middleware/uuid_tagger.rb +3 -5
  16. data/lib/serf/parcel_builder.rb +3 -6
  17. data/lib/serf/serfer.rb +5 -7
  18. data/lib/serf/util/uuidable.rb +3 -6
  19. data/lib/serf/version.rb +1 -1
  20. data/serf.gemspec +1 -0
  21. data/spec/serf/builder_spec.rb +4 -5
  22. data/spec/serf/errors/policy_failure_spec.rb +1 -1
  23. data/spec/serf/loader/loader_spec.rb +73 -0
  24. data/spec/serf/loader/registry_spec.rb +62 -0
  25. data/spec/serf/loader_spec.rb +57 -0
  26. data/spec/serf/middleware/error_handler_spec.rb +2 -2
  27. data/spec/serf/middleware/parcel_freezer_spec.rb +2 -2
  28. data/spec/serf/middleware/parcel_masher_spec.rb +5 -5
  29. data/spec/serf/middleware/policy_checker_spec.rb +3 -3
  30. data/spec/serf/middleware/request_timer_spec.rb +43 -0
  31. data/spec/serf/middleware/uuid_tagger_spec.rb +4 -4
  32. data/spec/serf/parcel_builder_spec.rb +7 -7
  33. data/spec/serf/serfer_spec.rb +4 -4
  34. data/spec/serf/util/error_handling_spec.rb +3 -3
  35. data/spec/serf/util/null_object_spec.rb +4 -4
  36. data/spec/serf/util/protected_call_spec.rb +4 -4
  37. data/spec/serf/util/uuidable_spec.rb +15 -15
  38. data/spec/support/factories.rb +7 -0
  39. metadata +34 -9
  40. data/lib/serf/util/options_extraction.rb +0 -117
  41. data/spec/serf/util/options_extraction_spec.rb +0 -62
  42. data/spec/support/options_extraction_wrapper.rb +0 -10
data/Gemfile CHANGED
@@ -26,4 +26,5 @@ group :development, :test do
26
26
 
27
27
  # Required by our Specs
28
28
  gem 'json-schema'
29
+ gem 'yell'
29
30
  end
data/Guardfile CHANGED
@@ -2,8 +2,7 @@ guard(
2
2
  :rspec,
3
3
  cli: '--format Fuubar --color',
4
4
  all_on_start: true,
5
- all_after_pass: false,
6
- :version => 2) do
5
+ all_after_pass: false) do
7
6
 
8
7
  # Watch our specs
9
8
  watch(%r{^spec/.+_spec\.rb$})
data/README.md CHANGED
@@ -3,6 +3,14 @@ serf
3
3
 
4
4
  Code your Interactors with policy protection.
5
5
 
6
+ Serf (a Serf App) -- an individual rack-like call chain.
7
+ * Interactors define your business logic
8
+ * Policies decide access
9
+ * Middleware augment the request processing
10
+
11
+ Serf Map -- a set of Serfs.
12
+ * A registry of Serfs, mapped by the parcel kinds.
13
+
6
14
  Serf Links
7
15
  ----------
8
16
 
@@ -23,10 +31,18 @@ the Domain Layer's Entities (Value Objects and Entity Gateways).
23
31
 
24
32
  1. Include the "Serf::Interactor" module in your class.
25
33
  2. Implement the 'call(message)' method.
26
- 3. Return the tuple: (message, kind)
27
- a. Hashie::Mash is recommended for the message, nil is acceptable
28
- b. The kind is the string representation of the message type,
29
- It is optional.
34
+ 3. Return the tuple: (kind, message)
35
+ a. The kind is the string representation of the message type,
36
+ This field is RECOMMENDED.
37
+ b. The message field provides detailed return data about the
38
+ interactor's processing.
39
+ Hashie::Mash is suggested for the message, nil is acceptable.
40
+
41
+ The reason that the interactor SHOULD return a kind is to properly
42
+ identify the semantic meaning of the returned message, even if
43
+ said returned message is empty. This also assists the handling
44
+ of response parcels in other pipelines without the need to
45
+ introspect the parcel's message.
30
46
 
31
47
  Example:
32
48
 
@@ -49,7 +65,7 @@ Example:
49
65
  response = Hashie::Mash.new
50
66
  response.item = item
51
67
  # Return the response 'kind' and the response data.
52
- return response, 'my_app/events/did_something'
68
+ return 'my_app/events/did_something', response
53
69
  end
54
70
  end
55
71
 
@@ -127,6 +143,23 @@ Policies only need to implement a single method:
127
143
  RECOMMENDED: Use `Serf::Errors::PolicyFailure` error type.
128
144
 
129
145
 
146
+ Thread Safety
147
+ -------------
148
+
149
+ Yes and No, it depends:
150
+ * Serf Middleware and Serf Utils are all *Thread Safe* by default.
151
+ It may not be the case if thread unsafe options are passed in the
152
+ instantiation of these objects.
153
+ * Built Serfs are *Thread Safe* **if** the developer took care
154
+ in the creation of the Interactors and in the dependency injection
155
+ wiring of the Serfs by the builder and loader.
156
+ * The Builder and Loader are *Thread UNSAFE* because it just doesn't make
157
+ sense that multiple threads should compete/coordinate in the creation
158
+ and wiring of the created Serfs (Serf Apps) and Serf Maps.
159
+ This is usually done at start up by the main thread.
160
+ This includes the utility classes that the loader uses.
161
+
162
+
130
163
  References
131
164
  ==========
132
165
 
@@ -190,8 +223,8 @@ The Domain Layer (from DDD):
190
223
  and "Application Agnostic Logic" in Entities.
191
224
 
192
225
 
193
- Example
194
- =======
226
+ Serf Builder Example
227
+ ====================
195
228
 
196
229
  # Require our libraries
197
230
  require 'json'
@@ -218,16 +251,16 @@ Example
218
251
  raise 'Error' if message.raise_an_error
219
252
 
220
253
  # And return a message as result. Nil is valid response.
221
- return { success: true }, 'my_lib/events/success_event'
254
+ return 'my_lib/events/success_event', { success: true }
222
255
 
223
- # Optionally just return the message w/o a tagged kind
224
- #return { success: true }
256
+ # Optionally just return the kind
257
+ # return 'my_lib/events/success_event'
225
258
  end
226
259
 
227
260
  end
228
261
 
229
- # Create a new builder for this serf app.
230
- app = Serf::Builder.new(
262
+ # Create a new builder for this Serf (aka Serf App).
263
+ serf = Serf::Builder.new(
231
264
  interactor: MyInteractor.new,
232
265
  policy_chain: [
233
266
  MyPolicy.new
@@ -236,11 +269,11 @@ Example
236
269
  # This will submit a 'my_message' message (as a hash) to Serfer.
237
270
  # Missing data field will raise an error within the interactor, which
238
271
  # will be caught by the serfer.
239
- results = app.call nil
272
+ results = serf.call nil
240
273
  my_logger.info "Call 1: #{results.to_json}"
241
274
 
242
275
  # Here is good result
243
- results = app.call(
276
+ results = serf.call(
244
277
  headers: {
245
278
  user: 'user_info_1'
246
279
  },
@@ -249,7 +282,7 @@ Example
249
282
  my_logger.info "Call 2: #{results.to_json}"
250
283
 
251
284
  # Here get an error that was raised from the interactor
252
- results = app.call(
285
+ results = serf.call(
253
286
  headers: {
254
287
  user: 'user_info_1'
255
288
  },
@@ -259,6 +292,89 @@ Example
259
292
  my_logger.info "Call 3: #{results.to_json}"
260
293
 
261
294
 
295
+ Serf Loader Example
296
+ ===================
297
+
298
+ Look inside the example subdirectory for the serf files in this example.
299
+
300
+
301
+ ####
302
+ ## File: example/serfs/create_widget.serf
303
+ ####
304
+
305
+ require 'json'
306
+ # require 'subsystem/commands/my_create_widget'
307
+ # Throwing in this class definition to make example work
308
+ class MyCreateWidget
309
+
310
+ def initialize(logger, success_message)
311
+ @logger = logger
312
+ @success_message = success_message
313
+ end
314
+
315
+ def call(parcel)
316
+ @logger.info "In My Create Widget, creating a widget: #{parcel.to_json}"
317
+ return 'subsystem/events/mywidget_created',
318
+ { success_message: @success_message }
319
+ end
320
+ end
321
+
322
+ ##
323
+ # Registers a serf that responds to a parcel with the given request "kind".
324
+ # The interactor is instantiated by asking for other components in the
325
+ # registry and for parameters set in the environment variable.
326
+ registry.add 'subsystem/requests/create_widget' do |r, env|
327
+ serf interactor: MyCreateWidget.new(r[:logger], env[:success_message])
328
+ end
329
+
330
+
331
+ ####
332
+ ## In another ruby script, where we may load and use serfs.
333
+ ####
334
+
335
+ require 'hashie'
336
+ require 'json'
337
+ require 'yell'
338
+
339
+ require 'serf/loader'
340
+
341
+ # Making a logger for the top level example
342
+ logger = Yell.new STDOUT
343
+
344
+ # Globs to search for serf files
345
+ globs = [
346
+ 'example/**/*.serf'
347
+ ]
348
+ # The serf requests that the loaded Serf Map will handle.
349
+ serfs = [
350
+ 'subsystem/requests/create_widget'
351
+ ]
352
+ # A simple environment variables hash, runtime configuration
353
+ env = Hashie::Mash.new(
354
+ success_message: 'Some environment variable like redis URL'
355
+ )
356
+
357
+ # Loading the configuration, creating the serfs.
358
+ serf_map = Serf::Loader.serfup globs: globs, serfs: serfs, env: env
359
+
360
+ # Make an example request parcel
361
+ request_parcel = {
362
+ headers: {
363
+ kind: 'subsystem/requests/create_widget'
364
+ },
365
+ message: {
366
+ name: 'some widget name'
367
+ }
368
+ }
369
+
370
+ #
371
+ # Look up the create widget serf by a request kind name,
372
+ # execute the serf, and log the results
373
+ serf = serf_map[request_parcel[:headers][:kind]]
374
+ results = serf.call request_parcel
375
+ logger.info results.to_json
376
+
377
+
262
378
  Contributing
263
379
  ============
264
380
 
@@ -0,0 +1,7 @@
1
+ require 'yell'
2
+
3
+ ##
4
+ # This is the registration of a component
5
+ registry.add 'logger' do |r|
6
+ Yell.new STDOUT
7
+ end
@@ -0,0 +1,24 @@
1
+ require 'json'
2
+ # require 'subsystem/commands/my_create_widget'
3
+ # Throwing in this class definition to make example work
4
+ class MyCreateWidget
5
+
6
+ def initialize(logger, success_message)
7
+ @logger = logger
8
+ @success_message = success_message
9
+ end
10
+
11
+ def call(parcel)
12
+ @logger.info "In My Create Widget, creating a widget: #{parcel.to_json}"
13
+ return 'subsystem/events/mywidget_created',
14
+ { success_message: @success_message }
15
+ end
16
+ end
17
+
18
+ ##
19
+ # Registers a serf that responds to a parcel with the given request "kind".
20
+ # The interactor is instantiated by asking for other components in the
21
+ # registry and for parameters set in the environment variable.
22
+ registry.add 'subsystem/requests/create_widget' do |r, env|
23
+ serf interactor: MyCreateWidget.new(r[:logger], env[:success_message])
24
+ end
data/lib/serf/builder.rb CHANGED
@@ -1,22 +1,22 @@
1
+ require 'optser'
2
+
1
3
  require 'serf/middleware/error_handler'
2
4
  require 'serf/middleware/parcel_freezer'
3
5
  require 'serf/middleware/parcel_masher'
4
6
  require 'serf/middleware/policy_checker'
7
+ require 'serf/middleware/request_timer'
5
8
  require 'serf/middleware/uuid_tagger'
6
9
  require 'serf/serfer'
7
- require 'serf/util/options_extraction'
8
10
 
9
11
  module Serf
10
12
 
11
13
  class Builder
12
- include Serf::Util::OptionsExtraction
13
-
14
14
  def initialize(*args, &block)
15
- extract_options! args
15
+ opts = Optser.extract_options! args
16
16
 
17
- @run = opts :interactor
17
+ @run = opts.get :interactor
18
18
  @use = []
19
- @policy_chain = opts :policy_chain, []
19
+ @policy_chain = opts.get :policy_chain, []
20
20
 
21
21
  if block_given?
22
22
  instance_eval(&block)
@@ -36,6 +36,7 @@ module Serf
36
36
  # use Serf::Serfer
37
37
  #
38
38
  def use_defaults
39
+ use Serf::Middleware::RequestTimer
39
40
  use Serf::Middleware::ParcelMasher
40
41
  use Serf::Middleware::UuidTagger
41
42
  use Serf::Middleware::ParcelFreezer
@@ -0,0 +1,16 @@
1
+ require 'serf/loader/loader'
2
+
3
+ module Serf
4
+
5
+ module Loader
6
+
7
+ ##
8
+ # @see Serf::Loader::Loader
9
+ #
10
+ def self.serfup(*args)
11
+ Serf::Loader::Loader.new.serfup *args
12
+ end
13
+
14
+ end
15
+
16
+ end
@@ -0,0 +1,103 @@
1
+ require 'hashie'
2
+ require 'optser'
3
+
4
+ require 'serf/builder'
5
+ require 'serf/loader/registry'
6
+
7
+ module Serf
8
+ module Loader
9
+
10
+ ##
11
+ # The main loader that takes a serfup configuration file and instance evals
12
+ # all the registered components and serfs, which populates the
13
+ # serfup loader registry; then returns a mapping of serf kind names to
14
+ # the instantiated serfs.
15
+ #
16
+ class Loader
17
+
18
+ def initialize(*args)
19
+ opts = Optser.extract_options! args
20
+ @registry_class = opts.get :registry_class, Serf::Loader::Registry
21
+ @builder_class = opts.get :builder_class, Serf::Builder
22
+ end
23
+
24
+ ##
25
+ # Loads up the components defined in a serfup configuration, wires up all
26
+ # the "exposed" serfs and returns them in a frozen map.
27
+ #
28
+ # Example Config:
29
+ #
30
+ # # Config is a simple hash
31
+ # config = Hashie::Mash.new
32
+ # # List out the globbed filenames to load up.
33
+ # config.globs = [
34
+ # 'example/**/*.serf'
35
+ # ]
36
+ # # List out the parcel kinds that we need to have serfs built up
37
+ # # and exposed in the returned Serf Map.
38
+ # config.serfs = [
39
+ # 'subsystem/requests/create_widget'
40
+ # ]
41
+ #
42
+ # Example Env Hash:
43
+ #
44
+ # env = Hashie::Mash.new
45
+ # env.web_service = 'http://example.com/'
46
+ #
47
+ # @param [Hash] opts env and basepath options
48
+ # @option opts [Array] :globs list of file globs to load serf configs
49
+ # @option opts [Array] :serfs list of serfs to export in Serf Map.
50
+ # @option opts [String] :base_path root of where to run the config
51
+ # @option opts [Hash] :env environmental variables for runtime config
52
+ # @returns a frozen Serf Map of request parcel kind to serf.
53
+ #
54
+ def serfup(*args)
55
+ opts = Optser.extract_options! args
56
+ globs = opts.get! :globs
57
+ serfs = opts.get! :serfs
58
+ base_path = opts.get :base_path, '.'
59
+ env = opts.get(:env) { Hashie::Mash.new }
60
+ @registry = @registry_class.new env: env
61
+
62
+ # Load in all the components listed
63
+ globs.each do |glob_pattern|
64
+ globs = Dir.glob File.join(base_path, glob_pattern)
65
+ globs.each do |filename|
66
+ File.open filename do |file|
67
+ contents = file.read
68
+ instance_eval(contents)
69
+ end
70
+ end
71
+ end
72
+
73
+ # Construct all the "serfs"
74
+ map = Hashie::Mash.new
75
+ serfs.each do |serf|
76
+ map[serf] = @registry[serf]
77
+ raise "Missing Serf: #{serf}" if map[serf].nil?
78
+ end
79
+
80
+ # return a frozen registry, clear the registry
81
+ @registry = nil
82
+ map.freeze
83
+ return map
84
+ end
85
+
86
+ private
87
+
88
+ ##
89
+ # Registry attr_reader for serf files to access in the instance eval.
90
+ def registry
91
+ @registry
92
+ end
93
+
94
+ ##
95
+ # Registry attr_reader for serf files to define a builder's work.
96
+ def serf(*args, &block)
97
+ @builder_class.new(*args, &block).to_app
98
+ end
99
+
100
+ end
101
+
102
+ end
103
+ end
@@ -0,0 +1,86 @@
1
+ require 'hashie'
2
+
3
+ module Serf
4
+ module Loader
5
+
6
+ ##
7
+ # A Registry of components that can then be wired up
8
+ # dependency injection style using the Service Locator Pattern.
9
+ # Components are lazily evaluated and memoized. Thus all components
10
+ # are singletons.
11
+ #
12
+ # # Create a new registry
13
+ # registry = Registry.new
14
+ #
15
+ # # Registers a component
16
+ # registry.add 'a_comp' do |r|
17
+ # 12345
18
+ # end
19
+ #
20
+ # # Registers b component that uses a component
21
+ # registry.add 'b_comp' do |r|
22
+ # {
23
+ # a_value: r['a_comp']
24
+ # }
25
+ # end
26
+ #
27
+ # # Registers a Serf app (serf is helper to make and execute
28
+ # # a Serf::Builder), using the long form builder DSL.
29
+ # registry.add 'subsystem/request/my_request' do |r|
30
+ # # Register a serf to handle this request
31
+ # serf do
32
+ # use_defaults
33
+ # run MyInteractor.new(b_comp: r['b_comp'])
34
+ # end
35
+ # end
36
+ #
37
+ # # Now obtain the build serf, all wired up, by the parcel kind
38
+ # # and execute the found serf.
39
+ # parcel = {
40
+ # headers: { kind: 'subsystem/request/my_request' },
41
+ # message: {}
42
+ # }
43
+ # serf = registry[parcel[:headers][:kind]]
44
+ # puts serf.call(parcel)
45
+ #
46
+ class Registry
47
+ attr_reader :blocks
48
+ attr_reader :values
49
+
50
+ def initialize(*args)
51
+ opts = Optser.extract_options! args
52
+ @blocks = {}
53
+ @values = {}
54
+ @env = opts.get(:env) { Hashie::Mash.new }
55
+ end
56
+
57
+ ##
58
+ # Adds a component to the registry.
59
+ #
60
+ # @params name the registration name of the component.
61
+ # @params &block the proc that generates the component instance.
62
+ #
63
+ def add(name, &block)
64
+ @blocks[name.to_sym] = block
65
+ end
66
+
67
+ ##
68
+ # Looks up a component instance by name.
69
+ #
70
+ # @params name the name of the component.
71
+ # @returns the singleton instance of the component.
72
+ #
73
+ def [](name)
74
+ name = name.to_sym
75
+ return @values[name] if @values.has_key? name
76
+ # No memoized value, so grab the block, call it and memoize it
77
+ # return the block's return value, or nil.
78
+ if block = @blocks.delete(name)
79
+ @values[name] = block.call self, @env
80
+ end
81
+ end
82
+
83
+ end
84
+
85
+ end
86
+ end