startback 0.2.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 +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: []
|