startback-websocket 0.14.0

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