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/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.
|