serf 0.11.0 → 0.12.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +1 -0
- data/Guardfile +1 -2
- data/README.md +131 -15
- data/example/components/logger.serf +7 -0
- data/example/serfs/create_widget.serf +24 -0
- data/lib/serf/builder.rb +7 -6
- data/lib/serf/loader.rb +16 -0
- data/lib/serf/loader/loader.rb +103 -0
- data/lib/serf/loader/registry.rb +86 -0
- data/lib/serf/middleware/error_handler.rb +4 -4
- data/lib/serf/middleware/parcel_freezer.rb +3 -6
- data/lib/serf/middleware/parcel_masher.rb +3 -6
- data/lib/serf/middleware/policy_checker.rb +3 -5
- data/lib/serf/middleware/request_timer.rb +47 -0
- data/lib/serf/middleware/uuid_tagger.rb +3 -5
- data/lib/serf/parcel_builder.rb +3 -6
- data/lib/serf/serfer.rb +5 -7
- data/lib/serf/util/uuidable.rb +3 -6
- data/lib/serf/version.rb +1 -1
- data/serf.gemspec +1 -0
- data/spec/serf/builder_spec.rb +4 -5
- data/spec/serf/errors/policy_failure_spec.rb +1 -1
- data/spec/serf/loader/loader_spec.rb +73 -0
- data/spec/serf/loader/registry_spec.rb +62 -0
- data/spec/serf/loader_spec.rb +57 -0
- data/spec/serf/middleware/error_handler_spec.rb +2 -2
- data/spec/serf/middleware/parcel_freezer_spec.rb +2 -2
- data/spec/serf/middleware/parcel_masher_spec.rb +5 -5
- data/spec/serf/middleware/policy_checker_spec.rb +3 -3
- data/spec/serf/middleware/request_timer_spec.rb +43 -0
- data/spec/serf/middleware/uuid_tagger_spec.rb +4 -4
- data/spec/serf/parcel_builder_spec.rb +7 -7
- data/spec/serf/serfer_spec.rb +4 -4
- data/spec/serf/util/error_handling_spec.rb +3 -3
- data/spec/serf/util/null_object_spec.rb +4 -4
- data/spec/serf/util/protected_call_spec.rb +4 -4
- data/spec/serf/util/uuidable_spec.rb +15 -15
- data/spec/support/factories.rb +7 -0
- metadata +34 -9
- data/lib/serf/util/options_extraction.rb +0 -117
- data/spec/serf/util/options_extraction_spec.rb +0 -62
- data/spec/support/options_extraction_wrapper.rb +0 -10
data/Gemfile
CHANGED
data/Guardfile
CHANGED
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: (
|
27
|
-
a.
|
28
|
-
|
29
|
-
|
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
|
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 }
|
254
|
+
return 'my_lib/events/success_event', { success: true }
|
222
255
|
|
223
|
-
# Optionally just return the
|
224
|
-
#return
|
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
|
230
|
-
|
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 =
|
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 =
|
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 =
|
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,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
|
data/lib/serf/loader.rb
ADDED
@@ -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
|