daylight 0.9.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +113 -0
  3. data/app/controllers/daylight_documentation/documentation_controller.rb +27 -0
  4. data/app/helpers/daylight_documentation/documentation_helper.rb +57 -0
  5. data/app/views/daylight_documentation/documentation/_header.haml +4 -0
  6. data/app/views/daylight_documentation/documentation/index.haml +12 -0
  7. data/app/views/daylight_documentation/documentation/model.haml +114 -0
  8. data/app/views/layouts/documentation.haml +22 -0
  9. data/config/routes.rb +8 -0
  10. data/doc/actions.md +70 -0
  11. data/doc/benchmarks.md +17 -0
  12. data/doc/contribute.md +80 -0
  13. data/doc/develop.md +1205 -0
  14. data/doc/environment.md +109 -0
  15. data/doc/example.md +3 -0
  16. data/doc/framework.md +31 -0
  17. data/doc/install.md +128 -0
  18. data/doc/principles.md +42 -0
  19. data/doc/testing.md +107 -0
  20. data/doc/usage.md +970 -0
  21. data/lib/daylight/api.rb +293 -0
  22. data/lib/daylight/associations.rb +247 -0
  23. data/lib/daylight/client_reloader.rb +45 -0
  24. data/lib/daylight/collection.rb +161 -0
  25. data/lib/daylight/errors.rb +94 -0
  26. data/lib/daylight/inflections.rb +7 -0
  27. data/lib/daylight/mock.rb +282 -0
  28. data/lib/daylight/read_only.rb +88 -0
  29. data/lib/daylight/refinements.rb +63 -0
  30. data/lib/daylight/reflection_ext.rb +67 -0
  31. data/lib/daylight/resource_proxy.rb +226 -0
  32. data/lib/daylight/version.rb +10 -0
  33. data/lib/daylight.rb +27 -0
  34. data/rails/daylight/api_controller.rb +354 -0
  35. data/rails/daylight/documentation.rb +13 -0
  36. data/rails/daylight/helpers.rb +32 -0
  37. data/rails/daylight/params.rb +23 -0
  38. data/rails/daylight/refiners.rb +186 -0
  39. data/rails/daylight/server.rb +29 -0
  40. data/rails/daylight/tasks.rb +37 -0
  41. data/rails/extensions/array_ext.rb +9 -0
  42. data/rails/extensions/autosave_association_fix.rb +49 -0
  43. data/rails/extensions/has_one_serializer_ext.rb +111 -0
  44. data/rails/extensions/inflections.rb +6 -0
  45. data/rails/extensions/nested_attributes_ext.rb +94 -0
  46. data/rails/extensions/read_only_attributes.rb +35 -0
  47. data/rails/extensions/render_json_meta.rb +99 -0
  48. data/rails/extensions/route_options.rb +47 -0
  49. data/rails/extensions/versioned_url_for.rb +22 -0
  50. data/spec/config/dependencies.rb +2 -0
  51. data/spec/config/factory_girl.rb +4 -0
  52. data/spec/config/simplecov_rcov.rb +26 -0
  53. data/spec/config/test_api.rb +1 -0
  54. data/spec/controllers/documentation_controller_spec.rb +24 -0
  55. data/spec/dummy/README.rdoc +28 -0
  56. data/spec/dummy/Rakefile +6 -0
  57. data/spec/dummy/app/assets/images/.keep +0 -0
  58. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  59. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  60. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  61. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  62. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  63. data/spec/dummy/app/mailers/.keep +0 -0
  64. data/spec/dummy/app/models/.keep +0 -0
  65. data/spec/dummy/app/models/concerns/.keep +0 -0
  66. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  67. data/spec/dummy/bin/bundle +3 -0
  68. data/spec/dummy/bin/rails +4 -0
  69. data/spec/dummy/bin/rake +4 -0
  70. data/spec/dummy/config/application.rb +24 -0
  71. data/spec/dummy/config/boot.rb +5 -0
  72. data/spec/dummy/config/database.yml +25 -0
  73. data/spec/dummy/config/environment.rb +5 -0
  74. data/spec/dummy/config/environments/development.rb +29 -0
  75. data/spec/dummy/config/environments/production.rb +80 -0
  76. data/spec/dummy/config/environments/test.rb +36 -0
  77. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  78. data/spec/dummy/config/initializers/daylight.rb +1 -0
  79. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  80. data/spec/dummy/config/initializers/inflections.rb +16 -0
  81. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  82. data/spec/dummy/config/initializers/secret_token.rb +12 -0
  83. data/spec/dummy/config/initializers/session_store.rb +3 -0
  84. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  85. data/spec/dummy/config/locales/en.yml +23 -0
  86. data/spec/dummy/config/routes.rb +59 -0
  87. data/spec/dummy/config.ru +4 -0
  88. data/spec/dummy/lib/assets/.keep +0 -0
  89. data/spec/dummy/log/.keep +0 -0
  90. data/spec/dummy/public/404.html +58 -0
  91. data/spec/dummy/public/422.html +58 -0
  92. data/spec/dummy/public/500.html +57 -0
  93. data/spec/dummy/public/favicon.ico +0 -0
  94. data/spec/helpers/documentation_helper_spec.rb +82 -0
  95. data/spec/lib/daylight/api_spec.rb +178 -0
  96. data/spec/lib/daylight/associations_spec.rb +325 -0
  97. data/spec/lib/daylight/collection_spec.rb +235 -0
  98. data/spec/lib/daylight/errors_spec.rb +111 -0
  99. data/spec/lib/daylight/mock_spec.rb +144 -0
  100. data/spec/lib/daylight/read_only_spec.rb +118 -0
  101. data/spec/lib/daylight/refinements_spec.rb +80 -0
  102. data/spec/lib/daylight/reflection_ext_spec.rb +50 -0
  103. data/spec/lib/daylight/resource_proxy_spec.rb +325 -0
  104. data/spec/rails/daylight/api_controller_spec.rb +421 -0
  105. data/spec/rails/daylight/helpers_spec.rb +41 -0
  106. data/spec/rails/daylight/params_spec.rb +45 -0
  107. data/spec/rails/daylight/refiners_spec.rb +178 -0
  108. data/spec/rails/extensions/array_ext_spec.rb +51 -0
  109. data/spec/rails/extensions/has_one_serializer_ext_spec.rb +135 -0
  110. data/spec/rails/extensions/nested_attributes_ext_spec.rb +177 -0
  111. data/spec/rails/extensions/render_json_meta_spec.rb +140 -0
  112. data/spec/rails/extensions/route_options_spec.rb +309 -0
  113. data/spec/rails/extensions/versioned_url_for_spec.rb +46 -0
  114. data/spec/spec_helper.rb +43 -0
  115. data/spec/support/migration_helper.rb +40 -0
  116. 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.