startback 0.4.5 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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'