startback-websocket 0.14.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +3 -0
- data/README.md +13 -0
- data/Rakefile +18 -0
- data/lib/startback/audit/prometheus.rb +87 -0
- data/lib/startback/audit/shared.rb +17 -0
- data/lib/startback/audit/trailer.rb +129 -0
- data/lib/startback/audit.rb +3 -0
- data/lib/startback/caching/entity_cache.rb +157 -0
- data/lib/startback/caching/no_store.rb +28 -0
- data/lib/startback/caching/store.rb +34 -0
- data/lib/startback/context/h_factory.rb +43 -0
- data/lib/startback/context/middleware.rb +53 -0
- data/lib/startback/context.rb +122 -0
- data/lib/startback/errors.rb +197 -0
- data/lib/startback/event/agent.rb +84 -0
- data/lib/startback/event/bus/bunny/async.rb +162 -0
- data/lib/startback/event/bus/bunny.rb +1 -0
- data/lib/startback/event/bus/memory/async.rb +45 -0
- data/lib/startback/event/bus/memory/sync.rb +35 -0
- data/lib/startback/event/bus/memory.rb +2 -0
- data/lib/startback/event/bus.rb +100 -0
- data/lib/startback/event/engine.rb +94 -0
- data/lib/startback/event/ext/context.rb +5 -0
- data/lib/startback/event/ext/operation.rb +13 -0
- data/lib/startback/event.rb +47 -0
- data/lib/startback/ext/date_time.rb +9 -0
- data/lib/startback/ext/time.rb +9 -0
- data/lib/startback/ext.rb +2 -0
- data/lib/startback/model.rb +6 -0
- data/lib/startback/operation/error_operation.rb +19 -0
- data/lib/startback/operation/multi_operation.rb +28 -0
- data/lib/startback/operation.rb +78 -0
- data/lib/startback/services.rb +11 -0
- data/lib/startback/support/data_object.rb +71 -0
- data/lib/startback/support/env.rb +41 -0
- data/lib/startback/support/fake_logger.rb +18 -0
- data/lib/startback/support/hooks.rb +48 -0
- data/lib/startback/support/log_formatter.rb +34 -0
- data/lib/startback/support/logger.rb +34 -0
- data/lib/startback/support/operation_runner.rb +150 -0
- data/lib/startback/support/robustness.rb +157 -0
- data/lib/startback/support/transaction_manager.rb +25 -0
- data/lib/startback/support/transaction_policy.rb +33 -0
- data/lib/startback/support/world.rb +54 -0
- data/lib/startback/support.rb +26 -0
- data/lib/startback/version.rb +8 -0
- data/lib/startback/web/api.rb +99 -0
- data/lib/startback/web/auto_caching.rb +85 -0
- data/lib/startback/web/catch_all.rb +52 -0
- data/lib/startback/web/cors_headers.rb +80 -0
- data/lib/startback/web/health_check.rb +49 -0
- data/lib/startback/web/magic_assets/ng_html_transformer.rb +80 -0
- data/lib/startback/web/magic_assets/rake_tasks.rb +64 -0
- data/lib/startback/web/magic_assets.rb +98 -0
- data/lib/startback/web/middleware.rb +13 -0
- data/lib/startback/web/prometheus.rb +16 -0
- data/lib/startback/web/shield.rb +58 -0
- data/lib/startback.rb +43 -0
- data/spec/spec_helper.rb +49 -0
- data/spec/unit/audit/test_prometheus.rb +72 -0
- data/spec/unit/audit/test_trailer.rb +105 -0
- data/spec/unit/caching/test_entity_cache.rb +136 -0
- data/spec/unit/context/test_abstraction_factory.rb +64 -0
- data/spec/unit/context/test_dup.rb +42 -0
- data/spec/unit/context/test_fork.rb +37 -0
- data/spec/unit/context/test_h_factory.rb +31 -0
- data/spec/unit/context/test_middleware.rb +45 -0
- data/spec/unit/context/test_with_world.rb +20 -0
- data/spec/unit/context/test_world.rb +17 -0
- data/spec/unit/event/bus/memory/test_async.rb +43 -0
- data/spec/unit/event/bus/memory/test_sync.rb +43 -0
- data/spec/unit/support/hooks/test_after_hook.rb +54 -0
- data/spec/unit/support/hooks/test_before_hook.rb +54 -0
- data/spec/unit/support/operation_runner/test_around_run.rb +156 -0
- data/spec/unit/support/operation_runner/test_before_after_call.rb +48 -0
- data/spec/unit/support/test_data_object.rb +156 -0
- data/spec/unit/support/test_env.rb +75 -0
- data/spec/unit/support/test_robusteness.rb +229 -0
- data/spec/unit/support/test_transaction_manager.rb +64 -0
- data/spec/unit/support/test_world.rb +72 -0
- data/spec/unit/test_event.rb +62 -0
- data/spec/unit/test_operation.rb +55 -0
- data/spec/unit/test_support.rb +40 -0
- data/spec/unit/web/fixtures/assets/app/hello.es6 +4 -0
- data/spec/unit/web/fixtures/assets/app/hello.html +1 -0
- data/spec/unit/web/fixtures/assets/index.es6 +1 -0
- data/spec/unit/web/test_api.rb +82 -0
- data/spec/unit/web/test_auto_caching.rb +81 -0
- data/spec/unit/web/test_catch_all.rb +77 -0
- data/spec/unit/web/test_cors_headers.rb +88 -0
- data/spec/unit/web/test_healthcheck.rb +59 -0
- data/spec/unit/web/test_magic_assets.rb +82 -0
- data/tasks/test.rake +14 -0
- metadata +237 -0
@@ -0,0 +1,150 @@
|
|
1
|
+
module Startback
|
2
|
+
module Support
|
3
|
+
#
|
4
|
+
# Support module for high-level architectural components that
|
5
|
+
# execute operations as part of their logic, see e.g. Web::Api.
|
6
|
+
#
|
7
|
+
# This module contributes a `run` instance method that allows
|
8
|
+
# binding an operation with a world, and executing it while
|
9
|
+
# supporting around runners.
|
10
|
+
#
|
11
|
+
# Example:
|
12
|
+
#
|
13
|
+
# class HighLevelComponent
|
14
|
+
# include Startback::Support::OperationRunner
|
15
|
+
#
|
16
|
+
# def some_method
|
17
|
+
# # Runs the operation passed after some binding
|
18
|
+
# run SomeOperation.new
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# protected
|
22
|
+
#
|
23
|
+
# # Overriden to inject some extra world
|
24
|
+
# def operation_world(op)
|
25
|
+
# super(op).merge({ hello: "world" })
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# # Execute this around op
|
29
|
+
# around_run do |op, then_block|
|
30
|
+
# puts "About to run #{op.inspect}"
|
31
|
+
# then_block.call
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# # SomeClass#call will be called with the operation
|
35
|
+
# # as first parameter and a block as continuation
|
36
|
+
# around_run SomeClass.new
|
37
|
+
#
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
module OperationRunner
|
41
|
+
|
42
|
+
# Contributes the hook DSL methods to classes that include
|
43
|
+
# the OperationRunner module
|
44
|
+
module ClassMethods
|
45
|
+
|
46
|
+
# Registers a callable to be executed around operation running.
|
47
|
+
#
|
48
|
+
# In its block form, the callable is `instance_exec`uted on the
|
49
|
+
# runner instance, with the operation passed as first parameter
|
50
|
+
# and a then_block callable as second parameter (continuation):
|
51
|
+
#
|
52
|
+
# around_run do |op,then_block|
|
53
|
+
# # do whatever you want with the op (already bounded)
|
54
|
+
# puts op.inspect
|
55
|
+
#
|
56
|
+
# # do not forget to call the continuation block
|
57
|
+
# then_block.call
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# With a parameter responding to `#call`, the latter is invoked
|
61
|
+
# with the operation as parameter and a block as continuation:
|
62
|
+
#
|
63
|
+
# class Arounder
|
64
|
+
#
|
65
|
+
# def call(op)
|
66
|
+
# # do whatever you want with the op (already bounded)
|
67
|
+
# puts op.inspect
|
68
|
+
#
|
69
|
+
# # do not forget to call the continuation block
|
70
|
+
# yield
|
71
|
+
# end
|
72
|
+
#
|
73
|
+
# end
|
74
|
+
#
|
75
|
+
def around_run(arounder = nil, &bl)
|
76
|
+
raise ArgumentError, "Arg or block required" unless arounder || bl
|
77
|
+
arounds(true) << [arounder || bl, arounder.nil?]
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def arounds(create = false)
|
83
|
+
if create
|
84
|
+
@arounds ||= superclass.respond_to?(:arounds, true) \
|
85
|
+
? superclass.send(:arounds, true).dup \
|
86
|
+
: []
|
87
|
+
end
|
88
|
+
@arounds || (superclass.respond_to?(:arounds, true) ? superclass.send(:arounds, true) : [])
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
# When included by a class/module, install the DSL methods
|
94
|
+
def self.included(by)
|
95
|
+
by.extend(ClassMethods)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Runs `operation`, taking care of binding it and executing
|
99
|
+
# hooks.
|
100
|
+
#
|
101
|
+
# This method is NOT intended to be overriden. Use hooks and
|
102
|
+
# `operation_world` to impact default behavior.
|
103
|
+
def run(operation)
|
104
|
+
op_world = operation_world(operation)
|
105
|
+
op_bound = operation.bind(op_world)
|
106
|
+
_run_befores(op_bound)
|
107
|
+
r = _run_with_arounds(op_bound, self.class.send(:arounds))
|
108
|
+
_run_afters(op_bound)
|
109
|
+
r
|
110
|
+
end
|
111
|
+
|
112
|
+
protected
|
113
|
+
|
114
|
+
# Returns the world to use to bind an operation.
|
115
|
+
#
|
116
|
+
# The default implementation returns an empty hash. This is
|
117
|
+
# intended to be overriden by classes including this module.
|
118
|
+
def operation_world(op)
|
119
|
+
{}
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
def _run_befores(op_bound)
|
125
|
+
op_bound.before_call if op_bound.respond_to?(:before_call, true)
|
126
|
+
end
|
127
|
+
|
128
|
+
def _run_with_arounds(operation, arounds = [])
|
129
|
+
if arounds.empty?
|
130
|
+
operation.call
|
131
|
+
else
|
132
|
+
arounder, iexec = arounds.first
|
133
|
+
after_first = ->() {
|
134
|
+
_run_with_arounds(operation, arounds[1..-1])
|
135
|
+
}
|
136
|
+
if iexec
|
137
|
+
self.instance_exec(operation, after_first, &arounder)
|
138
|
+
else
|
139
|
+
arounder.call(self, operation, &after_first)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def _run_afters(op_bound)
|
145
|
+
op_bound.after_call if op_bound.respond_to?(:after_call, true)
|
146
|
+
end
|
147
|
+
|
148
|
+
end # module OperationRunner
|
149
|
+
end # module Support
|
150
|
+
end # module Startback
|
@@ -0,0 +1,157 @@
|
|
1
|
+
module Startback
|
2
|
+
module Support
|
3
|
+
#
|
4
|
+
# This module provides helper methods for robustness of a software design.
|
5
|
+
#
|
6
|
+
# It is included by main Startback abstractions, and can be included by
|
7
|
+
# specific software components who needs fine-tuning of monitoring, logging
|
8
|
+
# and error handling.
|
9
|
+
#
|
10
|
+
# All public methods here follow the following free args parameters:
|
11
|
+
#
|
12
|
+
# 1. First (& second) argument(s) form the log message.
|
13
|
+
#
|
14
|
+
# A full log message is a Hash having :op (required), :op_took (optional),
|
15
|
+
# and :op_data (optional) keys.
|
16
|
+
#
|
17
|
+
# If a String (or two) are used instead, a log message will be built taking
|
18
|
+
# the former as the executer (a class or instance) and the second as a method.
|
19
|
+
# `{ op: "executer#method" }`
|
20
|
+
#
|
21
|
+
# 2. The second (or third) argument should be a Logger instance, a Context,
|
22
|
+
# or an instance knowing its context. The best logger is extracted from it
|
23
|
+
# and used for actual logging.
|
24
|
+
#
|
25
|
+
# Examples:
|
26
|
+
#
|
27
|
+
# log(:info, op: "hello", op_data: {foo: 12}) => logged as such on STDOUT
|
28
|
+
# log(:info, "A simple message") => { op: "A simple message" } on STDOUT
|
29
|
+
# log(:info, Startback, "hello") => { op: "Startback#hello" } on STDOUT
|
30
|
+
# log(:info, Event.new, "hello") => { op: "Event#hello" } on STDOUT
|
31
|
+
# log(:info, Event.new, "hello", "hello world") => { op: "Event#hello", op_data: { message: "hello world" } } on STDOUT
|
32
|
+
# log(:info, self, context) => { op: "..." } on context's logger or STDOUT
|
33
|
+
# log(:info, self, event) => { op: "..." } on event context's logger or STDOUT
|
34
|
+
# ...
|
35
|
+
#
|
36
|
+
module Robustness
|
37
|
+
|
38
|
+
# Included to avoid poluting the space of the including
|
39
|
+
# classes.
|
40
|
+
module Tools
|
41
|
+
|
42
|
+
def default_logger
|
43
|
+
@@default_logger ||= begin
|
44
|
+
l = ::Logger.new(STDOUT)
|
45
|
+
l.formatter = LogFormatter.new
|
46
|
+
l.warn(op: "#{self}", op_data: { msg: "Using default logger to STDOUT" })
|
47
|
+
@@default_logger = l
|
48
|
+
end
|
49
|
+
@@default_logger
|
50
|
+
end
|
51
|
+
module_function :default_logger
|
52
|
+
|
53
|
+
def logger_for(arg)
|
54
|
+
return arg if arg.is_a?(::Logger)
|
55
|
+
return arg.logger if arg.is_a?(Context) && arg.logger
|
56
|
+
return logger_for(arg.context) if arg.respond_to?(:context, false)
|
57
|
+
default_logger
|
58
|
+
end
|
59
|
+
module_function :logger_for
|
60
|
+
|
61
|
+
def parse_args(log_msg, method = nil, context = nil, extra = nil)
|
62
|
+
method, context, extra = nil, method, context unless method.is_a?(String)
|
63
|
+
context, extra = nil, context if context.is_a?(Hash) || context.is_a?(String) && extra.nil?
|
64
|
+
extra = { op_data: { message: extra } } if extra.is_a?(String)
|
65
|
+
logger = logger_for(context) || logger_for(log_msg)
|
66
|
+
log_msg = if log_msg.is_a?(Hash)
|
67
|
+
log_msg.dup
|
68
|
+
elsif log_msg.is_a?(String)
|
69
|
+
log_msg = { op: "#{log_msg}#{method.nil? ? '' : '#'+method.to_s}" }
|
70
|
+
elsif log_msg.is_a?(Exception)
|
71
|
+
log_msg = { error: log_msg }
|
72
|
+
else
|
73
|
+
log_msg = log_msg.class unless log_msg.is_a?(Module)
|
74
|
+
log_msg = { op: "#{log_msg.name}##{method}" }
|
75
|
+
end
|
76
|
+
log_msg.merge!(extra) if extra
|
77
|
+
[ log_msg, logger ]
|
78
|
+
end
|
79
|
+
module_function :parse_args
|
80
|
+
|
81
|
+
[:debug, :info, :warn, :error, :fatal].each do |meth|
|
82
|
+
define_method(meth) do |args, extra = nil, &bl|
|
83
|
+
act_args = (args + [extra]).compact
|
84
|
+
log_msg, logger = parse_args(*act_args)
|
85
|
+
logger.send(meth, log_msg)
|
86
|
+
end
|
87
|
+
module_function(meth)
|
88
|
+
end
|
89
|
+
|
90
|
+
end # module Tools
|
91
|
+
|
92
|
+
# Logs a specific message with a given severity.
|
93
|
+
#
|
94
|
+
# Severity can be :debug, :info, :warn, :error or :fatal.
|
95
|
+
# The args must follow module's conventions, see above.
|
96
|
+
def log(severity, *args)
|
97
|
+
Tools.send(severity, args)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Calls the block and monitors then log its execution time.
|
101
|
+
#
|
102
|
+
# The args must follow module's conventions, see above.
|
103
|
+
def monitor(*args, &bl)
|
104
|
+
result = nil
|
105
|
+
took = Benchmark.realtime {
|
106
|
+
result = bl.call
|
107
|
+
}
|
108
|
+
Tools.info(args, op_took: took)
|
109
|
+
result
|
110
|
+
end
|
111
|
+
|
112
|
+
# Executes the block without letting errors propagate.
|
113
|
+
# Errors are logged, though. Nothing is logged if everything
|
114
|
+
# goes fine.
|
115
|
+
#
|
116
|
+
# The args must follow module's conventions, see above.
|
117
|
+
def stop_errors(*args, &bl)
|
118
|
+
result = nil
|
119
|
+
took = Benchmark.realtime {
|
120
|
+
result = bl.call
|
121
|
+
}
|
122
|
+
result
|
123
|
+
rescue => ex
|
124
|
+
Tools.fatal(args, op_took: took, error: ex)
|
125
|
+
nil
|
126
|
+
end
|
127
|
+
|
128
|
+
# Tries executing the block up to `n` times, until an attempt
|
129
|
+
# succeeds (then returning the result). Logs the first and last
|
130
|
+
# fatal error, if any.
|
131
|
+
#
|
132
|
+
# The args must follow module's conventions, see above.
|
133
|
+
def try_max_times(n, *args, &bl)
|
134
|
+
retried = 0
|
135
|
+
took = 0
|
136
|
+
begin
|
137
|
+
result = nil
|
138
|
+
took += Benchmark.realtime {
|
139
|
+
result = bl.call
|
140
|
+
}
|
141
|
+
result
|
142
|
+
rescue => ex
|
143
|
+
Tools.error(args + [{op_took: took, error: ex}]) if retried == 0
|
144
|
+
retried += 1
|
145
|
+
if retried < n
|
146
|
+
sleep(retried)
|
147
|
+
retry
|
148
|
+
else
|
149
|
+
Tools.fatal(args + [{op_took: took, error: ex}])
|
150
|
+
raise
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
end # module Robustness
|
156
|
+
end # module Support
|
157
|
+
end # module Startback
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Startback
|
2
|
+
module Support
|
3
|
+
class TransactionManager
|
4
|
+
|
5
|
+
def initialize(db, method = :transaction)
|
6
|
+
@db = db
|
7
|
+
@method = method
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(runner, op, &then_block)
|
11
|
+
raise ArgumentError, "A block is required" unless then_block
|
12
|
+
|
13
|
+
before = (op.class.transaction_policy == :before_call)
|
14
|
+
if before
|
15
|
+
@db.send(@method) do
|
16
|
+
then_block.call
|
17
|
+
end
|
18
|
+
else
|
19
|
+
then_block.call
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end # class TransactionManager
|
24
|
+
end # module Support
|
25
|
+
end # module Startback
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Startback
|
2
|
+
module Support
|
3
|
+
module TransactionPolicy
|
4
|
+
|
5
|
+
# Returns the operation's transaction policy
|
6
|
+
def transaction_policy
|
7
|
+
@transaction_policy || :before_call
|
8
|
+
end
|
9
|
+
|
10
|
+
# Sets the transaction policy to use. Valid values are:
|
11
|
+
# - before_call : the transaction is started by the operation
|
12
|
+
# runner, right before calling the #call method on operation
|
13
|
+
# instance
|
14
|
+
# - within_call: the transaction is started by the operation
|
15
|
+
# itself, as part of its internal logic.
|
16
|
+
def transaction_policy=(policy)
|
17
|
+
unless [:before_call, :within_call].include?(policy)
|
18
|
+
raise ArgumentError, "Unknown policy `#{policy}`"
|
19
|
+
end
|
20
|
+
@transaction_policy = policy
|
21
|
+
end
|
22
|
+
|
23
|
+
def after_commit(&bl)
|
24
|
+
after_call do
|
25
|
+
db.after_commit do
|
26
|
+
instance_exec(&bl)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end # module TransactionPolicy
|
32
|
+
end # module Support
|
33
|
+
end # module Startback
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Startback
|
2
|
+
module Support
|
3
|
+
class World
|
4
|
+
include DataObject
|
5
|
+
|
6
|
+
attr_accessor :_factory
|
7
|
+
protected :_factory=
|
8
|
+
|
9
|
+
def factory(who, &block)
|
10
|
+
dup.tap do |x|
|
11
|
+
x._factory = (self._factory || {}).merge(who => block)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_accessor :_scope
|
16
|
+
protected :_scope=
|
17
|
+
|
18
|
+
def with_scope(scope)
|
19
|
+
dup.tap do |x|
|
20
|
+
x._scope = scope
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def with(hash)
|
25
|
+
dup.tap do |x|
|
26
|
+
x._data = to_data.merge(hash)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def _data_allow_camelize
|
33
|
+
false
|
34
|
+
end
|
35
|
+
|
36
|
+
def _data_allow_query
|
37
|
+
false
|
38
|
+
end
|
39
|
+
|
40
|
+
def _data_key_not_found(key)
|
41
|
+
raise Startback::Error, "Scope must be defined" unless s = _scope
|
42
|
+
|
43
|
+
block = (_factory || {})[key]
|
44
|
+
if block
|
45
|
+
factored = s.instance_exec(&block)
|
46
|
+
@_data = @_data.dup.merge(key => factored).freeze
|
47
|
+
[key, false]
|
48
|
+
else
|
49
|
+
nil
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end # class World
|
53
|
+
end # module Support
|
54
|
+
end # module Startback
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Startback
|
2
|
+
module Support
|
3
|
+
|
4
|
+
def logger
|
5
|
+
Startback::LOGGER
|
6
|
+
end
|
7
|
+
|
8
|
+
def deep_merge(h1, h2)
|
9
|
+
h1.merge(h2){|k,v1,v2|
|
10
|
+
v1.is_a?(Hash) && v2.is_a?(Hash) ? deep_merge(v1, v2) : v2
|
11
|
+
}
|
12
|
+
end
|
13
|
+
module_function :deep_merge
|
14
|
+
|
15
|
+
end # module Support
|
16
|
+
end # module Startback
|
17
|
+
require_relative 'support/env'
|
18
|
+
require_relative 'support/log_formatter'
|
19
|
+
require_relative 'support/logger'
|
20
|
+
require_relative 'support/robustness'
|
21
|
+
require_relative 'support/hooks'
|
22
|
+
require_relative 'support/operation_runner'
|
23
|
+
require_relative 'support/transaction_policy'
|
24
|
+
require_relative 'support/transaction_manager'
|
25
|
+
require_relative 'support/data_object'
|
26
|
+
require_relative 'support/world'
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Startback
|
2
|
+
module Web
|
3
|
+
class Api < Sinatra::Base
|
4
|
+
include Support
|
5
|
+
include Errors
|
6
|
+
|
7
|
+
set :raise_errors, true
|
8
|
+
set :show_exceptions, false
|
9
|
+
set :dump_errors, false
|
10
|
+
|
11
|
+
protected
|
12
|
+
|
13
|
+
###
|
14
|
+
### Facade over context
|
15
|
+
###
|
16
|
+
|
17
|
+
def context
|
18
|
+
env[Startback::Context::Middleware::RACK_ENV_KEY]
|
19
|
+
end
|
20
|
+
|
21
|
+
def with_context(ctx = nil)
|
22
|
+
old_context = self.context
|
23
|
+
new_context = ctx || self.context.dup
|
24
|
+
env[Startback::Context::Middleware::RACK_ENV_KEY] = new_context
|
25
|
+
result = ctx ? yield : yield(new_context)
|
26
|
+
env[Startback::Context::Middleware::RACK_ENV_KEY] = old_context
|
27
|
+
result
|
28
|
+
end
|
29
|
+
|
30
|
+
###
|
31
|
+
### Facade over third party tools
|
32
|
+
###
|
33
|
+
include Support::OperationRunner
|
34
|
+
|
35
|
+
def operation_world(op)
|
36
|
+
{ context: context }
|
37
|
+
end
|
38
|
+
|
39
|
+
###
|
40
|
+
### About the body / input
|
41
|
+
###
|
42
|
+
|
43
|
+
def loaded_body
|
44
|
+
@loaded_body ||= case ctype = request.content_type
|
45
|
+
when /json/
|
46
|
+
json_body
|
47
|
+
when /multipart\/form-data/
|
48
|
+
file = params[:file]
|
49
|
+
file_body file, Path(file[:filename]).extname
|
50
|
+
else
|
51
|
+
unsupported_media_type_error!(ctype)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def json_body(body = request.body.read)
|
56
|
+
JSON.load(body)
|
57
|
+
end
|
58
|
+
|
59
|
+
def file_body(file, ctype)
|
60
|
+
raise UnsupportedMediaTypeError, "Unable to use `#{ctype}` as input data"
|
61
|
+
end
|
62
|
+
|
63
|
+
###
|
64
|
+
### Various reusable responses
|
65
|
+
###
|
66
|
+
|
67
|
+
def serve_nothing
|
68
|
+
[ 204, {}, [] ]
|
69
|
+
end
|
70
|
+
|
71
|
+
def serve(entity_description, entity, ct = nil)
|
72
|
+
if entity.nil?
|
73
|
+
status 404
|
74
|
+
content_type :json
|
75
|
+
{ description: "#{entity_description} not found" }.to_json
|
76
|
+
elsif entity.respond_to?(:to_dto)
|
77
|
+
ct, body = entity.to_dto(context).to(env['HTTP_ACCEPT'], ct)
|
78
|
+
content_type ct
|
79
|
+
_serve(body)
|
80
|
+
elsif entity.is_a?(Path)
|
81
|
+
_serve(entity)
|
82
|
+
else
|
83
|
+
content_type ct || "application/json"
|
84
|
+
entity.to_json
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def _serve(body)
|
89
|
+
case body
|
90
|
+
when Path
|
91
|
+
send_file(body)
|
92
|
+
else
|
93
|
+
body
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
end # class Api
|
98
|
+
end # module Web
|
99
|
+
end # module Startback
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module Startback
|
2
|
+
module Web
|
3
|
+
#
|
4
|
+
# This rack middleware automatically mark response as non being cacheable
|
5
|
+
# in development, and being cacheble in production.
|
6
|
+
#
|
7
|
+
# The headers to set in development and production can be passed at
|
8
|
+
# construction, as well as whether the development environment must be
|
9
|
+
# forced. This class may also be configured through environment variables:
|
10
|
+
#
|
11
|
+
# - RACK_ENV: when "production" use the production headers, otherwise use
|
12
|
+
# the development ones
|
13
|
+
# - STARTBACK_AUTOCACHING_DEVELOPMENT_CACHE_CONTROL: Cache-Control header
|
14
|
+
# to use in development mode
|
15
|
+
# - STARTBACK_AUTOCACHING_PRODUCTION_CACHE_CONTROL: Cache-Control header
|
16
|
+
# to use in production mode
|
17
|
+
#
|
18
|
+
# Example:
|
19
|
+
#
|
20
|
+
# # Default configuration
|
21
|
+
# use Autocaching
|
22
|
+
#
|
23
|
+
# # Force development mode
|
24
|
+
# use Autocaching, true
|
25
|
+
#
|
26
|
+
# # Force production mode
|
27
|
+
# use Autocaching, false
|
28
|
+
#
|
29
|
+
# # Set production headers manually
|
30
|
+
# use Autocaching, { :production => "public, no-cache, no-store" }
|
31
|
+
#
|
32
|
+
class AutoCaching
|
33
|
+
|
34
|
+
# Cache-Control header to use in development mode
|
35
|
+
DEVELOPMENT_CACHE_CONTROL = ENV['STARTBACK_AUTOCACHING_DEVELOPMENT_CACHE_CONTROL'] || \
|
36
|
+
"no-cache, no-store, max-age=0, must-revalidate"
|
37
|
+
|
38
|
+
# Cache-Control header to use in produdction mode
|
39
|
+
PRODUCTION_CACHE_CONTROL = ENV['STARTBACK_AUTOCACHING_PRODUCTION_CACHE_CONTROL'] ||\
|
40
|
+
"public, must-revalidate, max-age=3600, s-max-age=3600"
|
41
|
+
|
42
|
+
def initialize(app, development = nil, cache_headers = {})
|
43
|
+
development, cache_headers = nil, development if development.is_a?(Hash)
|
44
|
+
@app = app
|
45
|
+
@development = development.nil? ? infer_is_development : development
|
46
|
+
@cache_headers = default_headers.merge(normalize_headers(cache_headers))
|
47
|
+
end
|
48
|
+
|
49
|
+
def call(env)
|
50
|
+
status, headers, body = @app.call(env)
|
51
|
+
[status, patch_response_headers(headers), body]
|
52
|
+
end
|
53
|
+
|
54
|
+
protected
|
55
|
+
|
56
|
+
def patch_response_headers(hs)
|
57
|
+
(development? ? @cache_headers[:development] : @cache_headers[:production]).merge(hs)
|
58
|
+
end
|
59
|
+
|
60
|
+
def development?
|
61
|
+
!!@development
|
62
|
+
end
|
63
|
+
|
64
|
+
def infer_is_development
|
65
|
+
ENV['RACK_ENV'] != "production"
|
66
|
+
end
|
67
|
+
|
68
|
+
def default_headers
|
69
|
+
{
|
70
|
+
development: {
|
71
|
+
"Cache-Control" => DEVELOPMENT_CACHE_CONTROL
|
72
|
+
},
|
73
|
+
production: {
|
74
|
+
"Cache-Control" => PRODUCTION_CACHE_CONTROL
|
75
|
+
}
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
def normalize_headers(h)
|
80
|
+
Hash[h.map{|k,v| [k, v.is_a?(Hash) ? v : {"Cache-Control" => v} ] }]
|
81
|
+
end
|
82
|
+
|
83
|
+
end # class AutoCaching
|
84
|
+
end # module Web
|
85
|
+
end # module Startback
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Startback
|
2
|
+
module Web
|
3
|
+
#
|
4
|
+
# This Rack middleware catches all exceptions that are raised by sublayers
|
5
|
+
# in the Rack chain. It converts them to correct 500 Errors, with a generic
|
6
|
+
# exception message encoded in json.
|
7
|
+
#
|
8
|
+
# This class aims at being used as top level of a Rack chain. It is not
|
9
|
+
# aimed at being subclassed.
|
10
|
+
#
|
11
|
+
# Fatal error cached are also sent as a `fatal` messange, on the error
|
12
|
+
# handler provided on Context#error_handler.fatal, if any.
|
13
|
+
#
|
14
|
+
# Examples:
|
15
|
+
#
|
16
|
+
# Rack::Builder.new do
|
17
|
+
# use Startback::Web::CatchAll
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
class CatchAll < Rack::Robustness
|
21
|
+
include Errors
|
22
|
+
include Support::Robustness
|
23
|
+
|
24
|
+
FATAL_ERROR = {
|
25
|
+
code: "Startback::Errors::InternalServerError",
|
26
|
+
description: "An error occured, sorry"
|
27
|
+
}.to_json
|
28
|
+
|
29
|
+
self.catch_all
|
30
|
+
self.on(Exception)
|
31
|
+
self.status 500
|
32
|
+
self.content_type 'application/json'
|
33
|
+
self.body FATAL_ERROR
|
34
|
+
|
35
|
+
self.ensure(true) do |ex|
|
36
|
+
context = env[Context::Middleware::RACK_ENV_KEY]
|
37
|
+
begin
|
38
|
+
if context && context.respond_to?(:error_handler, true) && context.error_handler
|
39
|
+
context.error_handler.fatal(ex)
|
40
|
+
else
|
41
|
+
log(:fatal, self, "ensure", context, error: ex)
|
42
|
+
end
|
43
|
+
rescue => ex2
|
44
|
+
STDERR.puts(ex2.message)
|
45
|
+
STDERR.puts(ex2.backtrace[0..10].join("\n"))
|
46
|
+
raise
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end # class CatchAll
|
51
|
+
end # class Web
|
52
|
+
end # module Startback
|