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