jersey 0.0.3

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.
@@ -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