/json_api_errors

An implementation of JSON API error spec using composition, dependency injection, and the builder pattern.

Primary LanguageRubyMIT LicenseMIT

JsonApiErrors

Handling errors gracefully is pretty awesome.

I was having a hard time at that and so one of the steps I took was to create a simple error object, one that adhered to the Json API Error spec, not coupled to any other library.

Not all errors need to be handled gracefully, but for those that do its nice to see a response formatted in a way like it doesn't feel your world is crashing down (even though it is!):

{
  "errors": [
    {
      "id": "10002",
      "status": "400",
      "links": {
        "about": "www.info-about-the-error.org"
      },
      "code": "Bad Request",
      "title": "Your request didn't have a title attribute.",
      "detail": "Oopps! You'll need to add a title before you can proceed.",
      "source": {
        "pointer": "/data/attribute/title",
        "parameter": "title"
      },
      "meta": {
        "extra_info": "Here's a lollypop!"
      }
    }
  ]
}

While it's nice to have an easy way to build errors, this project was mainly an excuse to play around with object-oriented and functional concepts in Ruby. Because errors are highly variable, a reusable solution called for using concepts like dependency injection and composition. The idea of using blocks, callables, and building up the error object naturally came on later.

I hope this is at least of some use to you or that it inspires you to make something even better.

Installation

Add this line to your application's Gemfile:

gem 'json_api_errors'

And then execute:

$ bundle

Or install it yourself as:

$ gem install json_api_errors

Usage

So you want an error object? Well, all you need to do is this:

  error = JsonApiErrors::Error.new

  #<JsonApiErrors::Error:0x007ff753941ce8
     @code=#<JsonApiErrors::Default::Code:0x007ff7539416a8>,
     @detail=#<JsonApiErrors::Default::Detail:0x007ff7539414c8>,
     @error=#<JsonApiErrors::Default::Error:0x007ff753941c20>,
     @id=#<JsonApiErrors::Default::Id:0x007ff753941b58>,
     @links=#<JsonApiErrors::Default::Links:0x007ff753941680>,
     @meta=#<JsonApiErrors::Default::Meta:0x007ff753941428>,
     @source=#<JsonApiErrors::Default::Source:0x007ff753941478>,
     @status=#<JsonApiErrors::Default::Status:0x007ff753941b30>,
     @title=#<JsonApiErrors::Default::Title:0x007ff753941568>>

A JsonApiErrors::Error implements #call. So to get a hash representation of your error, which can be serialized into JSON, you need to send the #call message to your error object:

  error.call

  {:id=>"default-id",
   :links=>{:about=>"default-links"},
   :status=>"422",
   :code=>"default-code",
   :title=>"default-title",
   :detail=>"default-detail",
   :source=>{:pointer=>"default-pointer", :parameter=>"default-parameter"},
   :meta=>{:extra_info=>"default-meta"}}

All those defaults suck!

Yup. The Json API spec says that all those keys and values are OPTIONAL as defined by the RFC Key words for use in RFCs to Indicate Requirement Levels. These are all placeholders for the actual keys and values you care about including in your error response. So how do you create and error object that is actually meaningful to your situation?

The easiest way is probably to use a block with literal values to define the attributes you want to send in your response:

  error = JsonApiErrors::Error.new do |config|
    config.id     = "90210"
    config.status = "500"
    config.code   = "your-internal-server-error-app-code"
    config.title  = "Oh my!"
    config.detail = "If I knew what happened I would tell you"
    config.links  = { about: "www.example.com" }
    config.source = { pointer: "data/attributes/hmm", parameter: "hmm" }
    config.meta   = { sunshine: "It feels nice" }
  end

  error.call

  {:id=>"90210",
   :links=>{:about=>"www.example.com"},
   :status=>"500",
   :code=>"your-internal-server-error-app-code",
   :title=>"Oh my!",
   :detail=>"If I knew what happened I would tell you",
   :source=>{:pointer=>"data/attributes/hmm", :parameter=>"hmm"},
   :meta=>{:sunshine=>"It feels nice"}}

Now let's say you only want to send back the status and the code attributes:

  error = JsonApiErrors::Error.new do |config|
    config.id     = "90210"
    config.status = "500"
  end

  error.call

  {:id=>"90210",
   :links=>{:about=>"default-links"},
   :status=>"500",
   :code=>"default-code",
   :title=>"default-title",
   :detail=>"default-detail",
   :source=>{:pointer=>"default-pointer", :parameter=>"default-parameter"},
   :meta=>{:extra_info=>"default-meta"}}

Gross. Let's modify the template the error object uses to generate the hash representation:

  template = ->(error) do
    {
      status: error.status,
      code:   error.code
    }
  end

  error = JsonApiErrors::Error.new do |config|
    config.error  = template
    config.code   = "my-apps-custom-error-code"
    config.status = "500"
  end

  error.call

  {
    :status=>"500",
    :code=>"my-apps-custom-error-code"
  }

Nice! We passed in a lambda as a template. I said previously that the JsonApiErrors::Error implements #call. Well, that method is delegated to the error template's #call method. So instead of a lambda you could have created a class which implements #call and accepts an error object as an argument and then injected it into the initializer:

  class CustomTemplate
    def call(error)
      {
        status: error.status.to_s,
        code:   error.code.to_s
      }
    end
  end

  error = JsonApiErrors::Error.new( error: CustomTemplate.new ) do |config|
    config.code   = "my-apps-custom-error-code"
    config.status = "500"
  end

  error.call

  {
    :status=>"500",
    :code=>"my-apps-custom-error-code"
  }

In fact, all the properties of JsonApiErrors::Error are available as keyword arguments, so you could inject all the dependencies into the initializer. Or just as well use the config object in the block to customize everything to your liking.

Note that certain config options take strings while others take hashes. If you decide to create a class to represent a particular attribute, you'll need to override either the #to_s or #to_h. (This project was built using Ruby 2.2.2)

  class CustomLinks
    def to_h
      { about: "www.i-am-hash.com" }
    end
  end

  class CustomTitle
    def to_s
      "i-am-title"
    end
  end

The Json API spec mentions that all errors MUST have the keyword errors at the root. For this purpose there is JsonApiErrors::ErrorCollection. To get a fully qualified hash representation of your error you can:

  collection = JsonApiErrors::ErrorCollection.new
  collection.add_error(error)
  collection.call

  {:errors=>[
    {
      :status=>"500",
      :code=>"my-apps-custom-error-code" }]}

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/json_api_errors. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.

License

The gem is available as open source under the terms of the MIT License.