culpa 1.1.0 → 1.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 +8 -8
- data/bin/culpa +13 -1
- data/lib/culpa/action.rb +12 -90
- data/lib/culpa/brickchain_helpers.rb +10 -0
- data/lib/culpa/coliseum_dsl.rb +65 -0
- data/lib/culpa/path_parser.rb +9 -3
- data/lib/culpa/renderer_describer.rb +68 -0
- data/lib/culpa/renderers/coliseum.rb +21 -0
- data/lib/culpa/renderers/file.rb +33 -0
- data/lib/culpa/renderers/json.rb +24 -0
- data/lib/culpa/renderers/status.rb +11 -0
- data/lib/culpa/routes_builder.rb +5 -0
- data/lib/culpa/test_helpers.rb +15 -1
- data/lib/culpa.rb +42 -16
- data/templates/culpa/Dockerfile +10 -4
- data/templates/culpa/Gemfile +1 -1
- data/templates/culpa/nginx.conf +47 -0
- data/templates/culpa/supervisord.conf +10 -0
- metadata +23 -2
- data/lib/culpa/file_streamer.rb +0 -14
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
ZWI1MTZlY2MxNTkwZjkwZGVmYjQ4YzliMzQxMDg1YjJiMzg3YThiOA==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
NmJmZjVkNDY3ZTYwMDJjZTc0ZjJjOTViMzA4YmNlOWYwZjY0OWY5NQ==
|
7
7
|
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
ZTBhNTg3N2RkZmIzNjRjMzQwYzNiNGUyZjJjOTE0N2NhM2FkNGU5ODZiMWVk
|
10
|
+
NGI2ZDJhNTMyMmI5ZmUwN2YyZGI3YjUwZGM4YjNmZDk0ODBjMWI0ZDIyZmI5
|
11
|
+
YzI5MzdlZTcwOTZkNjAwOWE2ZjE0ZmQzZmFhMjcwMWFkZjExMmE=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
NDliZGU4NmU0Y2IxY2EwODcxOWFhYmRhZWVjODg0MjFmOWVjMDhiMjliZjc2
|
14
|
+
YzcyYmM0YzEzNTUzNzUwYzkyNWFjZmFiOGJkNzA0NzI2NzcwNTI3YmQ1MGI4
|
15
|
+
Nzk2YWI1MTBkYmRmMThjNzgzZTQyNmIzMjRkNWQ4ZWY3YjdmMDU=
|
data/bin/culpa
CHANGED
@@ -21,6 +21,7 @@ def create_project(project_path)
|
|
21
21
|
FileUtils.mkdir "#{project_path}/public"
|
22
22
|
FileUtils.touch "#{project_path}/public/.keep"
|
23
23
|
FileUtils.mkdir "#{project_path}/config"
|
24
|
+
FileUtils.mkdir "#{project_path}/config/dockerized"
|
24
25
|
FileUtils.touch "#{project_path}/config/brickchains.rb"
|
25
26
|
FileUtils.mkdir "#{project_path}/config/initializers"
|
26
27
|
FileUtils.touch "#{project_path}/config/initializers/.keep"
|
@@ -37,6 +38,12 @@ def create_project(project_path)
|
|
37
38
|
dockerfile_rackup_path = File.join( File.dirname(__FILE__), '../templates/culpa/Dockerfile' )
|
38
39
|
FileUtils.cp dockerfile_rackup_path, "#{project_path}/Dockerfile"
|
39
40
|
|
41
|
+
puts '==> Copying Docker dependencies'
|
42
|
+
nginx_conf_path = File.join( File.dirname(__FILE__), '../templates/culpa/nginx.conf' )
|
43
|
+
supervisord_path = File.join( File.dirname(__FILE__), '../templates/culpa/supervisord.conf' )
|
44
|
+
FileUtils.cp nginx_conf_path, "#{project_path}/config/dockerized/nginx.conf"
|
45
|
+
FileUtils.cp supervisord_path, "#{project_path}/config/dockerized/supervisord.conf"
|
46
|
+
|
40
47
|
puts '==> Copying default public folder content'
|
41
48
|
index_html_rackup_path = File.join( File.dirname(__FILE__), '../templates/culpa/index.html' )
|
42
49
|
FileUtils.cp index_html_rackup_path, "#{project_path}/public/index.html"
|
@@ -100,7 +107,12 @@ end
|
|
100
107
|
def start_server
|
101
108
|
require 'rack'
|
102
109
|
ENV['RACK_ENV'] ||= 'development'
|
103
|
-
|
110
|
+
case ENV['RACK_ENV']
|
111
|
+
when 'development'
|
112
|
+
Rack::Handler.default.run(Culpa::Application.new, Port: 4748)
|
113
|
+
when 'production'
|
114
|
+
Rack::Handler.default.run(Culpa::Application.new, Host: '/var/run/culpa.sock')
|
115
|
+
end
|
104
116
|
end
|
105
117
|
|
106
118
|
case ARGV[0]
|
data/lib/culpa/action.rb
CHANGED
@@ -1,59 +1,14 @@
|
|
1
1
|
class Action
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
no_content: 204,
|
13
|
-
reset_content: 205,
|
14
|
-
partial_content: 206,
|
15
|
-
# Redirection codes
|
16
|
-
multiple_choices: 300,
|
17
|
-
moved_permanently: 301,
|
18
|
-
moved_temporarily: 302,
|
19
|
-
see_other: 303,
|
20
|
-
not_modified: 304,
|
21
|
-
use_proxy: 305,
|
22
|
-
temporary_redirect: 307,
|
23
|
-
permanent_redirect: 308,
|
24
|
-
too_many_redirects: 310,
|
25
|
-
# Clients error codes
|
26
|
-
bad_request: 400,
|
27
|
-
unauthorized: 401,
|
28
|
-
payment_required: 402,
|
29
|
-
forbidden: 403,
|
30
|
-
not_found: 404,
|
31
|
-
method_not_allowed: 405,
|
32
|
-
not_acceptable: 406,
|
33
|
-
proxy_authentication_required: 407,
|
34
|
-
request_time_out: 408,
|
35
|
-
conflict: 409,
|
36
|
-
gone: 410,
|
37
|
-
length_required: 411,
|
38
|
-
precondition_failed: 412,
|
39
|
-
request_entity_too_large: 413,
|
40
|
-
request_uri_too_long: 414,
|
41
|
-
unsupported_media_type: 415,
|
42
|
-
request_range_unsatisfiable: 416,
|
43
|
-
expectation_failed: 417,
|
44
|
-
im_a_tea_pot: 418,
|
45
|
-
bad_mapping: 412,
|
46
|
-
unavailable_for_legal_reason: 451,
|
47
|
-
# Server error codes
|
48
|
-
internal_server_error: 500,
|
49
|
-
not_implemented: 501,
|
50
|
-
bad_gateway: 502,
|
51
|
-
proxy_error: 502,
|
52
|
-
service_unavailable: 503,
|
53
|
-
gateway_time_out: 504,
|
54
|
-
bandwidth_limit_exceeded: 509,
|
55
|
-
unknown_error: 520
|
56
|
-
}
|
3
|
+
require_relative 'renderer_describer'
|
4
|
+
|
5
|
+
@@renderers = {}
|
6
|
+
|
7
|
+
def self.load_renderers
|
8
|
+
Dir[File.join( File.dirname(__FILE__), 'renderers/*.rb' )].each do |renderer|
|
9
|
+
@@renderers.merge!(RendererDescriber.new(renderer).result)
|
10
|
+
end
|
11
|
+
end
|
57
12
|
|
58
13
|
attr_reader :e, :r
|
59
14
|
|
@@ -67,7 +22,6 @@ class Action
|
|
67
22
|
def initialize(envelope, request)
|
68
23
|
@e = envelope
|
69
24
|
@r = request
|
70
|
-
@to_render = nil
|
71
25
|
end
|
72
26
|
|
73
27
|
def method_missing(sym)
|
@@ -75,42 +29,10 @@ class Action
|
|
75
29
|
end
|
76
30
|
|
77
31
|
def render(options={})
|
32
|
+
keyword = options.keys.first
|
78
33
|
options[:headers] ||= {}
|
79
|
-
|
80
|
-
|
81
|
-
to_render = {
|
82
|
-
format: :json,
|
83
|
-
status: RETURN_CODES[options[:status]] || 200,
|
84
|
-
headers: { 'Content-Type' => 'application/json' }.merge(options[:headers]),
|
85
|
-
object: options[:json]
|
86
|
-
}
|
87
|
-
else
|
88
|
-
to_render = {
|
89
|
-
format: :status,
|
90
|
-
headers: options[:headers],
|
91
|
-
status: RETURN_CODES[options[:or_status]]
|
92
|
-
}
|
93
|
-
end
|
94
|
-
elsif options.has_key? :file
|
95
|
-
to_render = {
|
96
|
-
format: :file_from_path,
|
97
|
-
headers: {
|
98
|
-
'Content-Type' => options[:content_type] || 'application/octet-stream',
|
99
|
-
'Content-Disposition' => "#{options[:disposition].to_s || 'attachment'}; filename=#{options[:filename] || 'unknown'}",
|
100
|
-
}.merge(options[:headers]),
|
101
|
-
status: RETURN_CODES[options[:status]] || 200,
|
102
|
-
object: Culpa::FileStreamer.new(options[:file])
|
103
|
-
}
|
104
|
-
to_render[:headers]['Content-Length'] = options[:size] if options.has_key? :size
|
105
|
-
elsif options.has_key? :status
|
106
|
-
to_render = {
|
107
|
-
format: :status,
|
108
|
-
headers: options[:headers],
|
109
|
-
status: RETURN_CODES[options[:status]]
|
110
|
-
}
|
111
|
-
else
|
112
|
-
raise Culpa::UnknownRenderCall.new
|
113
|
-
end
|
34
|
+
raise Culpa::UnknownRenderCall.new unless @@renderers.include?(keyword)
|
35
|
+
to_render = @@renderers[keyword].call(options, @envelope)
|
114
36
|
raise RenderNow.new(to_render)
|
115
37
|
end
|
116
38
|
|
@@ -15,6 +15,16 @@ module Culpa
|
|
15
15
|
asb.chs.each { |call_holder| call_holder.thread.join }
|
16
16
|
end
|
17
17
|
|
18
|
+
def render_coliseum(file)
|
19
|
+
coliseum = ColiseumDSL.new("./coliseums/#{file}.rb", envelope)
|
20
|
+
raise Action::RenderNow.new({
|
21
|
+
format: :json,
|
22
|
+
status: RendererDescriber::RETURN_CODES[coliseum.culpa_status] || 200,
|
23
|
+
headers: { 'Content-Type' => 'application/json' }.merge(coliseum.culpa_headers),
|
24
|
+
object: coliseum.dsl_attributes
|
25
|
+
})
|
26
|
+
end
|
27
|
+
|
18
28
|
def method_missing(sym)
|
19
29
|
return CallHolder.new(sym.to_s, @envelope, @request)
|
20
30
|
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'multi_json'
|
2
|
+
|
3
|
+
class ColiseumDSL
|
4
|
+
|
5
|
+
attr_reader :dsl_attributes, :culpa_status, :culpa_headers
|
6
|
+
|
7
|
+
def initialize(to_eval, envelope, eval_args=nil)
|
8
|
+
@dsl_attributes = {}
|
9
|
+
@culpa_headers = {}
|
10
|
+
@culpa_envelope = envelope
|
11
|
+
if to_eval.is_a?(String)
|
12
|
+
self.instance_eval(File.read(to_eval), to_eval)
|
13
|
+
else
|
14
|
+
self.instance_exec eval_args, &to_eval
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def e
|
19
|
+
@culpa_envelope
|
20
|
+
end
|
21
|
+
|
22
|
+
def set_status_code!(code)
|
23
|
+
@culpa_status = code
|
24
|
+
end
|
25
|
+
|
26
|
+
def add_http_headers!(hdrs={})
|
27
|
+
@culpa_headers.merge!(hdrs)
|
28
|
+
end
|
29
|
+
|
30
|
+
def array!(opts, &blk)
|
31
|
+
if opts.is_a? Hash
|
32
|
+
sym, items = opts.first
|
33
|
+
@dsl_attributes[sym] = []
|
34
|
+
items.each do |item|
|
35
|
+
@dsl_attributes[sym] << ColiseumDSL.new(blk, @culpa_envelope, item).dsl_attributes
|
36
|
+
end
|
37
|
+
else
|
38
|
+
@dsl_attributes = []
|
39
|
+
opts.each do |item|
|
40
|
+
@dsl_attributes << ColiseumDSL.new(blk, @culpa_envelope, item).dsl_attributes
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def extract!(target, *attrs)
|
46
|
+
attrs.each do |att|
|
47
|
+
@dsl_attributes[att] = target.send(att)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def method_missing(sym, *args, &blk)
|
52
|
+
@dsl_attributes[sym] = if ::Kernel.block_given?
|
53
|
+
ColiseumDSL.new(blk, @culpa_envelope).dsl_attributes
|
54
|
+
else
|
55
|
+
if args.count == 0
|
56
|
+
nil
|
57
|
+
elsif args.count == 1
|
58
|
+
args[0]
|
59
|
+
else
|
60
|
+
args
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
data/lib/culpa/path_parser.rb
CHANGED
@@ -6,13 +6,15 @@ module Culpa
|
|
6
6
|
@@route_patterns
|
7
7
|
end
|
8
8
|
|
9
|
-
|
9
|
+
##
|
10
|
+
# Parse a path and return the results of the parse
|
10
11
|
def self.parse(pattern, path)
|
11
12
|
result = path.match(pattern[:regex])
|
12
13
|
return unless result
|
13
14
|
return Hash[result.names.map{ |name| name }.zip(result.captures)]
|
14
15
|
end
|
15
16
|
|
17
|
+
##
|
16
18
|
# Use the brickchains to build the cache of route
|
17
19
|
def self.create_route_cache(brickchains, global_prefix)
|
18
20
|
custom_routes = []
|
@@ -47,18 +49,21 @@ module Culpa
|
|
47
49
|
@@route_patterns = custom_routes + fixed_routes
|
48
50
|
end
|
49
51
|
|
50
|
-
|
52
|
+
##
|
53
|
+
# Transform a string to its equivalent for the compose_regex
|
51
54
|
def self.dir_to_regex(dir)
|
52
55
|
return dir unless dir.start_with? ':'
|
53
56
|
"(?<#{dir[1..dir.length-1]}>\\w+)"
|
54
57
|
end
|
55
58
|
|
56
|
-
|
59
|
+
##
|
60
|
+
# Compose a regex from a human-readable http path pattern
|
57
61
|
def self.compose_regex(url)
|
58
62
|
regex = url.split('/').map{ |dir| self.dir_to_regex(dir) }.join('/')
|
59
63
|
Regexp.new("^#{regex}$")
|
60
64
|
end
|
61
65
|
|
66
|
+
##
|
62
67
|
# Search for the routes, extracts informations and returns
|
63
68
|
# the router method name and the request informations
|
64
69
|
def self.extract_vars(path, call_options)
|
@@ -74,6 +79,7 @@ module Culpa
|
|
74
79
|
end
|
75
80
|
end
|
76
81
|
|
82
|
+
##
|
77
83
|
# Try to return the router method name or infer it automatically
|
78
84
|
def self.infer_method_name(pattern, params, verb)
|
79
85
|
if params.has_key?('sub_call')
|
@@ -0,0 +1,68 @@
|
|
1
|
+
class RendererDescriber
|
2
|
+
|
3
|
+
RETURN_CODES = {
|
4
|
+
# Information codes
|
5
|
+
continue: 100,
|
6
|
+
switching_procotols: 101,
|
7
|
+
# Success codes
|
8
|
+
ok: 200,
|
9
|
+
created: 201,
|
10
|
+
accepted: 202,
|
11
|
+
non_authoritative_information: 203,
|
12
|
+
no_content: 204,
|
13
|
+
reset_content: 205,
|
14
|
+
partial_content: 206,
|
15
|
+
# Redirection codes
|
16
|
+
multiple_choices: 300,
|
17
|
+
moved_permanently: 301,
|
18
|
+
moved_temporarily: 302,
|
19
|
+
see_other: 303,
|
20
|
+
not_modified: 304,
|
21
|
+
use_proxy: 305,
|
22
|
+
temporary_redirect: 307,
|
23
|
+
permanent_redirect: 308,
|
24
|
+
too_many_redirects: 310,
|
25
|
+
# Clients error codes
|
26
|
+
bad_request: 400,
|
27
|
+
unauthorized: 401,
|
28
|
+
payment_required: 402,
|
29
|
+
forbidden: 403,
|
30
|
+
not_found: 404,
|
31
|
+
method_not_allowed: 405,
|
32
|
+
not_acceptable: 406,
|
33
|
+
proxy_authentication_required: 407,
|
34
|
+
request_time_out: 408,
|
35
|
+
conflict: 409,
|
36
|
+
gone: 410,
|
37
|
+
length_required: 411,
|
38
|
+
precondition_failed: 412,
|
39
|
+
request_entity_too_large: 413,
|
40
|
+
request_uri_too_long: 414,
|
41
|
+
unsupported_media_type: 415,
|
42
|
+
request_range_unsatisfiable: 416,
|
43
|
+
expectation_failed: 417,
|
44
|
+
im_a_tea_pot: 418,
|
45
|
+
bad_mapping: 412,
|
46
|
+
unavailable_for_legal_reason: 451,
|
47
|
+
# Server error codes
|
48
|
+
internal_server_error: 500,
|
49
|
+
not_implemented: 501,
|
50
|
+
bad_gateway: 502,
|
51
|
+
proxy_error: 502,
|
52
|
+
service_unavailable: 503,
|
53
|
+
gateway_time_out: 504,
|
54
|
+
bandwidth_limit_exceeded: 509,
|
55
|
+
unknown_error: 520
|
56
|
+
}
|
57
|
+
|
58
|
+
attr_reader :result
|
59
|
+
|
60
|
+
def initialize(file)
|
61
|
+
self.instance_eval(File.read(file), file)
|
62
|
+
end
|
63
|
+
|
64
|
+
def describe_renderer(keyword, &blk)
|
65
|
+
@result = {keyword => blk}
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
##
|
2
|
+
# This renderer loads the ColiseumDSL to eval the
|
3
|
+
# passed file, and renders it as a JSON.
|
4
|
+
##
|
5
|
+
|
6
|
+
require_relative '../coliseum_dsl'
|
7
|
+
|
8
|
+
describe_renderer :coliseum do |options, envelope|
|
9
|
+
|
10
|
+
coliseum = ColiseumDSL.new("./coliseums/#{options[:coliseum]}.rb", envelope)
|
11
|
+
|
12
|
+
sent_headers = { 'Content-Type' => 'application/json' }.merge(options[:headers]).merge(coliseum.culpa_headers)
|
13
|
+
|
14
|
+
{
|
15
|
+
format: :json,
|
16
|
+
status: RETURN_CODES[options[:status]] || RETURN_CODES[coliseum.culpa_status] || 200,
|
17
|
+
headers: sent_headers,
|
18
|
+
object: coliseum.dsl_attributes
|
19
|
+
}
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
##
|
2
|
+
# This renderer loads the FileStreamer helper
|
3
|
+
# used to stream a file to the client.
|
4
|
+
##
|
5
|
+
|
6
|
+
class FileStreamer
|
7
|
+
def initialize(element)
|
8
|
+
@element = element
|
9
|
+
end
|
10
|
+
def each(&blk)
|
11
|
+
@element.each(&blk)
|
12
|
+
ensure
|
13
|
+
@element.close
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe_renderer :file do |options, envelope|
|
18
|
+
|
19
|
+
to_render = {
|
20
|
+
format: :file_from_path,
|
21
|
+
headers: {
|
22
|
+
'Content-Type' => options[:content_type] || 'application/octet-stream',
|
23
|
+
'Content-Disposition' => "#{options[:disposition].to_s || 'attachment'}; filename=#{options[:filename] || 'unknown'}",
|
24
|
+
}.merge(options[:headers]),
|
25
|
+
status: RETURN_CODES[options[:status]] || 200,
|
26
|
+
object: FileStreamer.new(options[:file])
|
27
|
+
}
|
28
|
+
|
29
|
+
to_render[:headers]['Content-Length'] = options[:size] if options.has_key? :size
|
30
|
+
|
31
|
+
to_render
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
##
|
2
|
+
# This renderer helps render a JSON.
|
3
|
+
# You can add a or_status: :status_code to the render json: {...}
|
4
|
+
# to render a status code if the passed object to render is nil.
|
5
|
+
##
|
6
|
+
|
7
|
+
describe_renderer :json do |options, envelope|
|
8
|
+
|
9
|
+
if options[:json]
|
10
|
+
{
|
11
|
+
format: :json,
|
12
|
+
status: RETURN_CODES[options[:status]] || 200,
|
13
|
+
headers: { 'Content-Type' => 'application/json' }.merge(options[:headers]),
|
14
|
+
object: options[:json]
|
15
|
+
}
|
16
|
+
else
|
17
|
+
{
|
18
|
+
format: :status,
|
19
|
+
headers: options[:headers],
|
20
|
+
status: RETURN_CODES[options[:or_status]]
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
data/lib/culpa/routes_builder.rb
CHANGED
data/lib/culpa/test_helpers.rb
CHANGED
@@ -4,14 +4,20 @@ require 'rspec/expectations'
|
|
4
4
|
|
5
5
|
module CulpaHelpers
|
6
6
|
|
7
|
+
##
|
8
|
+
# Helper to access the result of the last call
|
7
9
|
def last_call
|
8
10
|
@@last_call
|
9
11
|
end
|
10
12
|
|
13
|
+
##
|
14
|
+
# Helper to call a brick
|
11
15
|
def call_brick(brick_name)
|
12
16
|
@@last_call = BrickCall.new(brick_name)
|
13
17
|
end
|
14
18
|
|
19
|
+
##
|
20
|
+
# Class used to mock a brick call
|
15
21
|
class BrickCall
|
16
22
|
|
17
23
|
attr_reader :from_done
|
@@ -49,14 +55,18 @@ module CulpaHelpers
|
|
49
55
|
|
50
56
|
end
|
51
57
|
|
58
|
+
##
|
59
|
+
# expect(last_call).to have_status(:i_am_a_teapot)
|
52
60
|
RSpec::Matchers.define :have_status do |expected|
|
53
61
|
match do |actual|
|
54
62
|
actual.from(described_class) unless actual.from_done or described_class.nil?
|
55
63
|
bc = actual.call
|
56
|
-
bc[:to_render][:status] ==
|
64
|
+
bc[:to_render][:status] == RendererDescriber::RETURN_CODES[expected]
|
57
65
|
end
|
58
66
|
end
|
59
67
|
|
68
|
+
##
|
69
|
+
# expect(last_call).to have_headers({'X-Header-1' => 'ASD' .... })
|
60
70
|
RSpec::Matchers.define :have_headers do |expected|
|
61
71
|
match do |actual|
|
62
72
|
expected.each do |k,v|
|
@@ -69,6 +79,8 @@ module CulpaHelpers
|
|
69
79
|
end
|
70
80
|
end
|
71
81
|
|
82
|
+
##
|
83
|
+
# expect(last_call).to have_body({a: 'a' ... }) (Can be a JSON or anything else)
|
72
84
|
RSpec::Matchers.define :have_body do |expected|
|
73
85
|
match do |actual|
|
74
86
|
actual.from(described_class) unless actual.from_done or described_class.nil?
|
@@ -77,6 +89,8 @@ module CulpaHelpers
|
|
77
89
|
end
|
78
90
|
end
|
79
91
|
|
92
|
+
##
|
93
|
+
# expect(last_call).to have_envelope({a: 'a' ... })
|
80
94
|
RSpec::Matchers.define :have_envelope do |expected|
|
81
95
|
match do |actual|
|
82
96
|
actual.from(described_class) unless actual.from_done or described_class.nil?
|
data/lib/culpa.rb
CHANGED
@@ -1,25 +1,26 @@
|
|
1
|
-
require 'rubygems'
|
2
1
|
require 'bundler'
|
3
|
-
require '
|
2
|
+
require 'multi_json'
|
4
3
|
require 'yaml'
|
5
4
|
require 'logger'
|
6
5
|
|
7
6
|
require_relative 'culpa/envelope'
|
8
7
|
require_relative 'culpa/path_parser'
|
9
|
-
require_relative 'culpa/file_streamer'
|
10
8
|
require_relative 'culpa/routes_builder'
|
11
9
|
require_relative 'culpa/brickchain_helpers'
|
12
10
|
|
13
|
-
CULPA_VERSION='1.
|
11
|
+
CULPA_VERSION='1.2.0'
|
14
12
|
|
15
13
|
ACTIONS_PATH ||= './actions/*.rb'
|
16
14
|
MODELS_PATH ||= './models/*.rb'
|
17
15
|
INITIALIZERS_PATH ||= './config/initializers/*.rb'
|
18
16
|
|
17
|
+
# Require the initializers
|
19
18
|
Dir[INITIALIZERS_PATH].each do |file|
|
20
19
|
require file
|
21
20
|
end
|
22
21
|
|
22
|
+
# Load all the actions in a module
|
23
|
+
# so it can be safely load
|
23
24
|
module Actions
|
24
25
|
require_relative 'culpa/action'
|
25
26
|
Dir[ACTIONS_PATH].each do |file|
|
@@ -27,26 +28,36 @@ module Actions
|
|
27
28
|
end
|
28
29
|
end
|
29
30
|
|
31
|
+
# The main module of the application
|
30
32
|
module Culpa
|
31
33
|
|
34
|
+
# Error called when a sub_call is not defined and it can't be inferred
|
32
35
|
class UnpredictableSubCallError < StandardError; end
|
36
|
+
# Error raised when a brickchain didn't called a render at all
|
33
37
|
class NoRenderCalled < StandardError; end
|
38
|
+
# Error raised when a brick tried to render with an unknown renderer
|
34
39
|
class UnknownRenderCall < StandardError; end
|
40
|
+
# Error raised when the route is not found in the router (aka 404)
|
35
41
|
class RouteNotFoundError < StandardError; end
|
36
42
|
|
43
|
+
# Loading models directly in the Culpa module, so it can be accessed easily.
|
37
44
|
Dir[MODELS_PATH].each do |file|
|
38
45
|
require file
|
39
46
|
end
|
40
47
|
|
48
|
+
# This class is the one instancied by Rack.
|
41
49
|
class Application
|
42
50
|
|
43
51
|
def initialize(options = {})
|
44
|
-
# Loading brickchains definitions
|
52
|
+
# Loading brickchains definitions and the associated routes
|
45
53
|
bc_path = options[:brickchains] || './config/brickchains.rb'
|
46
54
|
route_builder = RoutesBuilder.new(File.read(bc_path))
|
47
|
-
@public_folder = options[:public] || route_builder.public_folder || './public'
|
48
55
|
@router = route_builder.result
|
49
56
|
PathParser.create_route_cache(@router, route_builder.prefix || '')
|
57
|
+
# Setting static file directory
|
58
|
+
@public_folder = options[:public] || route_builder.public_folder || './public'
|
59
|
+
# Loading renderers
|
60
|
+
Action.load_renderers
|
50
61
|
# Logging helper
|
51
62
|
@logger = Logger.new(options[:log_output] || STDOUT)
|
52
63
|
@logger.level = case ENV['RACK_ENV']
|
@@ -58,6 +69,7 @@ module Culpa
|
|
58
69
|
# :nocov:
|
59
70
|
end
|
60
71
|
@logger.info 'Culpa fully initialized'
|
72
|
+
# If in dev or test env, serve the static assets
|
61
73
|
if %w(development test).include? ENV['RACK_ENV']
|
62
74
|
@rack_file = Rack::File.new(@public_folder)
|
63
75
|
end
|
@@ -66,9 +78,11 @@ module Culpa
|
|
66
78
|
##
|
67
79
|
# Rack entrypoint
|
68
80
|
def call(env)
|
81
|
+
# Checking if it is a static file before calling the router
|
69
82
|
path = env['PATH_INFO']
|
70
83
|
if @rack_file && (File.exists?("#{@public_folder}#{path}") || path == '/')
|
71
84
|
env['PATH_INFO'] = '/index.html' if path == '/'
|
85
|
+
@logger.info "Serving static file : #{path}"
|
72
86
|
return @rack_file.call(env)
|
73
87
|
end
|
74
88
|
@logger.info "Received request : #{path}"
|
@@ -79,7 +93,7 @@ module Culpa
|
|
79
93
|
}
|
80
94
|
# Parse body if in JSON, otherwise pass the rack.input directly
|
81
95
|
if env['CONTENT_TYPE'] == 'application/json'
|
82
|
-
body =
|
96
|
+
body = MultiJson.load(env['rack.input'].read)
|
83
97
|
request[:input] = if body.has_key?('data')
|
84
98
|
body['data']
|
85
99
|
else
|
@@ -92,13 +106,17 @@ module Culpa
|
|
92
106
|
# Extract vars from path, take route decision and go !
|
93
107
|
method_name, f_request = PathParser.extract_vars(path, request)
|
94
108
|
call_brickchain method_name, f_request
|
95
|
-
rescue UnpredictableSubCallError,
|
109
|
+
rescue UnpredictableSubCallError, MultiJson::ParseError
|
110
|
+
# The sub_call wasn't predictacle, or the body wans't correct JSON.
|
111
|
+
# In both cases, it is a bad_request.
|
96
112
|
rack_error 400
|
97
|
-
rescue RouteNotFoundError
|
113
|
+
rescue RouteNotFoundError => err
|
114
|
+
# The good old 404 not found
|
98
115
|
@logger.info "Route not found : #{env['PATH_INFO']}"
|
99
116
|
rack_error 404
|
100
117
|
rescue StandardError => err
|
101
|
-
|
118
|
+
# Something went wrong executing the chain !
|
119
|
+
if ENV['RACK_ENV'] != 'test'
|
102
120
|
# :nocov:
|
103
121
|
@logger.error "#{err.to_s}\n#{err.backtrace.join("\n")}"
|
104
122
|
# :nocov:
|
@@ -106,33 +124,41 @@ module Culpa
|
|
106
124
|
rack_error 500
|
107
125
|
end
|
108
126
|
|
127
|
+
##
|
128
|
+
# Call a brickchain using the method_name associated to it
|
109
129
|
def call_brickchain(router_method_name, options)
|
130
|
+
# Loading the router method
|
110
131
|
raise RouteNotFoundError.new unless @router.has_key? router_method_name
|
132
|
+
route = @router[router_method_name]
|
111
133
|
@logger.info "Executing brickchain : #{router_method_name}"
|
134
|
+
# Preparing brickchain scopped variables
|
112
135
|
envelope = Envelope.new
|
113
136
|
request = EnvelopeRequest.new(options)
|
114
|
-
|
137
|
+
# Execute before blocks in the brickchain
|
115
138
|
route[:before].each do |before_block|
|
116
139
|
BrickchainExecutor.new(envelope, request).instance_eval(&before_block)
|
117
140
|
end if route.has_key? :before
|
118
|
-
|
141
|
+
# Execute the brickchain itself
|
119
142
|
if route.has_key? :block
|
120
143
|
BrickchainExecutor.new(envelope, request).instance_eval(&route[:block])
|
121
144
|
else
|
122
145
|
ch = CallHolder.new(route[:brick_name], envelope, request)
|
123
146
|
ch.from(route[:class_name])
|
124
147
|
end
|
125
|
-
|
148
|
+
# If we come to this point, the brickchain haven't raised the RenderNow.
|
149
|
+
# It means there is a problem with the brickchain since it has a path
|
150
|
+
# that didn't render.
|
126
151
|
raise NoRenderCalled.new
|
127
152
|
rescue Action::RenderNow => renderer
|
128
153
|
return do_render(renderer.to_render)
|
129
154
|
end
|
130
155
|
|
131
|
-
|
156
|
+
##
|
157
|
+
# Method called after a correct Action::RenderNow have been raised
|
132
158
|
def do_render(to_render)
|
133
159
|
body = case to_render[:format]
|
134
160
|
when :json
|
135
|
-
[ to_render[:object]
|
161
|
+
[ ::MultiJson.dump(to_render[:object], pretty: ENV['RACK_ENV'] == 'development' ) ]
|
136
162
|
when :file_from_path, :file_from_io
|
137
163
|
to_render[:object]
|
138
164
|
when :status
|
@@ -142,7 +168,7 @@ module Culpa
|
|
142
168
|
end
|
143
169
|
|
144
170
|
##
|
145
|
-
#
|
171
|
+
# Return a rack pre-formatted error
|
146
172
|
def rack_error(code)
|
147
173
|
[code.to_s, {}, []]
|
148
174
|
end
|
data/templates/culpa/Dockerfile
CHANGED
@@ -1,8 +1,14 @@
|
|
1
1
|
FROM ruby:2.3
|
2
2
|
MAINTAINER Your Name <your@email.com>
|
3
3
|
|
4
|
-
#
|
5
|
-
|
4
|
+
# Install nginx and copy default configuration
|
5
|
+
RUN apt-get update && \
|
6
|
+
apt-get install -y nginx supervisor && \
|
7
|
+
apt-get clean && \
|
8
|
+
apt-get autoclean && \
|
9
|
+
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
10
|
+
COPY ./config/dockerized/nginx.conf /etc/nginx/nginx.conf
|
11
|
+
COPY ./config/dockerized/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
6
12
|
|
7
13
|
# Create app folder
|
8
14
|
RUN mkdir -p /srv/app
|
@@ -19,7 +25,7 @@ RUN bundle install
|
|
19
25
|
ADD ./ /srv/app
|
20
26
|
|
21
27
|
# Expose culpa port
|
22
|
-
EXPOSE
|
28
|
+
EXPOSE 80
|
23
29
|
|
24
30
|
# Command to start the application
|
25
|
-
CMD ["/usr/
|
31
|
+
CMD ["/usr/bin/supervisord"]
|
data/templates/culpa/Gemfile
CHANGED
@@ -0,0 +1,47 @@
|
|
1
|
+
user www-data;
|
2
|
+
worker_processes 4;
|
3
|
+
pid /run/nginx.pid;
|
4
|
+
daemon off;
|
5
|
+
|
6
|
+
events {
|
7
|
+
worker_connections 768;
|
8
|
+
}
|
9
|
+
|
10
|
+
http {
|
11
|
+
sendfile on;
|
12
|
+
tcp_nopush on;
|
13
|
+
tcp_nodelay on;
|
14
|
+
keepalive_timeout 65;
|
15
|
+
types_hash_max_size 2048;
|
16
|
+
|
17
|
+
include /etc/nginx/mime.types;
|
18
|
+
default_type application/octet-stream;
|
19
|
+
|
20
|
+
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
21
|
+
ssl_prefer_server_ciphers on;
|
22
|
+
|
23
|
+
access_log /var/log/nginx/access.log;
|
24
|
+
error_log /var/log/nginx/error.log;
|
25
|
+
|
26
|
+
gzip on;
|
27
|
+
gzip_disable "msie6";
|
28
|
+
|
29
|
+
upstream app {
|
30
|
+
server unix:/var/run/culp.sock fail_timeout=0;
|
31
|
+
}
|
32
|
+
|
33
|
+
server {
|
34
|
+
listen 80;
|
35
|
+
root /srv/app/public;
|
36
|
+
try_files $uri/index.html $uri @app;
|
37
|
+
location @app {
|
38
|
+
proxy_pass http://app;
|
39
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
40
|
+
proxy_set_header Host $http_host;
|
41
|
+
proxy_redirect off;
|
42
|
+
}
|
43
|
+
client_max_body_size 4G;
|
44
|
+
keepalive_timeout 10;
|
45
|
+
}
|
46
|
+
|
47
|
+
}
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: culpa
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jérémy SEBAN
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - ~>
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '3.4'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: multi_json
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.12'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.12'
|
55
69
|
description: Culpa is a framework following MEA principles. Learn more on http://culpaframework.org/.
|
56
70
|
email: jeremy@seban.eu
|
57
71
|
executables:
|
@@ -63,9 +77,14 @@ files:
|
|
63
77
|
- lib/culpa.rb
|
64
78
|
- lib/culpa/action.rb
|
65
79
|
- lib/culpa/brickchain_helpers.rb
|
80
|
+
- lib/culpa/coliseum_dsl.rb
|
66
81
|
- lib/culpa/envelope.rb
|
67
|
-
- lib/culpa/file_streamer.rb
|
68
82
|
- lib/culpa/path_parser.rb
|
83
|
+
- lib/culpa/renderer_describer.rb
|
84
|
+
- lib/culpa/renderers/coliseum.rb
|
85
|
+
- lib/culpa/renderers/file.rb
|
86
|
+
- lib/culpa/renderers/json.rb
|
87
|
+
- lib/culpa/renderers/status.rb
|
69
88
|
- lib/culpa/routes_builder.rb
|
70
89
|
- lib/culpa/test_helpers.rb
|
71
90
|
- templates/culpa/Dockerfile
|
@@ -74,6 +93,8 @@ files:
|
|
74
93
|
- templates/culpa/generators/action.tmpl
|
75
94
|
- templates/culpa/index.html
|
76
95
|
- templates/culpa/logo.png
|
96
|
+
- templates/culpa/nginx.conf
|
97
|
+
- templates/culpa/supervisord.conf
|
77
98
|
homepage: http://culpaframework.org/
|
78
99
|
licenses:
|
79
100
|
- MIT
|