startback 0.4.5 → 0.5.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/lib/startback/audit/trailer.rb +145 -0
  3. data/lib/startback/audit.rb +1 -0
  4. data/lib/startback/bus/bunny/async.rb +117 -0
  5. data/lib/startback/bus/bunny.rb +1 -0
  6. data/lib/startback/bus/memory/async.rb +40 -0
  7. data/lib/startback/bus/memory/sync.rb +30 -0
  8. data/lib/startback/bus/memory.rb +2 -0
  9. data/lib/startback/bus.rb +94 -0
  10. data/lib/startback/caching/entity_cache.rb +80 -0
  11. data/lib/startback/caching/store.rb +34 -0
  12. data/lib/startback/context/middleware.rb +1 -1
  13. data/lib/startback/context.rb +93 -4
  14. data/lib/startback/event.rb +43 -0
  15. data/lib/startback/operation.rb +39 -6
  16. data/lib/startback/support/fake_logger.rb +18 -0
  17. data/lib/startback/support/hooks.rb +48 -0
  18. data/lib/startback/support/operation_runner.rb +150 -0
  19. data/lib/startback/support/robustness.rb +153 -0
  20. data/lib/startback/support.rb +3 -0
  21. data/lib/startback/version.rb +2 -2
  22. data/lib/startback/web/api.rb +3 -4
  23. data/lib/startback/web/catch_all.rb +12 -5
  24. data/lib/startback/web/middleware.rb +13 -0
  25. data/lib/startback.rb +2 -0
  26. data/spec/spec_helper.rb +2 -0
  27. data/spec/unit/audit/test_trailer.rb +88 -0
  28. data/spec/unit/bus/memory/test_async.rb +41 -0
  29. data/spec/unit/bus/memory/test_sync.rb +41 -0
  30. data/spec/unit/caching/test_entity_cache.rb +109 -0
  31. data/spec/unit/context/test_abstraction_factory.rb +64 -0
  32. data/spec/unit/support/hooks/test_after_hook.rb +54 -0
  33. data/spec/unit/support/hooks/test_before_hook.rb +54 -0
  34. data/spec/unit/support/operation_runner/test_around_run.rb +157 -0
  35. data/spec/unit/support/operation_runner/test_before_after_call.rb +48 -0
  36. data/spec/unit/support/test_robusteness.rb +209 -0
  37. data/spec/unit/test_context.rb +51 -0
  38. data/spec/unit/test_event.rb +69 -0
  39. data/spec/unit/test_operation.rb +0 -3
  40. metadata +32 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '08fb9451c77df7efeee3bbbee9d1c540093d083cd5128d088cfd2c52e48169e9'
4
- data.tar.gz: ef62fa3c7e42f4ada0c35e9482fea0e81c23e9483c6b66b87586d439e26995e0
3
+ metadata.gz: 492399cde5cbfe2861575824a32c7788fc16137902d5a4194b3662e5306a5968
4
+ data.tar.gz: 192d23a34d668bb3ce495c0bb3c724ef58c4e7857bf8e0ed63d5948a76b43f49
5
5
  SHA512:
6
- metadata.gz: 53604f5706cfdf4f1ae4022f423e0ab9ae9f8582fc8fb92b7379243b16f0afb108209b9509db89e94056c9a4912b2f86827495462746ebf88db1077e656d2528
7
- data.tar.gz: 6a56f9fcf818278dd7646105363fb791b31bee1d3ab9afbbd65314eef28f01f34af20545df564846230f45c6add5cd04cf283965656caa4713a86941fba31df4
6
+ metadata.gz: 54ff69ead9f9b90eccbc9700a46229621cdfe31eb7b887207f6c69c0f755735ae80fc6257ff9ca6e3430d9fc75a2e04fd7b9ea4ecf6744557cb79d418e1ac3b4
7
+ data.tar.gz: 71de967d2ebee0cd0596b51532d91a1abc1197cd69d1b6db528fcf6bcd00977c484067a83ba266e37e8d807c3c2bba8efc3b67f822b0540eef9fea3c9578fc5b
@@ -0,0 +1,145 @@
1
+ require 'forwardable'
2
+ module Startback
3
+ module Audit
4
+ #
5
+ # Log & Audit trail abstraction, that can be registered as an around
6
+ # hook on OperationRunner and as an actual logger on Context instances.
7
+ #
8
+ # The trail is outputted as JSON lines, using a Logger on the "device"
9
+ # passed at construction. The following JSON entries are dumped:
10
+ #
11
+ # - severity : INFO or ERROR
12
+ # - time : ISO8601 Datetime of operation execution
13
+ # - op : class name of the operation executed
14
+ # - op_took : Execution duration of the operation
15
+ # - op_data : Dump of operation input data
16
+ # - context : Execution context, through its `h` information contract (IC)
17
+ #
18
+ # Dumping of operation data follows the following duck typing conventions:
19
+ #
20
+ # - If the operation instance responds to `to_trail`, this data is taken
21
+ # - If the operation instance responds to `input`, this data is taken
22
+ # - If the operation instance responds to `request`, this data is taken
23
+ # - Otherwise op_data is a JSON null
24
+ #
25
+ # By contributing to the Context's `h` IC, users can easily dump information that
26
+ # makes sense (such as the operation execution requester).
27
+ #
28
+ # The class implements a sanitization process when dumping the context and
29
+ # operation data. Blacklisted words taken in construction options are used to
30
+ # prevent dumping hash keys that match them (insentively). Default stop words
31
+ # are equivalent to:
32
+ #
33
+ # Trailer.new("/var/log/trail.log", {
34
+ # blacklist: "token password secret credential"
35
+ # })
36
+ #
37
+ # Please note that the sanitization process does not apply recursively if
38
+ # the operation data is hierarchic. It only applies to the top object of
39
+ # Hash and [Hash]. Use `Operation#to_trail` to fine-tune your audit trail.
40
+ #
41
+ # Given that this Trailer is intended to be used as around hook on an
42
+ # `OperationRunner`, operations that fail at construction time will not be
43
+ # trailed at all, since they can't be ran in the first place. This may lead
44
+ # to trails not containing important errors cases if operations check their
45
+ # input at construction time.
46
+ #
47
+ class Trailer
48
+ extend Forwardable
49
+ def_delegators :@logger, :debug, :info, :warn, :error, :fatal
50
+
51
+ class Formatter
52
+
53
+ def call(severity, time, progname, msg)
54
+ if msg[:error] && msg[:error].respond_to?(:message, true)
55
+ msg[:error] = msg[:error].message
56
+ end
57
+ {
58
+ severity: severity,
59
+ time: time,
60
+ }.merge(msg).to_json << "\n"
61
+ end
62
+
63
+ end # class Formatter
64
+
65
+ DEFAULT_OPTIONS = {
66
+
67
+ # Words used to stop dumping for, e.g., security reasons
68
+ blacklist: "token password secret credential"
69
+
70
+ }
71
+
72
+ def initialize(device, options = {})
73
+ @options = DEFAULT_OPTIONS.merge(options)
74
+ @logger = ::Logger.new(device, 'daily')
75
+ @logger.formatter = Formatter.new
76
+ end
77
+ attr_reader :logger, :options
78
+
79
+ def call(runner, op)
80
+ result = nil
81
+ time = Benchmark.realtime{ result = yield }
82
+ logger.info(op_to_trail(op, time))
83
+ result
84
+ rescue => ex
85
+ logger.error(op_to_trail(op, time, ex))
86
+ raise
87
+ end
88
+
89
+ protected
90
+
91
+ def op_to_trail(op, time, ex = nil)
92
+ log_msg = {
93
+ op_took: time ? time.round(8) : nil,
94
+ op: op_name(op),
95
+ context: op_context(op),
96
+ op_data: op_data(op)
97
+ }
98
+ log_msg[:error] = ex if ex
99
+ log_msg
100
+ end
101
+
102
+ def op_name(op)
103
+ case op
104
+ when String then op
105
+ when Class then op.name
106
+ else op.class.name
107
+ end
108
+ end
109
+
110
+ def op_context(op)
111
+ sanitize(op.respond_to?(:context, false) ? op.context.to_h : {})
112
+ end
113
+
114
+ def op_data(op)
115
+ data = if op.respond_to?(:to_trail, false)
116
+ op.to_trail
117
+ elsif op.respond_to?(:input, false)
118
+ op.input
119
+ elsif op.respond_to?(:request, false)
120
+ op.request
121
+ end
122
+ sanitize(data)
123
+ end
124
+
125
+ def sanitize(data)
126
+ case data
127
+ when Hash, OpenStruct
128
+ data.dup.delete_if{|k| k.to_s =~ blacklist_rx }
129
+ when Enumerable
130
+ data.map{|elm| sanitize(elm) }.compact
131
+ else
132
+ data
133
+ end
134
+ end
135
+
136
+ def blacklist_rx
137
+ @blacklist_rx ||= Regexp.new(
138
+ options[:blacklist].split(/\s+/).join("|"),
139
+ Regexp::IGNORECASE
140
+ )
141
+ end
142
+
143
+ end # class Trailer
144
+ end # module Audit
145
+ end # module Startback
@@ -0,0 +1 @@
1
+ require_relative 'audit/trailer'
@@ -0,0 +1,117 @@
1
+ require 'bunny'
2
+ module Startback
3
+ class Bus
4
+ module Bunny
5
+ #
6
+ # Asynchronous implementation of the bus abstraction, on top of RabbitMQ
7
+ # and using the 'bunny' gem (you need to include it in your Gemfile
8
+ # yourself: it is NOT a startback official dependency).
9
+ #
10
+ # This bus implementation emits events by dumping them to RabbitMQ using
11
+ # the event type as exchange name. Listeners may use the `processor`
12
+ # parameter to specify the queue name ; otherwise a default "main" queue
13
+ # is used.
14
+ #
15
+ # Examples:
16
+ #
17
+ # # Connects to RabbitMQ using all default options
18
+ # #
19
+ # # Uses the STARTBACK_BUS_BUNNY_ASYNC_URL environment variable for
20
+ # # connection URL if present.
21
+ # Startback::Bus::Bunny::Async.new
22
+ #
23
+ # # Connects to RabbitMQ using a specific URL
24
+ # Startback::Bus::Bunny::Async.new("amqp://rabbituser:rabbitpass@192.168.17.17")
25
+ # Startback::Bus::Bunny::Async.new(url: "amqp://rabbituser:rabbitpass@192.168.17.17")
26
+ #
27
+ # # Connects to RabbitMQ using specific connection options. See Bunny's own
28
+ # # documentation
29
+ # Startback::Bus::Bunny::Async.new({
30
+ # connection_options: {
31
+ # host: "192.168.17.17"
32
+ # }
33
+ # })
34
+ #
35
+ class Async
36
+ include Support::Robustness
37
+
38
+ DEFAULT_OPTIONS = {
39
+ # (optional) The URL to use for connecting to RabbitMQ.
40
+ url: ENV['STARTBACK_BUS_BUNNY_ASYNC_URL'],
41
+
42
+ # (optional) The options has to pass to ::Bunny constructor
43
+ connection_options: nil,
44
+
45
+ # (optional) The options to use for the emitter/listener fanout
46
+ fanout_options: {},
47
+
48
+ # (optional) The options to use for the listener queue
49
+ queue_options: {},
50
+
51
+ # (optional) Default event factory to use, if any
52
+ event_factory: nil,
53
+
54
+ # (optional) A default context to use for general logging
55
+ context: nil
56
+ }
57
+
58
+ # Creates a bus instance, using the various options provided to
59
+ # fine-tune behavior.
60
+ def initialize(options = {})
61
+ options = { url: options } if options.is_a?(String)
62
+ @options = DEFAULT_OPTIONS.merge(options)
63
+ retried = 0
64
+ conn = options[:connection_options] || options[:url]
65
+ try_max_times(10) do
66
+ @bunny = ::Bunny.new(conn)
67
+ @bunny.start
68
+ @channel = @bunny.create_channel
69
+ log(:info, {op: "#{self.class.name}#connect", op_data: conn}, options[:context])
70
+ end
71
+ end
72
+ attr_reader :channel, :options
73
+
74
+ def emit(event)
75
+ stop_errors(self, "emit", event.context) do
76
+ fanout = channel.fanout(event.type.to_s, fanout_options)
77
+ fanout.publish(event.to_json)
78
+ end
79
+ end
80
+
81
+ def listen(type, processor = nil, listener = nil, &bl)
82
+ raise ArgumentError, "A listener must be provided" unless listener || bl
83
+ fanout = channel.fanout(type.to_s, fanout_options)
84
+ queue = channel.queue((processor || "main").to_s, queue_options)
85
+ queue.bind(fanout)
86
+ queue.subscribe do |delivery_info, properties, body|
87
+ event = stop_errors(self, "listen") do
88
+ factor_event(body)
89
+ end
90
+ stop_errors(self, "listen", event.context) do
91
+ (listener || bl).call(event)
92
+ end
93
+ end
94
+ end
95
+
96
+ protected
97
+
98
+ def fanout_options
99
+ options[:fanout_options]
100
+ end
101
+
102
+ def queue_options
103
+ options[:queue_options]
104
+ end
105
+
106
+ def factor_event(body)
107
+ if options[:event_factory]
108
+ options[:event_factory].call(body)
109
+ else
110
+ Event.json(body, options)
111
+ end
112
+ end
113
+
114
+ end # class Async
115
+ end # module Bunny
116
+ end # class Bus
117
+ end # module Klaro
@@ -0,0 +1 @@
1
+ require_relative 'bunny/async'
@@ -0,0 +1,40 @@
1
+ module Startback
2
+ class Bus
3
+ module Memory
4
+ #
5
+ # Asynchronous implementation of the Bus abstraction, for use between
6
+ # components sharing the same process.
7
+ #
8
+ # This implementation actually calls listeners synchronously (it mays)
9
+ # but hides error raised by them. See Bus::Bunny::Async for another
10
+ # implementation that is truly asynchronous and relies on RabbitMQ.
11
+ #
12
+ class Async
13
+ include Support::Robustness
14
+
15
+ DEFAULT_OPTIONS = {
16
+ }
17
+
18
+ def initialize(options = {})
19
+ @options = DEFAULT_OPTIONS.merge(options)
20
+ @listeners = {}
21
+ end
22
+
23
+ def emit(event)
24
+ (@listeners[event.type.to_s] || []).each do |l|
25
+ stop_errors(self, "emit", event) {
26
+ l.call(event)
27
+ }
28
+ end
29
+ end
30
+
31
+ def listen(type, processor = nil, listener = nil, &bl)
32
+ raise ArgumentError, "A listener must be provided" unless listener || bl
33
+ @listeners[type.to_s] ||= []
34
+ @listeners[type.to_s] << (listener || bl)
35
+ end
36
+
37
+ end # class Sync
38
+ end # module Memory
39
+ end # class Bus
40
+ end # module Klaro
@@ -0,0 +1,30 @@
1
+ module Startback
2
+ class Bus
3
+ module Memory
4
+ #
5
+ # Synchronous implementation of the Bus abstraction, for use between
6
+ # components sharing the same process.
7
+ #
8
+ class Sync
9
+ include Support::Robustness
10
+
11
+ def initialize
12
+ @listeners = {}
13
+ end
14
+
15
+ def emit(event)
16
+ (@listeners[event.type.to_s] || []).each do |l|
17
+ l.call(event)
18
+ end
19
+ end
20
+
21
+ def listen(type, processor = nil, listener = nil, &bl)
22
+ raise ArgumentError, "A listener must be provided" unless listener || bl
23
+ @listeners[type.to_s] ||= []
24
+ @listeners[type.to_s] << (listener || bl)
25
+ end
26
+
27
+ end # class Sync
28
+ end # module Memory
29
+ end # class Bus
30
+ end # module Klaro
@@ -0,0 +1,2 @@
1
+ require_relative 'memory/sync'
2
+ require_relative 'memory/async'
@@ -0,0 +1,94 @@
1
+ require 'startback/event'
2
+ module Startback
3
+ #
4
+ # Sync and async bus abstraction allowing to register listeners and
5
+ # emitting events towards them.
6
+ #
7
+ # This bus actually decorates two busses, one in synchronous and the
8
+ # other one is asynchronous (optional).
9
+ #
10
+ # * A synchronous bus MUST call the listeners as part of emitting
11
+ # process, and MUST re-raise any error occuring during that process.
12
+ # See, e.g. Startback::Bus::Memory::Sync
13
+ #
14
+ # * An asynchronous bus MAY call the listeners later, but MUST hide
15
+ # errors to the emitter.
16
+ # See, e.g. Startback::Bus::Memory::Async
17
+ #
18
+ # This bus facade emits events to both sync and async busses (if any),
19
+ # and listen on the sync one by default.
20
+ #
21
+ # For emitters:
22
+ #
23
+ # # This will synchronously call every listeners who `listen`
24
+ # # on the synchronous bus (& reraise exceptions) then call
25
+ # # (possibly later) all listeners who `listen` on the
26
+ # # asynchronous bus if any (& hide exceptions).
27
+ # bus.emit(event)
28
+ #
29
+ # # This only reaches sync listeners
30
+ # bus.sync.emit(event)
31
+ #
32
+ # # This only reaches async listeners (an async bus must be set)
33
+ # bus.async.emit(event)
34
+ #
35
+ # Please note that there is currently no way to reach sync listeners
36
+ # without having to implement error handling on the emitter side.
37
+ #
38
+ # For listeners:
39
+ #
40
+ # # This will listen synchronously and make the emitter fail if
41
+ # # anything goes wrong with the callback:
42
+ # bus.listen(event_type) do |event|
43
+ # ...
44
+ # end
45
+ #
46
+ # # It is a shortcut for:
47
+ # bus.sync.listen(event_type) do |event| ... end
48
+ #
49
+ # This will listen asynchronously and could not make the emitter
50
+ # fail if something goes wrong with the callback.
51
+ # bus.async.listen(event_type) do |event|
52
+ # ...
53
+ # end
54
+ #
55
+ # Feel free to access the sync and async busses directly for specific
56
+ # cases though.
57
+ #
58
+ class Bus
59
+ include Support::Robustness
60
+
61
+ def initialize(sync = Memory::Sync.new, async = nil)
62
+ @sync = sync
63
+ @async = async
64
+ end
65
+ attr_reader :sync, :async
66
+
67
+ # Emits a particular event to the listeners.
68
+ #
69
+ # @arg event an event, should be an Event instance (through duck
70
+ # typing is allowed)
71
+ def emit(event)
72
+ monitor({
73
+ op: "Startback::Bus#emit",
74
+ op_data: {
75
+ event: { type: event.type }
76
+ }
77
+ }, event.context) do
78
+ sync.emit(event)
79
+ async.emit(event) if async
80
+ end
81
+ end
82
+
83
+ # Registers `listener` as being interested in receiving events of
84
+ # a specific type.
85
+ #
86
+ # @arg type: Symbol, the type of event the listener is interested in.
87
+ # @arg listener: Proc, the listener itself.
88
+ def listen(type, processor = nil, listener = nil, &bl)
89
+ sync.listen(type, processor, listener, &bl)
90
+ end
91
+
92
+ end # class Bus
93
+ end # module Klaro
94
+ require_relative 'bus/memory'
@@ -0,0 +1,80 @@
1
+ module Startback
2
+ module Caching
3
+ #
4
+ # A overriable caching abstraction aiming at making Entity-based caching easy.
5
+ #
6
+ # This class MUST be overriden:
7
+ #
8
+ # * the `load_raw_data` protected method MUST be implemented.
9
+ # * the `full_key` protected method MAY be overriden to provide specific caching
10
+ # keys, e.g. by using the context.
11
+ # * the `valid?` protected method MAY be overriden to check validity of data
12
+ # extracted from the cache.
13
+ #
14
+ # An EntityCache takes an actual store at construction. The object must meet the
15
+ # specification writtern in Store. The 'cache' ruby gem can be used in practice.
16
+ #
17
+ class EntityCache
18
+
19
+ class << self
20
+
21
+ # Default time to live, in seconds
22
+ attr_writer :default_ttl
23
+
24
+ def default_ttl
25
+ @default_ttl || (superclass.respond_to?(:default_ttl, true) && superclass.default_ttl) || 3600
26
+ end
27
+
28
+ end # class DSL
29
+
30
+ def initialize(store, context = nil)
31
+ @store = store
32
+ @context = context
33
+ end
34
+ attr_reader :store, :context
35
+
36
+ # Returns the entity corresponding to a given key.
37
+ #
38
+ # If the entity is not in cache, loads it and puts it in cache using
39
+ # the caching options passed as second parameter.
40
+ def get(short_key, caching_options = default_caching_options)
41
+ cache_key = encode_key(full_key(short_key))
42
+ if store.exist?(cache_key)
43
+ cached = store.get(cache_key)
44
+ return cached if valid?(cache_key, cached)
45
+ end
46
+ load_raw_data(short_key).tap{|to_cache|
47
+ store.set(cache_key, to_cache, caching_options)
48
+ }
49
+ end
50
+
51
+ # Invalidates the cache under a given key.
52
+ def invalidate(key)
53
+ store.delete(encode_key(full_key(key)))
54
+ end
55
+
56
+ protected
57
+
58
+ def encode_key(key)
59
+ JSON.fast_generate(key)
60
+ end
61
+
62
+ def default_caching_options
63
+ { ttl: self.class.default_ttl }
64
+ end
65
+
66
+ def valid?(cache_key, cached)
67
+ true
68
+ end
69
+
70
+ def full_key(key)
71
+ key
72
+ end
73
+
74
+ def load_raw_data(short_key)
75
+ raise NotImplementedError, "#{self.class.name}#load_raw_data"
76
+ end
77
+
78
+ end # class EntityCache
79
+ end # module Caching
80
+ end # module Startback
@@ -0,0 +1,34 @@
1
+ module Startback
2
+ module Caching
3
+ #
4
+ # Caching store specification & dummy implementation.
5
+ #
6
+ # This class should not be used in real project, as it implements
7
+ # See the 'cache' gem that provides conforming implementations.
8
+ #
9
+ class Store
10
+
11
+ def initialize
12
+ @saved = {}
13
+ end
14
+ attr_reader :saved
15
+
16
+ def exist?(key)
17
+ saved.has_key?(key)
18
+ end
19
+
20
+ def get(key)
21
+ saved[key]
22
+ end
23
+
24
+ def set(key, value, ttl)
25
+ saved[key] = value
26
+ end
27
+
28
+ def delete(key)
29
+ saved.delete(key)
30
+ end
31
+
32
+ end # class Store
33
+ end # module Caching
34
+ end # module Startback
@@ -42,7 +42,7 @@ module Startback
42
42
  attr_reader :options
43
43
 
44
44
  def call(env)
45
- env[RACK_ENV_KEY] ||= options[:context_class].new.tap{|c|
45
+ env[RACK_ENV_KEY] ||= options[:context_class].h({}).tap{|c|
46
46
  c.original_rack_env = env.dup
47
47
  }
48
48
  @app.call(env)
@@ -1,13 +1,35 @@
1
1
  module Startback
2
2
  #
3
- # Defines an execution context for Startback applications.
4
- #
5
- # This class is aimed at being subclassed for application required
6
- # extension.
3
+ # Defines an execution context for Startback applications, and provides
4
+ # a cached factory for related abstractions (see `factor`).
7
5
  #
8
6
  # In web application, an instance of a context can be set on the Rack
9
7
  # environment, using Context::Middleware.
10
8
  #
9
+ # This class SHOULD be subclassed for application required extensions
10
+ # to prevent touching the global Startback state itself.
11
+ #
12
+ # Also, for event handling in distributed architectures, a Context should
13
+ # be dumpable and reloadable to JSON. An `h` information contract if provided
14
+ # for that. Subclasses may contribute to the dumping and reloading process
15
+ # through the `h_dump` and `h_factory` methods
16
+ #
17
+ # module MyApp
18
+ # class Context < Startback::Context
19
+ #
20
+ # attr_accessor :foo
21
+ #
22
+ # h_dump do |h|
23
+ # h.merge!("foo" => foo)
24
+ # end
25
+ #
26
+ # h_factor do |c,h|
27
+ # c.foo = h["foo"]
28
+ # end
29
+ #
30
+ # end
31
+ # end
32
+ #
11
33
  class Context
12
34
  attr_accessor :original_rack_env
13
35
 
@@ -16,8 +38,75 @@ module Startback
16
38
  # instance, simply.
17
39
  #
18
40
  # Fatal errors catched by Web::CatchAll are sent on `error_handler#fatal`
41
+ #
42
+ # Deprecated, use the logger below instead.
19
43
  attr_accessor :error_handler
20
44
 
45
+ # A logger can be provided on the context, and will be used for everything
46
+ # related to logging, audit trailing and robustness. The logger receives
47
+ # object following the log & trail conventions of Startback, and must
48
+ # convert them to wathever log format is necessary.
49
+ attr_accessor :logger
50
+
51
+ # Implementation of the `h` information contract
52
+ class << self
53
+
54
+ def h(hash)
55
+ h_factor!(self.new, hash)
56
+ end
57
+
58
+ def h_factor!(context, hash)
59
+ h_factories.each do |f|
60
+ f.call(context, hash)
61
+ end
62
+ context
63
+ end
64
+
65
+ def h_factories
66
+ @h_factories ||= []
67
+ end
68
+
69
+ def h_factory(&factory)
70
+ h_factories << factory
71
+ end
72
+
73
+ ###
74
+
75
+ def h_dump!(context, hash = {})
76
+ h_dumpers.each do |d|
77
+ context.instance_exec(hash, &d)
78
+ end
79
+ hash
80
+ end
81
+
82
+ def h_dumpers
83
+ @h_dumpers ||= []
84
+ end
85
+
86
+ def h_dump(&dumper)
87
+ h_dumpers << dumper
88
+ end
89
+
90
+ end # class << self
91
+
92
+ # Factors an instance of `clazz`, which must be a Context-related
93
+ # abstraction (i.e. its constructor takes the context as last parameters).
94
+ #
95
+ # Factored abstractions are cached for a given context & arguments.
96
+ def factor(clazz, *args)
97
+ @factored ||= {}
98
+ key = args.empty? ? clazz : [clazz] + args
99
+ @factored[key] ||= clazz.new(*(args << self))
100
+ end
101
+
102
+ def to_h
103
+ self.class.h_dump!(self)
104
+ end
105
+
106
+ def to_json(*args, &bl)
107
+ to_h.to_json(*args, &bl)
108
+ end
109
+
21
110
  end # class Context
22
111
  end # module Startback
23
112
  require_relative 'context/middleware'