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