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.
- 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
|