serf 0.11.0 → 0.12.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.
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