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,80 @@
|
|
1
|
+
module Startback
|
2
|
+
module Web
|
3
|
+
#
|
4
|
+
# Sets Cross-Origin Response Headers on requests specifying an Origin
|
5
|
+
# HTTP header, according configuration passed at construction and/or
|
6
|
+
# environment variables.
|
7
|
+
#
|
8
|
+
# Example:
|
9
|
+
#
|
10
|
+
# # Default configuration, using environment variables when set
|
11
|
+
# use CorsHeaders
|
12
|
+
#
|
13
|
+
# # Force a bouncing of the origin, using the Origin request header
|
14
|
+
# # as Access-Control-Allow-Origin response header
|
15
|
+
# use CorsHeaders, bounce: true
|
16
|
+
#
|
17
|
+
# # Overrides a specific header
|
18
|
+
# use CorsHeaders, headers: { 'Access-Control-Allow-Methods' => 'POST' }
|
19
|
+
#
|
20
|
+
class CorsHeaders
|
21
|
+
|
22
|
+
ALLOW_ORIGIN = ENV['STARTBACK_CORS_ALLOW_ORIGIN'] || '*'
|
23
|
+
|
24
|
+
ALLOW_METHODS = ENV['STARTBACK_CORS_ALLOW_METHODS'] || 'OPTIONS, HEAD, GET, POST, PUT, PATCH, DELETE'
|
25
|
+
|
26
|
+
ALLOW_CREDENTIALS = ENV['STARTBACK_CORS_ALLOW_CREDENTIALS'] || 'true'
|
27
|
+
|
28
|
+
MAX_AGE = ENV['STARTBACK_CORS_MAX_AGE'] || '1728000'
|
29
|
+
|
30
|
+
ALLOW_HEADERS = ENV['STARTBACK_CORS_ALLOW_HEADERS'] || 'Authorization, Content-Type, Origin, Accept, If-Modified-Since, If-Match, If-None-Match'
|
31
|
+
|
32
|
+
EXPOSE_HEADERS = ENV['STARTBACK_CORS_EXPOSE_HEADERS'] || 'Location, ETag, Last-Modified, Content-Type'
|
33
|
+
|
34
|
+
DEFAULT_CORS_HEADERS = {
|
35
|
+
'Access-Control-Allow-Origin' => ALLOW_ORIGIN,
|
36
|
+
'Access-Control-Allow-Methods' => ALLOW_METHODS,
|
37
|
+
'Access-Control-Allow-Credentials' => ALLOW_CREDENTIALS,
|
38
|
+
'Access-Control-Max-Age' => MAX_AGE,
|
39
|
+
'Access-Control-Allow-Headers' => ALLOW_HEADERS,
|
40
|
+
'Access-Control-Expose-Headers' => EXPOSE_HEADERS
|
41
|
+
}
|
42
|
+
|
43
|
+
DEFAULT_OPTIONS = {
|
44
|
+
:headers => DEFAULT_CORS_HEADERS
|
45
|
+
}
|
46
|
+
|
47
|
+
def initialize(app, options = {})
|
48
|
+
@app = app
|
49
|
+
@options = Startback::Support.deep_merge(DEFAULT_OPTIONS, options)
|
50
|
+
end
|
51
|
+
|
52
|
+
def call(env)
|
53
|
+
status, headers, body = @app.call(env)
|
54
|
+
if origin = env['HTTP_ORIGIN']
|
55
|
+
headers = cors_headers(origin).merge(headers)
|
56
|
+
end
|
57
|
+
if env['REQUEST_METHOD'] == 'OPTIONS'
|
58
|
+
headers['Content-Length'] = '0'
|
59
|
+
status, headers, body = [204, headers, []]
|
60
|
+
end
|
61
|
+
[status, headers, body]
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def cors_headers(origin)
|
67
|
+
headers = @options[:headers].dup
|
68
|
+
if bounce?
|
69
|
+
headers['Access-Control-Allow-Origin'] = origin
|
70
|
+
end
|
71
|
+
headers
|
72
|
+
end
|
73
|
+
|
74
|
+
def bounce?
|
75
|
+
@options[:bounce]
|
76
|
+
end
|
77
|
+
|
78
|
+
end # class AllowCors
|
79
|
+
end # class CorsHeaders
|
80
|
+
end # module Samback
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Startback
|
2
|
+
module Web
|
3
|
+
#
|
4
|
+
# Can be used to easily implement a HealthCheck web service inside a Startback
|
5
|
+
# application.
|
6
|
+
#
|
7
|
+
# Examples:
|
8
|
+
#
|
9
|
+
# # Returns a 204 with no body
|
10
|
+
# run Startback::Web::HealthCheck.new
|
11
|
+
#
|
12
|
+
# # Returns a 204 with no body
|
13
|
+
# run Startback::Web::HealthCheck.new { nil }
|
14
|
+
#
|
15
|
+
# # Returns a 200 with Ok in plain text
|
16
|
+
# run Startback::Web::HealthCheck.new { "Ok" }
|
17
|
+
#
|
18
|
+
# # Re-raises the exception
|
19
|
+
# run Startback::Web::HealthCheck.new { raise "Something bad" }
|
20
|
+
#
|
21
|
+
# Please note that this rack app is not 100% Rack compliant, since it raises
|
22
|
+
# any error that the block itself raises. This class aims at being backed up
|
23
|
+
# by a Shield and/or CatchAll middleware.
|
24
|
+
#
|
25
|
+
# This class is not aimed at being subclassed.
|
26
|
+
#
|
27
|
+
class HealthCheck
|
28
|
+
|
29
|
+
def initialize(&bl)
|
30
|
+
@checker = bl
|
31
|
+
end
|
32
|
+
|
33
|
+
def call(env)
|
34
|
+
if debug_msg = check!(env)
|
35
|
+
[ 200, { "Content-Type" => "text/plain" }, Array(debug_msg) ]
|
36
|
+
else
|
37
|
+
[ 204, {}, [] ]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
protected
|
42
|
+
|
43
|
+
def check!(env)
|
44
|
+
@checker.call if @checker
|
45
|
+
end
|
46
|
+
|
47
|
+
end # class HealthCheck
|
48
|
+
end # module Web
|
49
|
+
end # module Startback
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Startback
|
2
|
+
module Web
|
3
|
+
class MagicAssets
|
4
|
+
#
|
5
|
+
# Plugin for MagicAssets that compiles .html angular templates in the
|
6
|
+
# assets structure to javascript files filling angular's template cache.
|
7
|
+
#
|
8
|
+
# Heavily inspired, yet over-simplified version, of angular-rails-templates
|
9
|
+
# See https://github.com/pitr/angular-rails-templates, licensed under MIT
|
10
|
+
#
|
11
|
+
# Example:
|
12
|
+
#
|
13
|
+
# use Startback::Web::MagicAssets, {
|
14
|
+
# plugins: [Startback::Web::MagicAssets::NgHtmlTransfomer.new]
|
15
|
+
# }
|
16
|
+
#
|
17
|
+
class NgHtmlTransformer
|
18
|
+
|
19
|
+
DEFAULT_OPTIONS = {
|
20
|
+
:path => '/assets',
|
21
|
+
:ng_module => 'templates',
|
22
|
+
:mime_type => 'text/ng-html',
|
23
|
+
:extensions => [".html"]
|
24
|
+
}
|
25
|
+
|
26
|
+
def initialize(options = {})
|
27
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
28
|
+
end
|
29
|
+
attr_reader :options
|
30
|
+
|
31
|
+
def install(sprockets)
|
32
|
+
sprockets.register_mime_type options[:mime_type], extensions: options[:extensions]
|
33
|
+
sprockets.register_transformer options[:mime_type], 'application/javascript', self
|
34
|
+
end
|
35
|
+
|
36
|
+
TPL = <<-EOF
|
37
|
+
angular.module("<%= ng_module %>").run(["$templateCache", function($templateCache) {
|
38
|
+
$templateCache.put("<%= angular_template_name %>", <%= html %>)
|
39
|
+
}]);
|
40
|
+
EOF
|
41
|
+
|
42
|
+
# inspired by Rails' action_view/helpers/javascript_helper.rb
|
43
|
+
JS_ESCAPE_MAP = {
|
44
|
+
'\\' => '\\\\',
|
45
|
+
"\r\n" => '\n',
|
46
|
+
"\n" => '\n',
|
47
|
+
"\r" => '\n',
|
48
|
+
'"' => '\\"',
|
49
|
+
"'" => "\\'"
|
50
|
+
}
|
51
|
+
|
52
|
+
# We want to deliver the shortist valid javascript escaped string
|
53
|
+
# Count the number of " vs '
|
54
|
+
# If more ', escape "
|
55
|
+
# If more ", escape '
|
56
|
+
# If equal, prefer to escape "
|
57
|
+
|
58
|
+
def escape_javascript(raw)
|
59
|
+
if raw
|
60
|
+
quote = raw.count(%{'}) >= raw.count(%{"}) ? %{"} : %{'}
|
61
|
+
escaped = raw.gsub(/(\\|\r\n|[\n\r#{quote}])/u) {|match| JS_ESCAPE_MAP[match] }
|
62
|
+
"#{quote}#{escaped}#{quote}"
|
63
|
+
else
|
64
|
+
'""'
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def call(input)
|
69
|
+
file_path = input[:filename]
|
70
|
+
angular_template_name = "#{options[:path]}/#{input[:name]}.html"
|
71
|
+
source_file = file_path
|
72
|
+
ng_module = options[:ng_module]
|
73
|
+
html = escape_javascript(input[:data].chomp)
|
74
|
+
ERB.new(TPL).result(binding)
|
75
|
+
end
|
76
|
+
|
77
|
+
end # class NgHtmlTransformer
|
78
|
+
end # class MagicAssets
|
79
|
+
end # module Web
|
80
|
+
end # module Startback
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Startback
|
2
|
+
module Web
|
3
|
+
class MagicAssets
|
4
|
+
class RakeTasks
|
5
|
+
|
6
|
+
DEFAULT_OPTIONS = {
|
7
|
+
:namespace => :assets
|
8
|
+
}
|
9
|
+
|
10
|
+
def initialize(rake, options)
|
11
|
+
@rake = rake
|
12
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
13
|
+
install
|
14
|
+
end
|
15
|
+
attr_reader :rake, :options
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def install
|
20
|
+
require 'securerandom'
|
21
|
+
|
22
|
+
ns = options[:namespace]
|
23
|
+
target_folder = options[:target]
|
24
|
+
assets = options[:assets]
|
25
|
+
assets = MagicAssets.new(assets) if assets.is_a?(Hash)
|
26
|
+
version = SecureRandom.urlsafe_base64
|
27
|
+
|
28
|
+
rake.instance_exec do
|
29
|
+
namespace(ns) do
|
30
|
+
|
31
|
+
desc 'Cleans generated assets'
|
32
|
+
task :clean do
|
33
|
+
FileUtils.rm_rf target_folder
|
34
|
+
end
|
35
|
+
|
36
|
+
task :prepare do
|
37
|
+
FileUtils.mkdir_p target_folder
|
38
|
+
(target_folder/"VERSION").write(version)
|
39
|
+
end
|
40
|
+
|
41
|
+
desc 'compile javascript assets'
|
42
|
+
task :compile_js do
|
43
|
+
assets['vendor.js'].write_to(target_folder/"vendor-#{version}.min.js")
|
44
|
+
assets['app.js'].write_to(target_folder/"app-#{version}.min.js")
|
45
|
+
puts "successfully compiled js assets"
|
46
|
+
end
|
47
|
+
|
48
|
+
desc 'compile css assets'
|
49
|
+
task :compile_css do
|
50
|
+
assets['vendor.css'].write_to(target_folder/"vendor-#{version}.min.css")
|
51
|
+
assets['app.css'].write_to(target_folder/"app-#{version}.min.css")
|
52
|
+
puts "successfully compiled css assets"
|
53
|
+
end
|
54
|
+
|
55
|
+
desc 'compile assets'
|
56
|
+
task :compile => [:clean, :prepare, :compile_js, :compile_css]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end # class MagicAssets
|
63
|
+
end # module Web
|
64
|
+
end # module Startback
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'sprockets'
|
2
|
+
module Startback
|
3
|
+
module Web
|
4
|
+
#
|
5
|
+
# Rack application & middleware that can be used to simplify javascript
|
6
|
+
# and css assets management, using Sprockets.
|
7
|
+
#
|
8
|
+
# Example:
|
9
|
+
#
|
10
|
+
# # Used as rack app, typically under a path
|
11
|
+
# Rack::Builder.new do
|
12
|
+
# map '/assets' do
|
13
|
+
# run Startback::Web::MagicAssets.new({
|
14
|
+
# folder: "/path/to/assets/src"
|
15
|
+
# })
|
16
|
+
# end
|
17
|
+
# run MyApp
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# # Used as a rack middleware, e.g. in a Sinatra application
|
21
|
+
# use Startback::Web::MagicAssets, {
|
22
|
+
# folder: "/path/to/assets/src",
|
23
|
+
# path: "/assets"
|
24
|
+
# }
|
25
|
+
#
|
26
|
+
# Sprocket configuration can be done through the `:sprocket` option:
|
27
|
+
#
|
28
|
+
# use Startback::Web::MagicAssets, {
|
29
|
+
# sprockets: {
|
30
|
+
# :css_compressor => :scss
|
31
|
+
# }
|
32
|
+
# }
|
33
|
+
#
|
34
|
+
class MagicAssets
|
35
|
+
|
36
|
+
DEFAULT_OPTIONS = {
|
37
|
+
sprockets: {},
|
38
|
+
plugins: {}
|
39
|
+
}
|
40
|
+
|
41
|
+
def initialize(app, options = {})
|
42
|
+
app, options = nil, app if app.is_a?(Hash)
|
43
|
+
@app = app
|
44
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
45
|
+
@sprockets = build_sprockets
|
46
|
+
end
|
47
|
+
attr_reader :sprockets
|
48
|
+
|
49
|
+
def call(env)
|
50
|
+
if new_env = is_match?(env)
|
51
|
+
@sprockets.call(new_env)
|
52
|
+
else
|
53
|
+
@app.call(env)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def [](*args, &bl)
|
58
|
+
@sprockets.[](*args, &bl)
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def path
|
64
|
+
@options[:path]
|
65
|
+
end
|
66
|
+
|
67
|
+
def is_match?(env)
|
68
|
+
if @app.nil?
|
69
|
+
# Not used as a middleware, use this env and match
|
70
|
+
env
|
71
|
+
elsif env['PATH_INFO'].start_with?(path)
|
72
|
+
# Used as a middleware, and PATH_INFO starts with the
|
73
|
+
# assets path => strip it for sprockets
|
74
|
+
env.merge("PATH_INFO" => env["PATH_INFO"].sub(path, ""))
|
75
|
+
else
|
76
|
+
# No match, let @app execute with the untouched environment
|
77
|
+
nil
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def build_sprockets
|
82
|
+
Sprockets::Environment.new.tap{|s|
|
83
|
+
Array(@options[:folder]).each do |folder|
|
84
|
+
s.append_path(folder)
|
85
|
+
end
|
86
|
+
@options[:sprockets].each_pair do |k,v|
|
87
|
+
s.public_send(:"#{k}=", v)
|
88
|
+
end
|
89
|
+
@options[:plugins].each do |p|
|
90
|
+
p.install(s)
|
91
|
+
end
|
92
|
+
}
|
93
|
+
end
|
94
|
+
|
95
|
+
end # class MagicAssets
|
96
|
+
end # module Web
|
97
|
+
end # module Startback
|
98
|
+
require_relative 'magic_assets/rake_tasks'
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Startback
|
2
|
+
module Web
|
3
|
+
module Middleware
|
4
|
+
|
5
|
+
protected
|
6
|
+
|
7
|
+
def context(env = @env)
|
8
|
+
::Startback::Context::Middleware.context(env) || Errors.server_error!("Unable to find context!!")
|
9
|
+
end
|
10
|
+
|
11
|
+
end # module Middleware
|
12
|
+
end # module Web
|
13
|
+
end # module Startback
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'prometheus/middleware/exporter'
|
2
|
+
module Startback
|
3
|
+
module Web
|
4
|
+
#
|
5
|
+
# Can be used to expose the prometheus metrics inside a Startback
|
6
|
+
# application.
|
7
|
+
#
|
8
|
+
# Example:
|
9
|
+
#
|
10
|
+
# use Startback::Web::Prometheus
|
11
|
+
#
|
12
|
+
class Prometheus < Prometheus::Middleware::Exporter
|
13
|
+
|
14
|
+
end # class Prometheus
|
15
|
+
end # module Web
|
16
|
+
end # module Startback
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Startback
|
2
|
+
module Web
|
3
|
+
#
|
4
|
+
# This Rack middleware catches all known exceptions raised by sublayers
|
5
|
+
# in the Rack chain. Those exceptions are converted to proper HTTP error
|
6
|
+
# codes and friendly error messages encoded in json.
|
7
|
+
#
|
8
|
+
# Please check the Errors module about status codes used for each Startback
|
9
|
+
# error.
|
10
|
+
#
|
11
|
+
# This class aims at being used as top level of a Rack chain.
|
12
|
+
#
|
13
|
+
# Examples:
|
14
|
+
#
|
15
|
+
# Rack::Builder.new do
|
16
|
+
# use Startback::Web::Shield
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
class Shield < Rack::Robustness
|
20
|
+
include Errors
|
21
|
+
|
22
|
+
self.no_catch_all
|
23
|
+
self.content_type 'application/json'
|
24
|
+
|
25
|
+
# Decoding errors from json and csv are considered user's fault
|
26
|
+
self.on(Finitio::TypeError){ 400 }
|
27
|
+
self.on(::NotImplementedError){ 501 }
|
28
|
+
|
29
|
+
# Various other codes for the framework specific error classes
|
30
|
+
self.on(Startback::Errors::Error) {|ex|
|
31
|
+
ex.class.status
|
32
|
+
}
|
33
|
+
|
34
|
+
# A bit of logic to choose the best error message for the user
|
35
|
+
# according to the error class
|
36
|
+
self.body{|ex|
|
37
|
+
body_for(ex).to_json
|
38
|
+
}
|
39
|
+
|
40
|
+
def body_for(ex)
|
41
|
+
ex = ex.root_cause if ex.is_a?(Finitio::TypeError)
|
42
|
+
body = { code: ex.class.name, description: ex.message }
|
43
|
+
return body unless ex.is_a?(Startback::Errors::Error)
|
44
|
+
return body unless ex.has_causes?
|
45
|
+
|
46
|
+
body[:causes] = ex.causes
|
47
|
+
.filter{|cause|
|
48
|
+
cause.is_a?(Startback::Errors::Error)
|
49
|
+
}
|
50
|
+
.map{|cause|
|
51
|
+
body_for(cause)
|
52
|
+
}
|
53
|
+
body
|
54
|
+
end
|
55
|
+
|
56
|
+
end # class Shield
|
57
|
+
end # module Web
|
58
|
+
end # module Startback
|
data/lib/startback.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'sinatra'
|
2
|
+
require 'rack/robustness'
|
3
|
+
require 'finitio'
|
4
|
+
require 'logger'
|
5
|
+
require 'path'
|
6
|
+
require 'ostruct'
|
7
|
+
require 'benchmark'
|
8
|
+
|
9
|
+
# Provides a reusable backend framework for backend components written
|
10
|
+
# in ruby.
|
11
|
+
#
|
12
|
+
# The framework installs conventions regarding:
|
13
|
+
#
|
14
|
+
# - The exposition of web service APIs (Framework::Api, on top of Sinatra)
|
15
|
+
# - Operations (Framework::Operation)
|
16
|
+
# - Error handling (Framework::Errors) and their handling in web APIs
|
17
|
+
# (based on Rack::Robustness)
|
18
|
+
# - General code support (Framework::Support modules & classes).
|
19
|
+
#
|
20
|
+
# Please refer to the documentation of those main abstractions for details.
|
21
|
+
#
|
22
|
+
module Startback
|
23
|
+
|
24
|
+
# Simply checks that a path exists of raise an error
|
25
|
+
def self._!(path)
|
26
|
+
Path(path).tap do |p|
|
27
|
+
raise "Missing #{p.basename}." unless p.exists?
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
require_relative 'startback/version'
|
32
|
+
require_relative 'startback/ext'
|
33
|
+
require_relative 'startback/errors'
|
34
|
+
require_relative 'startback/support'
|
35
|
+
require_relative 'startback/model'
|
36
|
+
require_relative 'startback/context'
|
37
|
+
require_relative 'startback/operation'
|
38
|
+
require_relative 'startback/services'
|
39
|
+
|
40
|
+
# Logger instance to use for the application
|
41
|
+
LOGGER = ::Startback::Support::Logger.new
|
42
|
+
|
43
|
+
end # module Startback
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'startback'
|
2
|
+
require 'startback/event'
|
3
|
+
require 'startback/support/fake_logger'
|
4
|
+
require 'rack/test'
|
5
|
+
require 'ostruct'
|
6
|
+
|
7
|
+
module SpecHelpers
|
8
|
+
end
|
9
|
+
|
10
|
+
RSpec.configure do |c|
|
11
|
+
c.include SpecHelpers
|
12
|
+
end
|
13
|
+
|
14
|
+
class SubContext < Startback::Context
|
15
|
+
|
16
|
+
attr_accessor :foo
|
17
|
+
|
18
|
+
h_factory do |c,h|
|
19
|
+
c.foo = h["foo"]
|
20
|
+
end
|
21
|
+
|
22
|
+
h_dump do |h|
|
23
|
+
h.merge!("foo" => foo)
|
24
|
+
end
|
25
|
+
|
26
|
+
world(:partner) do
|
27
|
+
Object.new
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
class SubContext
|
33
|
+
|
34
|
+
attr_accessor :bar
|
35
|
+
|
36
|
+
h_factory do |c,h|
|
37
|
+
c.bar = h["bar"]
|
38
|
+
end
|
39
|
+
|
40
|
+
h_dump do |h|
|
41
|
+
h.merge!("bar" => bar)
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
class User
|
47
|
+
class Changed < Startback::Event
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'startback/audit'
|
3
|
+
module Startback
|
4
|
+
module Audit
|
5
|
+
describe Prometheus do
|
6
|
+
|
7
|
+
EXPORTER = Prometheus.new({
|
8
|
+
prefix: "hello",
|
9
|
+
labels: {
|
10
|
+
app_version: "1.0"
|
11
|
+
}
|
12
|
+
})
|
13
|
+
|
14
|
+
class Runner
|
15
|
+
include Startback::Support::OperationRunner
|
16
|
+
|
17
|
+
class IdealOp < Startback::Operation
|
18
|
+
def call
|
19
|
+
42
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class ExceptionalOp < Startback::Operation
|
24
|
+
def call
|
25
|
+
raise "Oops"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
around_run(EXPORTER)
|
30
|
+
def test
|
31
|
+
run IdealOp.new
|
32
|
+
end
|
33
|
+
def test_exp
|
34
|
+
run ExceptionalOp.new
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe 'The ideal case' do
|
39
|
+
before do
|
40
|
+
expect(EXPORTER.calls).to receive(:observe).with(
|
41
|
+
kind_of(Numeric),
|
42
|
+
hash_including(labels: {
|
43
|
+
operation: "Startback::Audit::Runner::IdealOp",
|
44
|
+
startback_version: Startback::VERSION,
|
45
|
+
app_version: "1.0"
|
46
|
+
}))
|
47
|
+
expect(EXPORTER.errors).not_to receive(:increment)
|
48
|
+
end
|
49
|
+
it 'runs the operation' do
|
50
|
+
expect(Runner.new.test).to eql(42)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe 'The exceptional case' do
|
55
|
+
before do
|
56
|
+
expect(EXPORTER.errors).to receive(:increment).with(
|
57
|
+
hash_including(labels: {
|
58
|
+
operation: "Startback::Audit::Runner::ExceptionalOp",
|
59
|
+
startback_version: Startback::VERSION,
|
60
|
+
app_version: "1.0"
|
61
|
+
})
|
62
|
+
)
|
63
|
+
expect(EXPORTER.calls).not_to receive(:observe)
|
64
|
+
end
|
65
|
+
it 'let errors bubble up' do
|
66
|
+
expect { Runner.new.test_exp }.to raise_error(/Oops/)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|