daylight 0.9.0.rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +113 -0
- data/app/controllers/daylight_documentation/documentation_controller.rb +27 -0
- data/app/helpers/daylight_documentation/documentation_helper.rb +57 -0
- data/app/views/daylight_documentation/documentation/_header.haml +4 -0
- data/app/views/daylight_documentation/documentation/index.haml +12 -0
- data/app/views/daylight_documentation/documentation/model.haml +114 -0
- data/app/views/layouts/documentation.haml +22 -0
- data/config/routes.rb +8 -0
- data/doc/actions.md +70 -0
- data/doc/benchmarks.md +17 -0
- data/doc/contribute.md +80 -0
- data/doc/develop.md +1205 -0
- data/doc/environment.md +109 -0
- data/doc/example.md +3 -0
- data/doc/framework.md +31 -0
- data/doc/install.md +128 -0
- data/doc/principles.md +42 -0
- data/doc/testing.md +107 -0
- data/doc/usage.md +970 -0
- data/lib/daylight/api.rb +293 -0
- data/lib/daylight/associations.rb +247 -0
- data/lib/daylight/client_reloader.rb +45 -0
- data/lib/daylight/collection.rb +161 -0
- data/lib/daylight/errors.rb +94 -0
- data/lib/daylight/inflections.rb +7 -0
- data/lib/daylight/mock.rb +282 -0
- data/lib/daylight/read_only.rb +88 -0
- data/lib/daylight/refinements.rb +63 -0
- data/lib/daylight/reflection_ext.rb +67 -0
- data/lib/daylight/resource_proxy.rb +226 -0
- data/lib/daylight/version.rb +10 -0
- data/lib/daylight.rb +27 -0
- data/rails/daylight/api_controller.rb +354 -0
- data/rails/daylight/documentation.rb +13 -0
- data/rails/daylight/helpers.rb +32 -0
- data/rails/daylight/params.rb +23 -0
- data/rails/daylight/refiners.rb +186 -0
- data/rails/daylight/server.rb +29 -0
- data/rails/daylight/tasks.rb +37 -0
- data/rails/extensions/array_ext.rb +9 -0
- data/rails/extensions/autosave_association_fix.rb +49 -0
- data/rails/extensions/has_one_serializer_ext.rb +111 -0
- data/rails/extensions/inflections.rb +6 -0
- data/rails/extensions/nested_attributes_ext.rb +94 -0
- data/rails/extensions/read_only_attributes.rb +35 -0
- data/rails/extensions/render_json_meta.rb +99 -0
- data/rails/extensions/route_options.rb +47 -0
- data/rails/extensions/versioned_url_for.rb +22 -0
- data/spec/config/dependencies.rb +2 -0
- data/spec/config/factory_girl.rb +4 -0
- data/spec/config/simplecov_rcov.rb +26 -0
- data/spec/config/test_api.rb +1 -0
- data/spec/controllers/documentation_controller_spec.rb +24 -0
- data/spec/dummy/README.rdoc +28 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/images/.keep +0 -0
- data/spec/dummy/app/assets/javascripts/application.js +13 -0
- data/spec/dummy/app/assets/stylesheets/application.css +13 -0
- data/spec/dummy/app/controllers/application_controller.rb +5 -0
- data/spec/dummy/app/controllers/concerns/.keep +0 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/mailers/.keep +0 -0
- data/spec/dummy/app/models/.keep +0 -0
- data/spec/dummy/app/models/concerns/.keep +0 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/config/application.rb +24 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +29 -0
- data/spec/dummy/config/environments/production.rb +80 -0
- data/spec/dummy/config/environments/test.rb +36 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/daylight.rb +1 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +12 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +23 -0
- data/spec/dummy/config/routes.rb +59 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/lib/assets/.keep +0 -0
- data/spec/dummy/log/.keep +0 -0
- data/spec/dummy/public/404.html +58 -0
- data/spec/dummy/public/422.html +58 -0
- data/spec/dummy/public/500.html +57 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/helpers/documentation_helper_spec.rb +82 -0
- data/spec/lib/daylight/api_spec.rb +178 -0
- data/spec/lib/daylight/associations_spec.rb +325 -0
- data/spec/lib/daylight/collection_spec.rb +235 -0
- data/spec/lib/daylight/errors_spec.rb +111 -0
- data/spec/lib/daylight/mock_spec.rb +144 -0
- data/spec/lib/daylight/read_only_spec.rb +118 -0
- data/spec/lib/daylight/refinements_spec.rb +80 -0
- data/spec/lib/daylight/reflection_ext_spec.rb +50 -0
- data/spec/lib/daylight/resource_proxy_spec.rb +325 -0
- data/spec/rails/daylight/api_controller_spec.rb +421 -0
- data/spec/rails/daylight/helpers_spec.rb +41 -0
- data/spec/rails/daylight/params_spec.rb +45 -0
- data/spec/rails/daylight/refiners_spec.rb +178 -0
- data/spec/rails/extensions/array_ext_spec.rb +51 -0
- data/spec/rails/extensions/has_one_serializer_ext_spec.rb +135 -0
- data/spec/rails/extensions/nested_attributes_ext_spec.rb +177 -0
- data/spec/rails/extensions/render_json_meta_spec.rb +140 -0
- data/spec/rails/extensions/route_options_spec.rb +309 -0
- data/spec/rails/extensions/versioned_url_for_spec.rb +46 -0
- data/spec/spec_helper.rb +43 -0
- data/spec/support/migration_helper.rb +40 -0
- metadata +422 -0
data/doc/develop.md
ADDED
|
@@ -0,0 +1,1205 @@
|
|
|
1
|
+
# API Developer Guide
|
|
2
|
+
|
|
3
|
+
Daylight uses the MVC model provided by Rails to divide labor of an API request
|
|
4
|
+
with some constraints.
|
|
5
|
+
|
|
6
|
+
Instead of views, serializers are used to generate JSON/XML. Routes have a
|
|
7
|
+
great importance to the definition of the API. And the client becomes the
|
|
8
|
+
remote proxy for all API requests.
|
|
9
|
+
|
|
10
|
+
To better undertand Daylight's interactions, we define the following components:
|
|
11
|
+
* Rails **model** is the canonical version of the object
|
|
12
|
+
* A **serializer** defines what parts of the model are exposed to the client
|
|
13
|
+
* Rails **controller** defines which actions are performed on the model
|
|
14
|
+
* Rails **routes** defines what APIs are available to the client
|
|
15
|
+
* The **client** model is the remote representation of the Rails model
|
|
16
|
+
|
|
17
|
+
#### Table of Contents
|
|
18
|
+
* [Expectations](#expectations)
|
|
19
|
+
* [Building Your API](#building-your-api)
|
|
20
|
+
* [Models](#models)
|
|
21
|
+
* [Serializers](#serializers)
|
|
22
|
+
* [Controllers](#controllers)
|
|
23
|
+
* [Routes](#routes)
|
|
24
|
+
* [Client](#client)
|
|
25
|
+
* [Underlying Interaction](#underlying-interaction)
|
|
26
|
+
* [Symantic URLs](#symantic-urls)
|
|
27
|
+
* [Request Params](#request-params)
|
|
28
|
+
* [Symantic Data](#symantic-data)
|
|
29
|
+
* [Response Metadata](#response-metadata)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
## Expectations
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
* **Rails 4**: Daylight was built only using the most current version of Rails
|
|
36
|
+
4
|
|
37
|
+
* **Namespace APIs**: Client Models are all namespaced, by default under `API`
|
|
38
|
+
(namespace is customizable)
|
|
39
|
+
* **Versioned APIs**: URLs will be versioned, by default `v1` is the current
|
|
40
|
+
and only version (versions are customizable)
|
|
41
|
+
* **ActiveModelSerializer**: Serialization occurs via
|
|
42
|
+
`ActiveModel::Serailizer`, typically in JSON
|
|
43
|
+
|
|
44
|
+
## Building Your API
|
|
45
|
+
|
|
46
|
+
Building your Client from the bottom up you will need to develop your models,
|
|
47
|
+
controllers, routes that you are familiar with today. Add serializers to
|
|
48
|
+
describe the JSON generation of your object. Finally, build your client models
|
|
49
|
+
based on the API actions available and the response from the server.
|
|
50
|
+
|
|
51
|
+
### Models
|
|
52
|
+
|
|
53
|
+
Models are built exactly as they are in Rails, no changes are neccessary.
|
|
54
|
+
|
|
55
|
+
Through specifiecation on the routes, Daylight allows you to make scopes and
|
|
56
|
+
methods available to the client.
|
|
57
|
+
|
|
58
|
+
> NOTE: Daylight expects an model object or a collection when parsing results
|
|
59
|
+
> from a model method.
|
|
60
|
+
|
|
61
|
+
You can chose to allow models to be created, updated, and associated through
|
|
62
|
+
a "parent" model using the `accepts_nested_attributes_for` mechansism.
|
|
63
|
+
|
|
64
|
+
````ruby
|
|
65
|
+
class Post < ActiveRecord::Base
|
|
66
|
+
has_many :comments
|
|
67
|
+
|
|
68
|
+
accepts_nested_attributes_for :comments
|
|
69
|
+
end
|
|
70
|
+
````
|
|
71
|
+
|
|
72
|
+
Once the client is setup you can do the following:
|
|
73
|
+
|
|
74
|
+
````ruby
|
|
75
|
+
post = API::Post.find(1)
|
|
76
|
+
post << API::Comment.new(text: "This is an awesome post")
|
|
77
|
+
post.save
|
|
78
|
+
````
|
|
79
|
+
|
|
80
|
+
> INFO: ActiveResource looks up associations using foriegn keys but with
|
|
81
|
+
> `Daylight` you can call the associations defined on your model directly.
|
|
82
|
+
|
|
83
|
+
This is especially useful when you wish to preserve the richness of options on
|
|
84
|
+
your associations that are neccessary for your application to function
|
|
85
|
+
correctly. For example:
|
|
86
|
+
|
|
87
|
+
````ruby
|
|
88
|
+
class Post
|
|
89
|
+
has_many :comments
|
|
90
|
+
has_many :favorites, foreign_key: 'favorite_post_id', class_name: 'User'
|
|
91
|
+
has_many :commenters, -> { uniq }, through: :comments, class_name: 'User'
|
|
92
|
+
has_many :suppressed_comments, -> { where(spam: true) }, class_name: 'Comment'
|
|
93
|
+
end
|
|
94
|
+
````
|
|
95
|
+
|
|
96
|
+
Here we have 4 examples where using the model associations are neccesary. When
|
|
97
|
+
there is:
|
|
98
|
+
|
|
99
|
+
1. A configured foreign_key as in `favorites`
|
|
100
|
+
2. A through association as in `commenters`
|
|
101
|
+
3. A condindition block as `commenters` and `suppressed_comments` (eg. `uniq`
|
|
102
|
+
and `where`)
|
|
103
|
+
4. A class_name in all three `favorites`, `commenters`, and `suppressed_comments`
|
|
104
|
+
|
|
105
|
+
ActiveResource will not be able to resolve these associations correctly without
|
|
106
|
+
using the model-based associations, because it:
|
|
107
|
+
* Cannot determine endpoint or correct class to instantiate
|
|
108
|
+
* Uses the wrong lookup key (in through associations and foreign key option)
|
|
109
|
+
* Conditions will not be supplied in the request
|
|
110
|
+
|
|
111
|
+
> NOTE: Daylight includes `Daylight::Refiners` on all models that inherit from
|
|
112
|
+
> `ActiveRecord::Base`. At this time there is no way to exclude this module
|
|
113
|
+
> from any model. It does not modify existing ActiveRecord functionality.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
### Serializers
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
Daylight relies heavily on
|
|
121
|
+
[ActiveModelSerializers](https://github.com/rails-api/active_model_serializers)
|
|
122
|
+
and most information on how to use and customize it can be found in their
|
|
123
|
+
[README](https://github.com/rails-api/active_model_serializers/blob/master/README.md).
|
|
124
|
+
Serialize only the attributes you want to be public in your API. This allows
|
|
125
|
+
you to have a separation between the model data and the API data.
|
|
126
|
+
|
|
127
|
+
> NOTE: Make sure to include `:id` as an attribute so that Daylight will be
|
|
128
|
+
> able to make updates to the models correctly.
|
|
129
|
+
|
|
130
|
+
For example, `id`, `title` and `body` are exposed but there all other
|
|
131
|
+
attributes are not serialized:
|
|
132
|
+
|
|
133
|
+
````ruby
|
|
134
|
+
class PostSerializer < ActiveModel::Serializer
|
|
135
|
+
attributes :id, :title, :body
|
|
136
|
+
end
|
|
137
|
+
````
|
|
138
|
+
|
|
139
|
+
We encourage you to embed only ids to keep payloads down. Daylight will make
|
|
140
|
+
additional requests for the associated objects when accessed:
|
|
141
|
+
|
|
142
|
+
````ruby
|
|
143
|
+
class PostSerializer < ActiveModel::Serializer
|
|
144
|
+
embed :ids
|
|
145
|
+
|
|
146
|
+
attributes :id, :title, :body
|
|
147
|
+
|
|
148
|
+
has_one :category
|
|
149
|
+
has_one :author, key: 'created_by'
|
|
150
|
+
end
|
|
151
|
+
````
|
|
152
|
+
> NOTE: Make sure to use `key` option in serializers, not `foreign_key`
|
|
153
|
+
|
|
154
|
+
> INFO: `belongs_to` associations can be included using `has_one` in your
|
|
155
|
+
> serializer
|
|
156
|
+
|
|
157
|
+
There isn't any need for you to include your `has_many` associations in
|
|
158
|
+
your serializer. These collections will be looked up from the Daylight
|
|
159
|
+
client by a seperate request.
|
|
160
|
+
|
|
161
|
+
The serializer above will generate JSON like:
|
|
162
|
+
|
|
163
|
+
````json
|
|
164
|
+
{
|
|
165
|
+
"post": {
|
|
166
|
+
"id": 283,
|
|
167
|
+
"title": "100 Best Albums of 2014",
|
|
168
|
+
"body": "Here is my list...",
|
|
169
|
+
"category_id": 2,
|
|
170
|
+
"created_by": 101
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
````
|
|
174
|
+
|
|
175
|
+
There are 2 main additions Daylight adds to `ActiveModelSerializer` to enable
|
|
176
|
+
functionality for the client. They are _through associations_ and _read only
|
|
177
|
+
attributes_.
|
|
178
|
+
|
|
179
|
+
#### `has_one :through`
|
|
180
|
+
|
|
181
|
+
In Rails you can setup your model to have a `has_one :through`. This is a
|
|
182
|
+
special case for `ActiveModelSerializers` and for the Daylight client.
|
|
183
|
+
|
|
184
|
+
> NOTE: Rails does not have `belongs_to :through` associations.
|
|
185
|
+
|
|
186
|
+
For example, if your model has associations setup like so:
|
|
187
|
+
|
|
188
|
+
````ruby
|
|
189
|
+
class Post < ActiveRecord::Base
|
|
190
|
+
belongs_to :blog
|
|
191
|
+
has_one :company, through: :blog
|
|
192
|
+
end
|
|
193
|
+
````
|
|
194
|
+
|
|
195
|
+
To configure the `PostSerializer` to correctly use this through association
|
|
196
|
+
set it up like similarly to your model.
|
|
197
|
+
|
|
198
|
+
````ruby
|
|
199
|
+
class PostSerializer < ActiveModel::Serializer
|
|
200
|
+
embed :ids
|
|
201
|
+
|
|
202
|
+
attributes :id, :title, :body
|
|
203
|
+
|
|
204
|
+
has_one :blog # `has_one` in a serializer
|
|
205
|
+
has_one :company, through: :blog
|
|
206
|
+
end
|
|
207
|
+
````
|
|
208
|
+
|
|
209
|
+
This will create a special embedding in the JSON that the client will be able
|
|
210
|
+
to use to lookup the association:
|
|
211
|
+
|
|
212
|
+
````json
|
|
213
|
+
{
|
|
214
|
+
"post": {
|
|
215
|
+
"id": 283,
|
|
216
|
+
"title": "100 Best Albums of 2014",
|
|
217
|
+
"body": "Here is my list...",
|
|
218
|
+
"blog_id": 4,
|
|
219
|
+
"blog_attributes": {
|
|
220
|
+
"id": 4,
|
|
221
|
+
"company_id": 1
|
|
222
|
+
},
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
````
|
|
226
|
+
|
|
227
|
+
There's duplication in the JSON payload, but `post["blog_id"]` and
|
|
228
|
+
`post["blog_attributs"]["id"]` are used for different purposes.
|
|
229
|
+
|
|
230
|
+
````ruby
|
|
231
|
+
API::Post.first.blog #=> uses "blog_id"
|
|
232
|
+
API::Post.first.company #=> uses "blog_attributes"
|
|
233
|
+
````
|
|
234
|
+
|
|
235
|
+
> INFO: `blog_attributes` are also used for `accepts_nested_attributes_for`
|
|
236
|
+
> mechansism.
|
|
237
|
+
|
|
238
|
+
#### Read Only Attributes
|
|
239
|
+
|
|
240
|
+
There are cases when you want to expose data from the model as read only
|
|
241
|
+
attributes so they cannot be updated. These cases are when the attribute is:
|
|
242
|
+
* Evaluated and not stored in the database
|
|
243
|
+
* Stored into the database only when computed
|
|
244
|
+
* Readable but should not be updated
|
|
245
|
+
|
|
246
|
+
Here we have a `Post` object that does all three things. Assume there are
|
|
247
|
+
`updated_at` and `created_at` immutable attributes as well.
|
|
248
|
+
|
|
249
|
+
````ruby
|
|
250
|
+
class Post < ActiveRecord::Base
|
|
251
|
+
before_create do
|
|
252
|
+
self.slug = title.downcase.strip.gsub(' ', '-').gsub(/[^\w-]/, '')
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def published?
|
|
256
|
+
published_at.present?
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
````
|
|
260
|
+
|
|
261
|
+
To configure the `PostSerializer` to mark these attributes as read only:
|
|
262
|
+
|
|
263
|
+
````ruby
|
|
264
|
+
class PostSerializer < ActiveModel::Serializer
|
|
265
|
+
embed :ids
|
|
266
|
+
|
|
267
|
+
attributes :id, :title, :body
|
|
268
|
+
read_only :created_at, :updated_at, :slug, :published?
|
|
269
|
+
end
|
|
270
|
+
````
|
|
271
|
+
|
|
272
|
+
These attributes will be marked as read only in a special
|
|
273
|
+
[Metadata](#resposne-metadata) section in the object's JSON.
|
|
274
|
+
|
|
275
|
+
The client will be able to read each of these values but will raise a
|
|
276
|
+
`NoMethodError` when attempting to write to them.
|
|
277
|
+
|
|
278
|
+
````ruby
|
|
279
|
+
post = API::Post.first
|
|
280
|
+
post.created_at #=> "2014-05-02T19:58:09.248Z"
|
|
281
|
+
post.slug #=> "100-best-albums-of-2014"
|
|
282
|
+
post.published? #=> true
|
|
283
|
+
|
|
284
|
+
post.slug = '100-best-albums-of-all-time'
|
|
285
|
+
#=> NoMethodError: Cannot set read_only attribute: display_name
|
|
286
|
+
````
|
|
287
|
+
|
|
288
|
+
Because these attributes are read only, the client will exclude them from
|
|
289
|
+
being sent when the object is saved.
|
|
290
|
+
|
|
291
|
+
````ruby
|
|
292
|
+
post.title = "100 Best Albums of All Time"
|
|
293
|
+
post.save #=> true
|
|
294
|
+
````
|
|
295
|
+
|
|
296
|
+
In this case `published?`, `slug`, `created_at`, and `updated_at` are never
|
|
297
|
+
sent in a PUT update.
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
### Controllers
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
Controllers can be written without Daylight, but often times you must develop
|
|
305
|
+
boilerplate code for `index`, `create`, `show`, `update`, and `delete` actions.
|
|
306
|
+
Also, you may chose controllers that are for the API and controllers that are
|
|
307
|
+
for your application.
|
|
308
|
+
|
|
309
|
+
Daylight simplifies building API controllers:
|
|
310
|
+
|
|
311
|
+
````ruby
|
|
312
|
+
class PostController < APIController
|
|
313
|
+
end
|
|
314
|
+
````
|
|
315
|
+
|
|
316
|
+
> NOTE: Any functionality built in `ApplicationController` will be available to
|
|
317
|
+
> your `APIController` subclasses.
|
|
318
|
+
|
|
319
|
+
Since your controller is a subclass of `ActiveController::Base` continue to add
|
|
320
|
+
your own actions and routes for them as you do today in Rails.
|
|
321
|
+
|
|
322
|
+
There are predefined actions provided by Daylight, that handle both REST
|
|
323
|
+
actions and some specialized cases.
|
|
324
|
+
|
|
325
|
+
You must "turn on" these prede actions. Actions provided by Daylight are
|
|
326
|
+
turned off by default so what is exposed is determined by the developer.
|
|
327
|
+
|
|
328
|
+
For example, to turn on `show` action:
|
|
329
|
+
|
|
330
|
+
````ruby
|
|
331
|
+
class PostController < APIController
|
|
332
|
+
handles :show
|
|
333
|
+
end
|
|
334
|
+
````
|
|
335
|
+
|
|
336
|
+
This is equivalent to;
|
|
337
|
+
|
|
338
|
+
````ruby
|
|
339
|
+
class PostController < APIController
|
|
340
|
+
def show
|
|
341
|
+
render json: Post.find(params[:id])
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
````
|
|
345
|
+
|
|
346
|
+
Daylight uses the name of the controller to determine the related model to use.
|
|
347
|
+
Also, the `primary_key` name is retrived from that determined model. In fact,
|
|
348
|
+
all of the actions are just ruby methods, so you can overwrite them (and call
|
|
349
|
+
super) as you see fit:
|
|
350
|
+
|
|
351
|
+
````ruby
|
|
352
|
+
class PostController < APIController
|
|
353
|
+
handles :show
|
|
354
|
+
|
|
355
|
+
def show
|
|
356
|
+
super
|
|
357
|
+
|
|
358
|
+
@post.update_attributes(:view_count, @post.view_count+1)
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
````
|
|
362
|
+
|
|
363
|
+
To turn on multiple actions:
|
|
364
|
+
|
|
365
|
+
````ruby
|
|
366
|
+
class PostController < APIController
|
|
367
|
+
handles: :create, :show, :update, :destroy
|
|
368
|
+
end
|
|
369
|
+
````
|
|
370
|
+
|
|
371
|
+
Or you can turn them all (including the [Specialized Actions](#specialized-actions)):
|
|
372
|
+
|
|
373
|
+
````ruby
|
|
374
|
+
class PostController < APIController
|
|
375
|
+
handles: :all
|
|
376
|
+
end
|
|
377
|
+
````
|
|
378
|
+
|
|
379
|
+
For your reference, you can review the code of the equivalent actions in
|
|
380
|
+
[Controller Actions](actions.md)
|
|
381
|
+
|
|
382
|
+
#### Specialized Actions
|
|
383
|
+
|
|
384
|
+
Much of Daylight's features are offered through specialized controller actions.
|
|
385
|
+
These specialized actions are what enables:
|
|
386
|
+
* [Query Refinements](#index)
|
|
387
|
+
* [Model Associations](#associated)
|
|
388
|
+
* [Remote Methods](#remoted)
|
|
389
|
+
|
|
390
|
+
##### Index
|
|
391
|
+
|
|
392
|
+
You can refine queries of a resources collection by scopes, conditions, order,
|
|
393
|
+
limit, and offset.
|
|
394
|
+
|
|
395
|
+
This is accomplished with a method called `refine_by` which is added to your
|
|
396
|
+
models added by `Daylight::Refiners`
|
|
397
|
+
|
|
398
|
+
On the controller, see it called on the `index` action:
|
|
399
|
+
|
|
400
|
+
````ruby
|
|
401
|
+
class PostController < APIController
|
|
402
|
+
def index
|
|
403
|
+
render json: Post.refine_by(params)
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
````
|
|
407
|
+
|
|
408
|
+
##### Associated
|
|
409
|
+
|
|
410
|
+
Associations called through the model instance is accomplished using a method
|
|
411
|
+
called `associated` added by `Daylight::Refiners`. Which associations allowed
|
|
412
|
+
are defined in your [Routes](#routes).
|
|
413
|
+
|
|
414
|
+
On the controller, see it called by the (similarly named) `associated` action:
|
|
415
|
+
|
|
416
|
+
````ruby
|
|
417
|
+
class PostController < APIController
|
|
418
|
+
def associated
|
|
419
|
+
render json: Post.associated(params), root: associated_params
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
````
|
|
423
|
+
|
|
424
|
+
Associations can also be refined similarly to `index` where you can specify
|
|
425
|
+
scopes, conditions, order, limit, and offset. The associated action is
|
|
426
|
+
setup in [Through Associations](#through-associations) on the client model.
|
|
427
|
+
|
|
428
|
+
> NOTE: You can find more information on how to use these refinements in
|
|
429
|
+
> the [Daylight Users Guide](usage.md)
|
|
430
|
+
|
|
431
|
+
##### Remoted
|
|
432
|
+
|
|
433
|
+
Any public method is allowed to be called on the model instance by use of the
|
|
434
|
+
`remoted` method added by `Daylight::Refiners`. Which public methods are
|
|
435
|
+
allowed are defined in your [Routes](#routes).
|
|
436
|
+
|
|
437
|
+
> FUTURE [#4](https://github.com/att-cloud/daylight/issues/4):
|
|
438
|
+
> It would be nice to allow public methods on the model class to be exposed and
|
|
439
|
+
> called against the collection.
|
|
440
|
+
|
|
441
|
+
Remoted methods should return a record or collections of records so that they
|
|
442
|
+
may be instantiated correctly by the client and act as a proxy back to the API.
|
|
443
|
+
|
|
444
|
+
On the controller, see it called by the (similarly named) `remoted` action:
|
|
445
|
+
|
|
446
|
+
````ruby
|
|
447
|
+
class PostController < APIController
|
|
448
|
+
def remoted
|
|
449
|
+
render json: Post.remoted(params), root: remoted_params
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
````
|
|
453
|
+
|
|
454
|
+
All of the specialized actions can be enabled on your controller like the REST
|
|
455
|
+
actions:
|
|
456
|
+
|
|
457
|
+
````ruby
|
|
458
|
+
class PostController < APIController
|
|
459
|
+
handles :index, :associated, :remoted
|
|
460
|
+
end
|
|
461
|
+
````
|
|
462
|
+
|
|
463
|
+
They are also included when specifying `handles :all`.
|
|
464
|
+
|
|
465
|
+
> INFO: To understand how `root` option is being used in both `assoicated`
|
|
466
|
+
> and `remoted` please refer to the section on
|
|
467
|
+
[Symantic Data](#associated-and-remoted-responses)
|
|
468
|
+
|
|
469
|
+
#### Customization
|
|
470
|
+
|
|
471
|
+
Behind the scenes, the controller actions look up models based on its controller
|
|
472
|
+
name. The portion before the word _Controller_ (ie. when `PostController` is
|
|
473
|
+
the controller name it determines the model name to be `Post`).
|
|
474
|
+
|
|
475
|
+
You may specify a different model to use:
|
|
476
|
+
|
|
477
|
+
````ruby
|
|
478
|
+
class WelcomeController
|
|
479
|
+
set_model_name :post
|
|
480
|
+
end
|
|
481
|
+
````
|
|
482
|
+
|
|
483
|
+
In `create`, `show`, `update` and `destroy` actions (member) results are stored
|
|
484
|
+
in an instance variable. The instance variable name is based on the model
|
|
485
|
+
name (ie. when `PostController` is the controller name the instance variable is
|
|
486
|
+
called `@post`).
|
|
487
|
+
|
|
488
|
+
In `index`, `associated`, and `remoted` specialized actions results are stored
|
|
489
|
+
in an instance variable simply called `@collection`
|
|
490
|
+
|
|
491
|
+
Both of these instance variables may be customized:
|
|
492
|
+
|
|
493
|
+
````ruby
|
|
494
|
+
class PostController
|
|
495
|
+
set_record_name :result
|
|
496
|
+
set_collection_name :results
|
|
497
|
+
end
|
|
498
|
+
````
|
|
499
|
+
|
|
500
|
+
> NOTE: Daylight calls the instance variables for specialized actions
|
|
501
|
+
>`@collection` because in `associated` and `remoted` actions the results may be
|
|
502
|
+
> any type of model instances.
|
|
503
|
+
|
|
504
|
+
In all customizations can use a string, symbol, or constant as the value:
|
|
505
|
+
|
|
506
|
+
````ruby
|
|
507
|
+
class PostController
|
|
508
|
+
set_model_name Post
|
|
509
|
+
set_record_name 'result'
|
|
510
|
+
set_collection_name :results
|
|
511
|
+
end
|
|
512
|
+
````
|
|
513
|
+
|
|
514
|
+
Lastly, your application may already have an APIController and there could be
|
|
515
|
+
a name collision. Daylight will not use this constant if it's already defined.
|
|
516
|
+
|
|
517
|
+
In this case use `Daylight::APIController` to subclass from:
|
|
518
|
+
|
|
519
|
+
````ruby
|
|
520
|
+
class PostController < Daylight::APIController
|
|
521
|
+
handles :all
|
|
522
|
+
end
|
|
523
|
+
````
|
|
524
|
+
|
|
525
|
+
---
|
|
526
|
+
|
|
527
|
+
### Routes
|
|
528
|
+
|
|
529
|
+
Setup your routes as you do in Rails today. Since Daylight assumes that
|
|
530
|
+
your API is versioned, make sure to employ `namespace` in routes or use
|
|
531
|
+
a simple, powerful tool like
|
|
532
|
+
[Versionist](https://github.com/bploetz/versionist).
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
````ruby
|
|
536
|
+
API::Application.routes.draw do
|
|
537
|
+
namespace :v1 do
|
|
538
|
+
resources :users, :posts, :comments
|
|
539
|
+
end
|
|
540
|
+
end
|
|
541
|
+
````
|
|
542
|
+
|
|
543
|
+
You can modify the actions on each reasource as you see fit, matching your
|
|
544
|
+
`APIController` actions:
|
|
545
|
+
|
|
546
|
+
````ruby
|
|
547
|
+
API::Application.routes.draw do
|
|
548
|
+
namespace :v1 do
|
|
549
|
+
resources :users, :posts
|
|
550
|
+
resources :comments, except: [:index, :destroy]
|
|
551
|
+
end
|
|
552
|
+
end
|
|
553
|
+
````
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
To expose model assoications, you can do that with Daylight additions to
|
|
557
|
+
routing options.
|
|
558
|
+
|
|
559
|
+
> FUTURE [#7](https://github.com/att-cloud/daylight/issues/7):
|
|
560
|
+
> The client only supports model associations on `has_many` relationships. We
|
|
561
|
+
> will need to evaluate the need to support model associations on `has_one` and
|
|
562
|
+
> `has_many` (as we never had a case for it)
|
|
563
|
+
|
|
564
|
+
````ruby
|
|
565
|
+
API::Application.routes.draw do
|
|
566
|
+
namespace :v1 do
|
|
567
|
+
resources :users, associated: [:posts, :comments]
|
|
568
|
+
resources :posts, associated: [:comments]
|
|
569
|
+
resources :comments, except: [:index, :destroy]
|
|
570
|
+
end
|
|
571
|
+
end
|
|
572
|
+
````
|
|
573
|
+
|
|
574
|
+
Any of the rich `has_many` relationships setup may be exposed as a model
|
|
575
|
+
associations, choose which ones to expose:
|
|
576
|
+
|
|
577
|
+
````ruby
|
|
578
|
+
API::Application.routes.draw do
|
|
579
|
+
namespace :v1 do
|
|
580
|
+
resources :users, associated: [:comments, :posts]
|
|
581
|
+
resources :posts, associated: [:authors, :comments, :commenters]
|
|
582
|
+
resources :comments, except: [:index, :destroy]
|
|
583
|
+
end
|
|
584
|
+
end
|
|
585
|
+
````
|
|
586
|
+
|
|
587
|
+
To expose remoted methods, you can do that with Daylight additions to
|
|
588
|
+
routing options.
|
|
589
|
+
|
|
590
|
+
````ruby
|
|
591
|
+
API::Application.routes.draw do
|
|
592
|
+
namespace :v1 do
|
|
593
|
+
resources :users, associated: [:comments, :posts]
|
|
594
|
+
resources :posts, associated: [:authors, :comments, :commenters],
|
|
595
|
+
remoted: [:top_comments]
|
|
596
|
+
resources :comments, except: [:index, :destroy]
|
|
597
|
+
end
|
|
598
|
+
end
|
|
599
|
+
````
|
|
600
|
+
|
|
601
|
+
As you can see when you develop your API, the routes file becomes a
|
|
602
|
+
specification of what is exposed to the client.
|
|
603
|
+
|
|
604
|
+
---
|
|
605
|
+
|
|
606
|
+
### Client
|
|
607
|
+
|
|
608
|
+
The client is where all our server setup is put together. Client models
|
|
609
|
+
subclass from `Daylight::API` classes.
|
|
610
|
+
|
|
611
|
+
> INFO: `Daylight::API` subclasses `ActiveResource::Base` and extends it
|
|
612
|
+
|
|
613
|
+
You can build your client model as you do today as an `ActiveResource::Base`
|
|
614
|
+
as all functionality performs the same out of the box. (Only when using
|
|
615
|
+
Daylight features is when Daylight additions to `ActiveResource` enabled)
|
|
616
|
+
|
|
617
|
+
````ruby
|
|
618
|
+
class API::V1::Post < Daylight::API
|
|
619
|
+
end
|
|
620
|
+
````
|
|
621
|
+
|
|
622
|
+
Here again, we encourage you to namespace and version your client models.
|
|
623
|
+
You can do this using module names and Daylight will offer several
|
|
624
|
+
conviniences.
|
|
625
|
+
|
|
626
|
+
#### Aliased API
|
|
627
|
+
|
|
628
|
+
Daylight will _alias_ to the current version defined in your `setup!`.
|
|
629
|
+
Assuming you've have two versions of your client models:
|
|
630
|
+
|
|
631
|
+
````ruby
|
|
632
|
+
Daylight::API.setup!(version: 'v1', versions: %w[v1 v2])
|
|
633
|
+
API::Post #=> API::V1::Post
|
|
634
|
+
|
|
635
|
+
Daylight::API.setup!(version: 'v2')
|
|
636
|
+
reload!
|
|
637
|
+
|
|
638
|
+
API::Post #=> API::V2::Post
|
|
639
|
+
````
|
|
640
|
+
|
|
641
|
+
Using the aliased versions of your API is practical for your end users. They
|
|
642
|
+
will not need to update all of the constants in their codebase from
|
|
643
|
+
`API::V1::Post` to `API::V2::Post` after they migrate. Instead they can focus
|
|
644
|
+
on differences provided in the new API version.
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
> FUTURE [#2](https://github.com/att-cloud/daylight/issues/2):
|
|
648
|
+
> It may be possible to have different versions of a client model to run
|
|
649
|
+
> concurrently. This would aid end users of the API to move/keep some classes
|
|
650
|
+
> on a particular version.
|
|
651
|
+
|
|
652
|
+
#### Client Reloader
|
|
653
|
+
|
|
654
|
+
When developing your API when you `reload!` within your console, the aliased
|
|
655
|
+
constants will still reference the older class definitions. Currently, this
|
|
656
|
+
only works with IRB. To re-alias the constants during a `reload!` add the
|
|
657
|
+
following to an initializer:
|
|
658
|
+
|
|
659
|
+
````ruby
|
|
660
|
+
require 'daylight/client_reloader'
|
|
661
|
+
````
|
|
662
|
+
|
|
663
|
+
This should not be needed for your end-users but is available for debugging
|
|
664
|
+
purposes if needed.
|
|
665
|
+
|
|
666
|
+
#### Association Lookup
|
|
667
|
+
|
|
668
|
+
Daylight will lookup association classes using the namespace and version set
|
|
669
|
+
in your client. This simplifies setting up your relationships becaause you do
|
|
670
|
+
not need to define your `class_name` on each association:
|
|
671
|
+
|
|
672
|
+
````ruby
|
|
673
|
+
class API::V1::Post < Daylight::API
|
|
674
|
+
belongs_to :blog
|
|
675
|
+
|
|
676
|
+
has_many :comments
|
|
677
|
+
end
|
|
678
|
+
````
|
|
679
|
+
|
|
680
|
+
Once all client models are setup, associationed models will be fetched and
|
|
681
|
+
initialized:
|
|
682
|
+
|
|
683
|
+
````ruby
|
|
684
|
+
post = Daylight::Post.first
|
|
685
|
+
|
|
686
|
+
post.blog #=> #<API::V1::Blog:0x007fd8ca4717d8 ...>
|
|
687
|
+
post.comments #=> [#<API::V1::Comment:0x007fd8ca538ce8...>, ...]
|
|
688
|
+
````
|
|
689
|
+
|
|
690
|
+
There are times when you will need to specify a client model just like you do
|
|
691
|
+
in `ActiveRecord`:
|
|
692
|
+
|
|
693
|
+
````ruby
|
|
694
|
+
class API::V1::Post < Daylight::API
|
|
695
|
+
belongs_to :author, class_name: 'api/v1/user', foreign_key: 'created_by'
|
|
696
|
+
belongs_to :blog
|
|
697
|
+
|
|
698
|
+
has_many :comments
|
|
699
|
+
end
|
|
700
|
+
````
|
|
701
|
+
|
|
702
|
+
> NOTE: The foreign key needs to match the same key in your serailizer and the
|
|
703
|
+
> `foreign_key` in your `ActiveRecord` model.
|
|
704
|
+
|
|
705
|
+
The `User` will be correctly retrieved for the `author` association:
|
|
706
|
+
|
|
707
|
+
````ruby
|
|
708
|
+
Daylight::Post.first.author #=> #<API::V1::User:0x007fd8ca543e90 ...>
|
|
709
|
+
````
|
|
710
|
+
|
|
711
|
+
#### Through Associations
|
|
712
|
+
|
|
713
|
+
There are two types of Through Associations in Daylight:
|
|
714
|
+
* `has_one :through`
|
|
715
|
+
* `has_many :through`
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
First, once you've setup your [`has_one :through`](#has_one-through)
|
|
719
|
+
association in your model and serializer. You can use it in the client model.
|
|
720
|
+
This is setup similar to the `ActiveRecord` model:
|
|
721
|
+
|
|
722
|
+
````ruby
|
|
723
|
+
class API::V1::Post < Daylight::API
|
|
724
|
+
belongs_to :blog
|
|
725
|
+
has_one :company, through: :blog
|
|
726
|
+
end
|
|
727
|
+
````
|
|
728
|
+
|
|
729
|
+
The associations will be available:
|
|
730
|
+
|
|
731
|
+
````ruby
|
|
732
|
+
post = API::Post.first
|
|
733
|
+
post.blog #=> #<API::V1::Blog:0x007fd8ca4717d8 ...>
|
|
734
|
+
post.company #=> #<API::V1::Company:0x007f8f83f30b28 ...>
|
|
735
|
+
````
|
|
736
|
+
|
|
737
|
+
Second, once the `has_many :through` associations are exposed in the
|
|
738
|
+
[Routes](#routes) you can them up in the client model:
|
|
739
|
+
|
|
740
|
+
````ruby
|
|
741
|
+
class API::V1::Post < Daylight::API
|
|
742
|
+
has_many 'comments'
|
|
743
|
+
has_many 'commenters', through: :association
|
|
744
|
+
end
|
|
745
|
+
````
|
|
746
|
+
|
|
747
|
+
The value is always `:association` and is a directive to Daylight to use the
|
|
748
|
+
[associated](#associated) action on the `PostController`.
|
|
749
|
+
|
|
750
|
+
The associations will be available:
|
|
751
|
+
|
|
752
|
+
````ruby
|
|
753
|
+
post = API::Post.first
|
|
754
|
+
post.comments #=> [#<API::V1::Comment:0x007f8f83f91c20 ...>, ...]
|
|
755
|
+
post.commenters #=> [#<API::V1::Company:0x007f8f83fe1f40 ...>, ...]
|
|
756
|
+
````
|
|
757
|
+
|
|
758
|
+
Here we can see a typical `ActiveResource` association for `comments`is used
|
|
759
|
+
along-side our `has_many :through`. If there is no reason to use the model
|
|
760
|
+
assoication, the flexibility is up to you. Please review the reasons to use
|
|
761
|
+
[Model Association](#models).
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
You can setup both to use model associations:
|
|
765
|
+
````ruby
|
|
766
|
+
class API::V1::Post < Daylight::API
|
|
767
|
+
has_many 'comments', through: :association
|
|
768
|
+
has_many 'commenters', through: :association
|
|
769
|
+
end
|
|
770
|
+
````
|
|
771
|
+
|
|
772
|
+
Refer to the [Daylight Users Guide](usage.md) to see how to further work
|
|
773
|
+
associations.
|
|
774
|
+
|
|
775
|
+
#### Scopes and Remoted Methods
|
|
776
|
+
|
|
777
|
+
Adding adding scopes and remoted methods are very simple.
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
Given the `ActiveRecord` model setup:
|
|
781
|
+
|
|
782
|
+
````ruby
|
|
783
|
+
class Post < ActiveRecord::Base
|
|
784
|
+
scope :published, -> { where(published: true) }
|
|
785
|
+
scope :by_popularity, -> { order_by(:view_count) }
|
|
786
|
+
|
|
787
|
+
def top_comments
|
|
788
|
+
comments.order_by(:like_count)
|
|
789
|
+
end
|
|
790
|
+
end
|
|
791
|
+
````
|
|
792
|
+
|
|
793
|
+
Remoted methods are available once the [remoted](#remoted) action is handled
|
|
794
|
+
by the controller and the method name is included in your [routes](#routes).
|
|
795
|
+
|
|
796
|
+
> FUTURE [#6](https://github.com/att-cloud/daylight/issues/6):
|
|
797
|
+
> Scopes may need to be whitelisted like remoted methods.
|
|
798
|
+
|
|
799
|
+
Then you can setup the your client model:
|
|
800
|
+
|
|
801
|
+
````ruby
|
|
802
|
+
class API::V1::Post < Daylight::API
|
|
803
|
+
scopes :published, :by_popularity
|
|
804
|
+
remote :top_comments
|
|
805
|
+
end
|
|
806
|
+
````
|
|
807
|
+
And used like so:
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
````ruby
|
|
811
|
+
API::Post.published.by_popularity #=> [#<API::V1::Post:0x007f8f890219b0 ...>, ...]
|
|
812
|
+
API::Post.top_comments #=> [#<API::V1::Comment:0x007f8f89050da0 ...>, ...]
|
|
813
|
+
````
|
|
814
|
+
|
|
815
|
+
> FUTURE [#9](https://github.com/att-cloud/daylight/issues/9):
|
|
816
|
+
> Remote methods cannot be further refined like associations
|
|
817
|
+
|
|
818
|
+
## Underlying Interaction
|
|
819
|
+
|
|
820
|
+
This section is to help understanding what the client is doing so you can
|
|
821
|
+
access your API server directly through your browers. This is useful for
|
|
822
|
+
triaging bugs, but also can help examining requests and responses.
|
|
823
|
+
|
|
824
|
+
> NOTE: This information can be used for when a client would need to be
|
|
825
|
+
> built in another platform or language but wishes to use the server API.
|
|
826
|
+
|
|
827
|
+
### Symantic URLs
|
|
828
|
+
|
|
829
|
+
Daylight strives to continue to keep its API URLs symantic and RESTful.
|
|
830
|
+
`ActiveResource` does most of the work:
|
|
831
|
+
|
|
832
|
+
HTTP URL # ACTION CLIENT EXAMPLE
|
|
833
|
+
|
|
834
|
+
GET /v1/posts.json # index API::Post.all
|
|
835
|
+
POST /v1/posts.json # create API::Post.create({})
|
|
836
|
+
GET /v1/posts/1.json # show API::Post.find(1)
|
|
837
|
+
PATCH/PUT /v1/posts/1.json # update API::Post.find(1).update_attributes({})
|
|
838
|
+
DELETE /v1/posts/1.json # destroy API::Post.find(1).delete
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
Daylight adds to these symantic URLs with the `associated` and `remoted`
|
|
842
|
+
actions. In fact, they look similar to nested URLs:
|
|
843
|
+
|
|
844
|
+
HTTP URL # ACTION CLIENT EXAMPLE
|
|
845
|
+
|
|
846
|
+
GET /v1/posts/1/comments.json # associated API::Post.find(1).comments
|
|
847
|
+
GET /v1/posts/1/top_comments.json # remoted (collection) API::Post.find(1).top_comments
|
|
848
|
+
GET /v1/posts/1/statistics.json # remoted (record) API::Post.find(1).statistics
|
|
849
|
+
|
|
850
|
+
By URL alone, there's no way to distinguish between `associated` and `remoted`
|
|
851
|
+
requests (they are not RESTful per se). For all intents and purposes they
|
|
852
|
+
both are an associated data nested in a member of a `Post`.
|
|
853
|
+
|
|
854
|
+
To treat them differently, both the client and the server need to have
|
|
855
|
+
knowledge about what kind of specialized action they are. On the server this
|
|
856
|
+
is done through [Routes](#routes). On the client model, this is done by
|
|
857
|
+
setting up `remote` and `scopes`
|
|
858
|
+
|
|
859
|
+
The difference is in the response:
|
|
860
|
+
* `associated` is always a collection
|
|
861
|
+
* `remoted` may be a single record or a collection
|
|
862
|
+
|
|
863
|
+
> FUTURE [#4](https://github.com/att-cloud/daylight/issues/4):
|
|
864
|
+
> Is there any reason why `remoted` couldn't just be an `associated` from the
|
|
865
|
+
> client point of view?
|
|
866
|
+
|
|
867
|
+
### Request Parameters
|
|
868
|
+
|
|
869
|
+
Daylight supports scopes, conditions, order, limit, and offset. Together these
|
|
870
|
+
are called refinements. All of these refiniments are supplied through request
|
|
871
|
+
parameters.
|
|
872
|
+
|
|
873
|
+
HTTP URL # PARAMETER TYPE CLIENT EXAMPLE
|
|
874
|
+
|
|
875
|
+
GET /v1/posts.json?order=created_at # Literal API::Post.order(:created_at)
|
|
876
|
+
GET /v1/posts.json?limit=10 # Literal API::Post.limit(10)
|
|
877
|
+
GET /v1/posts.json?offset=30 # Literal API::Post.offset(30)
|
|
878
|
+
GET /v1/posts.json?scopes=published # Literal API::Post.published
|
|
879
|
+
GET /v1/posts.json?scopes[]=published&scopes[]=by_popularity # Array API::Post.published.by_popularity
|
|
880
|
+
GET /v1/posts.json?filters[tag]=music # Hash API::Post.where(tag: "music")
|
|
881
|
+
GET /v1/posts.json?filters[tag][]=music&[tag][]=best-of # Hash of Array API::Post.where(tag: %w[music best-of])
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
None, one, or any combination of refinements can be supplied in
|
|
885
|
+
the request. Combining all of the examples above:
|
|
886
|
+
|
|
887
|
+
````ruby
|
|
888
|
+
API::Post.published.by_popularity.where(tag: %w[music best-of]).order(:created_at).limit(10).offset(30)
|
|
889
|
+
````
|
|
890
|
+
|
|
891
|
+
Will yield the following URL:
|
|
892
|
+
|
|
893
|
+
/v1/posts.json?scopes[]=published&scopes[]=by_popularity&filters[tag][]=music&[tag][]=best-of&order=created_at&offset=30&limit=10
|
|
894
|
+
|
|
895
|
+
> NOTE: Collection of these parameters is how single requests to the server are
|
|
896
|
+
> are made by the client
|
|
897
|
+
|
|
898
|
+
Refinements are supported only on the `index` and `associated` actions because
|
|
899
|
+
these are requests for collections (as opposed to manipulating individual
|
|
900
|
+
members).
|
|
901
|
+
|
|
902
|
+
The only difference between `index` and `associated` is the target which the
|
|
903
|
+
refinements are applied. For example:
|
|
904
|
+
|
|
905
|
+
HTTP URL # ACTION TARGET
|
|
906
|
+
|
|
907
|
+
GET /v1/posts.json?order=created_at # index Orders all Posts
|
|
908
|
+
GET /v1/posts/1/comments.json?order_created_at # associated Orders all Comments for Post id=1
|
|
909
|
+
|
|
910
|
+
### Symantic Data
|
|
911
|
+
|
|
912
|
+
Data transmitted in requests and responses are formatted the same and use
|
|
913
|
+
the same conventions. Any data recieved can be encoded in a response without
|
|
914
|
+
any issues.
|
|
915
|
+
|
|
916
|
+
#### Root Element
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
Both requests and responses will have a root element. For responses, root
|
|
920
|
+
elmeents define which client model(s) will be instantiated. For requests,
|
|
921
|
+
root elements define the parameter key that object attributes are sent
|
|
922
|
+
under.
|
|
923
|
+
|
|
924
|
+
For an `Post` object, when encoded to JSON:
|
|
925
|
+
|
|
926
|
+
````json
|
|
927
|
+
{
|
|
928
|
+
"post": {
|
|
929
|
+
"id": 1,
|
|
930
|
+
"title": "100 Best Albums of 2014",
|
|
931
|
+
"created_by": 101
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
````
|
|
935
|
+
|
|
936
|
+
For collection of `Post` objects, when encoded to JSON:
|
|
937
|
+
|
|
938
|
+
````json
|
|
939
|
+
{
|
|
940
|
+
"posts": [
|
|
941
|
+
{
|
|
942
|
+
"id": 1,
|
|
943
|
+
"title": "100 Best Albums of 2014",
|
|
944
|
+
},
|
|
945
|
+
{
|
|
946
|
+
"id": 2,
|
|
947
|
+
"title": "Loving the new Son Lux album",
|
|
948
|
+
}
|
|
949
|
+
]
|
|
950
|
+
}
|
|
951
|
+
````
|
|
952
|
+
|
|
953
|
+
In both these cases, `post` is identified as the root, it's pluralized for
|
|
954
|
+
to `posts` for a collections.
|
|
955
|
+
|
|
956
|
+
#### Associated Attributes
|
|
957
|
+
|
|
958
|
+
Associations for `has_one` are delivered as specified by the
|
|
959
|
+
(serializers)[#serializer] and are embedded as IDs (eg. `blog_id`).
|
|
960
|
+
Foriegn key names (eg. `created_by`) when
|
|
961
|
+
specified are embedded as well:
|
|
962
|
+
|
|
963
|
+
````json
|
|
964
|
+
{
|
|
965
|
+
"zone": {
|
|
966
|
+
"id": 1,
|
|
967
|
+
"title": "100 Best Albums of 2014",
|
|
968
|
+
"blog_id": 2,
|
|
969
|
+
"created_by": 101
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
````
|
|
973
|
+
|
|
974
|
+
When setting a new object:
|
|
975
|
+
|
|
976
|
+
````ruby
|
|
977
|
+
p.author = API::User.new({username: 'reidmix', fullname: 'Reid MacDonald'})
|
|
978
|
+
````
|
|
979
|
+
|
|
980
|
+
The new object will be updated using the `accepts_nested_attributes_for`
|
|
981
|
+
mechanism on `ActiveRecord`. These attributes are passed along in its
|
|
982
|
+
own has which `accepts_nested_attributes_for` expects:
|
|
983
|
+
|
|
984
|
+
````json
|
|
985
|
+
{
|
|
986
|
+
"zone": {
|
|
987
|
+
"id": 1,
|
|
988
|
+
"title": "100 Best Albums of 2014",
|
|
989
|
+
"author_attributes": {
|
|
990
|
+
"username": "reidmix",
|
|
991
|
+
"fullname": "Reid MacDonald"
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
````
|
|
996
|
+
|
|
997
|
+
New items in a collections will be added to the existing set:
|
|
998
|
+
|
|
999
|
+
````ruby
|
|
1000
|
+
p.comments << API::Comment.new({created_by: 222, message: "New Comment"})
|
|
1001
|
+
````
|
|
1002
|
+
|
|
1003
|
+
And will be encoded as an array:
|
|
1004
|
+
|
|
1005
|
+
````json
|
|
1006
|
+
{
|
|
1007
|
+
"zone": {
|
|
1008
|
+
"id": 1,
|
|
1009
|
+
"title": "100 Best Albums of 2014",
|
|
1010
|
+
"comments_attributes": [
|
|
1011
|
+
{
|
|
1012
|
+
"created_by": 101,
|
|
1013
|
+
"message": "Existing Comment"
|
|
1014
|
+
},
|
|
1015
|
+
{
|
|
1016
|
+
"created_by": 222,
|
|
1017
|
+
"fullname": "New Comment"
|
|
1018
|
+
}
|
|
1019
|
+
]
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
````
|
|
1023
|
+
|
|
1024
|
+
> FUTURE [#10](https://github.com/att-cloud/daylight/issues/10):
|
|
1025
|
+
> It would be useful to know which associations the client model
|
|
1026
|
+
> `accepts_nested_attributes_for` so that we can turn "on/off"
|
|
1027
|
+
> the setter for associated objects.
|
|
1028
|
+
|
|
1029
|
+
Lastly, `has_one :through` associations also uses the
|
|
1030
|
+
`accepts_nested_attributes_for` mechanism to describe the relationship in an
|
|
1031
|
+
attributes subhash. For example
|
|
1032
|
+
|
|
1033
|
+
````json
|
|
1034
|
+
{
|
|
1035
|
+
"post": {
|
|
1036
|
+
"id": 283,
|
|
1037
|
+
"title": "100 Best Albums of 2014",
|
|
1038
|
+
"blog_id": 4,
|
|
1039
|
+
"blog_attributes": {
|
|
1040
|
+
"id": 4,
|
|
1041
|
+
"company_id": 1
|
|
1042
|
+
},
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
````
|
|
1046
|
+
|
|
1047
|
+
Our [previous example](#has_one-through) describes when a `Post` has a
|
|
1048
|
+
`Company` through a `Blog`. The `Blog` is referenced directly using the
|
|
1049
|
+
`blog_id`. `Company` is referenced _through_ the `Blog` using both of the
|
|
1050
|
+
`blog_attribtues`.
|
|
1051
|
+
|
|
1052
|
+
#### Associated and Remoted Responses
|
|
1053
|
+
|
|
1054
|
+
The root element for the associated and remoted methods simply use the name of
|
|
1055
|
+
the action in the response.
|
|
1056
|
+
|
|
1057
|
+
Typically this keeps things simple when retrieving `/v1/blog/1/top_comments.json`:
|
|
1058
|
+
|
|
1059
|
+
````json
|
|
1060
|
+
{
|
|
1061
|
+
"top_comments": [
|
|
1062
|
+
{
|
|
1063
|
+
"id": 2,
|
|
1064
|
+
"post_id": 1,
|
|
1065
|
+
"created_by": 101,
|
|
1066
|
+
"message": "Existing Comment"
|
|
1067
|
+
},
|
|
1068
|
+
{
|
|
1069
|
+
"id": 3,
|
|
1070
|
+
"post_id": 1,
|
|
1071
|
+
"created_by": 222,
|
|
1072
|
+
"fullname": "New Comment"
|
|
1073
|
+
}
|
|
1074
|
+
]
|
|
1075
|
+
}
|
|
1076
|
+
````
|
|
1077
|
+
|
|
1078
|
+
The associated and remoted methods will use configured name to look up the
|
|
1079
|
+
client model. In the case of `top_comments`, set the `class_name`
|
|
1080
|
+
correct to the corresponding client model (ie. `api/v1/comment`)
|
|
1081
|
+
|
|
1082
|
+
### Response Metadata
|
|
1083
|
+
|
|
1084
|
+
Metadata about an object and its usage in the framework is delivered in the
|
|
1085
|
+
`meta` section of the response data. Anything can be stored in this section
|
|
1086
|
+
(by the serializer).
|
|
1087
|
+
|
|
1088
|
+
For example:
|
|
1089
|
+
|
|
1090
|
+
````json
|
|
1091
|
+
{
|
|
1092
|
+
"post": {
|
|
1093
|
+
"id": 1,
|
|
1094
|
+
"title": "100 Best Albums of 2014",
|
|
1095
|
+
},
|
|
1096
|
+
"meta": {
|
|
1097
|
+
"frozen": true
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
````
|
|
1101
|
+
|
|
1102
|
+
It is retrieved using the `metadata` hash on the client model.
|
|
1103
|
+
|
|
1104
|
+
````ruby
|
|
1105
|
+
# example metadata that could specify when a Post cannot be updated
|
|
1106
|
+
Post.find(1).metadata[:frozen] #=> true
|
|
1107
|
+
````
|
|
1108
|
+
|
|
1109
|
+
Daylight uses metadata in two standard ways:
|
|
1110
|
+
* `read_only` attributes
|
|
1111
|
+
* `where_values` clauses.
|
|
1112
|
+
|
|
1113
|
+
#### read_only
|
|
1114
|
+
|
|
1115
|
+
The way that Daylight know which methods are read only and cannot be written
|
|
1116
|
+
is using the list of attributes that are `read_only` for that client model:
|
|
1117
|
+
|
|
1118
|
+
````json
|
|
1119
|
+
{
|
|
1120
|
+
"post": {
|
|
1121
|
+
"id": 1,
|
|
1122
|
+
"title": "100 Best Albums of 2014",
|
|
1123
|
+
},
|
|
1124
|
+
"meta": {
|
|
1125
|
+
"post": {
|
|
1126
|
+
"read_only": [
|
|
1127
|
+
"slug",
|
|
1128
|
+
"published",
|
|
1129
|
+
"created_at"
|
|
1130
|
+
]
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
````
|
|
1135
|
+
|
|
1136
|
+
Here, we will not be able to set `slug`, `published?`, and `created_at`
|
|
1137
|
+
and Daylight will raise a `NoMethodError`
|
|
1138
|
+
|
|
1139
|
+
> NOTE: ActiveResource handles predicate lookups for attributes
|
|
1140
|
+
> (eg. `published` vs. `published?`)
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
#### nested_resources
|
|
1144
|
+
|
|
1145
|
+
The way that Daylight know what Nested Resources are available to be set is
|
|
1146
|
+
is using a list of classes that are `nested_resources` for that client model:
|
|
1147
|
+
|
|
1148
|
+
````json
|
|
1149
|
+
{
|
|
1150
|
+
"post": {
|
|
1151
|
+
"id": 1,
|
|
1152
|
+
"title": "100 Best Albums of 2014",
|
|
1153
|
+
},
|
|
1154
|
+
"meta": {
|
|
1155
|
+
"post": {
|
|
1156
|
+
"nested_resources": [
|
|
1157
|
+
"author",
|
|
1158
|
+
"comments"
|
|
1159
|
+
]
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
````
|
|
1164
|
+
|
|
1165
|
+
Here, we will be able to create or associate the `author` resource when creating
|
|
1166
|
+
or updating a `post`. We can also create a new `comment` and add it to the
|
|
1167
|
+
collection in the same way.
|
|
1168
|
+
|
|
1169
|
+
> INFO: You can read up more in the User's Guide on how to use
|
|
1170
|
+
> [Nested Resources](usage.md#nested-resources).
|
|
1171
|
+
|
|
1172
|
+
#### where_values
|
|
1173
|
+
|
|
1174
|
+
How Daylight keeps track of how a model was looked up when using
|
|
1175
|
+
`find_or_initialize` and `find_or_create` is by returning the
|
|
1176
|
+
`where_values` from ActiveRecord. These will be merged when the
|
|
1177
|
+
`ActiveResource` is saved.
|
|
1178
|
+
|
|
1179
|
+
````json
|
|
1180
|
+
{
|
|
1181
|
+
"post": {
|
|
1182
|
+
"id": 1,
|
|
1183
|
+
"title": "100 Best Albums of 2014",
|
|
1184
|
+
},
|
|
1185
|
+
"meta": {
|
|
1186
|
+
"where_values": {
|
|
1187
|
+
"blog_id": 1
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
````
|
|
1192
|
+
|
|
1193
|
+
To see this in action, if the `Post` with the queried title was not found:
|
|
1194
|
+
|
|
1195
|
+
````ruby
|
|
1196
|
+
p = API::Blog.first.posts.find_or_create(title: "100 Best Albums of 2014")
|
|
1197
|
+
p.title #=> "100 Best Albums of 2014"
|
|
1198
|
+
|
|
1199
|
+
# from the `where_values` during the lookup
|
|
1200
|
+
p.blog_id #=> 1
|
|
1201
|
+
````
|
|
1202
|
+
|
|
1203
|
+
Since, `where_values` clauses can be quite complicated and are resolved by
|
|
1204
|
+
`ActiveRecord` we determine them server-side and send them as metadata in
|
|
1205
|
+
the response.
|