McCracken
A JSON API client for Ruby
Installation
Add this line to your application's Gemfile:
gem 'mccracken'
And then execute:
$ bundle
Or install it yourself as:
$ gem install mccracken
Basic Usage
class Article < McCracken::Resource
self.type = :articles
key_type :integer,
attribute :title, :string
end
Registering the JSONAPI 'type'
Calling Article.type = :articles
registers the class Article
as the handler for JSON API resources of the type articles
.
When the ActiveSupport method tableize
is present, this will be set automatically. A class can be bound to multiple types:
Article.type = :articles
McCracken.register_type(:posts, Article)
Querying
articles = Article.fetch
article = Article.find(9)
query = Article.fields(:title).include(:author, :comments).sort(id: :desc).filter(published: true)
query.to_params
query.to_query_string
query.fetch
The McCracken::Resource delegates a few methods to its underlying McCracken::Connection:
Filtering
query = Product.filter(min_price: 30, max_price: 65)
query.filter(category: 'Hats').filter(size: ['small', 'medium'])
query.to_params
query.fetch
Sorting
query = Product.sort(created_at: :desc)
query.sort(:price)
query.to_params
query.fetch
Including (Side loading related resources)
query = Product.include(:manufacturer)
query.include(:vendor)
query.to_params
query.fetch
Sparse Fieldsets
query = Product.fields(products: [:name, :price])
query.include(:manufacturer).fields(manufacturer: [:name])
query.to_params
query.fetch
All the things!
query = Product.
filter(min_price: 30, max_price: 65).
includes(:manufacturer).
sort(popularity: :desc, price: :asc).
fields(product: ['name', 'price'], manufacturer: ['name', 'website']).
page(number: 1, limit: 100)
query.to_params
query.fetch
Fetching a single resource
Product.find(1)
Accessing McCracken internals
Every McCracken::Resource has an internally managed client. It is accessible via .mccracken
Article.mccracken
Article.mccracken.path
Article.mccracken.agent
Article.mccracken.query
Article.mccracken.connection
Article.mccracken.connection.response_key_format
Article.mccracken.connection.url
Article.mccracken.connection.faraday
Article.mccracken.connection = SomeNewConnectionYouPrefer
Article.mccracken.connection.configure(opts) do { |faraday_conn| }
Persistence Resources
class Article < McCracken::Resource
attribute :title, :string
attribute :body, :string
attribute :created_at, :time
end
Creating a new resource
article = Article.new
article.title = "This is a great read!"
article.save
article.errors?
article.errors
Updating a resource
article = Article.find(9)
article.title = "This is a great read!"
article.save
article.errors?
article.errors
Accessing Side Loaded Resources
Given the following relationship:
class Article < McCracken::Resource
self.type = :articles
has_one :author
has_many :comments
key_type :integer
attribute :title, :string
end
class Person < McCracken::Resource
self.type = :people
has_many :articles
attribute :first_name, String
attribute :last_name, :string
attribute :twitter, :string
attribute :created_at, :time, default: ->{ Time.now }, serialize: ->(val){ val.to_s }
attribute :post_count, :integer
end
class Comment < McCracken::Resource
self.type = :comments
has_one :author
attribute :body, ->(val){ val.to_s }
attribute :score, :float
attribute :created_at, :time
attribute :is_spam, :boolean
attribute :mentions, :string, array: true
end
Note: When specifying relationships in McCracken, you are specifying the JSON API type name. You'll notice below that when .author
is called it returns a person object. That it is because in the HTTP response, the relationship name is author
but the resource type is people
.
{
"data": [{
"type": "articles",
"id": "1",
"attributes": {
"title": "JSON API paints my bikeshed!"
},
"relationships": {
"author": {
"links": {
"self": "http://example.com/articles/1/relationships/author",
"related": "http://example.com/articles/1/author"
},
"data": { "type": "people", "id": "9" }
}
}
}]
}
McCracken initializes objects for side loaded resources. Only 1 HTTP call is made.
article = Article.include(:author, :comments).find(9)
article.author
article.comments
article.author.first_name
Configuration
McCracken is designed to support multiple connections or API endpoints. A connection is a wrapper around Faraday::Connection that includes a few pieces of middleware for parsing and encoding requests and responses to JSON API Spec.
Setting the default connection:
McCracken.configure(url: 'http://api.example.com') do |c|
c.use MyCustomMiddleware
c.use AllTheMiddlewares
end
Each McCracken::Resource has its own McCracken::Client. The client copies the default connection so its easy to set general configuration options, and overwrite them on a resource by resource basis.
McCracken.configure(url: 'http://api.example.com', response_key_format: :dasherize)
class Kitten < McCracken::Resource
mccracken.url = "http://api.differentsite.com"
end
Kitten.mccracken.connection.url
Kitten.mccracken.connection.response_key_format
McCracken.default_connection.url
McCracken.default_connection.response_key_format
Configuration Options
McCracken.configure(url: 'http://api.example.com', response_key_format: :dasherize) do |conn|
conn.use SomeCoolFaradayMiddleware
end
Two special options can be passed into .configure
:
url
the base url for this endpointresponse_key_format
the format of the JSONAPI response keys. Valid values are: :dasherize
, :camelize
, nil
Additinally any Faraday Connection options can be passed. [Faraday::Connection options](https://github.com/lostisland/faraday/blob/master/lib/faraday/connection.rb Faraday::Connection)
Advanced Usage
Custom Query Builder
Since the filter param's format isn't specified in the spec
this implementation uses JSONAPI::Resource's implementation
To override, implement your own custom query builder inheriting from McCracken::Query
.
McCracken::Client
takes a Query class to use. This method could be overwritten in your Resource:
class MyBuilder < McCracken::Query
def filter_to_query_value
end
end
Article.mccracken.query_builder = MyBuilder
Article.mccracken.filter(:name => "Chauncy")
Without inheriting from McCracken::Resource
If for some reason you cannot inherit from McCracken::Resource, you can still get a lot of JSONAPI parsing functionality
class Album
attr_accessor :id
attr_accessor :title
def self.mccracken
return @mccracken if @mccracken
@mccracken = McCracken::Client.new
end
mccracken.type = :albums
McCracken.register_type(mccracken.type, self)
def self.mccracken_initializer(document)
new(document.id, document.attributes[:title])
end
def initialize(id, title)
@id = id
@title = title
end
end
albums = Album.mccracken.include(:songs).fetch
albums.first.title
Any ol' object (Register type, add mccracken_initializer)
As long as a class is registered with mccracken and it response to mccracken_initializer, McCracken will be able to initialize the object with or without a client
Extending the example above...
class Song
attr_reader :name
def self.mccracken_initializer(document)
new(document.attributes[:name])
end
def initialize(name)
@name = name
end
end
McCracken.register_type :songs, Song
album = Album.mccracken.include(:songs).find(9)
album.songs
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake spec
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/jgnagy/mccracken. Base hotfixes off of the master
branch and features off of the develop
branch.
TODOS