JsonApiResource
JsonApiResource is an abstraction layer that sits on top of JsonApiClient that lets the user interact with client objects the way they would with an ActiveRecord model. It provides objectification of attributes, population with defaults, metadata and error handling on server requests.
Installation
Add this line to your application's Gemfile:
gem 'json_api_resource'
And then execute:
$ bundle
Or install it yourself as:
$ gem install json_api_resource
Usage
Example
Let's say you have a JsonApi server Account, which has a RESTful controller, UsersController
and you have a JsonApiClient
that can talk to it. And you would like to interact with the server result as an object, rather than
user = Account::Client::User.find(15)
user["name"]
You can wrap it in a JsonApiResource
module Account
class User < JsonApiResource::Resource
# define what your client is
wraps Account::Client::User
# define what fields you expect, with defaults, should you create a new object
# or should the server omit fields in its response
properties id: nil,
name: "",
permissions: [],
friend_ids: []
# your custom code here
def friends
@friends ||= where(id: friend_ids)
end
#etc
end
end
Then you can interface with the Account::User
class, as you would any old AR class.
# find works the way you would expect
john = Account::User.find(38)
john.name = "Johnny"
# will make a PUT call to the server
john.save # => true
# if you preforma an action that fails validation
john.name = ""
john.save # => false
john.errors # ActiveModel::Errors errors that you can nicely pipe into your forms
mark = Account::User.new
mark.id # => nil
mark.name = "Mark"
mark.save # => true
# etc
Keywords
wraps
wraps
is an interface method that defines what JsonApiClient
class the resource will wrap and connect to.
module Account
class User < JsonApiResource::Resource
wraps Account::Client::User
end
end
property
Define a single property that will be populated on new object or if the field is missing.
class User < JsonApiResource::Resource
wraps Whatever
# defaults to nil
property :id
# defaults to ""
property :name, ""
end
properties
Define multiple properties
class User < JsonApiResource::Resource
wraps Whatever
properties id: nil,
name: ""
end
NOTE: all properties are optional. If you don't have it defined and it's in the payload, it will still be part of your resulting object. For example, if you define no properties, but your payload comes in as
{
"id": 10,
"name": "john",
"permissions": []
}
The object will still reply to id
, name
and permissions
, but you won't be able to assume they are there, and they will not appear when you do ResourceClass.new
.
Associations
interface
-
belongs_to
- your resource has the#{object}_id
for the association.- calls
object_class.find
- calls
-
has_one
- will call the server withopts.merge "#{root_class}_id => root_object.id
and grab the first thing that comes back- calls
object_class.where
- calls
-
has_many
- same ashas_one
but will give you the full array of objects- calls 'object_class.where`
options
-
:foreign_key
- this is what the server will get as the key forhas_one
andhas_many
.belongs_to
is weird and will send the key to the resource object. for example
class Admin < JsonApiResource::Resource
wraps Service::Client::Admin
property user_id
# this one is a little weird
belongs_to :person, foreign_key: :user_id, class: User
end
-
:action
- If you have a custom action you're using for lookup, you can override the defaultwhere
andfind
. For example
class Service::Client::Superuser < Service::Client::Base
# i don't know why you would have this, but whatever
custom_endpoint :superuser_from_user_id, :on=> :collection, :request_method=> :get
end
class User
wraps Service::Client::User
has_one :superuser, action: :superuser_from_user_id
end
NOTE: keep in mind, this will still make the call with the opts.merge foreign_key => root_object.id
hash. If you want to override the query, you may want to consider 1: if the API is RESTful 2: rolling your own association.
-
:class
- specifies the class for the association -
:class_name
- a string that specifies a class for the association. Handy for load order issues -
:prefetched_ids
(has_many
only) - in the case that the root object has a collection of ids that come preloaded in it
class User < JsonApiResource::Resource
wraps Service::Client::User
property address_ids, []
has_many :addresses, prefetched_ids: :address_ids
end
notes
-
:through
is not supported, nor will it ever be, because of the hidden complexity and cost of n HTTP calls. if you want to implementthrough
do -
the servers you're preloading from/associating have controller entities that respond to show(
find
) and index(where
) -
important: the index(
where
) action has to supportingore_pagination
. Otherwise you may lose associations as they get capped by the per-page limit
Preloader
Sometimes you have many objects that you need to fetch associations for. It's expensive to have to iterate over them one by one and annoying to have to assign the results. Well, now there's a Preloader
that can do all of that for you
example
Let's say you have user
s who have address
es.
class User < JsonApiResource::Resource
wraps Service::Client::User
# shiny new function yay
has_many :addresses
end
class Address < JsonApiResource::Resource
wraps Service::Client::Address
end
With the associations in place, you can now use them to preload all the addresses for your users in a single query.
@users = User.where id: [1, 2, 3, 4, 5]
# that's it. that's all you have to do.
JsonApiClient::Associations::Preloader.preload @users, :addresses
# all the users now have addresses assigned to them and will not hit the server again
puts @users.map &:addresses
interface
Preloader.preload( objects, preloads )
takes
- the objects you want the associations to be tied to (
@users
in our example) - the list of associations you want bulk fetched from the server as symbols.
- can be a single symbol, or a list
- has to have a corresponding association on the objects, and the name has to match
notes
This is a simple tool so don't expect too much magic. The Preloader will explode if
- the objects aren't the same class
- the results aren't the same class (although i don't know how that would be possible)
- the result set can't be matched to the objects
Error Handling
On an unsuccessful call to the server (this means that if you have multiple connections, they will necessarily all have to fail), errors will be routed through an overridable handle_failed_request(e)
method. By default it will re-raise the error, but you can handle it any way you want.
Other Features
Caching
JsonApiResource does a full digest of the json blob, so if your server schema changes, you will see your client side cache burst.
Error handling
As seen in the example above, JsonApiResource extracts the server errors from the metadata and wraps them nicely in ActiveModel::Errors
, for easy form integration.
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/avvo/json_api_resource.
License
The gem is available as open source under the terms of the MIT License.