caprese 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0f662bf206bc10125464b9779d47c1d6704e0062
4
- data.tar.gz: cd8f888d31eed365bc32e9efeb762903c04780b9
3
+ metadata.gz: 6fa772365d2becc2bae5be4328595931ffee6107
4
+ data.tar.gz: 9d2aee61283a8abd50d736b48bd255ab73e5f0b8
5
5
  SHA512:
6
- metadata.gz: 85b15e24d4f7f89d67d4741dd1e3d47d0ecf90414a6ce0e6b1499a24aa0d3f58b8aec2e8e47270c1c0e5d82d5cedd11f0525dfe9a4761a263ea2290d1a15cbe7
7
- data.tar.gz: a1c2e81f96bce6b52c80c7ca61aed31018121a542b9baa67c819ec9c66565495ee96d434211128cb6cc75399682a251be3d742764e8ab8a9c7bc4d8496006e8d
6
+ metadata.gz: d23e29b6a9f9f5933350566da1f199c15cb4537320d235a11377147154b80dc9a789b658b59b43ef88a264a97f65bf1a7a0631f2962b1e71c813339887423459
7
+ data.tar.gz: 70b9ba12711fcdf36b4d9834a11d5bfc2cb763855d2e5115bf2f94c920085cb52bbf306e62a5ad8e38dac114eb0b51dd8e20e380f050c6607a7379bb1cf871af
data/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # Caprese
2
2
 
3
3
  Caprese is a Rails library for creating RESTful APIs in as few words as possible. It handles all CRUD operations on resources and their associations for you, and you can customize how these operations
4
- are carried out by overriding intermediate helper methods and by defining callbacks around any CRUD action.
4
+ are carried out, allowing for infinite possibilities while focusing on work that matters to you, instead of writing repetitive code for each action of each resource in your application.
5
5
 
6
- What separates Caprese from similar gems is that it is trying to do as little as possible, instead allowing gems that are better at other things to do what they are good at, as opposed to Caprese trying to do it all.
6
+ For now, the only format that is supported by Caprese is the [JSON API schema](http://jsonapi.org/format/). In the future, Caprese will support a more straightforward (but less powerful) JSON format as well, for simpler use cases.
7
7
 
8
8
  ## Installation
9
9
 
@@ -21,9 +21,481 @@ Or install it yourself as:
21
21
 
22
22
  $ gem install caprese
23
23
 
24
+ ## Philosophy
25
+
26
+ Caprese provides a controller framework that can automatically carry out `index`, `show`, `create`, `update`, and `destroy` actions for you with as little configuration as possible. You could write these methods yourself for every resource in your application, but the thing is, these 5 actions essentially do the same three things:
27
+
28
+ 1. Find a resource or set of resources, based on the parameters provided
29
+ 2. Optionally apply a number of changes to them, based on the data provided and the action selected
30
+ 3. Serialize and respond with the resource(s), in the format that was requested
31
+
32
+ Caprese does all of this dirty work for you, so all you have to do is customize its behavior to fine-tune the results. You customize the behavior by creating resource representations using serializers, by overriding intermediate helper methods, and by defining any number of callbacks in and around the actions to fully control each step of the process outlined above.
33
+
34
+ There are four components to creating an API using Caprese: controllers, routes, serializers, and records.
35
+
36
+ Before reading any further, we recommend that you read the [JSON API schema](http://jsonapi.org/format/) in full. It covers a lot of important information on all three steps above: modifying resource sets through query parameters, making changes to resources, modifying the response format, and more. Rather than make this doc a deep JSON API tutorial, we are going to assume you have read the schema and know what we're talking about.
37
+
24
38
  ## Usage
25
39
 
26
- TODO: Write usage instructions here
40
+ ### Set up your controller structure
41
+
42
+ You are familiar with using `ApplicationController` and having your controllers inherit from it. In Caprese, your `ApplicationController` is a `Caprese::Controller`, and everything inherits from it:
43
+
44
+ ```
45
+ # app/controllers/api/application_controller.rb
46
+ module API
47
+ class ApplicationController < Caprese::Controller
48
+ # Global API configuration
49
+ end
50
+ end
51
+
52
+ # app/controllers/api/v1/application_controller.rb
53
+ module API
54
+ module V1
55
+ class ApplicationController < API::ApplicationController
56
+ # API V1 configuration
57
+ end
58
+ end
59
+ end
60
+ ```
61
+
62
+ This structure allows you to define global API configuration, as well as versioned configuration specific to your `API::V1`.
63
+
64
+ ### Defining endpoints for a resource
65
+
66
+ Let's say you have a model `Product`. You'd create a versioned controller for it, inheriting from your `API::V1::ApplicationController`:
67
+
68
+ ```
69
+ # app/controllers/api/v1/products_controller.rb
70
+ module API
71
+ module V1
72
+ class ProductsController < ApplicationController
73
+ end
74
+ end
75
+ end
76
+ ```
77
+
78
+ You should also create a `Serializer` for `Product`, so Caprese will know what attributes to render in the response to requests.
79
+
80
+ Defining your serializers is simple:
81
+
82
+ ```
83
+ # app/serializers/api/v1/application_serializer.rb
84
+ module API
85
+ module V1
86
+ class ApplicationSerializer < Caprese::Serializer
87
+ # API::V1 serializer configuration
88
+ end
89
+ end
90
+ end
91
+
92
+ # app/serializers/api/v1/product_serializer.rb
93
+ module API
94
+ module V1
95
+ class ProductSerializer < ApplicationSerializer
96
+ attributes :title, :description
97
+ end
98
+ end
99
+ end
100
+ ```
101
+
102
+ This tells Caprese that when rendering requests for products, it should only include the `product.title` and `product.description` in the response.
103
+
104
+ `Caprese::Serializer` inherits from `ActiveModel::Serializer`, thus leaving the functionality of serializers to be defined by [ActiveModelSerializers](https://github.com/rails-api/active_model_serializers), a powerful library in Rails API. `Caprese::Serializer` automatically creates `links` for both the resource itself, and *all* of the resource's relationships.
105
+
106
+ From there, add a route:
107
+
108
+ ```
109
+ # config/routes.rb
110
+ Rails.application.routes.draw do
111
+ namespace 'api' do
112
+ namespace 'v1' do
113
+ caprese_resources :products
114
+ end
115
+ end
116
+ end
117
+ ```
118
+
119
+ With just that, you've just created fully functioning endpoints for `index`, `show`, and `destroy`:
120
+
121
+ ```
122
+ GET /api/v1/products
123
+ GET /api/v1/products/:id
124
+ DELETE /api/v1/products/:id
125
+ ```
126
+
127
+ Make a request to any of these endpoints, and your response will be product(s) with their `title` and `description`.
128
+
129
+ You can also modify the requests using JSON API query parameters like `filter`, `sort`, `page`, `limit`, `offset`, `fields`, and `includes`.
130
+
131
+ You might ask: What about `create` and `update`? Well, those require some configuration in order to work:
132
+
133
+ ```
134
+ # app/controllers/api/v1/products_controller.rb
135
+ module API
136
+ module V1
137
+ class ProductsController < ApplicationController
138
+
139
+ def permitted_create_params
140
+ [:title, :description]
141
+ end
142
+
143
+ def permitted_update_params
144
+ [:description]
145
+ end
146
+
147
+ end
148
+ end
149
+ end
150
+ ```
151
+
152
+ With that, you've stated that when creating a product, the params that are permitted to be assigned to the new product are `title` and `description`. When updating a product, the params that are permitted to be updated are nothing but `description`. You can't update the `title`.
153
+
154
+ That's it. You now have five fully functioning endpoints:
155
+
156
+ ```
157
+ GET /api/v1/products
158
+ GET /api/v1/products/:id
159
+ POST /api/v1/products
160
+ PATCH/PUT /api/v1/products/:id
161
+ DELETE /api/v1/products/:id
162
+ ```
163
+
164
+ ### Managing relationships of resources
165
+
166
+ The above is a little misleading. After doing the steps above, you'll actually end up with EIGHT endpoints:
167
+
168
+ ```
169
+ GET /api/v1/products
170
+ GET /api/v1/products/:id
171
+ POST /api/v1/products
172
+ PATCH/PUT /api/v1/products/:id
173
+ DELETE /api/v1/products/:id
174
+ GET /api/v1/products/:id/:relationship
175
+ GET /api/v1/products/:id/relationships/:relationship
176
+ PATCH/PUT/DELETE /api/v1/products/:id/relationships/:relationship
177
+ ```
178
+
179
+ The three new endpoints are for reading and managing relationships (known as `associations` in Rails) of the resource.
180
+
181
+ You can also specify which relationships can be assigned to a resource when created or which relationships can be updated:
182
+
183
+ ```
184
+ def permitted_create_params
185
+ [:title, :description]
186
+ end
187
+
188
+ def permitted_update_params
189
+ [:description, :orders]
190
+ end
191
+ ```
192
+
193
+ You could thus update the orders of a product in by making a call to one of two endpoints:
194
+ ```
195
+ PATCH/PUT /api/v1/products/:id # include `orders` in `relationships` member
196
+ PATCH/POST/DELETE /api/v1/products/:id/relationships/orders # provide resource identifiers for orders
197
+ ```
198
+
199
+ ### Scoping resources to customize behavior
200
+
201
+ Let's say you don't want a user to be able to request all the products in your database, you only want them to be able to request the ones that belong to them. If you determined `current_user` based on the authentication credentials they used, you could scope your products to only `current_user` by overriding the intermediate helper method `record_scope`:
202
+
203
+ ```
204
+ # app/controllers/api/v1/products_controller.rb
205
+ module API
206
+ module V1
207
+ class ProductsController < ApplicationController
208
+ def record_scope(type)
209
+ case type
210
+ when :products
211
+ Product.where(user: current_user)
212
+ end
213
+ end
214
+ end
215
+ end
216
+ end
217
+ ```
218
+
219
+ Let's take that one step further and state that when that user updates the orders of an existing product, you only want them to be able to add orders that belong to them, too. Simple:
220
+
221
+ ```
222
+ def record_scope(type)
223
+ case type
224
+ when :products
225
+ Product.where(user: current_user)
226
+ when :orders
227
+ Order.where(user: current_user)
228
+ end
229
+ end
230
+ ```
231
+
232
+ Now, if they were to make a request like `POST /api/v1/products/1/relationships/orders` to append an order to one of their products, and they submit a resource identifier for an order that did not belong to them, the response would be `404 Not Found`.
233
+
234
+ There is one more consideration to be made here. What if, when this user makes a request to `GET /api/v1/products/1/orders` to get the list of orders for one of their products, you only want to return orders that have been created in the last week. This time, you override another intermediate helper method, `relationship_scope`:
235
+
236
+ ```
237
+ def relationship_scope(name, scope)
238
+ case name
239
+ when :orders
240
+ scope.where('created_at < ?', 1.week.ago)
241
+ end
242
+ end
243
+ ```
244
+
245
+ `name` is the name of the relationship to scope, and `scope` is the existing scope for the relationship, in this case, equal to `Product.find(1).orders`.
246
+
247
+ ### Modifying control flow with callbacks
248
+
249
+ You may want to customize the behavior of an action, but you don't want to go about the task of overriding it entirely. Caprese defines a number of callbacks to modify the control flow for any action, allowing you to keep controller logic where it belongs.
250
+
251
+ ```
252
+ # app/controllers/api/v1/orders_controller.rb
253
+ module API
254
+ module V1
255
+ class OrdersController < ApplicationController
256
+ after_initialize :calculate_price_from_line_items, :do_something_else
257
+ after_create :send_confirmation_email
258
+
259
+ private
260
+
261
+ # If `Order#line_items` is a has_many association, and is created with an order (see: nested association creation),
262
+ # iterate over each line item and sum its price to determine the total order price
263
+ def calculate_price_from_line_items(order)
264
+ if order.line_items.any?
265
+ order.price = order.line_items.inject(0) do |sum, li|
266
+ sum += li.price
267
+ end
268
+ else
269
+ order.errors.add(:line_items, :blank) # an order must have line items
270
+ end
271
+ end
272
+
273
+ # After creating an order, send the customer a confirmation email
274
+ def send_confirmation_email(order)
275
+ ConfirmationMailer.send(order)
276
+ end
277
+ end
278
+ end
279
+ end
280
+ ```
281
+
282
+ The following callbacks are defined by Caprese:
283
+
284
+ ```
285
+ before_query (called before anything)
286
+ after_query (called after the response is rendered)
287
+ after_initialize (called in #create after the resource is instantiated)
288
+ before_create (alias for after_initialize)
289
+ after_create (rest are self explanatory)
290
+ before_update
291
+ after_update
292
+ before_save
293
+ after_save
294
+ before_destroy
295
+ after_destroy
296
+ ```
297
+
298
+ Reading this, note that saying `before_action :do_something, only: [:create]` is not the same as saying `before_create :do_something`. But you can use the former if you like, to customize further.
299
+
300
+ ### Creating nested associations
301
+
302
+ You can also use Caprese to created nested associations when creating their owners. For example, if I have two models:
303
+
304
+ ```
305
+ class Order < ActiveRecord::Base
306
+ has_many :line_items, autosave: true
307
+ end
308
+
309
+ class LineItem < ActiveRecord::Base
310
+ belongs_to :order
311
+ end
312
+ ```
313
+
314
+ I can send a resource document to `POST /api/v1/orders` that looks like this:
315
+
316
+ ```json
317
+ {
318
+ "data": {
319
+ "type": "orders",
320
+ "attributes": {
321
+ "some_order_attribute": "..."
322
+ },
323
+ "relationships": {
324
+ "line_items": {
325
+ "data": [
326
+ {
327
+ "type": "line_items",
328
+ "attributes": {
329
+ "price": 5.0
330
+ }
331
+ },
332
+ {
333
+ "type": "line_items",
334
+ "attributes": {
335
+ "price": 6.0
336
+ }
337
+ }
338
+ ]
339
+ }
340
+ }
341
+ }
342
+ }
343
+ ```
344
+
345
+ and once I configure my controller to indicate that it is permitted to create line items with orders, it will work:
346
+
347
+ ```
348
+ # app/controllers/api/v1/orders_controller.rb
349
+ module API
350
+ module V1
351
+ class OrdersController < ApplicationController
352
+ def permitted_create_params
353
+ [:some_order_attribute, line_items: [:price]]
354
+ end
355
+ end
356
+ end
357
+ end
358
+ ```
359
+
360
+ You could apply the same logic to `permitted_update_params` to update line items through `PATCH /api/v1/orders/:id`, just add an `"id"` field to the relationship data of each line item so Caprese knows which one to update.
361
+
362
+ ### Handling errors
363
+
364
+ Errors in Caprese come in two forms: model errors, and controller errors.
365
+
366
+ #### Model Errors
367
+
368
+ Model errors are returned from the server looking like this:
369
+
370
+ ```json
371
+ {
372
+ "errors": [
373
+ {
374
+ "source": { "pointer": "/data/attributes/price" },
375
+ "code": "blank",
376
+ "detail": "Price cannot be blank."
377
+ },
378
+ {
379
+ "source": { "pointer": "/data/relationships/line_items" },
380
+ "code": "blank",
381
+ "detail": "Line items cannot be blank."
382
+ },
383
+ {
384
+ "source": { "pointer": "/data/relationships/line_items/data/attributes/price" },
385
+ "code": "blank",
386
+ "detail": "Price cannot be blank."
387
+ }
388
+ ]
389
+ }
390
+ ```
391
+
392
+ Model errors have the same interface as in ActiveRecord, but with some added functionality on top. ActiveRecord errors only contain a message (for example: `price: 'Price cannot be blank'`). Caprese model errors also have a code (for example: `price: { code: :blank, message: 'Price cannot be blank.' }`), which is a much more programmatic solution. Rails 5 fixes this, but since Caprese supports both Rails 4 and Rails 5, we defined our own functionality for the time being.
393
+
394
+ To make use of Caprese model errors, add the `Caprese::Record` concern to your models:
395
+
396
+ ```
397
+ class Product < ActiveRecord::Base
398
+ include Caprese::Record
399
+ end
400
+ ```
401
+
402
+ The other thing that `Caprese::Record` brings to the table is that it allows you to create separate translations for error messages depending on the context: API, or application. Application is what you're used to. You can define a translation like `en.active_record.errors.models.product.attributes.title.blank = 'Hey buddy, a product title can't be blank!'` and that user-friendly error message is what will show up in your application. But using the same user-friendly error message to a third party API developer is kinda weird.
403
+
404
+ You can define your own set of translations specifically for your API: `en.api.v1.errors.models.product.title.blank = 'Custom error message'`. This requires some configuration on your part. You have to tell Caprese where to look by setting `Caprese.config.i18n_scope = 'api.v1.errors'`
405
+
406
+ Caprese looks for translations in the following order, and if none of them are defined, it will use `code.to_s` as the error message:
407
+
408
+ ```
409
+ # for field errors (attribute or relationship)
410
+ api.v1.errors.models.[model_name].[field].[code]
411
+ api.v1.errors.field.[code]
412
+ api.v1.errors.[code]
413
+
414
+ # for errors on base
415
+ api.v1.errors.models.[model_name].[code]
416
+ api.v1.errors.[code]
417
+ ```
418
+
419
+ #### Controller errors
420
+
421
+ Controller errors are returned from the server looking like this:
422
+
423
+ ```json
424
+ {
425
+ "errors": [
426
+ {
427
+ "source": { "parameter": "filter" },
428
+ "code": "invalid",
429
+ "detail": "Filters provided are invalid."
430
+ }
431
+ ]
432
+ }
433
+ ```
434
+
435
+ Caprese provides a helper method to easily create controller errors that can have their own translation scope. If at any point in your control flow, say in a callback, you want to immediately halt the request and respond with an error message, you can do the following:
436
+
437
+ ```
438
+ fail error(
439
+ field: :filter,
440
+ code: :invalid,
441
+ t: { ... } # translation interpolation variables to use in the error message
442
+ )
443
+ ```
444
+
445
+ Caprese will search for controller errors in the following order, and if none of them are defined, it will use `code.to_s` as the error message:
446
+
447
+ ```
448
+ api.v1.errors.controllers.[controller].[action].[field].[code]
449
+ api.v1.errors.controllers.[controller].[action].[code]
450
+ api.v1.errors.[code]
451
+ ```
452
+
453
+ #### Customizing your error and error message
454
+
455
+ The raw error object for Caprese is as follows:
456
+
457
+ ```
458
+ fail Error.new(
459
+ model: ..., # model name
460
+ controller: ..., # only model name || controller && action should be provided, not both
461
+ action: ...,
462
+ field: ...,
463
+ code: :invalid,
464
+ t: { ... } # translation interpolation variables to use in the error message
465
+ )
466
+ ```
467
+
468
+ Two translation interpolation variables will be provided for you automatically, on top of whatever ones you pass in. Those two are: `%{field}` and `%{field_title}`. If `field == :my_title`, `%{field} == my_title` and `%{field_title} == My Title`
469
+
470
+ ### Configuration
471
+
472
+ As a guide to configuring Caprese, here is the portion of `lib/caprese.rb` that stores the defaults:
473
+
474
+ ```
475
+ # Defines the primary key to use when querying records
476
+ config.resource_primary_key ||= :id
477
+
478
+ # Define URL options for use in UrlHelpers
479
+ config.default_url_options ||= {}
480
+
481
+ # If true, relationship data will not be serialized unless it is in `include`, huge performance boost
482
+ config.optimize_relationships ||= true
483
+
484
+ # Defines the translation scope for model and controller errors
485
+ config.i18n_scope ||= '' # 'api.v1.errors'
486
+
487
+ # The default size of any page queried
488
+ config.default_page_size ||= 10
489
+
490
+ # The maximum size of any page queried
491
+ config.max_page_size ||= 100
492
+ ```
493
+
494
+ You should also look into the configuration for [ActiveModelSerializers](https://github.com/rails-api/active_model_serializers/blob/master/docs/general/configuration_options.md) to customize the serializer behavior further.
495
+
496
+ ### Overriding an action while still using Caprese helpers
497
+
498
+ Coming soon... :)
27
499
 
28
500
  ## Development
29
501
 
data/lib/caprese.rb CHANGED
@@ -18,12 +18,13 @@ module Caprese
18
18
  # Define URL options for use in UrlHelpers
19
19
  config.default_url_options ||= {}
20
20
 
21
- # If true, relationship data will not be serialized unless it is in `include`
22
- config.optimize_relationships ||= true
23
-
24
21
  # If true, links will be rendered as `only_path: true`
22
+ # TODO: Implement this
25
23
  config.only_path_links ||= true
26
24
 
25
+ # If true, relationship data will not be serialized unless it is in `include`
26
+ config.optimize_relationships ||= true
27
+
27
28
  # Defines the translation scope for model and controller errors
28
29
  config.i18n_scope ||= '' # 'api.v1.errors'
29
30
 
@@ -81,12 +81,10 @@ module Caprese
81
81
  # @note We use the term scope, because the collection may be all records of that type,
82
82
  # or the records may be scoped further by overriding this method
83
83
  #
84
- # @note If no `type` is provided, the type is assumed to be the controller_record_class
85
- #
86
84
  # @param [Symbol] type the type to get a record scope for
87
85
  # @return [Relation] the scope of records of type `type`
88
- def record_scope(type = nil)
89
- (type && record_class(type) || controller_record_class).all
86
+ def record_scope(type)
87
+ record_class(type).all
90
88
  end
91
89
 
92
90
  # Gets a record in a scope using a column/value to search by
@@ -170,7 +168,7 @@ module Caprese
170
168
  # @return [Relation] the record scope of the queried controller
171
169
  def queried_record_scope
172
170
  unless @queried_record_scope
173
- scope = record_scope
171
+ scope = record_scope(unversion(params[:controller]).to_sym)
174
172
 
175
173
  if scope.any? && query_params[:filter].try(:any?)
176
174
  if (valid_filters = query_params[:filter].select { |k, _| scope.column_names.include? k }).present?
data/lib/caprese/error.rb CHANGED
@@ -45,8 +45,10 @@ module Caprese
45
45
  I18n.t("#{i18n_scope}.models.#{@model}.#{field}.#{code}", t)
46
46
  elsif i18n_set?("#{i18n_scope}.field.#{code}", t)
47
47
  I18n.t("#{i18n_scope}.field.#{code}", t)
48
- else
48
+ elsif i18n_set? "#{i18n_scope}.#{code}", t
49
49
  I18n.t("#{i18n_scope}.#{code}", t)
50
+ else
51
+ code.to_s
50
52
  end
51
53
  else
52
54
  if i18n_set? "#{i18n_scope}.models.#{@model}.#{code}", t
@@ -62,8 +64,10 @@ module Caprese
62
64
  I18n.t("#{i18n_scope}.controllers.#{@controller}.#{@action}.#{field}.#{code}", t)
63
65
  elsif i18n_set?("#{i18n_scope}.controllers.#{@controller}.#{@action}.#{code}", t)
64
66
  I18n.t("#{i18n_scope}.controllers.#{@controller}.#{@action}.#{code}", t)
65
- else
67
+ elsif i18n_set? "#{i18n_scope}.#{code}", t
66
68
  I18n.t("#{i18n_scope}.#{code}", t)
69
+ else
70
+ code.to_s
67
71
  end
68
72
  elsif field && i18n_set?("#{i18n_scope}.field.#{code}", t)
69
73
  I18n.t("#{i18n_scope}.field.#{code}", t)
@@ -1,3 +1,3 @@
1
1
  module Caprese
2
- VERSION = '0.2.0'
2
+ VERSION = '0.2.1'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: caprese
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Landgrebe
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: exe
12
12
  cert_chain: []
13
- date: 2016-09-21 00:00:00.000000000 Z
13
+ date: 2016-09-22 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: active_model_serializers