jersey 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ module Jersey::Helpers
2
+ module Log
3
+ def log(*args)
4
+ env['logger'].log(*args)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,62 @@
1
+ module Jersey
2
+ module HTTP
3
+ module Errors
4
+ HTTPError = Class.new(StandardError)
5
+ ClientError = Class.new(HTTPError)
6
+ ServerError = Class.new(HTTPError)
7
+
8
+ # Define ALL the HTTP errors as constants
9
+ FOUR_HUNDRED = [
10
+ [400, 'BadRequest', :bad_request],
11
+ [401, 'Unauthorized', :unauthorized],
12
+ [402, 'PaymentRequired', :payment_required],
13
+ [403, 'Forbidden', :forbidden],
14
+ [404, 'NotFound', :not_found],
15
+ [405, 'MethodNotAllowed', :method_not_allowed],
16
+ [406, 'NotAcceptable', :not_acceptable],
17
+ [407, 'ProxyAuthenticationRequired', :proxy_authentication_required],
18
+ [408, 'RequestTimeout', :request_timeout],
19
+ [409, 'Conflict', :conflict],
20
+ [410, 'Gone', :gone],
21
+ [411, 'LengthRequired', :length_required],
22
+ [412, 'PreconditionFailed', :precondition_failed],
23
+ [413, 'RequestEntityTooLarge', :request_entity_too_large],
24
+ [414, 'RequestURITooLong', :request_uri_too_long],
25
+ [415, 'UnsupportedMediaType', :unsupported_media_type],
26
+ [416, 'RequestedRangeNotSatisfiable', :requested_range_not_satisfiable],
27
+ [417, 'ExpectationFailed', :expectation_failed],
28
+ [422, 'UnprocessableEntity', :unprocessable_entity],
29
+ [423, 'Locked', :locked],
30
+ [424, 'FailedDependency', :failed_dependency],
31
+ [426, 'UpgradeRequired', :upgrade_required],
32
+ ]
33
+
34
+ FIVE_HUNDRED = [
35
+ [500, 'InternalServerError', :internal_server_error],
36
+ [501, 'NotImplemented', :not_implemented],
37
+ [502, 'BadGateway', :bad_gateway],
38
+ [503, 'ServiceUnavailable', :service_unavailable],
39
+ [504, 'GatewayTimeout', :gateway_timeout],
40
+ [505, 'HTTPVersionNotSupported', :http_version_not_supported],
41
+ [507, 'InsufficientStorage', :insufficient_storage],
42
+ [510, 'NotExtended', :not_extended],
43
+ ]
44
+
45
+ (FOUR_HUNDRED + FIVE_HUNDRED).each do |s|
46
+ code, const_name, symbol = s[0],s[1],s[2]
47
+ if code >= 500
48
+ klass = Class.new(ServerError)
49
+ else
50
+ klass = Class.new(ClientError)
51
+ end
52
+ klass.const_set(:STATUS_CODE, code)
53
+ const_set(const_name,klass)
54
+ end
55
+
56
+ # Take over Sinatra's NotFound so we don't have to deal with the
57
+ # not_found block and can raise our own NotFound error
58
+ ::Sinatra.send(:remove_const, :NotFound)
59
+ ::Sinatra.const_set(:NotFound, ::Jersey::HTTP::Errors::NotFound)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,25 @@
1
+ require 'jersey/logging/base_logger'
2
+ require 'jersey/logging/logfmt_logger'
3
+ require 'jersey/logging/json_logger'
4
+ require 'jersey/logging/mixins'
5
+
6
+ module Jersey
7
+ class Logger < LogfmtLogger
8
+ include Logging::LogTime
9
+ include Logging::LogError
10
+ end
11
+
12
+ module LoggingSingleton
13
+ attr_writer :logger
14
+
15
+ def logger
16
+ @logger ||= Jersey::Logger.new
17
+ end
18
+
19
+ def log(loggable = {})
20
+ logger.log(loggable)
21
+ end
22
+ end
23
+
24
+ extend LoggingSingleton
25
+ end
@@ -0,0 +1,58 @@
1
+ module Jersey
2
+ class BaseLogger
3
+ attr_accessor :stream, :defaults
4
+
5
+ def initialize(opts = {})
6
+ @stream = opts.fetch(:stream, $stdout)
7
+ @defaults = opts.fetch(:defaults, {})
8
+ end
9
+
10
+ def <<(data)
11
+ @defaults.merge!(data)
12
+ end
13
+
14
+ def reset!(key = nil)
15
+ if key
16
+ @defaults.delete(key)
17
+ @defaults.delete(key.to_sym)
18
+ else
19
+ @defaults.clear
20
+ end
21
+ end
22
+
23
+ def log(data, &block)
24
+ log_to_stream(stream, @defaults.merge(data), &block)
25
+ end
26
+
27
+ private
28
+ def log_to_stream(stream, data, &block)
29
+ data.merge!(request_data || {})
30
+ unless block
31
+ str = unparse(data.merge(now: Time.now))
32
+ stream.print(str + "\n")
33
+ else
34
+ data = data.dup
35
+ start = Time.now
36
+ log_to_stream(stream, data.merge(at: "start"))
37
+ begin
38
+ res = yield
39
+ log_to_stream(stream, data.merge(
40
+ at: "finish", elapsed: (Time.now - start).to_f))
41
+ res
42
+ rescue
43
+ log_to_stream(stream, data.merge(
44
+ at: "exception", elapsed: (Time.now - start).to_f))
45
+ raise
46
+ end
47
+ end
48
+ end
49
+
50
+ def unparse
51
+ raise "Need to subclass and implement #unparse"
52
+ end
53
+
54
+ def request_data
55
+ RequestStore[:log] if defined? RequestStore
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,8 @@
1
+ module Jersey
2
+ class JSONLogger < BaseLogger
3
+ private
4
+ def unparse(attrs)
5
+ attrs.to_json
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,37 @@
1
+ module Jersey
2
+ class LogfmtLogger < BaseLogger
3
+ private
4
+ def unparse(attrs)
5
+ attrs.map { |k, v| unparse_pair(k, v) }.compact.join(" ")
6
+ end
7
+
8
+ def quote_string(k, v)
9
+ # try to find a quote style that fits
10
+ if !v.include?('"')
11
+ %{#{k}="#{v}"}
12
+ elsif !v.include?("'")
13
+ %{#{k}='#{v}'}
14
+ else
15
+ %{#{k}="#{v.gsub(/"/, '\\"')}"}
16
+ end
17
+ end
18
+
19
+ def unparse_pair(k, v)
20
+ v = v.call if v.is_a?(Proc)
21
+ # only quote strings if they include whitespace
22
+ if v == nil
23
+ nil
24
+ elsif v == true
25
+ k
26
+ elsif v.is_a?(Float)
27
+ "#{k}=#{format("%.3f", v)}"
28
+ elsif v.is_a?(String) && v =~ /\s/
29
+ quote_string(k, v)
30
+ elsif v.is_a?(Time)
31
+ "#{k}=#{v.iso8601}"
32
+ else
33
+ "#{k}=#{v}"
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,31 @@
1
+ module Jersey
2
+ module Logging
3
+ # always log the time
4
+ module LogTime
5
+ def log(hash = {})
6
+ super(hash.merge(now: Time.now))
7
+ end
8
+ end
9
+ # only log the first ten lines of a backtrace so error logs
10
+ # are digestible
11
+ module LogError
12
+ def log(loggable = {})
13
+ case loggable
14
+ when Hash
15
+ super(loggable)
16
+ when Exception
17
+ e = loggable
18
+ super(error: true, id: e.object_id, message: e.message)
19
+ lineno = 0
20
+ e.backtrace[0,10].each do |line|
21
+ lineno += 1
22
+ super(error: true,
23
+ id: e.object_id,
24
+ backtrace: line,
25
+ line_number: lineno)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,46 @@
1
+ require 'securerandom'
2
+
3
+ module Jersey::Middleware
4
+ class RequestID
5
+ UUID_PATTERN =
6
+ /\A[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\Z/
7
+
8
+ def initialize(app)
9
+ @app = app
10
+ end
11
+
12
+ def call(env)
13
+ request_ids = [SecureRandom.uuid] + extract_request_ids(env)
14
+
15
+ # make ID of the request accessible to consumers down the stack
16
+ env["REQUEST_ID"] = request_ids[0]
17
+ RequestStore[:log] ||= {}
18
+ RequestStore[:log][:request_id] = request_ids[0]
19
+
20
+ # Extract request IDs from incoming headers as well. Can be used for
21
+ # identifying a request across a number of components in SOA.
22
+ env["REQUEST_IDS"] = request_ids
23
+
24
+ status, headers, response = @app.call(env)
25
+
26
+ # tag all responses with a request ID
27
+ headers["Request-Id"] = request_ids[0]
28
+
29
+ [status, headers, response]
30
+ ensure
31
+ RequestStore[:log].delete(:request_id)
32
+ end
33
+
34
+ private
35
+
36
+ def extract_request_ids(env)
37
+ request_ids = []
38
+ if env["HTTP_REQUEST_ID"]
39
+ request_ids = env["HTTP_REQUEST_ID"].split(",")
40
+ request_ids.map! { |id| id.strip }
41
+ request_ids.select! { |id| id =~ UUID_PATTERN }
42
+ end
43
+ request_ids
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,38 @@
1
+ module Jersey::Middleware
2
+ # Logs request info using the configured logger or the Jersey singleton
3
+ #
4
+ # Adds request_id to the default logger params
5
+ class RequestLogger
6
+ def initialize(app, options={})
7
+ @app = app
8
+ @logger = options[:logger] || Jersey.logger
9
+ end
10
+
11
+ def call(env)
12
+ @request_start = Time.now
13
+ request = Rack::Request.new(env)
14
+ start_data = {
15
+ at: "start",
16
+ request_id: env['REQUEST_ID'],
17
+ method: request.request_method,
18
+ path: request.path_info,
19
+ content_type: request.content_type,
20
+ content_length: request.content_length
21
+ }
22
+ @logger.log(start_data)
23
+ status, headers, response = @app.call(env)
24
+ @logger.log(
25
+ at: "finish",
26
+ method: request.request_method,
27
+ path: request.path_info,
28
+ status: status,
29
+ content_length: headers['Content-Length'],
30
+ route_signature: env['ROUTE_SIGNATURE'],
31
+ elapsed: (Time.now - @request_start).to_f,
32
+ request_id: env['REQUEST_ID']
33
+ )
34
+ @logger.reset!(:request_id)
35
+ [status, headers, response]
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,40 @@
1
+ require 'env-conf'
2
+
3
+ module Jersey
4
+ #require bundler and the proper gems for the ENV
5
+ def self.require
6
+ Kernel.require 'bundler'
7
+ Config.dotenv!
8
+ $stderr.puts "Loading #{Config.app_env} environment..."
9
+ Bundler.require(:default, Config.app_env)
10
+ end
11
+
12
+ # adds ./lib dir to the load path
13
+ def self.load_path
14
+ $stderr.puts "Adding './lib' to path..."
15
+ $LOAD_PATH.unshift(File.expand_path('./lib'))
16
+ end
17
+
18
+ # sets TZ to UTC and Sequel timezone to :utc
19
+ def self.set_timezones
20
+ $stderr.puts "Setting timezones to UTC..."
21
+ Sequel.default_timezone = :utc if defined? Sequel
22
+ ENV['TZ'] = 'UTC'
23
+ end
24
+
25
+ def self.hack_time_class
26
+ $stderr.puts "Modifying Time#to_s to use #iso8601..." if ENV['DEBUG']
27
+ # use send to call private method
28
+ Time.send(:define_method, :to_s) do
29
+ self.iso8601
30
+ end
31
+ end
32
+
33
+ # all in one go
34
+ def self.setup
35
+ self.require
36
+ self.load_path
37
+ self.set_timezones
38
+ self.hack_time_class
39
+ end
40
+ end
@@ -0,0 +1,29 @@
1
+ class Time
2
+ def last_month
3
+ (self.to_datetime << 1).to_time
4
+ end
5
+
6
+ def next_month
7
+ (self.to_datetime >> 1).to_time
8
+ end
9
+
10
+ def tomorrow
11
+ (self.to_datetime + 1).to_time
12
+ end
13
+
14
+ def yesterday
15
+ (self.to_datetime - 1).to_time
16
+ end
17
+ end
18
+
19
+ class Fixnum
20
+ def weeks
21
+ self.days * 7
22
+ end
23
+ alias :week :weeks
24
+
25
+ def days
26
+ self * 24 * 60 * 60
27
+ end
28
+ alias :day :days
29
+ end
@@ -0,0 +1,3 @@
1
+ module Jersey
2
+ VERSION = "0.0.3"
3
+ end
@@ -0,0 +1,48 @@
1
+ require_relative 'helper'
2
+
3
+ class ErrorsTest < ApiTest
4
+ class SimpleApi < Jersey::API::Base
5
+ get '/test-409' do
6
+ raise Conflict, "bad"
7
+ end
8
+
9
+ get '/test-500' do
10
+ raise InternalServerError, "bad"
11
+ end
12
+
13
+ get '/test-runtime-error' do
14
+ raise "boom!"
15
+ end
16
+ end
17
+
18
+ def app
19
+ SimpleApi
20
+ end
21
+
22
+ def test_not_found
23
+ get '/not-found'
24
+ assert_equal(404, last_response.status)
25
+ assert_equal('NotFound', json['error']['type'])
26
+ end
27
+
28
+ def test_http_errors_409
29
+ get '/test-409'
30
+ assert_equal(409, last_response.status)
31
+ assert_equal('Conflict', json['error']['type'])
32
+ assert_equal('bad', json['error']['message'])
33
+ end
34
+
35
+ def test_http_errors_500
36
+ get '/test-500'
37
+ assert_equal(500, last_response.status)
38
+ assert_equal('InternalServerError', json['error']['type'])
39
+ assert_equal('bad', json['error']['message'])
40
+ end
41
+
42
+ def test_http_errors_Undefined
43
+ get '/test-runtime-error'
44
+ assert_equal(500, last_response.status)
45
+ assert_equal('RuntimeError', json['error']['type'])
46
+ assert_equal('boom!', json['error']['message'])
47
+ end
48
+ end