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 CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- YmFjM2ViYTZiMTdlNDM0OTI2Mjg5YzUyN2VkOWIwMTMyMDc3YTgxMg==
4
+ ZWI1MTZlY2MxNTkwZjkwZGVmYjQ4YzliMzQxMDg1YjJiMzg3YThiOA==
5
5
  data.tar.gz: !binary |-
6
- Y2VkODYxZGUwNWI2MzU3Yjg5OGZmOTI5NmI3NzljNGE3OWZjYThhNg==
6
+ NmJmZjVkNDY3ZTYwMDJjZTc0ZjJjOTViMzA4YmNlOWYwZjY0OWY5NQ==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- MTRhNDdhNWVmMTBjOTY1ZDJmOTViYTRkYTVhZDRjZmY1YjQ2ODNmZTNiOTAw
10
- M2YzNTM3ZjE2ZmMyZTk1Y2Y0YjVhOTljMWE0NzFiNTljMjVkMzMxYWMwNDVk
11
- YjM3OGQxMGI5ODk1YzljYjc3NWU2YjNjYjVjNmE4YjA1ZmEwM2Q=
9
+ ZTBhNTg3N2RkZmIzNjRjMzQwYzNiNGUyZjJjOTE0N2NhM2FkNGU5ODZiMWVk
10
+ NGI2ZDJhNTMyMmI5ZmUwN2YyZGI3YjUwZGM4YjNmZDk0ODBjMWI0ZDIyZmI5
11
+ YzI5MzdlZTcwOTZkNjAwOWE2ZjE0ZmQzZmFhMjcwMWFkZjExMmE=
12
12
  data.tar.gz: !binary |-
13
- NDUxYmQ5ZjllODdhMDU1NGFjN2I0NWZlYTBmNmMzOWI2YmQ4MDlkN2ZmOTA3
14
- ZDc4M2I3MmNjY2IzYjVkZTFiYmQyYTRlYzZjM2Q1ZjljYTI4MDkwMDk1ZWJi
15
- MTM3YmM1MTUxMTg3OTBlZGVlODQ5MzkzZDc0YmZkYTRjYTMyNTQ=
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
- Rack::Handler.default.run(Culpa::Application.new, Port: 4748)
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
- 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
- }
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
- if options.has_key? :json
80
- if options[:json]
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
@@ -6,13 +6,15 @@ module Culpa
6
6
  @@route_patterns
7
7
  end
8
8
 
9
- # Parsing a path
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
- # Transforms a string to its equivalent for the compose_regex
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
- # Composing a regex from a human-readable http path pattern
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
@@ -0,0 +1,11 @@
1
+ ##
2
+ # This renderer helps to return a simple status code the client
3
+ ##
4
+
5
+ describe_renderer :status do |options, envelope|
6
+ {
7
+ format: :status,
8
+ headers: options[:headers],
9
+ status: RETURN_CODES[options[:status]]
10
+ }
11
+ end
@@ -1,5 +1,10 @@
1
1
  module Culpa
2
2
 
3
+ ##
4
+ # This class helps to build the routes.
5
+ # This class is used to understand and parse the DSL of the brickchains.rb file.
6
+ ##
7
+
3
8
  class RoutesBuilder
4
9
 
5
10
  attr_reader :result
@@ -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] == Action::RETURN_CODES[expected]
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 'json'
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.1.0'
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 = JSON.parse(env['rack.input'].read)
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, JSON::ParserError
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
- if ENV['RACK_ENV'] == 'development'
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
- route = @router[router_method_name]
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
- # Renders a json or anything else
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].to_json ]
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
- # Rack pre-formatted error
171
+ # Return a rack pre-formatted error
146
172
  def rack_error(code)
147
173
  [code.to_s, {}, []]
148
174
  end
@@ -1,8 +1,14 @@
1
1
  FROM ruby:2.3
2
2
  MAINTAINER Your Name <your@email.com>
3
3
 
4
- # Set RACK_ENV to production
5
- ENV RACK_ENV production
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 4748
28
+ EXPOSE 80
23
29
 
24
30
  # Command to start the application
25
- CMD ["/usr/local/bundle/bin/culpa", "server"]
31
+ CMD ["/usr/bin/supervisord"]
@@ -1,6 +1,6 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- gem 'culpa', '~> 1.1'
3
+ gem 'culpa', '~> 1.2'
4
4
 
5
5
  # Use rspec to test your project.
6
6
  gem 'rspec', '~> 3.4'
@@ -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
+ }
@@ -0,0 +1,10 @@
1
+ [supervisord]
2
+ nodaemon=true
3
+
4
+ [program:culpa]
5
+ command=/usr/local/bundle/bin/culpa server
6
+ directory=/srv/app
7
+ environment=RACK_ENV="production"
8
+
9
+ [program:nginx]
10
+ command=/usr/sbin/nginx
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.1.0
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
@@ -1,14 +0,0 @@
1
- module Culpa
2
-
3
- class FileStreamer
4
- def initialize(element)
5
- @element = element
6
- end
7
- def each(&blk)
8
- @element.each(&blk)
9
- ensure
10
- @element.close
11
- end
12
- end
13
-
14
- end