Daylight uses the MVC model provided by Rails to divide labor of an API request with some constraints.
Instead of views, serializers are used to generate JSON/XML. Routes have a great importance to the definition of the API. And the client becomes the remote proxy for all API requests.
To better undertand Daylight's interactions, we define the following components:
- Rails model is the canonical version of the object
- A serializer defines what parts of the model are exposed to the client
- Rails controller defines which actions are performed on the model
- Rails routes defines what APIs are available to the client
- The client model is the remote representation of the Rails model
- Rails 4: Daylight was built only using the most current version of Rails 4
- Namespace APIs: Client Models are all namespaced, by default under
API
(namespace is customizable) - Versioned APIs: URLs will be versioned, by default
v1
is the current and only version (versions are customizable) - ActiveModelSerializer: Serialization occurs via
ActiveModel::Serailizer
, typically in JSON
Building your Client from the bottom up you will need to develop your models, controllers, routes that you are familiar with today. Add serializers to describe the JSON generation of your object. Finally, build your client models based on the API actions available and the response from the server.
Models are built exactly as they are in Rails, no changes are neccessary.
Through specifiecation on the routes, Daylight allows you to make scopes and methods available to the client.
NOTE: Daylight expects an model object or a collection when parsing results from a model method.
You can chose to allow models to be created, updated, and associated through
a "parent" model using the accepts_nested_attributes_for
mechansism.
class Post < ActiveRecord::Base
has_many :comments
accepts_nested_attributes_for :comments
end
Once the client is setup you can do the following:
post = API::Post.find(1)
post << API::Comment.new(text: "This is an awesome post")
post.save
INFO: ActiveResource looks up associations using foriegn keys but with
Daylight
you can call the associations defined on your model directly.
This is especially useful when you wish to preserve the richness of options on your associations that are neccessary for your application to function correctly. For example:
class Post
has_many :comments
has_many :favorites, foreign_key: 'favorite_post_id', class_name: 'User'
has_many :commenters, -> { uniq }, through: :comments, class_name: 'User'
has_many :suppressed_comments, -> { where(spam: true) }, class_name: 'Comment'
end
Here we have 4 examples where using the model associations are neccesary. When there is:
- A configured foreign_key as in
favorites
- A through association as in
commenters
- A condition block as
commenters
andsuppressed_comments
(eg.uniq
andwhere
) - A class_name in all three
favorites
,commenters
, andsuppressed_comments
ActiveResource will not be able to resolve these associations correctly without using the model-based associations, because it:
- Cannot determine endpoint or correct class to instantiate
- Uses the wrong lookup key (in through associations and foreign key option)
- Conditions will not be supplied in the request
NOTE: Daylight includes
Daylight::Refiners
on all models that inherit fromActiveRecord::Base
. At this time there is no way to exclude this module from any model. It does not modify existing ActiveRecord functionality.
You can specify a column that can be used to look-up a record by a natural_key
or "non-id dereferencing". This will allow records to be looked up by a unique
id that is not the artificial (primary) key.
Set the natural key on the model (there can only be one):
class Post
set_natural_key :slug
end
Within the client you will now be able to look up using both the artifical key and the natural key:
p = API::Post.find(1)
p.slug # => "100-best-albums-of-2014"
p = API::Post.find("100-best-albums-of-2014")
p.id # => 1
If the column values for the natural key is not unique, then the first will be returned by Rails. For consitent results, use natural key for only those columns that are unique.
Daylight relies heavily on ActiveModelSerializers and most information on how to use and customize it can be found in their README. Serialize only the attributes you want to be public in your API. This allows you to have a separation between the model data and the API data.
By default Daylight will serialize all your model's attributes and has_one
or
belongs_to
associations.
For example, for this model:
class Post < ActiveRecord::Base
belongs_to :blog
has_many :comments
has_one :company, through: :blog
# this has attributes id, title, body, summary
end
Will dynamically generate this default serializer if one doesn't exist:
class PostSerializer < ActiveModel::Serializer
embed :ids
attributes :id, :title, :body, :summary
has_one :blog
has_one :company, through: :blog
end
To customize this behavior create your own serializer.
For example, id
, title
and body
are exposed but all other attributes are
not serialized:
class PostSerializer < ActiveModel::Serializer
attributes :id, :title, :body
end
NOTE: Make sure to include
:id
as an attribute so that Daylight will be able to make updates to the models correctly.
We encourage you to embed only ids to keep payloads down. Daylight will make additional requests for the associated objects when accessed:
class PostSerializer < ActiveModel::Serializer
embed :ids
attributes :id, :title, :body
has_one :category
has_one :author, key: 'created_by'
end
NOTE: Make sure to use
key
option in serializers, notforeign_key
INFO:
belongs_to
associations can be included usinghas_one
in your serializer
There isn't any need for you to include your has_many
associations in
your serializer. These collections will be looked up from the Daylight
client by a seperate request.
The serializer above will generate JSON like:
{
"post": {
"id": 283,
"title": "100 Best Albums of 2014",
"body": "Here is my list...",
"category_id": 2,
"created_by": 101
}
}
There are 2 main additions Daylight adds to ActiveModelSerializer
to enable
functionality for the client. They are through associations and read only
attributes.
In Rails you can setup your model to have a has_one :through
. This is a
special case for ActiveModelSerializers
and for the Daylight client.
NOTE: Rails does not have
belongs_to :through
associations.
For example, if your model has associations setup like so:
class Post < ActiveRecord::Base
belongs_to :blog
has_one :company, through: :blog
end
To configure the PostSerializer
to correctly use this through association
set it up like similarly to your model.
class PostSerializer < ActiveModel::Serializer
embed :ids
attributes :id, :title, :body
has_one :blog # `has_one` in a serializer
has_one :company, through: :blog
end
This will create a special embedding in the JSON that the client will be able to use to lookup the association:
{
"post": {
"id": 283,
"title": "100 Best Albums of 2014",
"body": "Here is my list...",
"blog_id": 4,
"blog_attributes": {
"id": 4,
"company_id": 1
},
}
}
There's duplication in the JSON payload, but post["blog_id"]
and
post["blog_attributs"]["id"]
are used for different purposes.
API::Post.first.blog #=> uses "blog_id"
API::Post.first.company #=> uses "blog_attributes"
INFO:
blog_attributes
are also used foraccepts_nested_attributes_for
mechansism.
There are cases when you want to expose data from the model as read only attributes so they cannot be updated. These cases are when the attribute is:
- Evaluated and not stored in the database
- Stored into the database only when computed
- Readable but should not be updated
Here we have a Post
object that does all three things. Assume there are
updated_at
and created_at
immutable attributes as well.
class Post < ActiveRecord::Base
before_create do
self.slug = title.downcase.strip.gsub(' ', '-').gsub(/[^\w-]/, '')
end
def published?
published_at.present?
end
end
To configure the PostSerializer
to mark these attributes as read only:
class PostSerializer < ActiveModel::Serializer
embed :ids
attributes :id, :title, :body
read_only :created_at, :updated_at, :slug, :published?
end
These attributes will be marked as read only in a special Metadata section in the object's JSON.
The client will be able to read each of these values but will raise a
NoMethodError
when attempting to write to them.
post = API::Post.first
post.created_at #=> "2014-05-02T19:58:09.248Z"
post.slug #=> "100-best-albums-of-2014"
post.published? #=> true
post.slug = '100-best-albums-of-all-time'
#=> NoMethodError: Cannot set read_only attribute: display_name
Because these attributes are read only, the client will exclude them from being sent when the object is saved.
post.title = "100 Best Albums of All Time"
post.save #=> true
In this case published?
, slug
, created_at
, and updated_at
are never
sent in a PUT update.
Controllers can be written without Daylight, but often times you must develop
boilerplate code for index
, create
, show
, update
, and delete
actions.
Also, you may chose controllers that are for the API and controllers that are
for your application.
Daylight simplifies building API controllers:
class PostController < APIController
end
NOTE: Any functionality built in
ApplicationController
will be available to yourAPIController
subclasses.
Since your controller is a subclass of ActiveController::Base
continue to add
your own actions and routes for them as you do today in Rails.
There are predefined actions provided by Daylight, that handle both REST actions and some specialized cases.
You must "turn on" these prede actions. Actions provided by Daylight are turned off by default so what is exposed is determined by the developer.
For example, to turn on show
action:
class PostController < APIController
handles :show
end
This is equivalent to;
class PostController < APIController
def show
render json: Post.find(params[:id])
end
end
Daylight uses the name of the controller to determine the related model to use.
Also, the primary_key
name is retrived from that determined model. In fact,
all of the actions are just ruby methods, so you can overwrite them (and call
super) as you see fit:
class PostController < APIController
handles :show
def show
super
@post.update_attributes(:view_count, @post.view_count+1)
end
end
To turn on multiple actions:
class PostController < APIController
handles: :create, :show, :update, :destroy
end
Or you can turn them all (including the Specialized Actions):
class PostController < APIController
handles: :all
end
For your reference, you can review the code of the equivalent actions in Controller Actions
Much of Daylight's features are offered through specialized controller actions. These specialized actions are what enables:
You can refine queries of a resources collection by scopes, conditions, order, limit, and offset.
This is accomplished with a method called refine_by
which is added to your
models added by Daylight::Refiners
On the controller, see it called on the index
action:
class PostController < APIController
def index
render json: Post.refine_by(params)
end
end
Associations called through the model instance is accomplished using a method
called associated
added by Daylight::Refiners
. Which associations allowed
are defined in your Routes.
On the controller, see it called by the (similarly named) associated
action:
class PostController < APIController
def associated
render json: Post.associated(params), root: associated_params
end
end
Associations can also be refined similarly to index
where you can specify
scopes, conditions, order, limit, and offset. The associated action is
setup in Through Associations on the client model.
NOTE: You can find more information on how to use these refinements in the Daylight Users Guide
Any public method is allowed to be called on the model instance by use of the
remoted
method added by Daylight::Refiners
. Which public methods are
allowed are defined in your Routes.
FUTURE #4: It would be nice to allow public methods on the model class to be exposed and called against the collection.
Remoted methods should return a record or collections of records so that they may be instantiated correctly by the client and act as a proxy back to the API.
On the controller, see it called by the (similarly named) remoted
action:
class PostController < APIController
def remoted
render json: Post.remoted(params), root: remoted_params
end
end
All of the specialized actions can be enabled on your controller like the REST actions:
class PostController < APIController
handles :index, :associated, :remoted
end
They are also included when specifying handles :all
.
INFO: To understand how
root
option is being used in bothassoicated
andremoted
please refer to the section on Symantic Data
Behind the scenes, the controller actions look up models based on its controller
name. The portion before the word Controller (ie. when PostController
is
the controller name it determines the model name to be Post
).
You may specify a different model to use:
class WelcomeController
set_model_name :post
end
In create
, show
, update
and destroy
actions (member) results are stored
in an instance variable. The instance variable name is based on the model
name (ie. when PostController
is the controller name the instance variable is
called @post
).
In index
, associated
, and remoted
specialized actions results are stored
in an instance variable simply called @collection
Both of these instance variables may be customized:
class PostController
set_record_name :result
set_collection_name :results
end
NOTE: Daylight calls the instance variables for specialized actions
@collection
because inassociated
andremoted
actions the results may be any type of model instances.
In all customizations can use a string, symbol, or constant as the value:
class PostController
set_model_name Post
set_record_name 'result'
set_collection_name :results
end
Lastly, your application may already have an APIController and there could be a name collision. Daylight will not use this constant if it's already defined.
In this case use Daylight::APIController
to subclass from:
class PostController < Daylight::APIController
handles :all
end
Setup your routes as you do in Rails today. Since Daylight assumes that
your API is versioned, make sure to employ namespace
in routes or use
a simple, powerful tool like
Versionist.
API::Application.routes.draw do
namespace :v1 do
resources :users, :posts, :comments
end
end
You can modify the actions on each reasource as you see fit, matching your
APIController
actions:
API::Application.routes.draw do
namespace :v1 do
resources :users, :posts
resources :comments, except: [:index, :destroy]
end
end
To expose model assoications, you can do that with Daylight additions to routing options.
FUTURE #7: The client only supports model associations on
has_many
relationships. We will need to evaluate the need to support model associations onhas_one
andhas_many
(as we never had a case for it)
API::Application.routes.draw do
namespace :v1 do
resources :users, associated: [:posts, :comments]
resources :posts, associated: [:comments]
resources :comments, except: [:index, :destroy]
end
end
Any of the rich has_many
relationships setup may be exposed as a model
associations, choose which ones to expose:
API::Application.routes.draw do
namespace :v1 do
resources :users, associated: [:comments, :posts]
resources :posts, associated: [:authors, :comments, :commenters]
resources :comments, except: [:index, :destroy]
end
end
To expose remoted methods, you can do that with Daylight additions to routing options.
API::Application.routes.draw do
namespace :v1 do
resources :users, associated: [:comments, :posts]
resources :posts, associated: [:authors, :comments, :commenters],
remoted: [:top_comments]
resources :comments, except: [:index, :destroy]
end
end
By default all model scopes are available but you can whitelist particular scopes you want available to the client.
API::Application.routes.draw do
namespace :v1 do
resources :users, associated: [:comments, :posts]
resources :posts, associated: [:authors, :comments, :commenters],
remoted: [:top_comments],
scopes: [:published, :edited]
resources :comments, except: [:index, :destroy]
end
end
As you can see when you develop your API, the routes file becomes a specification of what is exposed to the client.
The client is where all our server setup is put together. Client models
subclass from Daylight::API
classes.
INFO:
Daylight::API
subclassesActiveResource::Base
and extends it
You can build your client model as you do today as an ActiveResource::Base
as all functionality performs the same out of the box. (Only when using
Daylight features is when Daylight additions to ActiveResource
enabled)
class API::V1::Post < Daylight::API
end
Here again, we encourage you to namespace and version your client models. You can do this using module names and Daylight will offer several conviniences.
Daylight will alias to the current version defined in your setup!
.
Assuming you've have two versions of your client models:
Daylight::API.setup!(version: 'v1', versions: %w[v1 v2])
API::Post #=> API::V1::Post
Daylight::API.setup!(version: 'v2')
reload!
API::Post #=> API::V2::Post
Using the aliased versions of your API is practical for your end users. They
will not need to update all of the constants in their codebase from
API::V1::Post
to API::V2::Post
after they migrate. Instead they can focus
on differences provided in the new API version.
FUTURE #2: It may be possible to have different versions of a client model to run concurrently. This would aid end users of the API to move/keep some classes on a particular version.
When developing your API when you reload!
within your console, the aliased
constants will still reference the older class definitions. Currently, this
only works with IRB. To re-alias the constants during a reload!
add the
following to an initializer:
require 'daylight/client_reloader'
This should not be needed for your end-users but is available for debugging purposes if needed.
Daylight will lookup association classes using the namespace and version set
in your client. This simplifies setting up your relationships becaause you do
not need to define your class_name
on each association:
class API::V1::Post < Daylight::API
belongs_to :blog
has_many :comments
end
Once all client models are setup, associationed models will be fetched and initialized:
post = API::V1::Post.first
post.blog #=> #<API::V1::Blog:0x007fd8ca4717d8 ...>
post.comments #=> [#<API::V1::Comment:0x007fd8ca538ce8...>, ...]
There are times when you will need to specify a client model just like you do
in ActiveRecord
:
class API::V1::Post < Daylight::API
belongs_to :author, class_name: 'api/v1/user', foreign_key: 'created_by'
belongs_to :blog
has_many :comments
end
NOTE: The foreign key needs to match the same key in your serailizer and the
foreign_key
in yourActiveRecord
model.
The User
will be correctly retrieved for the author
association:
API::V1::Post.first.author #=> #<API::V1::User:0x007fd8ca543e90 ...>
There are two types of Through Associations in Daylight:
has_one :through
has_many :through
First, once you've setup your has_one :through
association in your model and serializer. You can use it in the client model.
This is setup similar to the ActiveRecord
model:
class API::V1::Post < Daylight::API
belongs_to :blog
has_one :company, through: :blog
end
The associations will be available:
post = API::Post.first
post.blog #=> #<API::V1::Blog:0x007fd8ca4717d8 ...>
post.company #=> #<API::V1::Company:0x007f8f83f30b28 ...>
Second, once the has_many :through
associations are exposed in the
Routes you can them up in the client model:
class API::V1::Post < Daylight::API
has_many 'comments'
has_many 'commenters', through: :association
end
The value is always :association
and is a directive to Daylight to use the
associated action on the PostController
.
The associations will be available:
post = API::Post.first
post.comments #=> [#<API::V1::Comment:0x007f8f83f91c20 ...>, ...]
post.commenters #=> [#<API::V1::Company:0x007f8f83fe1f40 ...>, ...]
Here we can see a typical ActiveResource
association for comments
is used
along-side our has_many :through
. If there is no reason to use the model
assoication, the flexibility is up to you. Please review the reasons to use
Model Association.
You can setup both to use model associations:
class API::V1::Post < Daylight::API
has_many 'comments', through: :association
has_many 'commenters', through: :association
end
Refer to the Daylight Users Guide to see how to further work associations.
Adding scopes and remoted methods is very simple.
Given the ActiveRecord
model setup:
class Post < ActiveRecord::Base
scope :published, -> { where(published: true) }
scope :by_popularity, -> { order_by(:view_count) }
def top_comments
comments.order_by(:like_count)
end
end
Remoted methods are available once the remoted action is handled by the controller and the method name is included in your routes.
FUTURE #6: Scopes may need to be whitelisted like remoted methods.
Then you can setup the your client model:
class API::V1::Post < Daylight::API
scopes :published, :by_popularity
remote :top_comments
end
And used like so:
API::Post.published.by_popularity #=> [#<API::V1::Post:0x007f8f890219b0 ...>, ...]
API::Post.top_comments #=> [#<API::V1::Comment:0x007f8f89050da0 ...>, ...]
FUTURE #9: Remote methods cannot be further refined like associations
This section is to help understanding what the client is doing so you can access your API server directly through your browers. This is useful for triaging bugs, but also can help examining requests and responses.
NOTE: This information can be used for when a client would need to be built in another platform or language but wishes to use the server API.
Daylight strives to continue to keep its API URLs symantic and RESTful.
ActiveResource
does most of the work:
HTTP URL # ACTION CLIENT EXAMPLE
GET /v1/posts.json # index API::Post.all
POST /v1/posts.json # create API::Post.create({})
GET /v1/posts/1.json # show API::Post.find(1)
PATCH/PUT /v1/posts/1.json # update API::Post.find(1).update_attributes({})
DELETE /v1/posts/1.json # destroy API::Post.find(1).delete
Daylight adds to these symantic URLs with the associated
and remoted
actions. In fact, they look similar to nested URLs:
HTTP URL # ACTION CLIENT EXAMPLE
GET /v1/posts/1/comments.json # associated API::Post.find(1).comments
GET /v1/posts/1/top_comments.json # remoted (collection) API::Post.find(1).top_comments
GET /v1/posts/1/statistics.json # remoted (record) API::Post.find(1).statistics
By URL alone, there's no way to distinguish between associated
and remoted
requests (they are not RESTful per se). For all intents and purposes they
both are an associated data nested in a member of a Post
.
To treat them differently, both the client and the server need to have
knowledge about what kind of specialized action they are. On the server this
is done through Routes. On the client model, this is done by
setting up remote
and scopes
The difference is in the response:
associated
is always a collectionremoted
may be a single record or a collection
FUTURE #4: Is there any reason why
remoted
couldn't just be anassociated
from the client point of view?
Daylight supports scopes, conditions, order, limit, and offset. Together these are called refinements. All of these refiniments are supplied through request parameters.
HTTP URL # PARAMETER TYPE CLIENT EXAMPLE
GET /v1/posts.json?order=created_at # Literal API::Post.order(:created_at)
GET /v1/posts.json?limit=10 # Literal API::Post.limit(10)
GET /v1/posts.json?offset=30 # Literal API::Post.offset(30)
GET /v1/posts.json?scopes=published # Literal API::Post.published
GET /v1/posts.json?scopes[]=published&scopes[]=by_popularity # Array API::Post.published.by_popularity
GET /v1/posts.json?filters[tag]=music # Hash API::Post.where(tag: "music")
GET /v1/posts.json?filters[tag][]=music&[tag][]=best-of # Hash of Array API::Post.where(tag: %w[music best-of])
None, one, or any combination of refinements can be supplied in the request. Combining all of the examples above:
API::Post.published.by_popularity.where(tag: %w[music best-of]).order(:created_at).limit(10).offset(30)
Will yield the following URL:
/v1/posts.json?scopes[]=published&scopes[]=by_popularity&filters[tag][]=music&[tag][]=best-of&order=created_at&offset=30&limit=10
NOTE: Collection of these parameters is how single requests to the server are are made by the client
Refinements are supported only on the index
and associated
actions because
these are requests for collections (as opposed to manipulating individual
members).
The only difference between index
and associated
is the target which the
refinements are applied. For example:
HTTP URL # ACTION TARGET
GET /v1/posts.json?order=created_at # index Orders all Posts
GET /v1/posts/1/comments.json?order_created_at # associated Orders all Comments for Post id=1
Data transmitted in requests and responses are formatted the same and use the same conventions. Any data recieved can be encoded in a response without any issues.
Both requests and responses will have a root element. For responses, root elmeents define which client model(s) will be instantiated. For requests, root elements define the parameter key that object attributes are sent under.
For an Post
object, when encoded to JSON:
{
"post": {
"id": 1,
"title": "100 Best Albums of 2014",
"created_by": 101
}
}
For collection of Post
objects, when encoded to JSON:
{
"posts": [
{
"id": 1,
"title": "100 Best Albums of 2014",
},
{
"id": 2,
"title": "Loving the new Son Lux album",
}
]
}
In both these cases, post
is identified as the root, it's pluralized for
to posts
for a collections.
Associations for has_one
are delivered as specified by the
(serializers)[#serializer] and are embedded as IDs (eg. blog_id
).
Foriegn key names (eg. created_by
) when
specified are embedded as well:
{
"zone": {
"id": 1,
"title": "100 Best Albums of 2014",
"blog_id": 2,
"created_by": 101
}
}
When setting a new object:
p.author = API::User.new({username: 'reidmix', fullname: 'Reid MacDonald'})
The new object will be updated using the accepts_nested_attributes_for
mechanism on ActiveRecord
. These attributes are passed along in its
own has which accepts_nested_attributes_for
expects:
{
"zone": {
"id": 1,
"title": "100 Best Albums of 2014",
"author_attributes": {
"username": "reidmix",
"fullname": "Reid MacDonald"
}
}
}
New items in a collections will be added to the existing set:
p.comments << API::Comment.new({created_by: 222, message: "New Comment"})
And will be encoded as an array:
{
"zone": {
"id": 1,
"title": "100 Best Albums of 2014",
"comments_attributes": [
{
"created_by": 101,
"message": "Existing Comment"
},
{
"created_by": 222,
"fullname": "New Comment"
}
]
}
}
FUTURE #10: It would be useful to know which associations the client model
accepts_nested_attributes_for
so that we can turn "on/off" the setter for associated objects.
Lastly, has_one :through
associations also uses the
accepts_nested_attributes_for
mechanism to describe the relationship in an
attributes subhash. For example
{
"post": {
"id": 283,
"title": "100 Best Albums of 2014",
"blog_id": 4,
"blog_attributes": {
"id": 4,
"company_id": 1
},
}
}
Our previous example describes when a Post
has a
Company
through a Blog
. The Blog
is referenced directly using the
blog_id
. Company
is referenced through the Blog
using both of the
blog_attribtues
.
The root element for the associated and remoted methods simply use the name of the action in the response.
Typically this keeps things simple when retrieving /v1/blog/1/top_comments.json
:
{
"top_comments": [
{
"id": 2,
"post_id": 1,
"created_by": 101,
"message": "Existing Comment"
},
{
"id": 3,
"post_id": 1,
"created_by": 222,
"fullname": "New Comment"
}
]
}
The associated and remoted methods will use configured name to look up the
client model. In the case of top_comments
, set the class_name
correct to the corresponding client model (ie. api/v1/comment
)
Metadata about an object and its usage in the framework is delivered in the
meta
section of the response data. Anything can be stored in this section
(by the serializer).
For example:
{
"post": {
"id": 1,
"title": "100 Best Albums of 2014",
},
"meta": {
"frozen": true
}
}
It is retrieved using the metadata
hash on the client model.
# example metadata that could specify when a Post cannot be updated
Post.find(1).metadata[:frozen] #=> true
Daylight uses metadata in two standard ways:
read_only
attributeswhere_values
clauses.
The way that Daylight know which methods are read only and cannot be written
is using the list of attributes that are read_only
for that client model:
{
"post": {
"id": 1,
"title": "100 Best Albums of 2014",
},
"meta": {
"post": {
"read_only": [
"slug",
"published",
"created_at"
]
}
}
}
Here, we will not be able to set slug
, published?
, and created_at
and Daylight will raise a NoMethodError
NOTE: ActiveResource handles predicate lookups for attributes (eg.
published
vs.published?
)
The way that Daylight know what Nested Resources are available to be set is
is using a list of classes that are nested_resources
for that client model:
{
"post": {
"id": 1,
"title": "100 Best Albums of 2014",
},
"meta": {
"post": {
"nested_resources": [
"author",
"comments"
]
}
}
}
Here, we will be able to create or associate the author
resource when creating
or updating a post
. We can also create a new comment
and add it to the
collection in the same way.
INFO: You can read up more in the User's Guide on how to use Nested Resources.
How Daylight keeps track of how a model was looked up when using
find_or_initialize
and find_or_create
is by returning the
where_values
from ActiveRecord. These will be merged when the
ActiveResource
is saved.
{
"post": {
"id": 1,
"title": "100 Best Albums of 2014",
},
"meta": {
"where_values": {
"blog_id": 1
}
}
}
To see this in action, if the Post
with the queried title was not found:
p = API::Blog.first.posts.find_or_create(title: "100 Best Albums of 2014")
p.title #=> "100 Best Albums of 2014"
# from the `where_values` during the lookup
p.blog_id #=> 1
Since, where_values
clauses can be quite complicated and are resolved by
ActiveRecord
we determine them server-side and send them as metadata in
the response.