startback 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +2 -0
- data/README.md +13 -0
- data/Rakefile +18 -0
- data/lib/startback.rb +37 -0
- data/lib/startback/context.rb +16 -0
- data/lib/startback/context/middleware.rb +53 -0
- data/lib/startback/errors.rb +91 -0
- data/lib/startback/ext.rb +2 -0
- data/lib/startback/ext/date_time.rb +9 -0
- data/lib/startback/ext/time.rb +9 -0
- data/lib/startback/operation.rb +31 -0
- data/lib/startback/operation/error_operation.rb +19 -0
- data/lib/startback/operation/multi_operation.rb +28 -0
- data/lib/startback/support.rb +10 -0
- data/lib/startback/support/logger.rb +34 -0
- data/lib/startback/version.rb +8 -0
- data/lib/startback/web.rb +4 -0
- data/lib/startback/web/api.rb +74 -0
- data/lib/startback/web/catch_all.rb +36 -0
- data/lib/startback/web/health_check.rb +49 -0
- data/lib/startback/web/shield.rb +40 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/unit/context/test_middleware.rb +47 -0
- data/spec/unit/test_operation.rb +58 -0
- data/spec/unit/test_support.rb +8 -0
- data/spec/unit/web/test_healthcheck.rb +58 -0
- data/tasks/gem.rake +39 -0
- data/tasks/test.rake +17 -0
- metadata +169 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0a7f631f93683bc9a9fe0705fc5e3f7df55fa782
|
4
|
+
data.tar.gz: 6335d0ff1b733355c96116ce19f9094adea45694
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b1bec4146b344fac7d8ef51b130e149fb823e6d10ab5a3050be9165c877585f8dcf38092fa2548c884a8974d97868f0f161d7871908da0117af06fc851e5aaa1
|
7
|
+
data.tar.gz: f7482b4b5446cc1557841302cade907c4b597ff704b88ff8017ebf652a92809cf44d5ca24d9c59d0af63dd68b579eed30afd71b5deebc516b0ae94214f6e2cb2
|
data/Gemfile
ADDED
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
|
data/lib/startback.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'sinatra'
|
2
|
+
require 'rack/robustness'
|
3
|
+
require 'finitio'
|
4
|
+
require 'logger'
|
5
|
+
# Provides a reusable backend framework for backend components written
|
6
|
+
# in ruby.
|
7
|
+
#
|
8
|
+
# The framework installs conventions regarding:
|
9
|
+
#
|
10
|
+
# - The exposition of web service APIs (Framework::Api, on top of Sinatra)
|
11
|
+
# - Operations (Framework::Operation)
|
12
|
+
# - Error handling (Framework::Errors) and their handling in web APIs
|
13
|
+
# (based on Rack::Robustness)
|
14
|
+
# - General code support (Framework::Support modules & classes).
|
15
|
+
#
|
16
|
+
# Please refer to the documentation of those main abstractions for details.
|
17
|
+
#
|
18
|
+
module Startback
|
19
|
+
|
20
|
+
# Simply checks that a path exists of raise an error
|
21
|
+
def self._!(path)
|
22
|
+
Path(path).tap do |p|
|
23
|
+
raise "Missing #{p.basename}." unless p.exists?
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
require_relative 'startback/ext'
|
28
|
+
require_relative 'startback/errors'
|
29
|
+
require_relative 'startback/support'
|
30
|
+
require_relative 'startback/context'
|
31
|
+
require_relative 'startback/operation'
|
32
|
+
require_relative 'startback/web'
|
33
|
+
|
34
|
+
# Logger instance to use for the application
|
35
|
+
LOGGER = ::Startback::Support::Logger.new
|
36
|
+
|
37
|
+
end # module Startback
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Startback
|
2
|
+
#
|
3
|
+
# Defines an execution context for Startback applications.
|
4
|
+
#
|
5
|
+
# This class is aimed at being subclassed for application required
|
6
|
+
# extension.
|
7
|
+
#
|
8
|
+
# In web application, an instance of a context can be set on the Rack
|
9
|
+
# environment, using Context::Middleware.
|
10
|
+
#
|
11
|
+
class Context
|
12
|
+
attr_accessor :original_rack_env
|
13
|
+
|
14
|
+
end # class Context
|
15
|
+
end # module Startback
|
16
|
+
require_relative 'context/middleware'
|
@@ -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, context_class: MyContextClass
|
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
|
+
DEFAULT_OPTIONS = {
|
35
|
+
context_class: Context
|
36
|
+
}
|
37
|
+
|
38
|
+
def initialize(app, options = {})
|
39
|
+
@app = app
|
40
|
+
@options = DEFAULT_OPTIONS.merge(options || {})
|
41
|
+
end
|
42
|
+
attr_reader :options
|
43
|
+
|
44
|
+
def call(env)
|
45
|
+
env[RACK_ENV_KEY] ||= options[:context_class].new.tap{|c|
|
46
|
+
c.original_rack_env = env.dup
|
47
|
+
}
|
48
|
+
@app.call(env)
|
49
|
+
end
|
50
|
+
|
51
|
+
end # class Middleware
|
52
|
+
end # class Context
|
53
|
+
end # module Startback
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module Startback
|
2
|
+
module Errors
|
3
|
+
|
4
|
+
class Error < StandardError
|
5
|
+
class << self
|
6
|
+
|
7
|
+
def status(code = nil)
|
8
|
+
@code = code || @code
|
9
|
+
end
|
10
|
+
|
11
|
+
def keep_error(keep = nil)
|
12
|
+
@keep_error = keep unless keep.nil?
|
13
|
+
@keep_error
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def message
|
18
|
+
msg = super
|
19
|
+
return msg unless msg == self.class.name
|
20
|
+
parts = self.class.name.split('::').last.gsub(/[A-Z]/){|x|
|
21
|
+
" #{x.downcase}"
|
22
|
+
}.strip.split(" ")
|
23
|
+
parts = parts[0...-1] unless self.class.keep_error
|
24
|
+
parts.join(" ").capitalize
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class BadRequestError < Error
|
29
|
+
status 400
|
30
|
+
end
|
31
|
+
|
32
|
+
class UnauthorizedError < BadRequestError
|
33
|
+
status 401
|
34
|
+
end
|
35
|
+
|
36
|
+
class ForbiddenError < BadRequestError
|
37
|
+
status 403
|
38
|
+
end
|
39
|
+
|
40
|
+
class NotFoundError < BadRequestError
|
41
|
+
status 404
|
42
|
+
end
|
43
|
+
|
44
|
+
class MethodNotAllowedError < BadRequestError
|
45
|
+
status 405
|
46
|
+
end
|
47
|
+
|
48
|
+
class NotAcceptableError < BadRequestError
|
49
|
+
status 406
|
50
|
+
end
|
51
|
+
|
52
|
+
class ConflictError < BadRequestError
|
53
|
+
status 409
|
54
|
+
end
|
55
|
+
|
56
|
+
class GoneError < BadRequestError
|
57
|
+
status 410
|
58
|
+
end
|
59
|
+
|
60
|
+
class PreconditionFailedError < BadRequestError
|
61
|
+
status 412
|
62
|
+
end
|
63
|
+
|
64
|
+
class UnsupportedMediaTypeError < BadRequestError
|
65
|
+
status 415
|
66
|
+
end
|
67
|
+
|
68
|
+
class ExpectationFailedError < BadRequestError
|
69
|
+
status 417
|
70
|
+
end
|
71
|
+
|
72
|
+
class LockedError < BadRequestError
|
73
|
+
status 423
|
74
|
+
end
|
75
|
+
|
76
|
+
class PreconditionRequiredError < BadRequestError
|
77
|
+
status 428
|
78
|
+
end
|
79
|
+
|
80
|
+
class InternalServerError < Error
|
81
|
+
status 500
|
82
|
+
keep_error(true)
|
83
|
+
end
|
84
|
+
|
85
|
+
class NotImplementedError < InternalServerError
|
86
|
+
status 501
|
87
|
+
end
|
88
|
+
|
89
|
+
end # module Errors
|
90
|
+
include Errors
|
91
|
+
end # module Klaro
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Startback
|
2
|
+
class Operation
|
3
|
+
include Errors
|
4
|
+
|
5
|
+
attr_accessor :world
|
6
|
+
|
7
|
+
protected :world=
|
8
|
+
|
9
|
+
def bind(world)
|
10
|
+
return self unless world
|
11
|
+
dup.tap{|op|
|
12
|
+
op.world = world
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
def method_missing(name, *args, &bl)
|
17
|
+
return super unless args.empty? and bl.nil?
|
18
|
+
return super unless world
|
19
|
+
world.fetch(name){ super }
|
20
|
+
end
|
21
|
+
|
22
|
+
protected
|
23
|
+
|
24
|
+
def run(operation)
|
25
|
+
operation.bind(self.world).call
|
26
|
+
end
|
27
|
+
|
28
|
+
end # class Operation
|
29
|
+
end # module Startback
|
30
|
+
require_relative 'operation/error_operation'
|
31
|
+
require_relative 'operation/multi_operation'
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Startback
|
2
|
+
class Operation
|
3
|
+
class ErrorOperation < Operation
|
4
|
+
|
5
|
+
def initialize(details)
|
6
|
+
@details = details
|
7
|
+
end
|
8
|
+
attr_reader :details
|
9
|
+
|
10
|
+
def call
|
11
|
+
end
|
12
|
+
|
13
|
+
def bind(world)
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
end # class ErrorOperation
|
18
|
+
end # class Operation
|
19
|
+
end # module Startback
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Startback
|
2
|
+
class Operation
|
3
|
+
class MultiOperation
|
4
|
+
|
5
|
+
def initialize(ops = [])
|
6
|
+
@ops = ops
|
7
|
+
end
|
8
|
+
attr_reader :ops
|
9
|
+
|
10
|
+
def size
|
11
|
+
ops.size
|
12
|
+
end
|
13
|
+
|
14
|
+
def +(other)
|
15
|
+
MultiOperation.new(@ops + Array(other))
|
16
|
+
end
|
17
|
+
|
18
|
+
def bind(world)
|
19
|
+
MultiOperation.new(ops.map{|op| op.bind(world) })
|
20
|
+
end
|
21
|
+
|
22
|
+
def call
|
23
|
+
ops.map{|op| op.call }
|
24
|
+
end
|
25
|
+
|
26
|
+
end # class MultiOperation
|
27
|
+
end # class Operation
|
28
|
+
end # module Startback
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Startback
|
2
|
+
module Support
|
3
|
+
#
|
4
|
+
# A Logger extension that sends info and debug messages to STDOUT
|
5
|
+
# and other messages to STDERR. This is not configurable.
|
6
|
+
#
|
7
|
+
class Logger < ::Logger
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
super(STDOUT)
|
11
|
+
@err_logger = ::Logger.new(STDERR)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.level=(level)
|
15
|
+
super.tap{
|
16
|
+
@err_logger.level = level
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
def warn(*args, &bl)
|
21
|
+
@err_logger.warn(*args, &bl)
|
22
|
+
end
|
23
|
+
|
24
|
+
def error(*args, &bl)
|
25
|
+
@err_logger.error(*args, &bl)
|
26
|
+
end
|
27
|
+
|
28
|
+
def fatal(*args, &bl)
|
29
|
+
@err_logger.fatal(*args, &bl)
|
30
|
+
end
|
31
|
+
|
32
|
+
end # class Logger
|
33
|
+
end # module Support
|
34
|
+
end # module Startback
|
@@ -0,0 +1,74 @@
|
|
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
|
+
|
10
|
+
protected
|
11
|
+
|
12
|
+
###
|
13
|
+
### Facade over context
|
14
|
+
###
|
15
|
+
|
16
|
+
def context
|
17
|
+
env[Startback::Context::Middleware::RACK_ENV_KEY]
|
18
|
+
end
|
19
|
+
|
20
|
+
###
|
21
|
+
### Facade over third party tools
|
22
|
+
###
|
23
|
+
|
24
|
+
def run(operation)
|
25
|
+
operation
|
26
|
+
.bind({ context: context })
|
27
|
+
.call
|
28
|
+
end
|
29
|
+
|
30
|
+
###
|
31
|
+
### About the body / input
|
32
|
+
###
|
33
|
+
|
34
|
+
def loaded_body
|
35
|
+
@loaded_body ||= case ctype = request.content_type
|
36
|
+
when /json/
|
37
|
+
json_body
|
38
|
+
when /multipart\/form-data/
|
39
|
+
file_body params[:file], Path(file[:filename]).extname
|
40
|
+
else
|
41
|
+
unsupported_content_type(ctype)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def json_body(body = request.body.read)
|
46
|
+
JSON.load(body)
|
47
|
+
end
|
48
|
+
|
49
|
+
def file_body(file, ctype)
|
50
|
+
raise UnsupportedMediaTypeError, "Unable to use `#{ctype}` as input data"
|
51
|
+
end
|
52
|
+
|
53
|
+
###
|
54
|
+
### Various reusable responses
|
55
|
+
###
|
56
|
+
|
57
|
+
def serve_nothing
|
58
|
+
[ 204, {}, [] ]
|
59
|
+
end
|
60
|
+
|
61
|
+
def serve(entity_description, entity, ct = :json)
|
62
|
+
if entity.nil?
|
63
|
+
status 404
|
64
|
+
content_type :json
|
65
|
+
{ description: "#{entity_description} not found" }.to_json
|
66
|
+
else
|
67
|
+
content_type ct
|
68
|
+
entity.to_json
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
end # class Api
|
73
|
+
end # module Web
|
74
|
+
end # module Startback
|
@@ -0,0 +1,36 @@
|
|
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
|
+
# Examples:
|
12
|
+
#
|
13
|
+
# Rack::Builder.new do
|
14
|
+
# use Startback::Web::CatchAll
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
class CatchAll < Rack::Robustness
|
18
|
+
include Errors
|
19
|
+
|
20
|
+
FATAL_ERROR = {
|
21
|
+
code: "Gybr::Errors::ServerError",
|
22
|
+
description: "An error occured, sorry"
|
23
|
+
}.to_json
|
24
|
+
|
25
|
+
self.catch_all
|
26
|
+
self.status 500
|
27
|
+
self.content_type 'application/json'
|
28
|
+
self.body FATAL_ERROR.to_json
|
29
|
+
|
30
|
+
self.ensure(true) do |ex|
|
31
|
+
Startback::LOGGER.fatal(ex)
|
32
|
+
end
|
33
|
+
|
34
|
+
end # class CatchAll
|
35
|
+
end # class Web
|
36
|
+
end # module Startback
|
@@ -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" }, 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,40 @@
|
|
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
|
+
|
28
|
+
# Various other codes for the framework specific error classes
|
29
|
+
self.on(Startback::Errors::Error) {|ex| ex.class.status }
|
30
|
+
|
31
|
+
# A bit of logic to choose the best error message for the user
|
32
|
+
# according to the error class
|
33
|
+
self.body{|ex|
|
34
|
+
ex = ex.root_cause if ex.is_a?(Finitio::TypeError)
|
35
|
+
{ code: ex.class.name, description: ex.message }.to_json
|
36
|
+
}
|
37
|
+
|
38
|
+
end # class Shield
|
39
|
+
end # module Web
|
40
|
+
end # module Startback
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Startback
|
4
|
+
class Context
|
5
|
+
|
6
|
+
class MyContextSubClass < Context
|
7
|
+
end
|
8
|
+
|
9
|
+
describe Middleware do
|
10
|
+
include Rack::Test::Methods
|
11
|
+
|
12
|
+
def app
|
13
|
+
opts = middleware_options
|
14
|
+
Rack::Builder.new do
|
15
|
+
use Middleware, opts
|
16
|
+
run ->(env){
|
17
|
+
ctx = env[Startback::Context::Middleware::RACK_ENV_KEY]
|
18
|
+
[200, {}, ctx.class.to_s]
|
19
|
+
}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'when used without option' do
|
24
|
+
let(:middleware_options){ nil }
|
25
|
+
|
26
|
+
it 'sets the default context class' do
|
27
|
+
get '/'
|
28
|
+
expect(last_response.status).to eql(200)
|
29
|
+
expect(last_response.body).to eql("Startback::Context")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'when specifying the context class' do
|
34
|
+
let(:middleware_options){{
|
35
|
+
context_class: MyContextSubClass
|
36
|
+
}}
|
37
|
+
|
38
|
+
it 'sets the default context class' do
|
39
|
+
get '/'
|
40
|
+
expect(last_response.status).to eql(200)
|
41
|
+
expect(last_response.body).to eql("Startback::Context::MyContextSubClass")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end # module Web
|
47
|
+
end # module Startback
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Startback
|
4
|
+
describe Operation do
|
5
|
+
|
6
|
+
class FooOp < Operation
|
7
|
+
|
8
|
+
def initialize(foo = :bar)
|
9
|
+
@foo = foo
|
10
|
+
end
|
11
|
+
attr_accessor :foo
|
12
|
+
|
13
|
+
def call
|
14
|
+
@foo
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'can be bound, which returns a new operation' do
|
20
|
+
foo1 = FooOp.new
|
21
|
+
foo1.foo = :bar1
|
22
|
+
|
23
|
+
foo2 = foo1.bind({ db: :bar })
|
24
|
+
expect(foo1 == foo2).to eql(false)
|
25
|
+
expect(foo2.foo).to eql(:bar1)
|
26
|
+
|
27
|
+
expect(->(){ foo1.db }).to raise_error(NoMethodError)
|
28
|
+
expect(foo2.db).to eql(:bar)
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
describe Operation::MultiOperation do
|
34
|
+
|
35
|
+
it 'lets chain with +' do
|
36
|
+
mop = Operation::MultiOperation.new
|
37
|
+
mop2 = (mop + FooOp.new)
|
38
|
+
|
39
|
+
expect(mop == mop2).to eql(false)
|
40
|
+
expect(mop.size).to eql(0)
|
41
|
+
expect(mop2.size).to eql(1)
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'calls and collects the result on call' do
|
45
|
+
mop = Operation::MultiOperation.new + FooOp.new(:hello) + FooOp.new(:world)
|
46
|
+
expect(mop.call).to eql([:hello, :world])
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'binds every sub operation recursively' do
|
50
|
+
mop = Operation::MultiOperation.new + FooOp.new(:hello) + FooOp.new(:world)
|
51
|
+
mop2 = mop.bind({requester: :bar})
|
52
|
+
|
53
|
+
expect(mop == mop2).to eql(false)
|
54
|
+
expect(mop2.ops.all?{|op| op.requester == :bar })
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end # module Startback
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Startback
|
4
|
+
module Web
|
5
|
+
describe HealthCheck do
|
6
|
+
include Rack::Test::Methods
|
7
|
+
|
8
|
+
context 'when used without a block and no failure' do
|
9
|
+
def app
|
10
|
+
HealthCheck.new
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'returns a 204 when ok' do
|
14
|
+
get '/'
|
15
|
+
expect(last_response.status).to eql(204)
|
16
|
+
expect(last_response.body).to be_empty
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
context 'when used without a block a failure' do
|
21
|
+
def app
|
22
|
+
app = HealthCheck.new
|
23
|
+
def app.check!(env)
|
24
|
+
raise "Hello error"
|
25
|
+
end
|
26
|
+
app
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'raises when ko' do
|
30
|
+
expect(->(){ get '/' }).to raise_error("Hello error")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context 'when used with a block returning a debug message' do
|
35
|
+
def app
|
36
|
+
HealthCheck.new{ "Hello world" }
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'returns a 200 with plain text message' do
|
40
|
+
get '/'
|
41
|
+
expect(last_response.status).to eql(200)
|
42
|
+
expect(last_response.body).to eql("Hello world")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context 'when used with a block raising an exception' do
|
47
|
+
def app
|
48
|
+
HealthCheck.new{ raise("Hello error") }
|
49
|
+
end
|
50
|
+
|
51
|
+
it 're-raises it' do
|
52
|
+
expect(->(){ get '/' }).to raise_error("Hello error")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end # module Web
|
58
|
+
end # module Startback
|
data/tasks/gem.rake
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'rubygems/package_task'
|
2
|
+
|
3
|
+
# Dynamically load the gem spec
|
4
|
+
gemspec_file = File.expand_path('../../startback.gemspec', __FILE__)
|
5
|
+
gemspec = Kernel.eval(File.read(gemspec_file))
|
6
|
+
|
7
|
+
Gem::PackageTask.new(gemspec) do |t|
|
8
|
+
|
9
|
+
# Name of the package
|
10
|
+
t.name = gemspec.name
|
11
|
+
|
12
|
+
# Version of the package
|
13
|
+
t.version = gemspec.version
|
14
|
+
|
15
|
+
# Directory used to store the package files
|
16
|
+
t.package_dir = "pkg"
|
17
|
+
|
18
|
+
# True if a gzipped tar file (tgz) should be produced
|
19
|
+
t.need_tar = false
|
20
|
+
|
21
|
+
# True if a gzipped tar file (tar.gz) should be produced
|
22
|
+
t.need_tar_gz = false
|
23
|
+
|
24
|
+
# True if a bzip2'd tar file (tar.bz2) should be produced
|
25
|
+
t.need_tar_bz2 = false
|
26
|
+
|
27
|
+
# True if a zip file should be produced (default is false)
|
28
|
+
t.need_zip = false
|
29
|
+
|
30
|
+
# List of files to be included in the package.
|
31
|
+
t.package_files = gemspec.files
|
32
|
+
|
33
|
+
# Tar command for gzipped or bzip2ed archives.
|
34
|
+
t.tar_command = "tar"
|
35
|
+
|
36
|
+
# Zip command for zipped archives.
|
37
|
+
t.zip_command = "zip"
|
38
|
+
|
39
|
+
end
|
data/tasks/test.rake
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
namespace :test do
|
2
|
+
|
3
|
+
desc "Run RSpec unit tests"
|
4
|
+
task :unit do
|
5
|
+
system("rspec -Ilib -Ispec --pattern 'spec/unit/**/test_*.rb' --color --backtrace --fail-fast")
|
6
|
+
end
|
7
|
+
|
8
|
+
desc "Run the tests in the examples folder"
|
9
|
+
task :example do
|
10
|
+
Bundler.with_original_env do
|
11
|
+
system("cd example && bundle exec rake")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
task :all => [:unit, :example]
|
16
|
+
end
|
17
|
+
task :test => :'test:all'
|
metadata
ADDED
@@ -0,0 +1,169 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: startback
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Bernard Lambeau
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-09-26 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: sinatra
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2.0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rack-robustness
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.1'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.1'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: finitio
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.6'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.6'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: path
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '1.3'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '1.3'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rack-test
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
description: Yet another ruby backend framework, I'm afraid
|
112
|
+
email: blambeau@gmail.com
|
113
|
+
executables: []
|
114
|
+
extensions: []
|
115
|
+
extra_rdoc_files: []
|
116
|
+
files:
|
117
|
+
- Gemfile
|
118
|
+
- README.md
|
119
|
+
- Rakefile
|
120
|
+
- lib/startback.rb
|
121
|
+
- lib/startback/context.rb
|
122
|
+
- lib/startback/context/middleware.rb
|
123
|
+
- lib/startback/errors.rb
|
124
|
+
- lib/startback/ext.rb
|
125
|
+
- lib/startback/ext/date_time.rb
|
126
|
+
- lib/startback/ext/time.rb
|
127
|
+
- lib/startback/operation.rb
|
128
|
+
- lib/startback/operation/error_operation.rb
|
129
|
+
- lib/startback/operation/multi_operation.rb
|
130
|
+
- lib/startback/support.rb
|
131
|
+
- lib/startback/support/logger.rb
|
132
|
+
- lib/startback/version.rb
|
133
|
+
- lib/startback/web.rb
|
134
|
+
- lib/startback/web/api.rb
|
135
|
+
- lib/startback/web/catch_all.rb
|
136
|
+
- lib/startback/web/health_check.rb
|
137
|
+
- lib/startback/web/shield.rb
|
138
|
+
- spec/spec_helper.rb
|
139
|
+
- spec/unit/context/test_middleware.rb
|
140
|
+
- spec/unit/test_operation.rb
|
141
|
+
- spec/unit/test_support.rb
|
142
|
+
- spec/unit/web/test_healthcheck.rb
|
143
|
+
- tasks/gem.rake
|
144
|
+
- tasks/test.rake
|
145
|
+
homepage: http://www.enspirit.be
|
146
|
+
licenses:
|
147
|
+
- MIT
|
148
|
+
metadata: {}
|
149
|
+
post_install_message:
|
150
|
+
rdoc_options: []
|
151
|
+
require_paths:
|
152
|
+
- lib
|
153
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
154
|
+
requirements:
|
155
|
+
- - ">="
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: '0'
|
158
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
159
|
+
requirements:
|
160
|
+
- - ">="
|
161
|
+
- !ruby/object:Gem::Version
|
162
|
+
version: '0'
|
163
|
+
requirements: []
|
164
|
+
rubyforge_project:
|
165
|
+
rubygems_version: 2.6.11
|
166
|
+
signing_key:
|
167
|
+
specification_version: 4
|
168
|
+
summary: Got Your Ruby Back
|
169
|
+
test_files: []
|