diff --git a/lib/pliny/errors.rb b/lib/pliny/errors.rb index abfd7ca4..b2169795 100644 --- a/lib/pliny/errors.rb +++ b/lib/pliny/errors.rb @@ -1,117 +1,64 @@ +require "i18n" + module Pliny module Errors class Error < StandardError - attr_accessor :id - def self.render(error) - headers = { "Content-Type" => "application/json; charset=utf-8" } - data = { id: error.id, message: error.message } - [error.status, headers, [MultiJson.encode(data)]] - end + class << self + attr_accessor :error_class_id, :error_class_status - def initialize(message, id) - @id = id - super(message) + def render(error) + headers = { "Content-Type" => "application/json; charset=utf-8" } + data = { id: error.id, message: error.user_message }.merge(error.metadata) + [error.status, headers, [MultiJson.encode(data)]] + end end - end - class HTTPStatusError < Error - attr_accessor :status + attr_accessor :id, :status, :metadata - def initialize(message = nil, id = nil, status = nil) - meta = Pliny::Errors::META[self.class] - message = message || meta[1] + "." - id = id || meta[1].downcase.tr(' ', '_').to_sym - @status = status || meta[0] - super(message, id) + def initialize(id=nil, metadata: {}) + @id = (id || self.class.error_class_id).to_sym + @status = self.class.error_class_status + @metadata = metadata + super(@id.to_s) + end + + def user_message + I18n.t("errors.#{self.id}") end end - class Continue < HTTPStatusError; end # 100 - class SwitchingProtocols < HTTPStatusError; end # 101 - class OK < HTTPStatusError; end # 200 - class Created < HTTPStatusError; end # 201 - class Accepted < HTTPStatusError; end # 202 - class NonAuthoritativeInformation < HTTPStatusError; end # 203 - class NoContent < HTTPStatusError; end # 204 - class ResetContent < HTTPStatusError; end # 205 - class PartialContent < HTTPStatusError; end # 206 - class MultipleChoices < HTTPStatusError; end # 300 - class MovedPermanently < HTTPStatusError; end # 301 - class Found < HTTPStatusError; end # 302 - class SeeOther < HTTPStatusError; end # 303 - class NotModified < HTTPStatusError; end # 304 - class UseProxy < HTTPStatusError; end # 305 - class TemporaryRedirect < HTTPStatusError; end # 307 - class BadRequest < HTTPStatusError; end # 400 - class Unauthorized < HTTPStatusError; end # 401 - class PaymentRequired < HTTPStatusError; end # 402 - class Forbidden < HTTPStatusError; end # 403 - class NotFound < HTTPStatusError; end # 404 - class MethodNotAllowed < HTTPStatusError; end # 405 - class NotAcceptable < HTTPStatusError; end # 406 - class ProxyAuthenticationRequired < HTTPStatusError; end # 407 - class RequestTimeout < HTTPStatusError; end # 408 - class Conflict < HTTPStatusError; end # 409 - class Gone < HTTPStatusError; end # 410 - class LengthRequired < HTTPStatusError; end # 411 - class PreconditionFailed < HTTPStatusError; end # 412 - class RequestEntityTooLarge < HTTPStatusError; end # 413 - class RequestURITooLong < HTTPStatusError; end # 414 - class UnsupportedMediaType < HTTPStatusError; end # 415 - class RequestedRangeNotSatisfiable < HTTPStatusError; end # 416 - class ExpectationFailed < HTTPStatusError; end # 417 - class UnprocessableEntity < HTTPStatusError; end # 422 - class TooManyRequests < HTTPStatusError; end # 429 - class InternalServerError < HTTPStatusError; end # 500 - class NotImplemented < HTTPStatusError; end # 501 - class BadGateway < HTTPStatusError; end # 502 - class ServiceUnavailable < HTTPStatusError; end # 503 - class GatewayTimeout < HTTPStatusError; end # 504 + def self.make_error(status, id) + Class.new(Pliny::Errors::Error) do + @error_class_id = id + @error_class_status = status + end + end - # Messages for nicer exceptions, from rfc2616 - META = { - Continue => [100, 'Continue'], - SwitchingProtocols => [101, 'Switching protocols'], - OK => [200, 'OK'], - Created => [201, 'Created'], - Accepted => [202, 'Accepted'], - NonAuthoritativeInformation => [203, 'Non-authoritative information'], - NoContent => [204, 'No content'], - ResetContent => [205, 'Reset content'], - PartialContent => [206, 'Partial content'], - MultipleChoices => [300, 'Multiple choices'], - MovedPermanently => [301, 'Moved permanently'], - Found => [302, 'Found'], - SeeOther => [303, 'See other'], - NotModified => [304, 'Not modified'], - UseProxy => [305, 'Use proxy'], - TemporaryRedirect => [307, 'Temporary redirect'], - BadRequest => [400, 'Bad request'], - Unauthorized => [401, 'Unauthorized'], - PaymentRequired => [402, 'Payment required'], - Forbidden => [403, 'Forbidden'], - NotFound => [404, 'Not found'], - MethodNotAllowed => [405, 'Method not allowed'], - NotAcceptable => [406, 'Not acceptable'], - ProxyAuthenticationRequired => [407, 'Proxy authentication required'], - RequestTimeout => [408, 'Request timeout'], - Conflict => [409, 'Conflict'], - Gone => [410, 'Gone'], - LengthRequired => [411, 'Length required'], - PreconditionFailed => [412, 'Precondition failed'], - RequestEntityTooLarge => [413, 'Request entity too large'], - RequestURITooLong => [414, 'Request-URI too long'], - UnsupportedMediaType => [415, 'Unsupported media type'], - RequestedRangeNotSatisfiable => [416, 'Requested range not satisfiable'], - ExpectationFailed => [417, 'Expectation failed'], - UnprocessableEntity => [422, 'Unprocessable entity'], - TooManyRequests => [429, 'Too many requests'], - InternalServerError => [500, 'Internal server error'], - NotImplemented => [501, 'Not implemented'], - BadGateway => [502, 'Bad gateway'], - ServiceUnavailable => [503, 'Service unavailable'], - GatewayTimeout => [504, 'Gateway timeout'], - }.freeze + BadRequest = make_error(400, :bad_request) + Unauthorized = make_error(401, :unauthorized) + PaymentRequired = make_error(402, :payment_required) + Forbidden = make_error(403, :forbidden) + NotFound = make_error(404, :not_found) + MethodNotAllowed = make_error(405, :method_not_allowed) + NotAcceptable = make_error(406, :not_acceptable) + ProxyAuthenticationRequired = make_error(407, :proxy_authentication_required) + RequestTimeout = make_error(408, :request_timeout) + Conflict = make_error(409, :conflict) + Gone = make_error(410, :gone) + LengthRequired = make_error(411, :length_required) + PreconditionFailed = make_error(412, :precondition_failed) + RequestEntityTooLarge = make_error(413, :request_entity_too_large) + RequestURITooLong = make_error(414, :request_uri_too_long) + UnsupportedMediaType = make_error(415, :unsupported_media_type) + RequestedRangeNotSatisfiable = make_error(416, :requested_range_not_satisfiable) + ExpectationFailed = make_error(417, :expectation_failed) + UnprocessableEntity = make_error(422, :unprocessable_entity) + TooManyRequests = make_error(429, :too_many_requests) + InternalServerError = make_error(500, :internal_server_error) + NotImplemented = make_error(501, :not_implemented) + BadGateway = make_error(502, :bad_gateway) + ServiceUnavailable = make_error(503, :service_unavailable) + GatewayTimeout = make_error(504, :gateway_timeout) end end diff --git a/lib/template/Gemfile b/lib/template/Gemfile index 0ad58177..90b38db7 100644 --- a/lib/template/Gemfile +++ b/lib/template/Gemfile @@ -1,6 +1,7 @@ source "https://rubygems.org" ruby "2.2.3" +gem "i18n", "~> 0.7" gem "multi_json" gem "oj" gem "pg" diff --git a/lib/template/config/locales/en.yml b/lib/template/config/locales/en.yml new file mode 100644 index 00000000..373de593 --- /dev/null +++ b/lib/template/config/locales/en.yml @@ -0,0 +1,27 @@ +en: + errors: + bad_gateway: Bad gateway + bad_request: Bad request + conflict: Conflict + expectation_failed: Expectation failed + forbidden: Forbidden + gateway_timeout: Gateway timed out + gone: Gone + internal_server_error: Internal server error + length_required: Length required + method_not_allowed: Method not allowed + not_acceptable: Not acceptable + not_found: Not found + not_implemented: Not implemented + payment_required: Payment required + precondition_failed: Precondition failed + proxy_authentication_required: Proxy authentication required + request_entity_too_large: Request entity too large + request_timeout: Request timed out + request_uri_too_long: Requrest URI too long + requested_range_not_satisfiable: Requested range not satisfiable + service_unavailable: Service unavailable + too_many_requests: Too many requests + unauthorized: Unauthorized + unprocessable_entity: Unprocessable entity + unsupported_media_type: Unsupported media type diff --git a/lib/template/lib/endpoints/base.rb b/lib/template/lib/endpoints/base.rb index acc1d1bd..0c9d80ec 100644 --- a/lib/template/lib/endpoints/base.rb +++ b/lib/template/lib/endpoints/base.rb @@ -16,7 +16,7 @@ class Base < Sinatra::Base also_reload "#{Config.root}/lib/**/*.rb" end - error Sinatra::NotFound do + error Sinatra::NotFound, Sequel::NoMatchingRow do raise Pliny::Errors::NotFound end end diff --git a/lib/template/lib/initializer.rb b/lib/template/lib/initializer.rb index 02e6fd57..07396334 100644 --- a/lib/template/lib/initializer.rb +++ b/lib/template/lib/initializer.rb @@ -1,6 +1,7 @@ module Initializer def self.run require_config + load_locales require_lib require_initializers require_models @@ -10,6 +11,12 @@ def self.require_config require_relative "../config/config" end + def self.load_locales + I18n.config.enforce_available_locales = true + I18n.load_path += Dir[Config.root + "/config/locales/*.{rb,yml}"] + I18n.backend.load_translations + end + def self.require_lib require! %w( lib/endpoints/base diff --git a/pliny.gemspec b/pliny.gemspec index 96475c7c..66ba138d 100644 --- a/pliny.gemspec +++ b/pliny.gemspec @@ -16,6 +16,7 @@ Gem::Specification.new do |gem| gem.files = %x{ git ls-files }.split("\n").select { |d| d =~ %r{^(License|README|bin/|data/|ext/|lib/|spec/|test/)} } gem.add_dependency "activesupport", "~> 4.1", ">= 4.1.0" + gem.add_dependency "i18n", "~> 0.7", ">= 0.7" gem.add_dependency "multi_json", "~> 1.9", ">= 1.9.3" gem.add_dependency "prmd", "~> 0.7.0" gem.add_dependency "sinatra", "~> 1.4", ">= 1.4.5" diff --git a/spec/errors_spec.rb b/spec/errors_spec.rb index 2c115aa2..4c910a9c 100644 --- a/spec/errors_spec.rb +++ b/spec/errors_spec.rb @@ -1,24 +1,32 @@ require "spec_helper" describe Pliny::Errors do - it "includes a general error that requires an identifier" do - e = Pliny::Errors::Error.new("General error.", :general_error) - assert_equal "General error.", e.message - assert_equal :general_error, e.id + it "bundles classes to represent the different status codes" do + error = Pliny::Errors::BadRequest.new + assert_equal :bad_request, error.id + assert_equal 400, error.status + assert_equal "Bad request", error.user_message + + error = Pliny::Errors::InternalServerError.new + assert_equal :internal_server_error, error.id + assert_equal 500, error.status + assert_equal "Internal server error", error.user_message + end + + it "keeps the error id stored as the internal message" do + error = Pliny::Errors::BadRequest.new + assert_equal "bad_request", error.message end - it "includes an HTTP error that will take generic parameters" do - e = Pliny::Errors::HTTPStatusError.new( - "Custom HTTP error.", :custom_http_error, 499) - assert_equal "Custom HTTP error.", e.message - assert_equal :custom_http_error, e.id - assert_equal 499, e.status + it "takes a custom id" do + error = Pliny::Errors::BadRequest.new(:invalid_json) + assert_equal :invalid_json, error.id + assert_equal 400, error.status end - it "includes pre-defined HTTP error templates" do - e = Pliny::Errors::NotFound.new - assert_equal "Not found.", e.message - assert_equal :not_found, e.id - assert_equal 404, e.status + it "takes optional metadata" do + metadata = { foo: "bar" } + error = Pliny::Errors::BadRequest.new(:invalid_json, metadata: metadata) + assert_equal metadata, error.metadata end end diff --git a/spec/middleware/rescue_errors_spec.rb b/spec/middleware/rescue_errors_spec.rb index 85494540..c0a2cda3 100644 --- a/spec/middleware/rescue_errors_spec.rb +++ b/spec/middleware/rescue_errors_spec.rb @@ -23,7 +23,7 @@ def app assert_equal 503, last_response.status error_json = MultiJson.decode(last_response.body) assert_equal "service_unavailable", error_json["id"] - assert_equal "Service unavailable.", error_json["message"] + assert_equal "Service unavailable", error_json["message"] end it "intercepts exceptions and renders" do @@ -32,7 +32,7 @@ def app assert_equal 500, last_response.status error_json = MultiJson.decode(last_response.body) assert_equal "internal_server_error", error_json["id"] - assert_equal "Internal server error.", error_json["message"] + assert_equal "Internal server error", error_json["message"] end it "raises given the raise option" do diff --git a/spec/support/i18n.rb b/spec/support/i18n.rb new file mode 100644 index 00000000..c2bfc766 --- /dev/null +++ b/spec/support/i18n.rb @@ -0,0 +1,4 @@ +# configure i18n to use locales from the template app +I18n.config.enforce_available_locales = true +I18n.load_path += Dir[File.dirname(__FILE__) + "/../../lib/template/config/locales/*.{rb,yml}"] +I18n.backend.load_translations