startback-websocket 0.14.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 (95) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +3 -0
  3. data/README.md +13 -0
  4. data/Rakefile +18 -0
  5. data/lib/startback/audit/prometheus.rb +87 -0
  6. data/lib/startback/audit/shared.rb +17 -0
  7. data/lib/startback/audit/trailer.rb +129 -0
  8. data/lib/startback/audit.rb +3 -0
  9. data/lib/startback/caching/entity_cache.rb +157 -0
  10. data/lib/startback/caching/no_store.rb +28 -0
  11. data/lib/startback/caching/store.rb +34 -0
  12. data/lib/startback/context/h_factory.rb +43 -0
  13. data/lib/startback/context/middleware.rb +53 -0
  14. data/lib/startback/context.rb +122 -0
  15. data/lib/startback/errors.rb +197 -0
  16. data/lib/startback/event/agent.rb +84 -0
  17. data/lib/startback/event/bus/bunny/async.rb +162 -0
  18. data/lib/startback/event/bus/bunny.rb +1 -0
  19. data/lib/startback/event/bus/memory/async.rb +45 -0
  20. data/lib/startback/event/bus/memory/sync.rb +35 -0
  21. data/lib/startback/event/bus/memory.rb +2 -0
  22. data/lib/startback/event/bus.rb +100 -0
  23. data/lib/startback/event/engine.rb +94 -0
  24. data/lib/startback/event/ext/context.rb +5 -0
  25. data/lib/startback/event/ext/operation.rb +13 -0
  26. data/lib/startback/event.rb +47 -0
  27. data/lib/startback/ext/date_time.rb +9 -0
  28. data/lib/startback/ext/time.rb +9 -0
  29. data/lib/startback/ext.rb +2 -0
  30. data/lib/startback/model.rb +6 -0
  31. data/lib/startback/operation/error_operation.rb +19 -0
  32. data/lib/startback/operation/multi_operation.rb +28 -0
  33. data/lib/startback/operation.rb +78 -0
  34. data/lib/startback/services.rb +11 -0
  35. data/lib/startback/support/data_object.rb +71 -0
  36. data/lib/startback/support/env.rb +41 -0
  37. data/lib/startback/support/fake_logger.rb +18 -0
  38. data/lib/startback/support/hooks.rb +48 -0
  39. data/lib/startback/support/log_formatter.rb +34 -0
  40. data/lib/startback/support/logger.rb +34 -0
  41. data/lib/startback/support/operation_runner.rb +150 -0
  42. data/lib/startback/support/robustness.rb +157 -0
  43. data/lib/startback/support/transaction_manager.rb +25 -0
  44. data/lib/startback/support/transaction_policy.rb +33 -0
  45. data/lib/startback/support/world.rb +54 -0
  46. data/lib/startback/support.rb +26 -0
  47. data/lib/startback/version.rb +8 -0
  48. data/lib/startback/web/api.rb +99 -0
  49. data/lib/startback/web/auto_caching.rb +85 -0
  50. data/lib/startback/web/catch_all.rb +52 -0
  51. data/lib/startback/web/cors_headers.rb +80 -0
  52. data/lib/startback/web/health_check.rb +49 -0
  53. data/lib/startback/web/magic_assets/ng_html_transformer.rb +80 -0
  54. data/lib/startback/web/magic_assets/rake_tasks.rb +64 -0
  55. data/lib/startback/web/magic_assets.rb +98 -0
  56. data/lib/startback/web/middleware.rb +13 -0
  57. data/lib/startback/web/prometheus.rb +16 -0
  58. data/lib/startback/web/shield.rb +58 -0
  59. data/lib/startback.rb +43 -0
  60. data/spec/spec_helper.rb +49 -0
  61. data/spec/unit/audit/test_prometheus.rb +72 -0
  62. data/spec/unit/audit/test_trailer.rb +105 -0
  63. data/spec/unit/caching/test_entity_cache.rb +136 -0
  64. data/spec/unit/context/test_abstraction_factory.rb +64 -0
  65. data/spec/unit/context/test_dup.rb +42 -0
  66. data/spec/unit/context/test_fork.rb +37 -0
  67. data/spec/unit/context/test_h_factory.rb +31 -0
  68. data/spec/unit/context/test_middleware.rb +45 -0
  69. data/spec/unit/context/test_with_world.rb +20 -0
  70. data/spec/unit/context/test_world.rb +17 -0
  71. data/spec/unit/event/bus/memory/test_async.rb +43 -0
  72. data/spec/unit/event/bus/memory/test_sync.rb +43 -0
  73. data/spec/unit/support/hooks/test_after_hook.rb +54 -0
  74. data/spec/unit/support/hooks/test_before_hook.rb +54 -0
  75. data/spec/unit/support/operation_runner/test_around_run.rb +156 -0
  76. data/spec/unit/support/operation_runner/test_before_after_call.rb +48 -0
  77. data/spec/unit/support/test_data_object.rb +156 -0
  78. data/spec/unit/support/test_env.rb +75 -0
  79. data/spec/unit/support/test_robusteness.rb +229 -0
  80. data/spec/unit/support/test_transaction_manager.rb +64 -0
  81. data/spec/unit/support/test_world.rb +72 -0
  82. data/spec/unit/test_event.rb +62 -0
  83. data/spec/unit/test_operation.rb +55 -0
  84. data/spec/unit/test_support.rb +40 -0
  85. data/spec/unit/web/fixtures/assets/app/hello.es6 +4 -0
  86. data/spec/unit/web/fixtures/assets/app/hello.html +1 -0
  87. data/spec/unit/web/fixtures/assets/index.es6 +1 -0
  88. data/spec/unit/web/test_api.rb +82 -0
  89. data/spec/unit/web/test_auto_caching.rb +81 -0
  90. data/spec/unit/web/test_catch_all.rb +77 -0
  91. data/spec/unit/web/test_cors_headers.rb +88 -0
  92. data/spec/unit/web/test_healthcheck.rb +59 -0
  93. data/spec/unit/web/test_magic_assets.rb +82 -0
  94. data/tasks/test.rake +14 -0
  95. metadata +237 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 296fd63b702924e7a5952b60a67bbb90e89b99a415be62e52a4f7f63e5638753
4
+ data.tar.gz: 8234e5f95a05e91f6698da787bc8479ce924a2bfc537866ea68d188f89df7b54
5
+ SHA512:
6
+ metadata.gz: 48169cfbda94301c4a8ebbfea5f0ba93bedba26f59860a4b8d5376d51ec19cf0f23dcbbda9d7c8c366d483f1bd22d9d36dec2ce90ec362870adc731e7c28a7f6
7
+ data.tar.gz: e304a39fb8f4ce6a3c3df896c014f8b60eda3a58b18b4e9f52c7de1eeee67b3f7fd3b12e9652d7e50ccc7344377a9c289fd9c0aa14d4a6c13dd9ee75fcf39fab
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+ gem 'startback', path: "."
3
+ gemspec :name => 'startback-web'
data/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # Startback - Got Your Ruby Back
2
+
3
+ Yet another ruby framework, I'm afraid. Here, we srongly seperate between:
4
+
5
+ 1. the web layer, in charge of a quality HTTP handling
6
+ 2. the operations layer, in charge of the high-level software operations
7
+ 3. the database layer, abstracted using the Relations As First Class Citizen pattern
8
+
9
+ Currently,
10
+
11
+ 1. is handled using extra support on top of Sinatra
12
+ 2. is handled using Startback specific classes
13
+ 3. is handled using Bmg
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ $:.unshift File.expand_path('../lib', __FILE__)
2
+
3
+ def shell(*cmds)
4
+ cmd = cmds.join("\n")
5
+ puts cmd
6
+ system cmd
7
+ end
8
+
9
+ #
10
+ # Install all tasks found in tasks folder
11
+ #
12
+ # See .rake files there for complete documentation.
13
+ #
14
+ Dir["tasks/*.rake"].each do |taskfile|
15
+ load taskfile
16
+ end
17
+
18
+ task :default => :test
@@ -0,0 +1,87 @@
1
+ require_relative 'shared'
2
+ require 'prometheus/client'
3
+
4
+ module Startback
5
+ module Audit
6
+ #
7
+ # Prometheus exporter abstraction, that can be registered as an around
8
+ # hook on OperationRunner and as a prometheus client on Context instances.
9
+ #
10
+ # The exporter uses the ruby client for prometheus to expose metrics regarding Operation runs.
11
+ #
12
+ # The following metrics are exported:
13
+ #
14
+ # A counter 'operation_errors' (failed runs)
15
+ # A histogram 'operation_calls'
16
+ #
17
+ # All these metrics use the following labels
18
+ # - operation : class name of the operation executed
19
+ #
20
+ # Given that this Exporter is intended to be used as around hook on an
21
+ # `OperationRunner`, operations that fail at construction time will not be
22
+ # exported at all, since they can't be ran in the first place. This may lead
23
+ # to metrics not containing important errors cases if operations check their
24
+ # input at construction time.
25
+ #
26
+ class Prometheus
27
+ include Shared
28
+
29
+ def initialize(options = {})
30
+ @prefix = options[:prefix] || "startback"
31
+ @options = options
32
+ @registry = ::Prometheus::Client.registry
33
+ all_labels = [:operation, :startback_version] + option_labels.keys
34
+ @errors = @registry.counter(
35
+ :"#{prefix}_operation_errors",
36
+ docstring: 'A counter of operation errors',
37
+ labels: all_labels)
38
+ @calls = @registry.histogram(
39
+ :"#{prefix}_operation_calls",
40
+ docstring: 'A histogram of operation latency',
41
+ labels: all_labels)
42
+ end
43
+ attr_reader :registry, :calls, :errors, :options, :prefix
44
+
45
+ def call(runner, op)
46
+ name = op_name(op)
47
+ result = nil
48
+ time = Benchmark.realtime{
49
+ result = yield
50
+ }
51
+ ignore_safely {
52
+ @calls.observe(time, labels: get_labels(name))
53
+ }
54
+ result
55
+ rescue => ex
56
+ ignore_safely {
57
+ @errors.increment(labels: get_labels(name))
58
+ }
59
+ raise
60
+ end
61
+
62
+ protected
63
+
64
+ def ignore_safely
65
+ yield
66
+ rescue => ex
67
+ nil
68
+ end
69
+
70
+ def get_labels(op_name)
71
+ option_labels.merge({
72
+ operation: op_name,
73
+ startback_version: version
74
+ })
75
+ end
76
+
77
+ def option_labels
78
+ @options[:labels] || {}
79
+ end
80
+
81
+ def version
82
+ Startback::VERSION
83
+ end
84
+
85
+ end # class Prometheus
86
+ end # module Audit
87
+ end # module Startback
@@ -0,0 +1,17 @@
1
+ module Startback
2
+ module Audit
3
+ module Shared
4
+
5
+ def op_name(op)
6
+ return op.op_name if op.respond_to?(:op_name)
7
+
8
+ case op
9
+ when String then op
10
+ when Class then op.name
11
+ else op.class.name
12
+ end
13
+ end
14
+
15
+ end # module Shared
16
+ end # module Audit
17
+ end # module Startback
@@ -0,0 +1,129 @@
1
+ require_relative 'shared'
2
+ require 'forwardable'
3
+ module Startback
4
+ module Audit
5
+ #
6
+ # Log & Audit trail abstraction, that can be registered as an around
7
+ # hook on OperationRunner and as an actual logger on Context instances.
8
+ #
9
+ # The trail is outputted as JSON lines, using a Logger on the "device"
10
+ # passed at construction. The following JSON entries are dumped:
11
+ #
12
+ # - severity : INFO or ERROR
13
+ # - time : ISO8601 Datetime of operation execution
14
+ # - op : class name of the operation executed
15
+ # - op_took : Execution duration of the operation
16
+ # - op_data : Dump of operation input data
17
+ # - context : Execution context, through its `h` information contract (IC)
18
+ #
19
+ # Dumping of operation data follows the following duck typing conventions:
20
+ #
21
+ # - If the operation instance responds to `to_trail`, this data is taken
22
+ # - If the operation instance responds to `input`, this data is taken
23
+ # - If the operation instance responds to `request`, this data is taken
24
+ # - Otherwise op_data is a JSON null
25
+ #
26
+ # By contributing to the Context's `h` IC, users can easily dump information that
27
+ # makes sense (such as the operation execution requester).
28
+ #
29
+ # The class implements a sanitization process when dumping the context and
30
+ # operation data. Blacklisted words taken in construction options are used to
31
+ # prevent dumping hash keys that match them (insentively). Default stop words
32
+ # are equivalent to:
33
+ #
34
+ # Trailer.new("/var/log/trail.log", {
35
+ # blacklist: "token password secret credential"
36
+ # })
37
+ #
38
+ # Please note that the sanitization process does not apply recursively if
39
+ # the operation data is hierarchic. It only applies to the top object of
40
+ # Hash and [Hash]. Use `Operation#to_trail` to fine-tune your audit trail.
41
+ #
42
+ # Given that this Trailer is intended to be used as around hook on an
43
+ # `OperationRunner`, operations that fail at construction time will not be
44
+ # trailed at all, since they can't be ran in the first place. This may lead
45
+ # to trails not containing important errors cases if operations check their
46
+ # input at construction time.
47
+ #
48
+ class Trailer
49
+ include Shared
50
+ extend Forwardable
51
+ def_delegators :@logger, :debug, :info, :warn, :error, :fatal
52
+
53
+ DEFAULT_OPTIONS = {
54
+
55
+ # Words used to stop dumping for, e.g., security reasons
56
+ blacklist: "token password secret credential"
57
+
58
+ }
59
+
60
+ def initialize(device, options = {})
61
+ @options = DEFAULT_OPTIONS.merge(options)
62
+ @logger = ::Logger.new(device, 'daily')
63
+ @logger.formatter = Support::LogFormatter.new
64
+ end
65
+ attr_reader :logger, :options
66
+
67
+ def call(runner, op)
68
+ result = nil
69
+ time = Benchmark.realtime{ result = yield }
70
+ logger.info(op_to_trail(op, time))
71
+ result
72
+ rescue => ex
73
+ logger.error(op_to_trail(op, time, ex))
74
+ raise
75
+ end
76
+
77
+ protected
78
+
79
+ def op_to_trail(op, time = nil, ex = nil)
80
+ log_msg = {
81
+ op_took: time ? time.round(8) : nil,
82
+ op: op_name(op),
83
+ context: op_context(op),
84
+ op_data: op_data(op)
85
+ }.compact
86
+ log_msg[:error] = ex if ex
87
+ log_msg
88
+ end
89
+
90
+ def op_context(op)
91
+ sanitize(op.respond_to?(:context, false) ? op.context.to_h : {})
92
+ end
93
+
94
+ def op_data(op)
95
+ data = if op.respond_to?(:op_data, false)
96
+ op.op_data
97
+ elsif op.respond_to?(:to_trail, false)
98
+ op.to_trail
99
+ elsif op.respond_to?(:input, false)
100
+ op.input
101
+ elsif op.respond_to?(:request, false)
102
+ op.request
103
+ elsif op.is_a?(Operation::MultiOperation)
104
+ op.ops.map{ |sub_op| op_to_trail(sub_op) }
105
+ end
106
+ sanitize(data)
107
+ end
108
+
109
+ def sanitize(data)
110
+ case data
111
+ when Hash, OpenStruct
112
+ data.dup.delete_if{|k| k.to_s =~ blacklist_rx }
113
+ when Enumerable
114
+ data.map{|elm| sanitize(elm) }.compact
115
+ else
116
+ data
117
+ end
118
+ end
119
+
120
+ def blacklist_rx
121
+ @blacklist_rx ||= Regexp.new(
122
+ options[:blacklist].split(/\s+/).join("|"),
123
+ Regexp::IGNORECASE
124
+ )
125
+ end
126
+
127
+ end # class Trailer
128
+ end # module Audit
129
+ end # module Startback
@@ -0,0 +1,3 @@
1
+ require_relative 'audit/shared'
2
+ require_relative 'audit/trailer'
3
+ require_relative 'audit/prometheus'
@@ -0,0 +1,157 @@
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_entity` protected method MUST be implemented to load data from
9
+ # a primary & context unaware key.
10
+ #
11
+ # * the `primary_key` protected method MAY be implemented to convert candidate
12
+ # keys (received from ultimate callers) to primary keys. The method is also
13
+ # a good place to check and/or log the keys actually used by callers.
14
+ #
15
+ # * the `context_free_key` protected method MAY be overriden to provide
16
+ # domain unrelated caching keys from primary keys, e.g. by encoding the
17
+ # context into the caching key itself, if needed.
18
+ #
19
+ # * the `valid?` protected method MAY be overriden to check validity of data
20
+ # extracted from the cache and force a refresh even if found.
21
+ #
22
+ # An EntityCache takes an actual store at construction. The object must meet the
23
+ # specification writtern in Store. The 'cache' ruby gem can be used in practice.
24
+ #
25
+ # Cache hits, outdated and miss are logged in debug, info, and info severity.
26
+ # The `cache_hit`, `cache_outdated`, `cache_miss` protected methods MAY be
27
+ # overriden to change that behavior.
28
+ #
29
+ class EntityCache
30
+ include Support::Robustness
31
+
32
+ class << self
33
+
34
+ # Default time to live, in seconds
35
+ attr_writer :default_ttl
36
+
37
+ def default_ttl
38
+ @default_ttl || (superclass.respond_to?(:default_ttl, true) && superclass.default_ttl) || 3600
39
+ end
40
+
41
+ end # class DSL
42
+
43
+ def initialize(store, context = nil)
44
+ @store = store
45
+ @context = context
46
+ end
47
+ attr_reader :store, :context
48
+
49
+ # Returns the entity corresponding to a given key.
50
+ #
51
+ # If the entity is not in cache, loads it and puts it in cache using
52
+ # the caching options passed as second parameter.
53
+ def get(candidate_key, caching_options = default_caching_options)
54
+ pkey = primary_key(candidate_key)
55
+ cache_key = encode_key(context_free_key(pkey))
56
+ if store.exist?(cache_key)
57
+ cached = store.get(cache_key)
58
+ if valid?(pkey, cached)
59
+ cache_hit(pkey, cached)
60
+ return cached
61
+ else
62
+ cache_outdated(pkey, cached)
63
+ end
64
+ end
65
+ cache_miss(pkey)
66
+ load_entity(pkey).tap{|to_cache|
67
+ store.set(cache_key, to_cache, caching_options)
68
+ }
69
+ end
70
+
71
+ # Invalidates the cache under a given key.
72
+ def invalidate(candidate_key)
73
+ pkey = primary_key(candidate_key)
74
+ cache_key = encode_key(context_free_key(pkey))
75
+ store.delete(cache_key)
76
+ end
77
+
78
+ protected
79
+
80
+ def cache_hit(pkey, cached)
81
+ log(:debug, self, "cache_hit", context, op_data: pkey)
82
+ end
83
+
84
+ def cache_outdated(pkey, cached)
85
+ log(:info, self, "cache_outdated", context, op_data: pkey)
86
+ end
87
+
88
+ def cache_miss(pkey)
89
+ log(:info, self, "cache_miss", context, op_data: pkey)
90
+ end
91
+
92
+ def default_caching_options
93
+ { ttl: self.class.default_ttl }
94
+ end
95
+
96
+ # Converts a candidate key to a primary key, so as to prevent
97
+ # cache duplicates if callers are allowed to request an entity
98
+ # through various keys.
99
+ #
100
+ # The default implementation returns the candidate key and MAY
101
+ # be overriden.
102
+ def primary_key(candidate_key)
103
+ candidate_key
104
+ end
105
+
106
+ # Encodes a context free key to an actual cache key.
107
+ #
108
+ # Default implementation uses JSON.fast_generate but MAY be
109
+ # overriden.
110
+ def encode_key(context_free_key)
111
+ JSON.fast_generate(context_free_key)
112
+ end
113
+
114
+ # Returns whether `cached` entity seems fresh enough to
115
+ # be returned as a cache hit.
116
+ #
117
+ # This method provides a way to check freshness using, e.g.
118
+ # `updated_at` or `etag` kind of entity fields. The default
119
+ # implementation returns true and MAY be overriden.
120
+ def valid?(primary_key, cached)
121
+ true
122
+ end
123
+
124
+ # Converts a primary_key to a context_free_key, using the
125
+ # context (instance variable) to encode the context itself
126
+ # into the actual cache key.
127
+ #
128
+ # The default implementation simply returns the primary key
129
+ # and MAY be overriden.
130
+ def context_free_key(primary_key)
131
+ full_key(primary_key)
132
+ end
133
+
134
+ # Deprecated, will be removed in 0.6.0. Use context_free_key
135
+ # instead.
136
+ def full_key(primary_key)
137
+ primary_key
138
+ end
139
+
140
+ # Actually loads the entity using the given primary key, and
141
+ # possibly the cache context.
142
+ #
143
+ # This method MUST be implemented and raises a NotImplementedError
144
+ # by default.
145
+ def load_entity(primary_key)
146
+ load_raw_data(primary_key)
147
+ end
148
+
149
+ # Deprecated, will be removed in 0.6.0. Use load_entity
150
+ # instead.
151
+ def load_raw_data(*args, &bl)
152
+ raise NotImplementedError, "#{self.class.name}#load_entity"
153
+ end
154
+
155
+ end # class EntityCache
156
+ end # module Caching
157
+ end # module Startback
@@ -0,0 +1,28 @@
1
+ module Startback
2
+ module Caching
3
+ #
4
+ # Caching store implementation that caches nothing at all.
5
+ #
6
+ class NoStore
7
+
8
+ def initialize
9
+ end
10
+
11
+ def exist?(key)
12
+ false
13
+ end
14
+
15
+ def get(key)
16
+ nil
17
+ end
18
+
19
+ def set(key, value, ttl)
20
+ value
21
+ end
22
+
23
+ def delete(key)
24
+ end
25
+
26
+ end # class NoStore
27
+ end # module Caching
28
+ 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
@@ -0,0 +1,43 @@
1
+ module Startback
2
+ class Context
3
+ module HFactory
4
+
5
+ def h(hash)
6
+ h_factor!(self.new, hash)
7
+ end
8
+
9
+ def h_factor!(context, hash)
10
+ h_factories.each do |f|
11
+ f.call(context, hash)
12
+ end
13
+ context
14
+ end
15
+
16
+ def h_factories
17
+ @h_factories ||= []
18
+ end
19
+
20
+ def h_factory(&factory)
21
+ h_factories << factory
22
+ end
23
+
24
+ ###
25
+
26
+ def h_dump!(context, hash = {})
27
+ h_dumpers.each do |d|
28
+ context.instance_exec(hash, &d)
29
+ end
30
+ hash
31
+ end
32
+
33
+ def h_dumpers
34
+ @h_dumpers ||= []
35
+ end
36
+
37
+ def h_dump(&dumper)
38
+ h_dumpers << dumper
39
+ end
40
+
41
+ end # module HFactory
42
+ end # class Context
43
+ end # module Startback
@@ -0,0 +1,53 @@
1
+ module Startback
2
+ class Context
3
+ #
4
+ # Rack middleware that installs a particular context instance
5
+ # on the Rack environment.
6
+ #
7
+ # Examples:
8
+ #
9
+ # # Use the default context class
10
+ # Rack::Builder.new do
11
+ # use Startback::Context::Middleware
12
+ #
13
+ # run ->(env){
14
+ # ctx = env[Startback::Context::Middleware::RACK_ENV_KEY]
15
+ # ctx.is_a?(Startback::Context) # => true
16
+ # }
17
+ # end
18
+ #
19
+ # # Use a user defined context class
20
+ # Rack::Builder.new do
21
+ # use Startback::Context::Middleware, MyContextClass.new
22
+ #
23
+ # run ->(env){
24
+ # ctx = env[Startback::Context::Middleware::RACK_ENV_KEY]
25
+ # ctx.is_a?(MyContextClass) # => true (your subclass)
26
+ # ctx.is_a?(Startback::Context) # => true (required!)
27
+ # }
28
+ # end
29
+ #
30
+ class Middleware
31
+
32
+ RACK_ENV_KEY = 'SAMBACK_CONTEXT'
33
+
34
+ def initialize(app, context = Context.new)
35
+ @app = app
36
+ @context = context
37
+ end
38
+ attr_reader :context
39
+
40
+ def call(env)
41
+ env[RACK_ENV_KEY] ||= context.dup.tap{|c|
42
+ c.original_rack_env = env.dup
43
+ }
44
+ @app.call(env)
45
+ end
46
+
47
+ def self.context(env)
48
+ env[RACK_ENV_KEY]
49
+ end
50
+
51
+ end # class Middleware
52
+ end # class Context
53
+ end # module Startback