daylight 0.9.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- 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/usage.md
ADDED
@@ -0,0 +1,970 @@
|
|
1
|
+
# Daylight Users Guide
|
2
|
+
|
3
|
+
Daylight is extensions built on top of
|
4
|
+
[ActiveResource](https://github.com/rails/activeresource).
|
5
|
+
Everything you can do with `ActiveResource` is available to you in Daylight.
|
6
|
+
|
7
|
+
Once you have an API [developed](develop.md) with Daylight, you will want to be
|
8
|
+
able to use it. As an end-user of an API, it may be distributed to you in a
|
9
|
+
gem (or other means) and you may not have access to how the API is fulfilled.
|
10
|
+
|
11
|
+
Your API developers will either supply documentation or you can look at the
|
12
|
+
client models. The client models will describe what functionality is available
|
13
|
+
to you. Follow the your API developers instructions on how to setup the API or
|
14
|
+
refer to the [installation steps](install.md) for options.
|
15
|
+
|
16
|
+
#### Table of Contents
|
17
|
+
* [Client Model Example](#client-model-example)
|
18
|
+
* [Namespace and Version](#namespace-and-version)
|
19
|
+
* [ActiveResource Overview](#activeresource-overview)
|
20
|
+
* [Refinements](#refinements)
|
21
|
+
* [Conditions Additions](#condition-additions)
|
22
|
+
* [Order](#order)
|
23
|
+
* [Limit and Offset](#limit-and-offset)
|
24
|
+
* [Scopes](#scopes)
|
25
|
+
* [Chaining](#chaining)
|
26
|
+
* [Remote Methods](#remote-methods)
|
27
|
+
* [Associations](#associations)
|
28
|
+
* [Nested Resources](#nested-resources)
|
29
|
+
* [More Chaining](#more-chaining)
|
30
|
+
* [Building Objects](#building-objects)
|
31
|
+
* [`first_or_create`](#first_or_create)
|
32
|
+
* [`first_or_initialize`](#first_or_initialize)
|
33
|
+
* [Building using an Association](#building-using-an-associations)
|
34
|
+
* [Error Handling](#error-handling)
|
35
|
+
* [Understanding Interaction](#understanding-interaction)
|
36
|
+
* [Request Frequency](#request-frequency)
|
37
|
+
* [Response Size](#response-size)
|
38
|
+
|
39
|
+
## Client Model Example
|
40
|
+
|
41
|
+
Imagine you are building a blog, the client models that act as proxy to the
|
42
|
+
server-side are with which you will be interacting.
|
43
|
+
|
44
|
+
As we describe what Daylight can do in addition to `ActiveResource`
|
45
|
+
refer to these client models in the following `Post` example:
|
46
|
+
|
47
|
+
````ruby
|
48
|
+
class API::V1::Post < Daylight::API
|
49
|
+
scope :published, :updated
|
50
|
+
|
51
|
+
belongs_to :blog
|
52
|
+
belongs_to :author, class_name: 'api/v1/user'
|
53
|
+
|
54
|
+
has_one :company, through: :blog
|
55
|
+
|
56
|
+
has_many :comments
|
57
|
+
has_many :commenters, through: :associated, class_name: 'api/v1/user'
|
58
|
+
|
59
|
+
remote :top_comments, class_name: 'api/v1/comment'
|
60
|
+
end
|
61
|
+
````
|
62
|
+
|
63
|
+
All of the client models can be interacted with in the
|
64
|
+
[example application](example.md).
|
65
|
+
|
66
|
+
### Namespace and Version
|
67
|
+
|
68
|
+
Namespace is the root module for all your client models and can be seen
|
69
|
+
in this example as the 'API' in the module. By default, without a supplied
|
70
|
+
namespace to the `setup!`, the 'API' module will be used. You can examine
|
71
|
+
the version:
|
72
|
+
|
73
|
+
````ruby
|
74
|
+
Daylight::API.namespace #=> 'API'
|
75
|
+
````
|
76
|
+
|
77
|
+
Daylight client models will be versioned and this can be seen in this example
|
78
|
+
as the `V1` module. By default, without a supplied version to the `setup!`,
|
79
|
+
the most recent version will be selected. You can examine the version:
|
80
|
+
|
81
|
+
````ruby
|
82
|
+
Daylight::API.version #=> 'V1'
|
83
|
+
````
|
84
|
+
|
85
|
+
When you develop using a Daylight API. You do not need to specify the version
|
86
|
+
in your constant names as they are _aliased_ to the currently selected version
|
87
|
+
for your convinience:
|
88
|
+
|
89
|
+
````ruby
|
90
|
+
API::Post #=> API::V1::Post
|
91
|
+
````
|
92
|
+
|
93
|
+
We will use the _aliased_ version of the constant names in the following
|
94
|
+
examples unless otherwise noted.
|
95
|
+
|
96
|
+
---
|
97
|
+
|
98
|
+
## ActiveResource Overview
|
99
|
+
|
100
|
+
With a `Post` you can use the following `ActiveResource` functionality as
|
101
|
+
you've become accustomed. Find a `Post` and examine its attributes:
|
102
|
+
|
103
|
+
|
104
|
+
````ruby
|
105
|
+
post = API::Post.find(1) #=> #<API::V1::Post:0x007ffa8c4159e0 ..>
|
106
|
+
post.title #=> "100 Best Albums of 2014"
|
107
|
+
````
|
108
|
+
|
109
|
+
Get all instances of `Post`, or just the first or last:
|
110
|
+
|
111
|
+
````ruby
|
112
|
+
API::Post.all #=> [#<API::V1::Post:0x007ffa8d59abe8 ..>,
|
113
|
+
#=> #<API::V1::Post:0x007ffa8d59a788 ..>, ...]
|
114
|
+
|
115
|
+
API::Post.first #=> #<API::V1::Post:0x007ffa8d59abe8 ..>
|
116
|
+
API::Post.last #=> #<API::V1::Post:0x007ffa8c4763d0 ..>
|
117
|
+
````
|
118
|
+
|
119
|
+
> NOTE: Daylight add a [limit](#limit-and-offset) condition to get the first
|
120
|
+
> `Post` as an optimization. There is no optimization for `last` as it is
|
121
|
+
> equivalent to `Post.all.to_a.last`
|
122
|
+
|
123
|
+
You can `create`, `update`, `delete` a `Post`. Here's an example of an `update`:
|
124
|
+
|
125
|
+
````ruby
|
126
|
+
post = API::Post.find(1) #=> #<Bluesky::V1::Zone:0x007ffa8c44fde8 ..>
|
127
|
+
post.title = "100 Best Albums of All Time"
|
128
|
+
post.save #=> true
|
129
|
+
````
|
130
|
+
|
131
|
+
Get associated resources:
|
132
|
+
|
133
|
+
````ruby
|
134
|
+
post = API::Post.find(1) #=> #<API::V1::Post:0x007ffa8c44fde8 ..>
|
135
|
+
post.comments #=> [#<API::V1::Comment:0x007ffa8c4843b8 ..>,
|
136
|
+
# #<API::V1::Comment:0x007ffa8c48e728 ..>, ...]
|
137
|
+
````
|
138
|
+
|
139
|
+
Search across the collection of resources:
|
140
|
+
|
141
|
+
````ruby
|
142
|
+
posts = API::Post.where(created_by: 101)
|
143
|
+
posts.size #=> 23
|
144
|
+
posts.first.created_by #=> 101
|
145
|
+
````
|
146
|
+
|
147
|
+
You can use any multiple conditions:
|
148
|
+
|
149
|
+
````ruby
|
150
|
+
posts = API::Post.where(created_by: 101, blog_id: 1, published: true)
|
151
|
+
posts.size #=> 15
|
152
|
+
posts.first.created_by #=> 101
|
153
|
+
posts.first.blog_id #=> 1
|
154
|
+
posts.first.published #=> true
|
155
|
+
````
|
156
|
+
|
157
|
+
You can use conditions based on results of other searches:
|
158
|
+
|
159
|
+
````ruby
|
160
|
+
posts = API::Post.where(created_by: API::User.find_by(username: "reidmix"))
|
161
|
+
posts.size #=> 23
|
162
|
+
posts.first.created_by #=> 101
|
163
|
+
````
|
164
|
+
|
165
|
+
> NOTE: This will issue two requests, the first by `find_by` and the second
|
166
|
+
> by `where`.
|
167
|
+
|
168
|
+
Please refer to the [ActiveResource](https://github.com/rails/activeresource)
|
169
|
+
documenation for more information.
|
170
|
+
|
171
|
+
---
|
172
|
+
|
173
|
+
## Refinements
|
174
|
+
|
175
|
+
Daylight offers many ways to refine queries across collections. These include
|
176
|
+
conditions, scopes, order, offset, and limit.
|
177
|
+
|
178
|
+
### Condition Additions
|
179
|
+
|
180
|
+
There are several additions to `ActiveResource` conditions. Which attributes
|
181
|
+
may be refined need to be documented by your API developer but can be inspected
|
182
|
+
on a retrieved instance:
|
183
|
+
|
184
|
+
````ruby
|
185
|
+
post = API::Post.find(1)
|
186
|
+
post.attributes.keys #=> ["id", "blog_id", "title", "body", "slug", "published", "published_on", "created_by"]
|
187
|
+
````
|
188
|
+
|
189
|
+
If you know there to be one result or only need the first result, use `find_by`:
|
190
|
+
|
191
|
+
````ruby
|
192
|
+
post = API::Post.find_by(slug: "100-best-albums-of-2014")
|
193
|
+
posts.slug #=> "100-best-albums-of-2014"
|
194
|
+
````
|
195
|
+
|
196
|
+
And `where` clauses may be chained together similarly to `ActiveRecord`:
|
197
|
+
|
198
|
+
````ruby
|
199
|
+
posts = API::Post.where(created_by: 101).where(blog_id: 1).where(published: true)
|
200
|
+
posts.size #=> 15
|
201
|
+
posts.first.created_by #=> 101
|
202
|
+
posts.first.blog_id #=> 1
|
203
|
+
posts.first.published #=> true
|
204
|
+
````
|
205
|
+
|
206
|
+
In fact there's more to [chaining](#chaining) than just `where` clauses.
|
207
|
+
|
208
|
+
### Order
|
209
|
+
|
210
|
+
As in `ActiveRecord` you can also refine by `limit`, `offset`, and `order`
|
211
|
+
|
212
|
+
````ruby
|
213
|
+
posts = API::Post.order(:published_on)
|
214
|
+
posts.map(&:published_on) #=> ['2014-01-01', '2014-06-21', '2014-06-26']
|
215
|
+
````
|
216
|
+
|
217
|
+
You can also specify the direction or reverse the direction:
|
218
|
+
|
219
|
+
````ruby
|
220
|
+
posts = API::Post.order('published_on ASC')
|
221
|
+
posts.map(&:published_on) #=> ['2014-01-01', '2014-06-21', '2014-06-26']
|
222
|
+
|
223
|
+
posts = API::Post.order('published_on DESC')
|
224
|
+
posts.map(&:published_on) #=> ['2014-06-26', '2014-06-21', '2014-01-01']
|
225
|
+
````
|
226
|
+
|
227
|
+
### Limit and Offset
|
228
|
+
|
229
|
+
You can `limit` the results that are returned by the API:
|
230
|
+
|
231
|
+
````ruby
|
232
|
+
posts = API::Post.limit(1)
|
233
|
+
posts.size #=> 1
|
234
|
+
|
235
|
+
posts = API::Post.limit(10)
|
236
|
+
posts.size #=> 10
|
237
|
+
````
|
238
|
+
|
239
|
+
And you can `offset` which resources to be returned:
|
240
|
+
|
241
|
+
````ruby
|
242
|
+
posts = API::Post.all
|
243
|
+
posts.map(&:id) #=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
244
|
+
|
245
|
+
posts = API::Post.offset(5)
|
246
|
+
posts.map(&:id) #=> [6, 7, 8, 9, 10]
|
247
|
+
````
|
248
|
+
|
249
|
+
### Scopes
|
250
|
+
|
251
|
+
Scopes are conditions made available on the client-side model and executed
|
252
|
+
server-side. The function of a scope needs to be documented by your API
|
253
|
+
developer but which scopes are available be inspected in client model
|
254
|
+
or find in the instance:
|
255
|
+
|
256
|
+
````ruby
|
257
|
+
API::Post.scope_names #=> [:published, :updated]
|
258
|
+
````
|
259
|
+
|
260
|
+
You can call a scope directly on the model class:
|
261
|
+
|
262
|
+
````ruby
|
263
|
+
posts = API::Post.published
|
264
|
+
|
265
|
+
# assuming published scope on the server-side is
|
266
|
+
# scope :published, -> {where.not(published_on: nil)}
|
267
|
+
|
268
|
+
posts.first.published_on #=> true
|
269
|
+
posts.all? {|p| p.published_on.present? } #=> true
|
270
|
+
````
|
271
|
+
|
272
|
+
You may call multiple scopes on a model:
|
273
|
+
|
274
|
+
````ruby
|
275
|
+
posts = API::Post.published.edited
|
276
|
+
|
277
|
+
# assuming published scope on the server-side is
|
278
|
+
# scope :edited, -> {where.not(edited_on: nil)}
|
279
|
+
|
280
|
+
posts.first.published_on #=> true
|
281
|
+
posts.first.edited_on #=> true
|
282
|
+
|
283
|
+
posts.all? {|p| p.published_on.present? } #=> true
|
284
|
+
posts.all? {|p| p.edited_on.present? } #=> true
|
285
|
+
````
|
286
|
+
|
287
|
+
### Chaining
|
288
|
+
|
289
|
+
All of the above refinements are as limited to the one being used. Daylight
|
290
|
+
allows all or any combination of the refinements to be chained together for
|
291
|
+
better searches:
|
292
|
+
|
293
|
+
````ruby
|
294
|
+
# NONE: get all posts
|
295
|
+
posts = API::Post.all
|
296
|
+
posts.map(&:id) #=> [10, 3, 2, 4, 7, 5, 6, 1, 9, 8]
|
297
|
+
|
298
|
+
# SCOPE: get published posts
|
299
|
+
posts = API::Post.published
|
300
|
+
posts.map(&:id) #=> [3, 2, 7, 5, 6, 1, 9, 8]
|
301
|
+
posts.first.published_on #=> '2013-09-03'
|
302
|
+
|
303
|
+
# WHERE 1 condition: get posts for blog_id=2
|
304
|
+
posts = API::Post.where(blog_id: 2)
|
305
|
+
posts.map(&:id) #=> [2, 5, 1, 9, 8]
|
306
|
+
posts.map(&:blog_id) #=> [2, 2, 2, 2, 2]
|
307
|
+
|
308
|
+
# WHERE 2 conditions: get posts for blog_id=101 AND created_by=2
|
309
|
+
posts = API::Post.where(blog_id: 2).where(created_by: 101)
|
310
|
+
posts.map(&:id) #=> [2, 9, 8]
|
311
|
+
posts.map(&:created_by) #=> [101, 101, 101]
|
312
|
+
|
313
|
+
# ORDER: get posts for blog_id=2 AND created_by=101 order by published_on
|
314
|
+
posts = API::Post.where(blog_id: 2).where(created_by: 101).order(:published_on)
|
315
|
+
posts.map(&:id) #=> [2, 8, 9]
|
316
|
+
posts.map(&:published_on) #=> ['2014-01-01', '2014-06-21', '2014-06-26']
|
317
|
+
|
318
|
+
# OFFSET: get posts for blog_id=2 AND created_by=101 order by published_on after the first one
|
319
|
+
posts = API::Post.where(blog_id: 2).where(created_by: 101).order(:published_on).offset(1)
|
320
|
+
posts.map(&:id) #=> [8, 9]
|
321
|
+
|
322
|
+
# LIMIT: get posts for blog_id=2 AND created_by=2 order_by published_on and just the second one
|
323
|
+
posts = API::Post.where(blog_id: 2).where(created_by: 101).order(:published_on).offset(1).limit(1)
|
324
|
+
posts.map(&:id) #=> [8]
|
325
|
+
|
326
|
+
post = API::Post.where(blog_id: 2).where(created_by: 101).order(:published_on).offset(1).limit(1).first
|
327
|
+
post.id #=> 8
|
328
|
+
post.blog_id #=> 2
|
329
|
+
post.created_by #=> 101
|
330
|
+
post.published_on # '2014-06-21'
|
331
|
+
````
|
332
|
+
|
333
|
+
> NOTE: Since `offset` and `limit` can be chained together, you can use these
|
334
|
+
> with your favorite paginator.
|
335
|
+
|
336
|
+
In all of these cases, Daylight issues only one request per search.
|
337
|
+
See [Request Parameters](develop.md#request-parameters) for further reading.
|
338
|
+
|
339
|
+
Just like `ActiveRecord`, each part of the chain has its own context and can be
|
340
|
+
inspected individually.
|
341
|
+
|
342
|
+
````ruby
|
343
|
+
published_posts = API::Post.published
|
344
|
+
first_published = published_posts.order(:published_on).first
|
345
|
+
|
346
|
+
first_published.id #=> 2
|
347
|
+
published_posts.map(&:id) #=> [3, 2, 7, 5, 6, 1, 9, 8]
|
348
|
+
````
|
349
|
+
|
350
|
+
Here you can see a result set can be further refined while not affecting the
|
351
|
+
original result set.
|
352
|
+
|
353
|
+
---
|
354
|
+
|
355
|
+
## Associations
|
356
|
+
|
357
|
+
Associations work as they do today in `ActiveResource` One one notable
|
358
|
+
exception. Client models that have the `has_many through: :associated` will
|
359
|
+
perform the lookup for associated objects server-side.
|
360
|
+
|
361
|
+
> NOTE: This is useful if conditions or configuration is defined on the
|
362
|
+
> server-side model to perform correctly. Refer to
|
363
|
+
> [developing models](develop.md#models) for more information.
|
364
|
+
|
365
|
+
Daylight adds additional functionality directly on the association:
|
366
|
+
* add new resources
|
367
|
+
* update existing resources
|
368
|
+
* add a new resource to a collection
|
369
|
+
* associate two existing resources
|
370
|
+
|
371
|
+
Currently, `ActiveResource` will only let you associate a resource by setting
|
372
|
+
the `foreign_key` directly on a model.
|
373
|
+
|
374
|
+
### Nested Resources
|
375
|
+
|
376
|
+
When manipulating resources on an association, we call these _Nested Resources_.
|
377
|
+
|
378
|
+
> INFO: We call it "Nested Resource" because data for them are sent
|
379
|
+
> as a nested hash on the parent resource and server-side employ the
|
380
|
+
> `accepts_nested_attributes_for` mechanism.
|
381
|
+
|
382
|
+
Not all nested resources can be manipulated on the model, you can see which
|
383
|
+
objects are accepted by inspecting the instance:
|
384
|
+
|
385
|
+
````
|
386
|
+
post = API::Post.find(1)
|
387
|
+
post.nested_resources #=> [:author, :comments]
|
388
|
+
````
|
389
|
+
|
390
|
+
In this example, posts will reject updates to `blog`, `company`, and
|
391
|
+
`commenters` nested objects.
|
392
|
+
|
393
|
+
To create a new nested object is simple, create the object and set it on the
|
394
|
+
`has_one` or `has_many` association:
|
395
|
+
|
396
|
+
#### Creating a Nested Resource
|
397
|
+
|
398
|
+
You can create a new nested resource for a new or existing resources. For
|
399
|
+
example a new `post`:
|
400
|
+
|
401
|
+
````ruby
|
402
|
+
post = API::Post.new
|
403
|
+
post.title = "100 Best Albums of 2014"
|
404
|
+
post.author = API::User.new(username: 'reidmix')
|
405
|
+
post.save #=> true
|
406
|
+
post.id #=> 43
|
407
|
+
|
408
|
+
# reload the original object to see the new user
|
409
|
+
post = API::Post.find(43)
|
410
|
+
post.author.id #=> 101
|
411
|
+
post.created_by #=> 101 (foreign_key on post)
|
412
|
+
|
413
|
+
# you can look up the new user directly
|
414
|
+
user = API::User.find(101)
|
415
|
+
user.username #=> "reidmix"
|
416
|
+
````
|
417
|
+
|
418
|
+
This will work on an existing post:
|
419
|
+
|
420
|
+
````ruby
|
421
|
+
post = API::Post.first
|
422
|
+
post.author = API::User.new(username: 'dmcinnes')
|
423
|
+
post.save #=> true
|
424
|
+
|
425
|
+
# reload the original object to see the new user
|
426
|
+
post = API::Post.first
|
427
|
+
post.author.id #=> 102
|
428
|
+
post.created_by #=> 102 (foreign_key on post)
|
429
|
+
|
430
|
+
# you can look up the new user directly
|
431
|
+
user = API::User.find(102)
|
432
|
+
user.username #=> "dmcinness"
|
433
|
+
````
|
434
|
+
|
435
|
+
#### Creating a Nested Resource in a Collection
|
436
|
+
|
437
|
+
You can also create a nested object via a collection on a new or existing
|
438
|
+
resource. For example, on our new `post`:
|
439
|
+
|
440
|
+
````ruby
|
441
|
+
post = API::Post.new
|
442
|
+
post.comments #=> []
|
443
|
+
post.comments << API::Comment.new(message: 'First!')
|
444
|
+
post.save #=> true
|
445
|
+
|
446
|
+
# reload the original object to see the new comment
|
447
|
+
post = API::Post.first
|
448
|
+
post.comments.first.id #=> 321
|
449
|
+
post.comments.first.message #=> "First!"
|
450
|
+
|
451
|
+
# you can look up the new comment
|
452
|
+
comment = API::Comment.find(321)
|
453
|
+
comment.post_id #=> 1
|
454
|
+
````
|
455
|
+
|
456
|
+
You can also add a nested object to an existing collection:
|
457
|
+
|
458
|
+
````ruby
|
459
|
+
post = API::Post.first
|
460
|
+
post.comments #=> []
|
461
|
+
post.comments << API::Comment.new(message: 'Last!')
|
462
|
+
post.save #=> true
|
463
|
+
|
464
|
+
# reload the original object to see the new comment
|
465
|
+
post = API::Post.first
|
466
|
+
post.comments.last.id #=> 322
|
467
|
+
post.comments.last.message #=> "Last!"
|
468
|
+
|
469
|
+
# you can look up the new comment
|
470
|
+
comment = API::Comment.find(322)
|
471
|
+
comment.post_id #=> 1
|
472
|
+
````
|
473
|
+
|
474
|
+
#### Updating a Nested Resource
|
475
|
+
|
476
|
+
Updates to nested resources are not saved by saving the parent resource.
|
477
|
+
You must save the nested resources directly:
|
478
|
+
|
479
|
+
````ruby
|
480
|
+
post = API::Post.first
|
481
|
+
post.author.full_name = "Reid MacDonald"
|
482
|
+
post.author.save #=> true
|
483
|
+
|
484
|
+
post = API::Post.first
|
485
|
+
post.author.full_name #=> "Reid MacDonald"
|
486
|
+
````
|
487
|
+
|
488
|
+
This is the same as saying:
|
489
|
+
|
490
|
+
````ruby
|
491
|
+
post = API::Post.first
|
492
|
+
|
493
|
+
author = post.author
|
494
|
+
author.full_name = "Reid MacDonald"
|
495
|
+
author.save #=> true
|
496
|
+
|
497
|
+
post = API::Post.first
|
498
|
+
post.author.full_name #=> "Reid MacDonald"
|
499
|
+
````
|
500
|
+
|
501
|
+
The same is true of nested objects in collections:
|
502
|
+
|
503
|
+
````ruby
|
504
|
+
post = API::Post.first
|
505
|
+
|
506
|
+
first_comment = post.comments.first
|
507
|
+
first_comment.message = "First!"
|
508
|
+
first_comment.save #=> true
|
509
|
+
|
510
|
+
post = API::Post.first
|
511
|
+
post.comments.first.message #=> "First!"
|
512
|
+
````
|
513
|
+
|
514
|
+
> FUTURE [#5](https://github.com/att-cloud/daylight/issues/5):
|
515
|
+
> Updates to the associated nested resource do not get saved when the parent
|
516
|
+
> resources are saved and they should be.
|
517
|
+
|
518
|
+
#### Associating an Existing Nested Resources
|
519
|
+
|
520
|
+
Associating using an existing nested records is possible with Daylight. The
|
521
|
+
nested record does not need to be new as they do in `ActiveRecord`.
|
522
|
+
|
523
|
+
Setting an existing nested resource on a new or existing parent resource will
|
524
|
+
associate them:
|
525
|
+
|
526
|
+
````ruby
|
527
|
+
post = API::Post.first
|
528
|
+
|
529
|
+
post.author = API::User.find_by(username: 'reidmix')
|
530
|
+
post.save #=> true
|
531
|
+
|
532
|
+
post.created_by #=> 101
|
533
|
+
post.author.id #=> 101
|
534
|
+
````
|
535
|
+
|
536
|
+
This also will work to add to a collection on a new or existing resource:
|
537
|
+
|
538
|
+
````ruby
|
539
|
+
post = API::Post.first
|
540
|
+
|
541
|
+
post.commenters << API::User.find_by(username: 'reidmix')
|
542
|
+
post.save #=> true
|
543
|
+
|
544
|
+
post = API::Post.first
|
545
|
+
post.commenters.find {|c| c.username == 'reidmix'} # #<API::V1::User:0x007fe2cfc45ce8 ..>
|
546
|
+
````
|
547
|
+
|
548
|
+
> FUTURE [#15](https://github.com/att-cloud/daylight/issues/15):
|
549
|
+
> There is no way to remove an nested resource from a collection nor empty the collection.
|
550
|
+
|
551
|
+
### More Chaining
|
552
|
+
|
553
|
+
Along with the collection returned by queries across collections, you may
|
554
|
+
continue to apply refinements to associations.
|
555
|
+
|
556
|
+
Similar to [chaining](#chainging), refinements on assoications.:
|
557
|
+
|
558
|
+
````ruby
|
559
|
+
# NONE: get all comments for a post
|
560
|
+
comments = API::Post.find(1).comments
|
561
|
+
comments.map(&:id) #=> [11, 33, 32, 54, 17, 15, 16, 1, 90, 81]
|
562
|
+
|
563
|
+
# SCOPE: get a post's edited comments
|
564
|
+
comments = API::Post.find(1).comments.edited
|
565
|
+
comments.map(&:id) #=> [33, 32, 17, 15, 16, 1, 90, 81]
|
566
|
+
comments.first.edited_on #=> '2013-09-03'
|
567
|
+
|
568
|
+
# WHERE 1 condition: get a post's comments for blog_id=2
|
569
|
+
comments = API::Post.find(1).comments.where(has_images: true)
|
570
|
+
comments.map(&:id) #=> [32, 15, 1, 90, 81]
|
571
|
+
comments.map(&:has_images) #=> [true, true, true, true, true]
|
572
|
+
|
573
|
+
# WHERE 2 conditions: get a post's comments that has_images AND created_by=101
|
574
|
+
comments = API::Post.find(1).comments.where(has_images: true).where(created_by: 101))
|
575
|
+
comments.map(&:id) #=> [32, 90, 81]
|
576
|
+
comments.map(&:created_by) #=> [101, 101, 101]
|
577
|
+
|
578
|
+
# ORDER: get a post's comments that has_images AND created_by=101 order by edited_on
|
579
|
+
comments = API::Post.find(1).where(has_images: true).where(created_by: 101)).order(:edited_on)
|
580
|
+
comments.map(&:id) #=> [32, 81, 90]
|
581
|
+
comments.map(&:published_on) #=> ['2014-01-01', '2014-06-21', '2014-06-26']
|
582
|
+
|
583
|
+
# OFFSET: get post's comments that has_images AND created_by=101 order by edited_on after the first one
|
584
|
+
comments = API::Post.find(1),where(has_images: true).where(created_by: 101)).order(:edited_on).offset(1)
|
585
|
+
comments.map(&:id) #=> [80, 91]
|
586
|
+
|
587
|
+
# LIMIT: get post's comments that has_images AND created_by=101 order by edited_on and just the second one
|
588
|
+
comments = API::Post.find(1).where(has_images: true).where(created_by: 101)).order(:edited_on).offset(1).limit(1)
|
589
|
+
comments.map(&:id) #=> [80]
|
590
|
+
|
591
|
+
comments = API::Post.find(1).where(has_images: true).where(created_by: 101)).order(:edited_on).offset(1).limit(1).first
|
592
|
+
comments.id #=> 80
|
593
|
+
comments.has_images #=> true
|
594
|
+
comments.created_by #=> 101
|
595
|
+
comments.published_on # '2014-06-21'
|
596
|
+
````
|
597
|
+
|
598
|
+
As you could guess, you could end up with very sophisticated queries traversing
|
599
|
+
multiple associations. For example:
|
600
|
+
|
601
|
+
`API::Post.published.updated.find_by(slug: '100-best-albums-of-2014').comments.edited.where(has_images: true).first.images.approved`
|
602
|
+
|
603
|
+
Please review [Request Frequency](#request-frequency) to better understand how
|
604
|
+
the requests are composed.
|
605
|
+
|
606
|
+
As before with [chaining](#chaining) each part of the chain has its own context
|
607
|
+
and can be inspected individually.
|
608
|
+
|
609
|
+
````ruby
|
610
|
+
first_published_post = API::Post.published.first
|
611
|
+
comments_with_images = first_published_post.comments.where(has_images: true)
|
612
|
+
my_last_edited_comment = comments_with_images.where(created_by: 101)).order(:edited_on).last
|
613
|
+
|
614
|
+
my_last_edited_comment.id #=> 90
|
615
|
+
comments_with_images.map(&:id) #=> [32, 15, 1, 90, 81]
|
616
|
+
````
|
617
|
+
|
618
|
+
Here you can see a result set can be further refined while not affecting the
|
619
|
+
original result set fetched for the association.
|
620
|
+
|
621
|
+
---
|
622
|
+
|
623
|
+
## Building Objects
|
624
|
+
|
625
|
+
Most of the time, you want to check to see if an object already exists and if
|
626
|
+
it doesn't build that object. `ActiveResource` already supplies this
|
627
|
+
functionality with `first_or_create` and `first_or_initialze`.
|
628
|
+
|
629
|
+
Daylight ensures that these methods work with refinements & chaining and
|
630
|
+
ensures the requests are properly formatted for the server.
|
631
|
+
|
632
|
+
> NOTE: The refinements are expressive but can become very complicated quickly.
|
633
|
+
> Daylight uses the [where_values](develop.md#where_values) generated by the
|
634
|
+
> server to build the objects.
|
635
|
+
|
636
|
+
### `first_or_create`
|
637
|
+
|
638
|
+
The `first_or_create` method will save the object if it does not already exist.
|
639
|
+
|
640
|
+
````ruby
|
641
|
+
post = API::Post.where(slug: '100-best-albums-of-2014').first_or_create
|
642
|
+
post.new? #=> false
|
643
|
+
|
644
|
+
# set an attribute directly
|
645
|
+
post.exerpt = "Ranked list of the 100 best albums so far in 2014"
|
646
|
+
post.save #=> true
|
647
|
+
````
|
648
|
+
|
649
|
+
If there are [validation errors](#handling-errors) the object will be
|
650
|
+
instantiated but it will not be saved. You will be able to view the
|
651
|
+
error messages and see that the object is still new:
|
652
|
+
|
653
|
+
````ruby
|
654
|
+
post = API::Post.where(slug: '100-best-albums-of-2014').first_or_create
|
655
|
+
post.new? #=> true
|
656
|
+
post.errors.present? #=> true
|
657
|
+
post.errors.messages #=> {:base=>[Author must be present]}
|
658
|
+
````
|
659
|
+
|
660
|
+
You can use all of Daylight's refinement [chaining](#chaining) to search for a
|
661
|
+
match:
|
662
|
+
|
663
|
+
````ruby
|
664
|
+
latest_post = API::Post.where(created_by: 101).order(:published_on).first_or_create
|
665
|
+
latest_post.new? #=> false
|
666
|
+
latest_post.author.id #=> 101
|
667
|
+
````
|
668
|
+
|
669
|
+
### `first_or_initialize`
|
670
|
+
|
671
|
+
The `first_or_initialize` will instatiate the object but not save it
|
672
|
+
automatically.
|
673
|
+
|
674
|
+
````ruby
|
675
|
+
post = API::Post.where(slug: '100-best-albums-of-2014').first_or_initialize({
|
676
|
+
exerpt: "Ranked list of the 100 best albums so far in 2014"
|
677
|
+
})
|
678
|
+
post.new? #=> true
|
679
|
+
post.save #=> true
|
680
|
+
````
|
681
|
+
|
682
|
+
Again, all of the Daylight's refinement chaining can be used.
|
683
|
+
|
684
|
+
### Building using an Associations
|
685
|
+
|
686
|
+
You can create an object based on a collection for an association.
|
687
|
+
|
688
|
+
> NOTE: Specifically, only a `has_many` association. The `belongs_to` or
|
689
|
+
> `has_one` asscoiations will have a `nil` object if they are not set
|
690
|
+
> (ie. there's no foriegn_key) and will not work.
|
691
|
+
|
692
|
+
For example if there is no `comment` for the the `post`:
|
693
|
+
|
694
|
+
````ruby
|
695
|
+
comment = API::Post.find(1).comments.first_or_initialize({
|
696
|
+
message: "Am I the first comment?"
|
697
|
+
})
|
698
|
+
comment.new? #=> true
|
699
|
+
comment.post_id #=> 1
|
700
|
+
comment.save #=> true
|
701
|
+
````
|
702
|
+
|
703
|
+
You may apply any refinement to the association:
|
704
|
+
|
705
|
+
````ruby
|
706
|
+
comment = API::Post.find(1).comments.where(is_liked: true).first_or_create
|
707
|
+
comment.new? #=> false
|
708
|
+
comment.post_id #=> 1
|
709
|
+
|
710
|
+
# Update the message
|
711
|
+
comment.message = "You really like me when I said: '#{comment.message}'"
|
712
|
+
comment.save #=> true
|
713
|
+
````
|
714
|
+
|
715
|
+
---
|
716
|
+
|
717
|
+
## Remote Methods
|
718
|
+
|
719
|
+
Remote methods are any associated record or collection that is available via a
|
720
|
+
public instance method server-side. For all intents and purposes, the
|
721
|
+
differences between a remote method and an associations are:
|
722
|
+
- Remote methods may return a single record
|
723
|
+
- Remote methods cannot be chained
|
724
|
+
|
725
|
+
> FUTURE [#4](https://github.com/att-cloud/daylight/issues/4)
|
726
|
+
> Remoted methods may be implemented using the association mechanism.
|
727
|
+
|
728
|
+
The function of a remoted method needs to be documented by your API developer
|
729
|
+
but which remoted methods are available be inspected in client model.
|
730
|
+
|
731
|
+
Given the `top_comments` remoted method:
|
732
|
+
|
733
|
+
````ruby
|
734
|
+
API::Post.find(1).top_comments #=> [#<API::V1::Comment:0x007ffa8c4843b8 ..>,
|
735
|
+
# #<API::V1::Comment:0x007ffa8c48e728 ..>, ...]
|
736
|
+
````
|
737
|
+
|
738
|
+
As you can see, remote methods cannot be chained:
|
739
|
+
|
740
|
+
````ruby
|
741
|
+
API::Post.find(1).top_comments.find_by(user_id: 1)
|
742
|
+
|
743
|
+
#=> NoMethodError: undefined method `find_by' for #<ActiveResource::Collection:0x007f83208937a8>
|
744
|
+
````
|
745
|
+
|
746
|
+
> FUTURE [#9](https://github.com/att-cloud/daylight/issues/9):
|
747
|
+
> Remote methods cannot be further refined like associations
|
748
|
+
|
749
|
+
---
|
750
|
+
|
751
|
+
## Error Handling
|
752
|
+
|
753
|
+
A goal of Daylight is to offer better handling and messaging to the client when
|
754
|
+
expected errors occur. This will aid in development of both the API and when
|
755
|
+
users of that API are having issues.
|
756
|
+
|
757
|
+
### Validation Errors
|
758
|
+
|
759
|
+
Daylight exposes validation errors on creates and updates. Given a validation
|
760
|
+
on a model:
|
761
|
+
|
762
|
+
````ruby
|
763
|
+
class Post < ActiveRecord::Base
|
764
|
+
validates :title, presence: true
|
765
|
+
end
|
766
|
+
````
|
767
|
+
|
768
|
+
When saving this model from the client errors will be exposed similar to
|
769
|
+
`ActiveRecord`:
|
770
|
+
|
771
|
+
````ruby
|
772
|
+
post = API::Post.new
|
773
|
+
post.save # => false
|
774
|
+
post.errors.messages # => {:base=>["Title can't be blank"]}
|
775
|
+
````
|
776
|
+
|
777
|
+
With the introduction of and use of
|
778
|
+
[Strong Parameters](http://guides.rubyonrails.org/action_controller_overview.html#strong-parameters)
|
779
|
+
unpermitted or missing attributes will be detected.
|
780
|
+
|
781
|
+
> FUTURE [#8](https://github.com/att-cloud/daylight/issues/8):
|
782
|
+
> Would be nice to know which parameter is raising the error and if it was a
|
783
|
+
> _required_ parameter or an _unpermitted_ one.
|
784
|
+
|
785
|
+
Lets say `created_at` is not permitted on the `PostController`:
|
786
|
+
````ruby
|
787
|
+
post = API::Post.new(created_at: Time.now)
|
788
|
+
post.save # => false
|
789
|
+
post.errors.messages # => {:base=>["Unpermitted or missing attribute"]}
|
790
|
+
````
|
791
|
+
|
792
|
+
### Bad Requests
|
793
|
+
|
794
|
+
Daylight will raise an error on unknown attributes. This differes from
|
795
|
+
`ActiveRecord` where it will be raised immediately because the error is
|
796
|
+
detected by `APIController` during a `save` action.
|
797
|
+
|
798
|
+
For example, given the same `Post` model above:
|
799
|
+
````ruby
|
800
|
+
post = API::Post.new(foo: 'bar')
|
801
|
+
post.save
|
802
|
+
#=> ActiveResource::BadRequest: Failed. Response code = 400.
|
803
|
+
# Response message = Bad Request. Root Cause = unknown attribute: foo
|
804
|
+
````
|
805
|
+
|
806
|
+
Similarly, Daylight raises errors on unknown keys, associations, scopes,
|
807
|
+
or remoted methods. The error will be raise as soon as the request is
|
808
|
+
issued, not just on `save` actions.
|
809
|
+
|
810
|
+
For example, when providing an incorrect condition:
|
811
|
+
````ruby
|
812
|
+
API::Post.find_by(foo: 'bar')
|
813
|
+
#=> ActiveResource::BadRequest: Failed. Response code = 400.
|
814
|
+
# Response message = Bad Request. Root Cause = unknown key: foo
|
815
|
+
````
|
816
|
+
If invalid statements are issued server-side they will be raised:
|
817
|
+
|
818
|
+
````ruby
|
819
|
+
API::Post.published.limit(:foo)
|
820
|
+
#=> ActiveResource::BadRequest: Failed. Response code = 400.
|
821
|
+
# Response message = Bad Request. Root Cause = invalid value for Integer(): "foo"
|
822
|
+
````
|
823
|
+
|
824
|
+
This is also useful developing and detecting errors in your client models
|
825
|
+
Given the client model:
|
826
|
+
|
827
|
+
````ruby
|
828
|
+
class API::V1::Post < Daylight::API
|
829
|
+
scopes :published
|
830
|
+
remote :top_comments
|
831
|
+
|
832
|
+
has_many :author, through: :associated
|
833
|
+
end
|
834
|
+
````
|
835
|
+
|
836
|
+
If neither `published`, `top_comments`, nor `author` are not setup on the
|
837
|
+
server-side, errors will be raised.
|
838
|
+
|
839
|
+
````ruby
|
840
|
+
API::Post.published
|
841
|
+
#=> ActiveResource::BadRequest: Failed. Response code = 400.
|
842
|
+
# Response message = Bad Request. Root Cause = unknown scope: published
|
843
|
+
|
844
|
+
API::Post.by_popularirty
|
845
|
+
#=> ActiveResource::BadRequest: Failed. Response code = 400.
|
846
|
+
# Response message = Bad Request. Root Cause = unknown remote: top_comments
|
847
|
+
|
848
|
+
API::Post.find(1).author
|
849
|
+
#=> ActiveResource::BadRequest: Failed. Response code = 400.
|
850
|
+
# Response message = Bad Request. Root Cause = unknown association: author
|
851
|
+
````
|
852
|
+
|
853
|
+
---
|
854
|
+
|
855
|
+
## Understanding Interaction
|
856
|
+
|
857
|
+
To help understand how requests from the client will produce load on the server
|
858
|
+
API, will aid in understanding what load is produced on the API server(s).
|
859
|
+
|
860
|
+
Daylight does its best to collect information about a query before issuing the
|
861
|
+
request, but can only do so much. Daylight will still suffer from putting a
|
862
|
+
request in a tight loop as any other web application will.
|
863
|
+
|
864
|
+
### Request Frequency
|
865
|
+
|
866
|
+
A request is issued for any query for a resource or collection of resources.
|
867
|
+
Everytime an association is traversed, a new request sent. All the refinements
|
868
|
+
on a collection is sent along with the request.
|
869
|
+
|
870
|
+
Given a large request like:
|
871
|
+
|
872
|
+
````ruby
|
873
|
+
API::Post.published.updated.find_by(slug: '100-best-albums-of-2014'). # Post request (with refinements)
|
874
|
+
comments.edited.where(has_images: true).first. # Comment request (with refinements)
|
875
|
+
images.liked.limit(1). # Image request (with refinements)
|
876
|
+
map(&:caption).first # No request: iterating over data structure
|
877
|
+
````
|
878
|
+
|
879
|
+
There are 3 resources/collections retrieved from the server. One each for
|
880
|
+
`post`, `comment`, and `image`. You can see this in the API server logs:
|
881
|
+
|
882
|
+
GET "/v1/posts.json?filters[slug]=100-best-albums-of-2014&limit=1&scopes[]=published&scopes[]=updated"
|
883
|
+
GET "/v1/posts/8/comments.json?filters[has_images]=true&scopes[]=comments"
|
884
|
+
GET "/v1/comments/1161/images.json?scopes[]=liked&limit=1"
|
885
|
+
|
886
|
+
|
887
|
+
Multi-step requests pretty much match up to the action being performed.
|
888
|
+
From our example on in the [README](../README.doc) we show creating a `post`
|
889
|
+
and `user` and associating the two:
|
890
|
+
|
891
|
+
post = API::Post.find_by(slug: '100-best-albums-2014')
|
892
|
+
post.author = API::User.find_or_create(username: 'reidmix')
|
893
|
+
post.save
|
894
|
+
|
895
|
+
There are 3 queries to the server:
|
896
|
+
|
897
|
+
1. Initial lookup for the `post`
|
898
|
+
2. The creation of the `user`
|
899
|
+
3. Save the `post` to associate the newly created `user`
|
900
|
+
|
901
|
+
### Response Size
|
902
|
+
|
903
|
+
Responses are in JSON, but XML can be supported. Response size depends
|
904
|
+
several factors:
|
905
|
+
1. The length of each attribute
|
906
|
+
1. The number of attributes per resource
|
907
|
+
2. The number of resources
|
908
|
+
3. The metadata
|
909
|
+
|
910
|
+
For example, for a collection of `posts`:
|
911
|
+
|
912
|
+
````json
|
913
|
+
{
|
914
|
+
"posts": [
|
915
|
+
{
|
916
|
+
"id": 1,
|
917
|
+
"blog_id": "1",
|
918
|
+
"title": "100 Best Albums of 2014",
|
919
|
+
"created_by": "101",
|
920
|
+
"slug": "100-best-albums-of-2014"
|
921
|
+
"exerpt":"Ranked list of the 100 best albums so far in 2014",
|
922
|
+
"body": "2014 is a year of many albums, here is a...",
|
923
|
+
"published": true,
|
924
|
+
"updated": false
|
925
|
+
},
|
926
|
+
{
|
927
|
+
"id": 2,
|
928
|
+
"blog_id": "1",
|
929
|
+
"title": "100 Best Albums of All Time",
|
930
|
+
"created_by": "101",
|
931
|
+
"slug": "100-best-albums-of-all-time"
|
932
|
+
"exerpt":"Ranked list of the 100 best albums evar.",
|
933
|
+
"body": "Here is my favorite albums of all time...",
|
934
|
+
"published": true,
|
935
|
+
"updated": true
|
936
|
+
}
|
937
|
+
],
|
938
|
+
"meta": {
|
939
|
+
"where_values": {
|
940
|
+
"blog_id": 1
|
941
|
+
},
|
942
|
+
"post": {
|
943
|
+
"read_only": [
|
944
|
+
"slug",
|
945
|
+
"published",
|
946
|
+
"updated"
|
947
|
+
],
|
948
|
+
"nested_resources": [
|
949
|
+
"author",
|
950
|
+
"comments"
|
951
|
+
]
|
952
|
+
}
|
953
|
+
}
|
954
|
+
}
|
955
|
+
````
|
956
|
+
|
957
|
+
Here we show 2 posts, but imagine showing every `post` in each request.
|
958
|
+
Each time a request can be made that will reduce the size of the collection
|
959
|
+
will speed up response times from the server.
|
960
|
+
|
961
|
+
Metadata about the response and elements in the collection are also returned
|
962
|
+
per request. Find out more about this in the
|
963
|
+
[API Developers Guide](develop.md#response-metadata)
|
964
|
+
|
965
|
+
For expensive requests, your API developers may automatically limit the
|
966
|
+
"page size" returned by the server and you will need to paginate through
|
967
|
+
the results.
|
968
|
+
|
969
|
+
Please refer to [Benchmarks](benchmarks.md#) for further reading
|
970
|
+
about response times between the client and the server.
|