GithubHelp home page GithubHelp logo

tiagopog / jsonapi-utils Goto Github PK

View Code? Open in Web Editor NEW
215.0 14.0 82.0 404 KB

Build JSON API-compliant APIs on Rails with no (or less) learning curve.

License: MIT License

Ruby 99.89% Shell 0.11%
json-api rails ruby json api activerecord rest serializer resource utils

jsonapi-utils's Introduction

JSONAPI::Utils

Code Climate Gem Version Build Status

Simple yet powerful way to get your Rails API compliant with JSON API.

JSONAPI::Utils (JU) is built on top of JSONAPI::Resources taking advantage of its resource-driven style and bringing a set of helpers to easily build modern JSON APIs with no or less learning curve.

After installing the gem and defining the resources/routes, it's as simple as calling a render helper:

class UsersController < ActionController::Base
  include JSONAPI::Utils

  def index
    jsonapi_render json: User.all
  end
end

Table of Contents

Installation

Support:

  • Ruby 1.9+ with Rails 4
  • Ruby 2.4+ with Rails 5

For Rails 4 add this to your application's Gemfile:

gem 'jsonapi-utils', '~> 0.4.9'

For Rails 5+:

gem 'jsonapi-utils', '~> 0.7.3'

And then execute:

$ bundle

Why JSONAPI::Utils?

One of the main motivations behind JSONAPI::Utils is to keep things explicit in controllers (no hidden actions :-) so that developers can easily understand and maintain their code.

Unlike JSONAPI::Resources (JR), JU doesn't care about how you will operate your controller actions. The gem deals only with the request validation and response rendering (via JR's objects) and provides a set of helpers (renders, formatters etc) along the way. Thus developers can decide how to actually operate their actions: service objects, interactors etc.

Usage

Response

Renders

JU brings two main renders to the game, working pretty much the same way as Rails' ActionController#render method:

  • jsonapi_render
  • jsonapi_render_errors

jsonapi_render

It renders a JSON API-compliant response.

# app/controllers/users_controller.rb
# GET /users
def index
  jsonapi_render json: User.all
end

# GET /users/:id
def show
  jsonapi_render json: User.find(params[:id])
end

Arguments:

  • json: object to be rendered as a JSON document: ActiveRecord object, Hash or Array;
  • status: HTTP status code (Integer, String or Symbol). If ommited a status code will be automatically infered;
  • options:
    • resource: explicitly points the resource to be used in the serialization. By default, JU will select resources by inferencing from controller's name.
    • count: explicitly points the total count of records for the request in order to build a proper pagination. By default, JU will count the total number of records.
    • model: sets the model reference in cases when json is a Hash or a collection of Hashes.

Other examples:

# Specify a particular HTTP status code
jsonapi_render json: new_user, status: :created

# Forcing a different resource
jsonapi_render json: User.all, options: { resource: V2::UserResource }

# Using a specific count
jsonapi_render json: User.some_weird_scope, options: { count: User.some_weird_scope_count }

# Hash rendering
jsonapi_render json: { data: { id: 1, first_name: 'Tiago' } }, options: { model: User }

# Collection of Hashes rendering
jsonapi_render json: { data: [{ id: 1, first_name: 'Tiago' }, { id: 2, first_name: 'Doug' }] }, options: { model: User }

jsonapi_render_errors

It renders a JSON API-compliant error response.

# app/controllers/users_controller.rb
# POST /users
  def create
    user = User.new(user_params)
    if user.save
      jsonapi_render json: user, status: :created
    else
      jsonapi_render_errors json: user, status: :unprocessable_entity
    end
  end

Arguments:

  • Exception
  • json: object to be rendered as a JSON document: ActiveRecord, Exception, Array or any object which implements the errors method;
  • status: HTTP status code (Integer, String or Symbol). If ommited a status code will be automatically infered from the error body.

Other examples:

# Render errors from a custom exception:
jsonapi_render_errors Exceptions::MyCustomError.new(user)

# Render errors from an Array<Hash>:
errors = [{ id: 'validation', title: 'Something went wrong', code: '100' }]
jsonapi_render_errors json: errors, status: :unprocessable_entity

Formatters

In the backstage these are the guys which actually parse the ActiveRecord/Hash object to build a new Hash compliant with JSON API's specs. Formatters can be called anywhere in controllers being very useful if you need to do some work with the response's body before rendering the actual response.

Note: the resulting Hash from those methods can not be passed as argument to JSONAPI::Utils#jsonapi_render or JSONAPI::Utils#jsonapi_render_error, instead it needs to be rendered by the usual ActionController#render.

jsonapi_format

Because of semantic reasons JSONAPI::Utils#jsonapi_serialize was renamed to JSONAPI::Utils#jsonapi_format.

# app/controllers/users_controller.rb
def index
  body = jsonapi_format(User.all)
  render json: do_some_magic_with(body)
end

Arguments:

  • First: ActiveRecord object, Hash or Array;
  • Last: Hash of options (same as JSONAPI::Utils#jsonapi_render).

Paginators

Pagination works out of the box on JU, you just need to decide which kind of paginator you'd like to use.

It's really easy to work with pagination on JU, actually it's just a matter of chosing the paginator you wish in your JR's config file:

# config/initializers/jsonapi_resources.rb
JSONAPI.configure do |config|
  # :none, :offset, :paged, or a custom paginator name
  config.default_paginator = :paged

  # Output pagination links at top level
  config.top_level_links_include_pagination = true
  
  # Default sizes
  config.default_page_size = 70
  config.maximum_page_size = 100
end

As you may have noticed above, it's possible to use custom paginators. In order to create your own paginator your just need to define a class which inherits from JSONAPI::Paginator and implements the #pagination_range method which in turn must return the range to be applied over the resulting collection.

For example, if you would like to paginate over a collection of hashes, you may implement the #pagination_range method as below:

class CustomPaginator < JSONAPI::Paginator
  def pagination_range(page_params)
    offset = page_params['offset']
    limit  = JSONAPI.configuration.default_page_size
    offset..offset + limit - 1 # resulting range
  end

And then it can be either set at the resource class level (e.g. UserResource.paginator :custom) or via config initializer:

# config/initializers/jsonapi_resources.rb
JSONAPI.configure do |config|
  config.default_paginator = :custom
end

Request

Before a controller action gets executed, JSONAPI::Utils will validate the request against JSON API's specs as well as evaluating the eventual query string params to check if they match the resource's definition. If something goes wrong during the validation process, JU will render an error response like this examples below:

HTTP/1.1 400 Bad Request
Content-Type: application/vnd.api+json

{
  "errors": [
    {
      "title": "Invalid resource",
      "detail": "foo is not a valid resource.",
      "code": "101",
      "status": "400"
    },
    {
      "title": "Invalid resource",
      "detail": "foobar is not a valid resource.",
      "code": "101",
      "status": "400"
    },
    {
      "title": "Invalid field",
      "detail": "bar is not a valid relationship of users",
      "code": "112",
      "status": "400"
    }
  ]
}

Params helpers

JU brings helper methods as a shortcut to get values from permitted params based on the resource's configuration.

  • resource_params:
    • Returns the permitted params present in the attributes JSON member;
      • Example: { name: 'Bilbo', gender: 'male', city: 'Shire' }
    • Same of calling: params.require(:data).require(:attributes).permit(:name, :gender, :city)
  • relationship_params:
    • Returns the relationship ids, distinguished by key, present in relationships JSON member;
      • Example: { author: 1, posts: [1, 2, 3] }
    • Same as calling: params.require(:relationships).require(:author).require(:data).permit(:id)

Full example

After installing the gem you simply need to:

  1. Include the gem's module (include JSONAPI::Utils) in a controller (eg. BaseController);
  2. Define the resources which will be exposed via REST API;
  3. Define the application's routes;
  4. Use JSONAPI Utils' helper methods (eg. renders, formatters, params helpers etc).

Ok, now it's time to start our complete example. So, let's say we have a Rails application for a super simple blog:

Models

# app/models/user.rb
class User < ActiveRecord::Base
  has_many :posts
  validates :first_name, :last_name, presence: true
end

# app/models/post.rb
class Post < ActiveRecord::Base
  belongs_to :author, class_name: 'User', foreign_key: 'user_id'
  validates :title, :body, presence: true
end

Resources

Here is where we define how our models are exposed as resources on the API:

# app/resources/user_resource.rb
class UserResource < JSONAPI::Resource
  attributes :first_name, :last_name, :full_name, :birthday

  has_many :posts

  def full_name
    "#{@model.first_name} #{@model.last_name}"
  end
end

# app/resources/post_resource.rb
class PostResource < JSONAPI::Resource
  attributes :title, :body
  has_one :author
end

Routes & Controllers

Let's define the routes using the jsonapi_resources method provided by JR:

Rails.application.routes.draw do
  jsonapi_resources :users do
    jsonapi_resources :posts
  end
end

In controllers we just need to include the JSONAPI::Utils module.

Note: some default rendering can be set like the below example where jsonapi_render_not_found is used when a record is not found in the database.

# app/controllers/base_controller.rb
class BaseController < ActionController::Base
  include JSONAPI::Utils
  protect_from_forgery with: :null_session
  rescue_from ActiveRecord::RecordNotFound, with: :jsonapi_render_not_found
end

With the helper methods inhirited from JSONAPI::Utils in our BaseController, now it's all about to write our actions like the following:

# app/controllers/users_controller.rb
class UsersController < BaseController
  # GET /users
  def index
    users = User.all
    jsonapi_render json: users
  end

  # GET /users/:id
  def show
    user = User.find(params[:id])
    jsonapi_render json: user
  end

  # POST /users
  def create
    user = User.new(resource_params)
    if user.save
      jsonapi_render json: user, status: :created
    else
      jsonapi_render_errors json: user, status: :unprocessable_entity
    end
  end

  # PATCH /users/:id
  def update
    user = User.find(params[:id])
    if user.update(resource_params)
      jsonapi_render json: user
    else
      jsonapi_render_errors json: user, status: :unprocessable_entity
    end
  end

  # DELETE /users/:id
  def destroy
    User.find(params[:id]).destroy
    head :no_content
  end
end

And:

# app/controllers/posts_controller.rb
class PostsController < BaseController
  before_action :load_user, except: :create

  # GET /users/:user_id/posts
  def index
    jsonapi_render json: @user.posts, options: { count: 100 }
  end

  # GET /users/:user_id/posts/:id
  def show
    jsonapi_render json: @user.posts.find(params[:id])
  end

  # POST /posts
  def create
    post = Post.new(post_params)
    if post.save
      jsonapi_render json: post, status: :created
    else
      jsonapi_render_errors json: post, status: :unprocessable_entity
    end
  end

  private

  def post_params
    resource_params.merge(user_id: relationship_params[:author])
  end

  def load_user
    @user = User.find(params[:user_id])
  end
end

Initializer

In order to enable a proper pagination, record count etc, an initializer could be defined such as:

# config/initializers/jsonapi_resources.rb
JSONAPI.configure do |config|
  config.json_key_format = :underscored_key
  config.route_format = :dasherized_route

  config.allow_include = true
  config.allow_sort = true
  config.allow_filter = true

  config.raise_if_parameters_not_allowed = true

  config.default_paginator = :paged

  config.top_level_links_include_pagination = true

  config.default_page_size = 10
  config.maximum_page_size = 20

  config.top_level_meta_include_record_count = true
  config.top_level_meta_record_count_key = :record_count

  config.top_level_meta_include_page_count = true
  config.top_level_meta_page_count_key = :page_count

  config.use_text_errors = false

  config.exception_class_whitelist = []

  config.always_include_to_one_linkage_data = false
end

You may want a different configuration for your API. For more information check this.

Requests & Responses

Here are examples of requests – based on those sample controllers – and their respective JSON responses.

Index

Request:

GET /users HTTP/1.1
Accept: application/vnd.api+json

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": [
    {
      "id": "1",
      "type": "users",
      "links": {
        "self": "http://api.myblog.com/users/1"
      },
      "attributes": {
        "first_name": "Tiago",
        "last_name": "Guedes",
        "full_name": "Tiago Guedes",
        "birthday": null
      },
      "relationships": {
        "posts": {
          "links": {
            "self": "http://api.myblog.com/users/1/relationships/posts",
            "related": "http://api.myblog.com/users/1/posts"
          }
        }
      }
    },
    {
      "id": "2",
      "type": "users",
      "links": {
        "self": "http://api.myblog.com/users/2"
      },
      "attributes": {
        "first_name": "Douglas",
        "last_name": "André",
        "full_name": "Douglas André",
        "birthday": null
      },
      "relationships": {
        "posts": {
          "links": {
            "self": "http://api.myblog.com/users/2/relationships/posts",
            "related": "http://api.myblog.com/users/2/posts"
          }
        }
      }
    }
  ],
  "meta": {
    "record_count": 2
  },
  "links": {
    "first": "http://api.myblog.com/users?page%5Bnumber%5D=1&page%5Bsize%5D=10",
    "last": "http://api.myblog.com/users?page%5Bnumber%5D=1&page%5Bsize%5D=10"
  }
}

Index (options)

Request:

GET /users?include=posts&fields[users]=first_name,last_name,posts&fields[posts]=title&sort=first_name,last_name&page[number]=1&page[size]=1 HTTP/1.1
Accept: application/vnd.api+json

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": [
    {
      "id": "2",
      "type": "users",
      "links": {
        "self": "http://api.myblog.com/users/2"
      },
      "attributes": {
        "first_name": "Douglas",
        "last_name": "André"
      },
      "relationships": {
        "posts": {
          "links": {
            "self": "http://api.myblog.com/users/2/relationships/posts",
            "related": "http://api.myblog.com/users/2/posts"
          },
          "data": []
        }
      }
    },
    {
      "id": "1",
      "type": "users",
      "links": {
        "self": "http://api.myblog.com/users/1"
      },
      "attributes": {
        "first_name": "Tiago",
        "last_name": "Guedes"
      },
      "relationships": {
        "posts": {
          "links": {
            "self": "http://api.myblog.com/users/1/relationships/posts",
            "related": "http://api.myblog.com/users/1/posts"
          },
          "data": [
            {
              "type": "posts",
              "id": "1"
            }
          ]
        }
      }
    }
  ],
  "included": [
    {
      "id": "1",
      "type": "posts",
      "links": {
        "self": "http://api.myblog.com/posts/1"
      },
      "attributes": {
        "title": "An awesome post"
      }
    }
  ],
  "meta": {
    "record_count": 2
  },
  "links": {
    "first": "http://api.myblog.com/users?fields%5Bposts%5D=title&fields%5Busers%5D=first_name%2Clast_name%2Cposts&include=posts&page%5Blimit%5D=2&page%5Boffset%5D=0&sort=first_name%2Clast_name",
    "last": "http://api.myblog.com/users?fields%5Bposts%5D=title&fields%5Busers%5D=first_name%2Clast_name%2Cposts&include=posts&page%5Blimit%5D=2&page%5Boffset%5D=0&sort=first_name%2Clast_name"
  }
}

Show

Request:

GET /users/1 HTTP/1.1
Accept: application/vnd.api+json

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": {
    "id": "1",
    "type": "users",
    "links": {
      "self": "http://api.myblog.com/users/1"
    },
    "attributes": {
      "first_name": "Tiago",
      "last_name": "Guedes",
      "full_name": "Tiago Guedes",
      "birthday": null
    },
    "relationships": {
      "posts": {
        "links": {
          "self": "http://api.myblog.com/users/1/relationships/posts",
          "related": "http://api.myblog.com/users/1/posts"
        }
      }
    }
  }
}

Show (options)

Request:

GET /users/1?include=posts&fields[users]=full_name,posts&fields[posts]=title HTTP/1.1
Accept: application/vnd.api+json

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": {
    "id": "1",
    "type": "users",
    "links": {
      "self": "http://api.myblog.com/users/1"
    },
    "attributes": {
      "full_name": "Tiago Guedes"
    },
    "relationships": {
      "posts": {
        "links": {
          "self": "http://api.myblog.com/users/1/relationships/posts",
          "related": "http://api.myblog.com/users/1/posts"
        },
        "data": [
          {
            "type": "posts",
            "id": "1"
          }
        ]
      }
    }
  },
  "included": [
    {
      "id": "1",
      "type": "posts",
      "links": {
        "self": "http://api.myblog.com/posts/1"
      },
      "attributes": {
        "title": "An awesome post"
      }
    }
  ]
}

Relationships (identifier objects)

Request:

GET /users/1/relationships/posts HTTP/1.1
Accept: application/vnd.api+json

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "links": {
    "self": "http://api.myblog.com/users/1/relationships/posts",
    "related": "http://api.myblog.com/users/1/posts"
  },
  "data": [
    {
      "type": "posts",
      "id": "1"
    }
  ]
}

Nested resources

Request:

GET /users/1/posts HTTP/1.1
Accept: application/vnd.api+json

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": [
    {
      "id": "1",
      "type": "posts",
      "links": {
        "self": "http://api.myblog.com/posts/1"
      },
      "attributes": {
        "title": "An awesome post",
        "body": "Lorem ipsum dolot sit amet"
      },
      "relationships": {
        "author": {
          "links": {
            "self": "http://api.myblog.com/posts/1/relationships/author",
            "related": "http://api.myblog.com/posts/1/author"
          }
        }
      }
    }
  ],
  "meta": {
    "record_count": 1
  },
  "links": {
    "first": "http://api.myblog.com/posts?page%5Bnumber%5D=1&page%5Bsize%5D=10",
    "last": "http://api.myblog.com/posts?page%5Bnumber%5D=1&page%5Bsize%5D=10"
  }
}

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake rspec 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/tiagopog/jsonapi-utils. 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.

jsonapi-utils's People

Contributors

alwesam avatar andrewpaek avatar austenito avatar brateq avatar chrisdpeters avatar corps avatar coupling avatar danielgomezrico avatar douglasandre avatar grk avatar jayfredlund avatar krasnoukhov avatar mecampbellsoup avatar parad0x avatar tiagopog avatar tramuntanal avatar yellow5 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

jsonapi-utils's Issues

Possible to jsonapi_render and pass an `include` option?

I need to render an Application instance (AR::Base inherited) which has associated users.

I want to side-load those users, i.e. have them serialized beneath an included key, following this type of pattern:

{
  "data": {
    "type": "application",
    "id": "f18ba709-4903-4024-856d-cb0a4539f498",
    "links": {
      "self": "https://sandbox.opentransact.com/v1/applications/f18ba709-4903-4024-856d-cb0a4539f498"
    },
    "attributes": {
      "name": "Zipmark, Inc. Deposit Application"
    },
    "relationships": {
      "users": {
        "data": [
          { "type": "user", "id": "de4a83b6-ba4f-40db-89d2-a5c4078d9a19" }
        ]
      }
  },
  "included": [{
    "type": "user",
    "id": "de4a83b6-ba4f-40db-89d2-a5c4078d9a19",
    "links": {
      "self": "https://sandbox.opentransact.com/v1/users/de4a83b6-ba4f-40db-89d2-a5c4078d9a19"
    },
    "attributes": {
      "email": "[email protected]",
      "password": "password",
      "name": "James Dean"
    }
  }
}

I tried invoking it as jsonapi_render json: application, include: ['credentials'] which is how jsonapi-resources does it.

Is this possible?

Filter keys not being passed to the model in the correct format

A simple example resource with filterable multi-word attribute:

class PageResource < JSONAPI::Resource
  attribute :trashed_at
  filter :trashed_at
end

And a contrived spec that attempts to use the attribute:

it 'returns an array containing the non-trashed page', :aggregate_failures do
  get('/pages', { filter: { 'trashed-at' => nil })
  expect(response).to be_ok
end

The response ends up including an error that indicates that the filter isn't being translated back to underscored before being passed to the model:

{
  "errors": [
    {
      "title": "Internal Server Error",
      "detail": "Internal Server Error",
      "code": "500",
      "status": "500",
      "meta": {
        "exception": "PG::UndefinedColumn:    ERROR:  column pages.trashed-at does not exist ..."
      }
    }
  ]
}

I have these configurations in place:

#:underscored_key, :camelized_key, :dasherized_key, or custom
config.json_key_format = :dasherized_key

#:underscored_route, :camelized_route, :dasherized_route, or custom
config.route_format = :dasherized_route

One To One Relation; relation_link is broken

Hi !!
I have one to one relation between model such as

class Plan < ActiveRecord::Base
  has_one :subscription
end

class Subscription < ActiveRecord::Base
  belongs_to :plan
end

So that I could define resources like,

class PlanResource < JSONAPI::Resource
  immutable

  attributes :uuid,
             :amount,
             :name,
             :created_at,
             :updated_at

  has_one :subscription, class_name: 'Subscription'
end

class SubscriptionResource < JSONAPI::Resource
  immutable

  attributes :stripe_subscription_id,
             :created_at,
             :updated_at

  has_one :plan, class_name: 'Plan', foreign_key: 'plan_id'
end

And then when I send the request to
http://localhost:3000/plans/1/relationships/subscription.
I can see the errors below;

{
  "errors": [
    {
      "title": "Internal Server Error",
      "detail": "Internal Server Error",
      "id": null,
      "href": null,
      "code": 500,
      "source": null,
      "links": null,
      "status": "500",
      "meta": {
        "exception": "undefined method `subscription_id' for class `#<Class:0x007ff5e0e4ffb0>'",
        "backtrace": [
          "/Users/toshikiinami/Desktop/billing/vendor/bundle/ruby/2.3.0/gems/jsonapi-resources-0.7.0/lib/jsonapi/resource.rb:804:in `method'",
          "/Users/toshikiinami/Desktop/billing/vendor/bundle/ruby/2.3.0/gems/jsonapi-resources-0.7.0/lib/jsonapi/resource.rb:804:in `block (2 levels) in _add_relationship'",
          "/Users/toshikiinami/Desktop/billing/vendor/bundle/ruby/2.3.0/gems/jsonapi-resources-0.7.0/lib/jsonapi/resource_serializer.rb:300:in `public_send'",
          "/Users/toshikiinami/Desktop/billing/vendor/bundle/ruby/2.3.0/gems/jsonapi-resources-0.7.0/lib/jsonapi/resource_serializer.rb:300:in `foreign_key_value'",
          "/Users/toshikiinami/Desktop/billing/vendor/bundle/ruby/2.3.0/gems/jsonapi-resources-0.7.0/lib/jsonapi/resource_serializer.rb:249:in `to_one_linkage'",
          "/Users/toshikiinami/Desktop/billing/vendor/bundle/ruby/2.3.0/gems/jsonapi-resources-0.7.0/lib/jsonapi/resource_serializer.rb:61:in `serialize_to_links_hash'",
          "/Users/toshikiinami/Desktop/billing/vendor/bundle/ruby/2.3.0/gems/jsonapi-resources-0.7.0/lib/jsonapi/response_document.rb:113:in `results_to_hash'",
          "/Users/toshikiinami/Desktop/billing/vendor/bundle/ruby/2.3.0/gems/jsonapi-resources-0.7.0/lib/jsonapi/response_document.rb:11:in `contents'",
          "/Users/toshikiinami/Desktop/billing/vendor/bundle/ruby/2.3.0/gems/jsonapi-resources-0.7.0/lib/jsonapi/acts_as_resource_controller.rb:155:in `render_results'",
          "/Users/toshikiinami/Desktop/billing/vendor/bundle/ruby/2.3.0/gems/jsonapi-resources-0.7.0/lib/jsonapi/acts_as_resource_controller.rb:64:in `process_request'",
          "/Users/toshikiinami/Desktop/billing/vendor/bundle/ruby/2.3.0/gems/jsonapi-resources-0.7.0/lib/jsonapi/acts_as_resource_controller.rb:21:in `show_relationship'",
          "/Users/toshikiinami/Desktop/billing/vendor/bundle/ruby/2.3.0/gems/actionpack-4.2.5.1/lib/action_controller/metal/implicit_render.rb:4:in `send_action'",
          "/Users/toshikiinami/Desktop/billing/vendor/bundle/ruby/2.3.0/gems/actionpack-4.2.5.1/lib/abstract_controller/base.rb:198:in `process_action'",
          "/Users/toshikiinami/Desktop/billing/vendor/bundle/ruby/2.3.0/gems/actionpack-4.2.5.1/lib/action_controller/metal/rendering.rb:10:in `process_action'",
          "/Users/toshikiinami/Desktop/billing/vendor/bundle/ruby/2.3.0/gems/actionpack-4.2.5.1/lib/abstract_controller/callbacks.rb:20:in `block in process_action'",
          "/Users/toshikiinami/Desktop/billing/vendor/bundle/ruby/2.3.0/gems/activesupport-4.2.5.1/lib/active_support/callbacks.rb:117:in `call'",
          "/Users/toshikiinami/Desktop/billing/vendor/bundle/ruby/2.3.0/gems/activesupport-4.2.5.1/lib/active_support/callbacks.rb:555:in `block (2 levels) in compile'",
          "/Users/toshikiinami/Desktop/billing/vendor/bundle/ruby/2.3.0/gems/activesupport-4.2.5.1/lib/active_support/callbacks.rb:505:in `call'",
          "/Users/toshikiinami/Desktop/billing/vendor/bundle/ruby/2.3.0/gems/activesupport-4.2.5.1/lib/active_support/callbacks.rb:92:in `__run_callbacks__'",
          "/Users/toshikiinami/Desktop/billing/vendor/bundle/ruby/2.3.0/gems/activesupport-4.2.5.1/lib/active_support/callbacks.rb:778:in `_run_process_action_callbacks'",
          "/Users/toshikiinami/Desktop/billing/vendor/bundle/ruby/2.3.0/gems/activesupport-4.2.5.1/lib/active_support/callbacks.rb:81:in `run_callbacks'",
          "/Users/toshikiinami/Desktop/billing/vendor/bundle/ruby/2.3.0/gems/actionpack-4.2.5.1/lib/abstract_controller/callbacks.rb:19:in `process_action'",
          "/Users/toshikiinami/Desktop/billing/vendor/bundle/ruby/2.3.0/gems/actionpack-4.2.5.1/lib/action_controller/metal/rescue.rb:29:in `process_action'",
          "/Users/toshikiinami/Desktop/billing/vendor/bundle/ruby/2.3.0/gems/actionpack-4.2.5.1/lib/action_controller/metal/instrumentation.rb:32:in `block in process_action'",
          "/Users/toshikiinami/Desktop/billing/vendor/bundle/ruby/2.3.0/gems/activesupport-4.2.5.1/lib/active_support/notifications.rb:164:in `block in instrument'",
          "/Users/toshikiinami/Desktop/billing/vendor/bundle/ruby/2.3.0/gems/activesupport-4.2.5.1/lib/active_support/notifications/instrumenter.rb:20:in `instrument'",
          "/Users/toshikiinami/Desktop/billing/vendor/bundle/ruby/2.3.0/gems/activesupport-4.2.5.1/lib/active_support/notifications.rb:164:in `instrument'",
          "/Users/toshikiinami/Desktop/billing/vendor/bundle/ruby/2.3.0/gems/actionpack-4.2.5.1/lib/action_controller/metal/instrumentation.rb:30:in `process_action'",

When I used to use JR the result is following:

{
  "links": {
    "self": "http://localhost:3000/plans/1/relationships/subscription",
    "related": "http://localhost:3000/plans/1/subscription"
  },
  "data": null
}

Maybe the controller is not generated automatically? or I've been missing something?

screen shot 2017-01-29 at 11 27 52

**I removed api/v1 in the path for the sake of clarity within the code above. **

Support for page_count in response meta field

Howdy,
first I'd like to thank you for your good work.

jsonapi-resources have a config to show both the record count and the page count. it would be nice to be able to support both, and the flexibility of choosing the key name:

  # Metadata
  # Output record count in top level meta for find operation
  config.top_level_meta_include_record_count = true
  config.top_level_meta_record_count_key     = :record_count
  config.top_level_meta_include_page_count   = true
  config.top_level_meta_page_count_key       = :page_count

the line I found useful in the original gem is the following:

https://github.com/cerebris/jsonapi-resources/blob/0eaa40c509a21e2d4cf02a52e6139fe30465260b/lib/jsonapi/processor.rb#L94

what I did in the meantime is override the module like so:

module JSONAPI
  module Utils
    module Response
      module Formatters
        private

        def calculate_page_count record_count
          (record_count / paginator.size.to_f).ceil
        end

        # Overriding method to also return page count in the meta response
        def result_options records, options
          {}.tap do |data|
            if JSONAPI.configuration.default_paginator != :none &&
              JSONAPI.configuration.top_level_links_include_pagination
              data[:pagination_params] = pagination_params(records, options)
            end

            if JSONAPI.configuration.top_level_meta_include_record_count
              data[:record_count] = count_records(records, options)
              data[:page_count]   = calculate_page_count(data[:record_count])
            end
          end
        end
      end
    end
  end
end

would be nice to have this baked in to the gem instead of having to do an override.

Thanks!

JSONAPI::Utils::Exceptions::BadRequest undefined

Howdy!

I'm using the following gems:

  • jsonapi-resources (0.9.0)
  • jsonapi-utils (0.7.2)

I recently started using this gem and have found it quite useful. Thanks. However I tried to use jsonapi_render_bad_request and I got the following error:

# NameError:
#   uninitialized constant JSONAPI::Utils::Exceptions::BadRequest

It looks like the original refactoring to split up the exceptions into separate files resulted in BadRequest getting skipped/forgotten.

It seems, and this is without knowing the current design philosophy, updating jsonapi_render_bad_request to use JSONAPI::Exceptions::BadRequest directly, overriding detail: to match the original Utils BadRequest implementation could be one solution.

Another would be to implement JSONAPI::Utils::Exceptions::BadRequest.

Another would be to remove jsonapi_render_bad_request.

I'm happy to submit a pull request for any of those proposed solutions.

Rendering scopes with `includes` is slow for no reason

We have in our codebase something like this:

class AppointmentsController < BaseController
  # GET /api/v2/appointments
  def index
    appointments = Appointment.all.includes(:professional, :service)
    jsonapi_render json: appointments
  end
end

We use includes to eager load associations and avoid the N+1 query problem.
However, the record_count query becomes very slow since there is now a LEFT OUTER JOIN for each eager loded association:

SELECT DISTINCT COUNT(DISTINCT appointments.id) FROM "appointments" LEFT OUTER JOIN "professionals" ON "professionals"."id" = "appointments"."employment_id" LEFT OUTER JOIN "services" ON "services"."id" = "appointments"."service_id"

I suppose a simple fix could be done by changing this line from this:

def count_records(records, options)
  ...
  records.except(:group, :order).count("DISTINCT #{records.table.name}.id")
end

to this:

def count_records(records, options)
  ...
  records.except(:group, :includes, :order).count("DISTINCT #{records.table.name}.id")
end

By the way, can you please backport this patch to the 0.4 series as well?

Primary Key: uuid

I've been using json-api resource and want to move on to this library.

However I just wonder if I could use uuid instead of id for resource sometimes.

In json-api resource, we can do something like:

class ContactResource < JSONAPI::Resource
  attribute :id
  attributes :name_first, :name_last, :email, :twitter
  key_type :uuid
end

I wonder if I could do the same thing in jsonapi-utils.

Customized resource filters are not applied

It appears that trying to customize a filter doesn't work. Given a resource that looks like this:

class UserResource < JSONAPI::Resource
  attributes :first_name, :last_name, :full_name

  attribute :full_name

  has_many :posts

  filter :first_name, apply: ->(records, value, _options) {
    # customize how the filter is applied - this never runs
    records.where(first_name: 'Steve')
  }

  def full_name
    "#{@model.first_name} #{@model.last_name}"
  end
end

Assuming a stock controller and routes, the following request:

/users?filter[first_name]=Mark

results in the default filter being applied, not the customized lambda specified.

Passing resource relationship in POST request

I have two models that we'd like to be created in the same request.

Resource

module API
  module V1
    class ProjectToolboxTalkResource < JSONAPI::Resource
      attributes :date

      has_one :toolbox_talk
      has_one :superintendent
      has_one :project
      has_many :attendees
      has_many :project_toolbox_talk_users
    end
  end
end

POST Request

{
  "data": {
    "type": "project_toolbox_talks",
    "attributes": { "date": "2017-12-11" },
    "relationships": {
      "project_toolbox_talk_users": {
        "data": [
          { 
            "type": "project_toolbox_talk_users",
            "attributes": { "user_id": "559ff2c9-beb6-47cd-9757-66104617403b" }
          }
        ]
      },
      "projects": {
        "data": {
          "type": "projects", "id": "d9b28ffd-6f30-4dd0-a227-720caa9b881e"
        }
      }
    }
  }
}

When I make the POST request I get the following error even though I have linked project with has_one :project

{
  "errors": [
    {
      "code": "105",
      "detail": "projects is not allowed.",
      "status": "400",
      "title": "Param not allowed"
    }
  ]
}

What am I not understanding?

Adding JSONAPI::Utils to controller causes PATCH operations to malfunction

Affects jsonapi-utils 0.5.0.beta4, jsonapi-resources 0.8.1 and Rails 5.0.0.1.

Given a model SampleModel:

class SampleModel < ApplicationRecord
  # name: :string
end

And a resource SampleModelResource:

class SampleModelResource < JSONAPI::Resource
  attributes :name
end

With controllers:

class JsonApiController < JSONAPI::ResourceController
  include JSONAPI::Utils
end

class SampleModelsController < JsonApiController
end

And routes:

Rails.application.routes.draw do
  jsonapi_resources :sample_models
end

And a controller spec that performs a patch operation:

require 'rails_helper'

describe SampleModelsController, type: :controller do
  before do
    request.headers['Content-Type'] = "application/vnd.api+json"
  end

  describe 'PATCH #update' do
    subject { patch :update, params: params, body: body.to_json }

    let(:params) { { id: id } }
    let(:body) { { data: { id: id, type: 'sample_models', attributes: { name: 'bar' } } }
    let(:sample_model) { SampleModel.new(name: 'foo').tap { |m| m.save } }
    let(:id) { sample_model.id }
    
    it { expect(subject.status).to eq(200) }
  end
end

Running PATCH raises a 400 Bad Request, which manifests as:

{"errors":[{"title":"A key is required","detail":"The resource object does not contain a key.","code":"109","status":"400"}]}

I dug deeper, and what I found was that JSONAPI::Utils adds a Rails callback that creates a JSONAPI::RequestParser prior to the request being processed:

def setup_request
  @request ||=
    JSONAPI::RequestParser.new(
      params.dup,
      context: context,
      key_formatter: key_formatter,
      server_error_callbacks: (self.class.server_error_callbacks || [])
    )
end

This ends up running some code in jsonapi-resources which parses the data parameters for the PATCH operation. In the process of parsing this data, it performs a delete on the data hash.

def parse_single_replace_operation(data, keys, id_key_presence_check_required: true)
  fail JSONAPI::Exceptions::MissingKey.new if data[:id].nil?

  key = data[:id].to_s
  if id_key_presence_check_required && !keys.include?(key)
    fail JSONAPI::Exceptions::KeyNotIncludedInURL.new(key)
  end

  data.delete(:id) unless keys.include?(:id)

  # ...
end

Now the JSONAPI::Utils callback finishes without issue. However, when JSONAPI::RequestParseris invoked again as Rails runs the actual action, the data hash in params no long has id, which is required. It raises a MissingKey error, and the request fails.

In summary...

It seems like calling JSONAPI::RequestHelper twice with the same parameters is not safe. Despite JSONAPI::Util's effort to params.dup prior to invoking it, the JSONAPI::RequestHelper code still modifies the underlying data hash in a pretty bad way.

Frankly, this seems to be more of an issue with jsonapi-resources than jsonapi-utils. But I thought I'd bring it to your attention, because as it stands, it means one cannot compose your package into a JSON API controller.

Are there any feasible workarounds? Or should an issue be opened with jsonapi-resources?

Hide relationships links

Hi,
I would like to hide the relationship links from the response, while still displaying the included data of the related resources (using the include parameter).
Is that possible with the gem, or with some work-around?

Thank you!

Pagination for each resources

First off, this gem is pretty awesome!

This is not an issue but I just wonder about the pagination.

I know we can define the default pagination, but could I set the pagination for each models/resources?
Should I use library kaminari or something?

0.5.0.beta2 not on rubygems

README says to use gem 'jsonapi-utils', '0.5.0.beta2' but beta2 is not yet released to rubygems. Only beta1 is available.

Inclusion of JSONAPI::Utils in a specific controller obfuscates errors

Hi there,

Just had a hard time tracking down an issue with an update I was doing on an a record.
I included JSONAPI::Utils in my controller, and implemented it in my index method.

The update method was failing in jsonapi-resources because an attribute was included in the request that wasn't supposed to be there, so it was failing. However as long as JSONAPI::Utils was included, the error that came back was that the key was not included {"errors":[{"title":"A key is required","detail":"...

This was extremely confusing and sent me down a bunch of rabbit holes.

Possible to render a record that hasn't been persisted?

One of my endpoints needs to just build a record and return the attributes, rather than persisting it. When using jsonapi_render with the unsaved model, all I get on the frontend is { "data": null }.

Is there a way to do this or is it by design?

Ability to hard-code an `include` query string value

This is a new issue spawned from discussion in #19.

Honestly, I've just started using this gem and don't know any details about its internals. So I'm not quite sure where to start in recommending how it should work. Would it be an option passed to jsonapi_render?

From the spec, it seems that we should encourage only overriding the include value if none is already provided in the query string.

Custom filters list applies only first filter

Looks like in current implementation (ver. 0.5.0) only first filter from the custom_filters list added to the @_allowed_custom_filters. In this method elsif condition doesn't work properly.

module JSONAPI::Utils::Support::Filter
  module Custom
    ...

    def custom_filter(attr)
      attr = attr.to_sym
      @_allowed_filters[attr] = {}

      if !@_allowed_custom_filters.is_a?(Array)
        @_allowed_custom_filters = Array(attr)
      elsif @_allowed_custom_filters.include?(attr)
        @_allowed_custom_filters.push(attr)
      end
    end
  end
end

Should implementers always define controller actions?

This is a question/confirmation issue and not a bug/code flaw issue.

When using JSONAPI::Utils, are the controller actions expected to explicitly be defined?

When mixing in JSONAPI::Utils, we also get the JSONAPI::ActsAsResourceController module and its behavior. Within that module, default actions are defined.

The first sentence in the "How does it work?" section of the README made me think more about it:

One of the main motivations behind JSONAPI::Utils is to keep things explicit in controllers so that developers can easily understand and maintain code.

With a primary motivation being explicit controller behavior, does that in turn mean that explicit controller actions are implied?

Context

While test driving some resources using JSONAPI::Utils + JSONAPI::Resources, I noticed that my immutable endpoints were working without defining any actions. At the time, I did not know why, and the specs were green so I let it be.

Recently, I encountered some trouble attempting to add a fully mutable resource, in particular with PATCH requests. After verifying that the API request body was correct, I narrowed the issue down to double parsing of the request (once in JSONAPI::Utils and a second time in the default action definition in JSONAPI::Resources).

The double parsing happened since it passed through a before filter than used the RequestParser instance and then entered the default update action which initialized a second RequestParser instance.

While looking closer at the specs for JSONAPI::Utils, I noticed that the controllers were always defining the actions. This led me back to the README, since I figured that I must have missed something.

To sum it up, I think that I have been doing it wrong (from the JSONAPI::Utils perspective) and I am looking for confirmation. 😄

NoMethodError when triggering a jsonapi_render_not_found error

Hi there,

Firstly, thank you for this gem, it makes using JR much easier when dealing with API's more complex than the simple examples in the docs. I really appreciate it.

I'm getting this weird error when triggering a jsonapi_render_not_found error. It seems the params hash gets lost somewhere. Anyway, here's the code (it's for a multi-tenant system, which is why I'm searching from the current_user.

module API
  module V1
    class AccountsController < AuthenticatedController
      include JSONAPI::ActsAsResourceController

      def show
        jsonapi_render json: current_user.accounts.find(params[:id])
      end
    end
  end
end

And here's the exception and stacktrace... I hope you can help...

NoMethodError in API::V1::AccountsController#show: undefined method `params' for nil:NilClass

jsonapi-utils (0.4.0) lib/jsonapi/utils.rb:46:in `jsonapi_render_not_found'
activesupport (4.2.6) lib/active_support/rescuable.rb:80:in `call'
activesupport (4.2.6) lib/active_support/rescuable.rb:80:in `rescue_with_handler'
actionpack (4.2.6) lib/action_controller/metal/rescue.rb:15:in `rescue_with_handler'
actionpack (4.2.6) lib/action_controller/metal/rescue.rb:32:in `rescue in process_action'
actionpack (4.2.6) lib/action_controller/metal/rescue.rb:29:in `process_action'
actionpack (4.2.6) lib/action_controller/metal/instrumentation.rb:32:in `block in process_action'
activesupport (4.2.6) lib/active_support/notifications.rb:164:in `block in instrument'
activesupport (4.2.6) lib/active_support/notifications/instrumenter.rb:20:in `instrument'
activesupport (4.2.6) lib/active_support/notifications.rb:164:in `instrument'
actionpack (4.2.6) lib/action_controller/metal/instrumentation.rb:30:in `process_action'
actionpack (4.2.6) lib/action_controller/metal/params_wrapper.rb:250:in `process_action'
activerecord (4.2.6) lib/active_record/railties/controller_runtime.rb:18:in `process_action'
actionpack (4.2.6) lib/abstract_controller/base.rb:137:in `process'
actionview (4.2.6) lib/action_view/rendering.rb:30:in `process'
actionpack (4.2.6) lib/action_controller/metal.rb:196:in `dispatch'
actionpack (4.2.6) lib/action_controller/metal/rack_delegation.rb:13:in `dispatch'
actionpack (4.2.6) lib/action_controller/metal.rb:237:in `block in action'
actionpack (4.2.6) lib/action_dispatch/routing/route_set.rb:74:in `dispatch'
actionpack (4.2.6) lib/action_dispatch/routing/route_set.rb:43:in `serve'
actionpack (4.2.6) lib/action_dispatch/journey/router.rb:43:in `block in serve'
actionpack (4.2.6) lib/action_dispatch/journey/router.rb:30:in `each'
actionpack (4.2.6) lib/action_dispatch/journey/router.rb:30:in `serve'
actionpack (4.2.6) lib/action_dispatch/routing/route_set.rb:817:in `call'
rack-pjax (0.8.0) lib/rack/pjax.rb:12:in `call'
actionpack (4.2.6) lib/action_dispatch/middleware/flash.rb:260:in `call'
warden (1.2.4) lib/warden/manager.rb:35:in `block in call'
warden (1.2.4) lib/warden/manager.rb:34:in `catch'
warden (1.2.4) lib/warden/manager.rb:34:in `call'
rack (1.6.4) lib/rack/etag.rb:24:in `call'
rack (1.6.4) lib/rack/conditionalget.rb:25:in `call'
rack (1.6.4) lib/rack/head.rb:13:in `call'
remotipart (1.2.1) lib/remotipart/middleware.rb:27:in `call'
actionpack (4.2.6) lib/action_dispatch/middleware/params_parser.rb:27:in `call'
actionpack (4.2.6) lib/action_dispatch/middleware/flash.rb:260:in `call'
rack (1.6.4) lib/rack/session/abstract/id.rb:225:in `context'
rack (1.6.4) lib/rack/session/abstract/id.rb:220:in `call'
actionpack (4.2.6) lib/action_dispatch/middleware/cookies.rb:560:in `call'
activerecord (4.2.6) lib/active_record/query_cache.rb:36:in `call'
activerecord (4.2.6) lib/active_record/connection_adapters/abstract/connection_pool.rb:653:in `call'
activerecord (4.2.6) lib/active_record/migration.rb:377:in `call'
actionpack (4.2.6) lib/action_dispatch/middleware/callbacks.rb:29:in `block in call'
activesupport (4.2.6) lib/active_support/callbacks.rb:88:in `__run_callbacks__'
activesupport (4.2.6) lib/active_support/callbacks.rb:778:in `_run_call_callbacks'
activesupport (4.2.6) lib/active_support/callbacks.rb:81:in `run_callbacks'
actionpack (4.2.6) lib/action_dispatch/middleware/callbacks.rb:27:in `call'
actionpack (4.2.6) lib/action_dispatch/middleware/reloader.rb:73:in `call'
actionpack (4.2.6) lib/action_dispatch/middleware/remote_ip.rb:78:in `call'
actionpack (4.2.6) lib/action_dispatch/middleware/debug_exceptions.rb:17:in `call'
web-console (2.3.0) lib/web_console/middleware.rb:28:in `block in call'
web-console (2.3.0) lib/web_console/middleware.rb:18:in `catch'
web-console (2.3.0) lib/web_console/middleware.rb:18:in `call'
actionpack (4.2.6) lib/action_dispatch/middleware/show_exceptions.rb:30:in `call'
railties (4.2.6) lib/rails/rack/logger.rb:38:in `call_app'
railties (4.2.6) lib/rails/rack/logger.rb:20:in `block in call'
activesupport (4.2.6) lib/active_support/tagged_logging.rb:68:in `block in tagged'
activesupport (4.2.6) lib/active_support/tagged_logging.rb:26:in `tagged'
activesupport (4.2.6) lib/active_support/tagged_logging.rb:68:in `tagged'
railties (4.2.6) lib/rails/rack/logger.rb:20:in `call'
request_store (1.2.1) lib/request_store/middleware.rb:8:in `call'
actionpack (4.2.6) lib/action_dispatch/middleware/request_id.rb:21:in `call'
rack (1.6.4) lib/rack/methodoverride.rb:22:in `call'
rack (1.6.4) lib/rack/runtime.rb:18:in `call'
activesupport (4.2.6) lib/active_support/cache/strategy/local_cache_middleware.rb:28:in `call'
rack (1.6.4) lib/rack/lock.rb:17:in `call'
actionpack (4.2.6) lib/action_dispatch/middleware/static.rb:120:in `call'
rack (1.6.4) lib/rack/sendfile.rb:113:in `call'
rack-cors (0.4.0) lib/rack/cors.rb:80:in `call'
railties (4.2.6) lib/rails/engine.rb:518:in `call'
railties (4.2.6) lib/rails/application.rb:165:in `call'
puma (2.15.3) lib/puma/commonlogger.rb:31:in `call'
puma (2.15.3) lib/puma/configuration.rb:79:in `call'
puma (2.15.3) lib/puma/server.rb:541:in `handle_request'
puma (2.15.3) lib/puma/server.rb:388:in `process_client'
puma (2.15.3) lib/puma/server.rb:270:in `block in run'
puma (2.15.3) lib/puma/thread_pool.rb:106:in `block in spawn_thread'

Support for JSONAPI::Resources 0.10

I'd love to use this library in conjunction with the latest version of JSONAPI::Resources. There are a number of breaking changes from what I can see, particularly with the removal of JSONAPI::OperationResults.

I'm new to this library and JSONAPI::Resources so I don't think I have the context yet to get this up and running myself so I wanted to open a ticket to see if it's on anyone else's radar.

Support for sorting and pagination from JR

Since https://github.com/cerebris/jsonapi-resources/releases/tag/v0.9.4 JR allows to filter records on nested relationship values while JU allows filtering only by where condition.

Are there any reasons why you decided to rewrite filter/sort/pagination workflow?

I've played a little with monkey patching, and looks like using filters from JR is possible.

Here is my code, it's working within my application.
Note: it breaks core functionality for usage with non-AR objects, and can break something else, be careful if you want to use it.

module JSONAPI::Utils
  module Response
    module Formatters
      def jsonapi_format(object, options = {})
        operations = @request.operations
        unless JSONAPI.configuration.resource_cache.nil?
          operations.each {|op| op.options[:cache_serializer] = resource_serializer }
        end
        if object.respond_to?(:to_ary)
          operations.each { |op| op.options[:context][:records] = object }
          results = process_operations(operations)
        else
          results = JSONAPI::OperationResults.new
          record = turn_into_resource(object, options)
          results.add_result(JSONAPI::ResourceOperationResult.new(:ok, record))
        end
        @_response_document = create_response_document(results)
        @_response_document.contents
      end
    end
  end
end


module JSONAPI
  class Resource
    class << self
      def records(options = {})
        options.dig(:context, :records) || _model_class.all
      end

      def apply_included_resources_filters(records, options = {})
        include_directives = options[:include_directives]
        return records unless include_directives
        related_directives = include_directives.include_directives.fetch(:include_related)
        related_directives.reduce(records) do |memo, (relationship_name, config)|
          relationship = _relationship(relationship_name)
          next memo unless relationship
          filtering_resource = relationship.resource_klass

          # Don't try to merge where clauses when relation isn't already being joined to query.
          next memo unless config[:include_in_join]

          filters = config[:include_filters]
          next memo unless filters

          rel_records = filtering_resource.apply_filters(filtering_resource.records({}), filters, options).references(relationship_name)
          memo.merge(rel_records)
        end
      end
    end
  end
end

Param not allowed

Hi!

I've been struggling to pass attributes when I migrate JU from JR.

{
  "errors": [
    {
      "title": "Param not allowed",
      "detail": "stripe_plan_id is not allowed.",
      "code": "105",
      "status": "400"
    }
  ]
}

I could turn this config.raise_if_parameters_not_allowed = true into false, however I still see some error within meta.

Could you suggest how I could work this around?
Maybe resource_params checks stripe_plan_id and raise an error somehow so that it can not reach the endpoint #create.

#app/resources/api/v1/plan_resource.rb
class Api::V1::PlanResource < JSONAPI::Resource
  immutable

  attributes :uuid,
             :stripe_plan_id,
             :name,
             :description,
             :amount,
             :currency,
             :interval,
             :is_active,
             :activated_at,
             :disabled_at,
             :created_at,
             :updated_at

  has_one :subscription, class_name: 'Subscription'
end
#app/controllers/api/v1/plans_controller.rb
module Api
  module V1
    class PlansController < Api::V1::ApiController
      before_action only: [:create] { check_type_of('plans') }

      def index
        jsonapi_render json: Plan.all
      end

      def show
        jsonapi_render json: Plan.find(params[:id])
      end

      def create
        form = PlanCreateForm.new plan_params
        form.validate!

        @result = Plan::Create.call(form.attributes)
        if @result.errors.blank?
          # render json: jsonapi_serialized_body(result: @result, resource_name: 'plan'), status: 201 # :created
          jsonapi_render json: @result, status: 201 # :created
        else
          errors = jsonapi_errors(@result)
          response = { errors: errors }
          render json: response, status: 422 # :unprocessable_entity
        end
      end

      private

      def plan_params
        @plan_params ||= params.require(:data).require(:attributes)
      end
    end
  end
end

Use the cache with jsonapi_render.

I noticed than this doesn't use the cache :

  def index
    jsonapi_render json: ProjectGroup.all
  end

But this does :

  def index
    process_request
  end

Is there a way to use the cache for both? Should I cache records myself? It can be useful to have this "built-in".

Getting the id from the object instance still not working properly

     Failure/Error: expect(response_body).to eq({

       expected: "{\"errors\":[{\"title\":\"Record not found\",\"detail\":\"The record identified by 06264908-21e5-4a1e-9d33-2eeb3eb0ba88 could not be found.\",\"id\":null,\"href\":null,\"code\":\"404\",\"source\":null,\"links\":null,\"status\":\"404\",\"meta\":null}]}"
            got: "{\"errors\":[{\"title\":\"Record not found\",\"detail\":\"The record identified by (no identifier) could not be found.\",\"code\":\"404\",\"status\":\"404\"}]}"

Updating a relationship?

When I add jsonapi_resources :foo to routes.rb, given I have a FooResource and a BarResource where FooResource has_one Bar, I get additional routes like api/v2/foo/:foo_id/relationships/bar. I notice that the route created references a method called update_relationship.

However, I could not find any examples of what an update would look like using this methodology. According to JSON API, updating a resource looks like this:

PATCH /articles/1/relationships/author HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json

{
  "data": { "type": "people", "id": "12" }
}

(http://jsonapi.org/format/#crud-updating-to-one-relationships)

How is update_relationship supposed to work with data coming in in this format? Is there some sort of jsonapi-utils helper for doing something like foo.update(bar) in this case?

undefined local variable or method context

When I'm including JSONAPI::Utils in my base controller I get an error:

undefined local variable or method `context' for #<Api::V1::AuthController:0x007fa0f2ec1158>

It seems like the variable context on line 13 in setup_request in JSONAPI::Utils:Request does not exist. I'm not sure where context should come from. I thought is might be because i used ActionController::Metal, but the error shows when I use ActionController::Base.

Has anyone else experienced this before?

I'm using JU 0.4.6 with rails 4.2.3

Add support for custom paginators

Hello, it looks like custom paginators aren't supported as JSONAPI::Utils::Support::Pagination looks explicitly for Page and Offset paginators. I'm using version 0.4.6.

Here are the links to the code:

https://github.com/b2beauty/jsonapi-utils/blob/master/lib/jsonapi/utils/support/pagination.rb#L47

and

https://github.com/b2beauty/jsonapi-utils/blob/master/lib/jsonapi/utils/support/pagination.rb#L38

What would be great would to have support for:

  • Global paginator configuration in the json-resources config
  • Per resource pagination overrides

If this hasn't been fixed (perhaps I missed a configuration option), I can give this a shot.

Why does `jsonapi_render` call a method called `setup_request`?

This makes no sense to me... By the time you want to invoke jsonapi_render the request object has been processed and you're formulating the response.

I assume you're doing it to set some context/variables that you share among your util methods, but is there another reason?

This is an example of why this puzzles and frustrates me.

At debugger in controller:

     8:   if creator.save
 =>  9:     binding.pry
    10:     jsonapi_render json: creator.session, status: :created

jsonapi_serialize is fine:

[1] pry(#<Api::V1::SessionsController>)> jsonapi_serialize creator.session                                                                                                                                                                                     
{:data=>{"id"=>"9a5ad4ef-4e97-40a8-a884-daffd1c43db1", "type"=>"sessions", "links"=>{:self=>"http://example.org/api/v1/sessions/9a5ad4ef-4e97-40a8-a884-daffd1c43db1"}, "relationships"=>{"credential"=>{:links=>{:self=>"http://example.org/api/v1/sessions/9a5ad4ef-4e97-40a8-a884-daffd1c43db1/relationships/credential", :related=>"http://example.org/api/v1/sessions/9a5ad4ef-4e97-40a8-a884-daffd1c43db1/credential"}}}}}

jsonapi_render is not:

[3] pry(#<Api::V1::SessionsController>)> jsonapi_render json: creator.session, status: :created
"{\"errors\":[{\"title\":\"Param not allowed\",\"detail\":\"email is not allowed.\",\"id\":null,\"href\":null,\"code\":\"105\",\"source\":null,\"links\":null,\"status\":\"400\",\"meta\":null},{\"title\":\"Param not allowed\",\"detail\":\"password is not allowed.\",\"id\":null,\"href\":null,\"code\":\"105\",\"source\":null,\"links\":null,\"status\":\"400\",\"meta\":null}]}"

Sorry for venting a bit here. I just don't understand why we need to have such an explicit tie between the request and the response. Depending on the application they may not share parameters, like in our case.

Doesn't support dasherized keys

The JSONAPI spec recommends using dasherized keys
http://jsonapi.org/recommendations/#naming

However, that breaks with jsonapi-utils.

jsonapi-resources 0.7
jsonapi-utils (~> 0.4.5)

With a request such as this:

{
  "data": {
    "id": "58",
    "type": "doctor-family-members",
    "links": {
      "self": "http://localhost:3000/doctor-family-members/58"
    },
    "attributes": {
      "doctor-id": 14,
      "family-member-id": 5,
      "min-visits": 2000,
      "avg-extra-visits": 3000,
      "doctor-name": "MATTHEW PARSONS"
    },
    "relationships": {
      "doctor": {
        "links": {
          "self": "http://localhost:3000/doctor-family-members/58/relationships/doctor",
          "related": "http://localhost:3000/doctor-family-members/58/doctor"
        }
      },
      "family-member": {
        "links": {
          "self": "http://localhost:3000/doctor-family-members/58/relationships/family-member",
          "related": "http://localhost:3000/doctor-family-members/58/family-member"
        }
      }
    }
  }
}

a param method of:
params.require(:data).require(:attributes).permit(:min_visits, :avg_extra_visits)

Shows min-visits and avg-extra-visits as not permitted.

@request not getting set properly

In our application resource controller:

     7:   if creator.save
 =>  8:     binding.pry
     9:     jsonapi_render json: creator.application, status: :created
    10:   else

Calling #jsonapi_serialize to check what the issue is:

    149: def turn_into_resource(record, options = {})
    150:   if options[:resource]
    151:     options[:resource].to_s.constantize.new(record, context)
    152:   else
 => 153:     binding.pry
    154:     @request.resource_klass.new(record, context)
    155:   end

[1] pry(#<Api::V1::ApplicationsController>)> @request
=> nil

Getting Invalid resource for custom resource response on patch request

I am able to override the resource type in the response with

# Forcing a different resource
jsonapi_render json: User.all, options: { resource: V2::UserResource }

However on a patch request, i get

{
    "errors": [
        {
            "title": "Invalid resource",
            "detail": "notifications is not a valid resource.",
            "code": "101",
            "status": "400"
        }
    ]
}

Validation error messages not fully translated in response (I18n)

I have object with validation error. It correctly translated with I18n:

angle.errors.to_a
=> ["Камера не имеет сохранённых координат"] 

Translated as Camera doesn't have coordindates in :en locale.

But in request I got partly translated response:

{"errors"=>[{"title"=>"Camera не имеет сохранённых координат", "id"=>"camera", "code"=>"100", "source"=>{"pointer"=>"/data/relationships/camera"}, "status"=>"422"}]}

Where validation message is not fully translated: Camera word not translated, but rest of message translated.

Support for operation processors?

@tiagopog Is there a way to use jsonapi-authorization, which plugs into JR as a custom operation processor? Based on the JR controller logic here (https://github.com/cerebris/jsonapi-resources/blob/v0.9.0/lib/jsonapi/acts_as_resource_controller.rb#L77), there seems to be some logic around how these processors are invoked -- but jsonapi-utils seems to be overriding this functionality, if I understand it correctly.

Do you have any recommendations for how best to integrate with something like jsonapi-authorization? Would love to take your ideas and make them work.

Thanks!

Bring support to fast_jsonapi

Netflix recently released a blazing fast JSON:API serializer which seems to be a very suitable choice for jsonapi-utils.

Although it will require a deep change in the JU's structure, the perfomance gains for serialization are quite impressive:

I still need to take some time to get through the fast_jsonapi's code and then I may start planning the migration from the old serializer (jsonapi-resource) to this new one.

For more information see the Netflix's article.

Pagination of a nested resource, without using active record

Hi,
In our project, we are using JU for our API, currently only for get calls.
We have had some difficulties since we do not have direct access to our models in the API project, meaning we only have resources and controllers, and we are retrieving the data manually in the controller actions, into a hash which we then pass to jsonapi_format and render.

Specifically with pagination, what happens is the following:

We have some nested resources, with the routes declared this way:

jsonapi_resources :accounts, :only => [:show] do
     jsonapi_resources :bills, :only => [:index]
end

So for our bills index route, we end up with the expected request url of "http://localhost:3005/accounts/2/bills" for example,
but the pagination links get generated with the following url, which doesn't exist in our routing:

"http://localhost:3005/bills?page%5Bnumber%5D=2&page%5Bsize%5D=1" (example for the "next" link)

If i manually add the account part to the request url, it seems like the paging functionality works as expected and I see the next page in the response.

My question is if there is any support for this scenario of pagination of a nested resource, or do I have no alternative but to manually alter the links section of the response and concatenate the missing part of the URL, which seems far from an ideal solution to me.

Not sure if this is a JU or JR issue, but any help would be much appreciated!

Thanks!

Non-REST actions?

I'm trying to use JSONAPI Utils for a non-RESTful endpoint users/invite and would like to be able to use the resource_params and relationship_params helpers, however they're both returning {} because @request.operations is empty. Is there any guidance on how to approach this?

Support abstract interfaces instead of specific ActiveRecord objects in Formatters

This is in reference to v0.6.0.

I use active_interaction for services in lots of my projects, and recently started using this gem to enforce JSONAPI spec more easily.

When I have an ActiveInteraction instance - interaction - and pass it into jsonapi_render_errors json: interaction in my controller, I get TypeError: no implicit conversion of Symbol into Integer because it tries to sanitize the errors. However, ActiveInteraction's errors follows the same interface as ActiveRecord's errors. The first line of JSONAPI::Utils::Response::Formatters#jsonapi_format_errors would work for ActiveInteraction, except that it's explicitly checking if data is an active_record_obj?, which of course it isn't.

Would it be possible to make this not rely on whether it's an ActiveRecord object, but instead just check that it responds to the necessary methods?

How can we use custom filters?

I'm having some trouble while filtering. I need my resource to be filtered by its name using a LIKE query (autocomplete field). If I implement JSONAPI::Resources approach, using a lambda, it doesn't work, the lambda it's not called. JSONAPI::Utils provides a custom_filters method, which allows me to apply the filter in the controller, but I don't think that is the best solution. Am I missing something? I can't find any further documentation. Could it be possible to implement all custom filtering code in the resource instead of the controller, as JR proposes?

Format of error object

While poking around with this gem, I noticed the error formatter provides this sort of response:

{
  "errors": [
    {
      "title": "Title can't be blank",
      "id": "title",
      "code": "100",
      "status": "422"
    }
  ]
}

I've seen the format prescribed like so in examples:

{
  "errors": [
    {
      "status": "422",
      "source": { "pointer": "/data/attributes/title" },
      "title":  "Invalid Attribute",
      "detail": "Title can't be blank.",
      "code": "100"
    }
  ]
}

Also, I noticed that according to the spec, title may not be used properly as this gem currently stands because it is displaying an error message instead of a generic summary that could be localized:

title: a short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of localization.

Do you think that JSONAPI::Utils should be updated to use the source.pointer style of error reporting instead of using id like it current is?

Add helper/util method for rendering an object's errors?

It's very common to see a pattern like this in a Rails controller (I'm using jsonapi-utils methods in the code):

if model.save
  jsonapi_render json: model
else
  jsonapi_render json: model.errors # doesn't work
end

Seems like this gem should expose a helper for this type of failed POST request, e.g. jsonapi_render_unprocessable model.errors which would serve 422 response with a response body listing the reasons why the model couldn't be written to the DB.

Not sure what JSONAPI spec says about this, however...

Setting the type of a resource

Maybe I'm not understanding how jsonapi-utils/jsonapi-resource works, but I have a OrderResource and a OrderListResource (which is a simplified version of OrderResource). I am trying to render OrderListResource, but have the type value be order. Currently, it's setting it to order_list. Here's the render statement:

jsonapi_render json: Order.last(2), options: { resource: Api::V2::OrderListResource }

OrderListResource

module Api
  module V2
    class OrderListResource < JSONAPI::Resource
      attribute :status_name
    end
  end
end

Response

{
  "data": [
    {
      "id": "1",
      "type": "order_lists",  # <-- wrong, should be "order"
      "links": {
        "self": "http://localhost:3001/api/v2/order_lists/1"  # <-- wrong, should be order/1
      },
      "attributes": {
        "status_name": "some_status"
      }
    },
    {
      "id": "2",
      "type": "order_lists",   # <-- wrong, should be "order"
      "links": {
        "self": "http://localhost:3001/api/v2/order_lists/2" # <-- wrong, should be order/2
      },
      "attributes": {
        "status_name": "some_other_status"
      }
    }
  ]
}

EDIT:

I'm not sure if this is the proper solution, or if I'm using jsonapi-resource incorrectly, but I fixed it by adding the following to OrderListResource:

      def self.name
        'Order'
      end

      def self._type
        'order'
      end

resource_params => NoMethodError: undefined method `keys' for nil:NilClass

rails (4.2.8)
jsonapi-utils (0.4.9)
jsonapi-resources (~> 0.8.0)

I have a nested resource where the index action has a code path that executes resource_params which ends up producing:

NoMethodError: undefined method `keys` for nil:NilClass

Due to:

operation = @request.operations.find { |e| e.options[:data].keys & keys == keys }

https://github.com/tiagopog/jsonapi-utils/blob/v0.4.9/lib/jsonapi/utils/request.rb#L77

@request.operations contains:

[#<JSONAPI::Operation:0x007fea7b3b2640 
@operation_type=:find, 
@resource_klass=Api::V2::ArticleResource, 
@options={
  :context=>{}, 
  :filters=>{}, 
  :include_directives=>nil, 
  :sort_criteria=>nil, 
  :paginator=>#<PagedPaginator:0x007fea7b3b2690 @number=1, @size=20>, :fields=>{}}>]

Which does not include a :data key, hence the resulting exception.

One option is to update the offending piece of code to:

operation = @request.operations.find { |e| e.options.fetch(:data, {}).keys & keys == keys }

I'll look into submitting a PR. In the meantime I've rescued the exception.

Update to use jsonapi-resources 0.8.0

Hi @tiagopog,

Thanks for maintaining this gem. I've forked a copy and changed the gem to use version jsonapi-resources version 0.8.0 final, which was released today.

Separately, I've removed the dependency for piped_ruby, which (it appears) you added into beta2, and is why I haven't proposed my fork back into yours. I'm not sure why the dependency was added, and I can't find any discussion about it, but it doesn't seem to be necessary for this gem to work properly. It also seems like requiring piped_ruby forces someone into a design decision unrelated to the use of jsonapi.

I'm happy to propose a PR with the updated version, and/or rename this issue title, but I just was hoping for some clarification on the piped_ruby dependency and why it was added as a requirement.

Thanks!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.