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.
- 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
|