caprese 0.3.25 → 0.3.26

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: 34252b10961eccda9b87897794f97016566a8e2d
4
- data.tar.gz: f8b6e715a0cbbdbd9b71a81ef864872c09ab8aaa
3
+ metadata.gz: 1e98d646da7b21fc84de1e4e9c9954b0b67d0b94
4
+ data.tar.gz: e98309e9ef3b0d9102103dbd16670c69259793af
5
5
  SHA512:
6
- metadata.gz: 289224faf57d9e726b555d8b10bf4114127f40d8469157f5320f7a30b6e5c01a7fdfeaf9802e61739beb4f6c2d966ead2fc7881c1ea99797c8e97ab2df2aecea
7
- data.tar.gz: 525fb2eff4783c7f1c584db0ee9d4fb7ca2f0bd133661538d8ef28fb1f670898c631a965940c27b83190e86e0c29d081f4947afaa51f758de77b56cd82a33286
6
+ metadata.gz: 7c222c3c9ae7e98cd7ce548ba21a000e3c2a446cd31102b72720bfeb61a5d4eccab72d069055e28b79a41ef3fd70788251dcec1a36184c219b6481bb9b795596
7
+ data.tar.gz: 028fca901e3a6164720a8375618213560a42f81fa1c0331a528906e490c2cb39a81f178d23ce6be8775c472cb86ae1938615d4d949502660342b455c0212b97a
data/README.md CHANGED
@@ -23,318 +23,445 @@ Or install it yourself as:
23
23
 
24
24
  ## Philosophy
25
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:
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 API, but the thing is, these 5 actions essentially do the same three things:
27
27
 
28
28
  1. Find a resource or set of resources, based on the parameters provided
29
29
  2. Optionally apply a number of changes to them, based on the data provided and the action selected
30
30
  3. Serialize and respond with the resource(s), in the format that was requested
31
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.
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 using serializers, overriding methods, and defining any number of callbacks in and around the actions to fully control each step of the process outlined above.
33
33
 
34
- There are four components to creating an API using Caprese: controllers, routes, serializers, and records.
34
+ In the real world, Caprese is a style of dish combining tomatoes, mozzarella, and basil pesto, and is usually put in a salad or on a sandwich. Just like the food, there are four components to creating an API using Caprese: models, serializers, controllers, routes.
35
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.
36
+ Let's create a working API endpoint using Caprese to do something useful: allowing users to create, read, update and delete sandwiches.
37
37
 
38
- ## Usage
38
+ ## Building an API for sandwiches
39
39
 
40
- ### Set up your controller structure
40
+ ### Prep the tomatoes (models)
41
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:
42
+ ```ruby
43
+ class ApplicationRecord < ActiveRecord::Base
44
+ include Caprese::Record
45
+ end
43
46
 
44
- ```
45
- # app/controllers/api/application_controller.rb
46
- module API
47
- class ApplicationController < Caprese::Controller
48
- # Global API configuration
49
- end
47
+ # == Schema Information
48
+ #
49
+ # Table name: sandwiches
50
+ #
51
+ # id :id not null, primary key
52
+ # price :decimal not null
53
+ # description :text
54
+ # size :string(255) not null
55
+ # restaurant_id :integer not null
56
+ #
57
+ class Sandwich < ApplicationRecord
58
+ belongs_to :restaurant
59
+
60
+ has_many :condiments
50
61
  end
51
62
 
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
63
+ # == Schema Information
64
+ #
65
+ # Table name: restaurants
66
+ #
67
+ # id :id not null, primary key
68
+ # name :string(255) not null
69
+ #
70
+ class Restaurant < ApplicationRecord
71
+ has_many :sandwiches
72
+ end
73
+
74
+ # == Schema Information
75
+ #
76
+ # Table name: condiments
77
+ #
78
+ # id :id not null, primary key
79
+ # name :string(255) not null
80
+ # serving_size :integer not null
81
+ # sandwich_id :integer not null
82
+ #
83
+ class Condiment < ApplicationRecord
84
+ belongs_to :sandwich
59
85
  end
60
86
  ```
61
87
 
62
- This structure allows you to define global API configuration, as well as versioned configuration specific to your `API::V1`.
88
+ Tomatoes: Plain and hearty; an essential part of any true stack. The models of your application are just like them - you need them, but you can't consume them raw - your API has to decide what parts taste good for consumers. We say that models in Caprese are plain, because they're just Rails models...Caprese hasn't done much to them at all. So we create a `Sandwich` model with an association to a `Restaurant` and some `Condiment`s and then work on giving them a better taste with serializers.
63
89
 
64
- ### Defining endpoints for a resource
90
+ ### Put on the mozzarella (serializers)
65
91
 
66
- Let's say you have a model `Product`. You'd create a versioned controller for it, inheriting from your `API::V1::ApplicationController`:
92
+ ```ruby
93
+ class SandwichSerializer < Caprese::Serializer
94
+ attributes :price, :description, :size
67
95
 
68
- ```
69
- # app/controllers/api/v1/products_controller.rb
70
- module API
71
- module V1
72
- class ProductsController < ApplicationController
73
- end
74
- end
96
+ belongs_to :restaurant
97
+
98
+ has_many :condiments
75
99
  end
76
- ```
77
100
 
78
- You should also create a `Serializer` for `Product`, so Caprese will know what attributes to render in the response to requests.
101
+ class RestaurantSerializer < Caprese::Serializer
102
+ attributes :name
103
+ end
79
104
 
80
- Defining your serializers is simple:
105
+ class CondimentSerializer < Caprese::Serializer
106
+ attributes :name, :serving_size
81
107
 
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
108
+ belongs_to :sandwich
90
109
  end
110
+ ```
91
111
 
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
112
+ Mozzarella is so delicious - you can put it on anything and it's amazing. Mozzarella transforms the bland taste of tomatoes into something edible. Serializers are kinda the same way - you can use them to take a complex data model and turn it into something more consumable for people: JSON. When a user requests a sandwich from our API, Caprese will use the serializers above to define the fields (attributes and relationships) that the user sees, and by default, the response will look something like this:
113
+
114
+ ```json
115
+ {
116
+ "data": {
117
+ "type": "sandwiches",
118
+ "id": "1",
119
+ "attributes": {
120
+ "price": 10.0,
121
+ "description": "Tomato, mozzarella, and basil pesto between two pieces of bread.",
122
+ "size": "large"
123
+ },
124
+ "relationships": {
125
+ "condiments": {
126
+ "data": [
127
+ { "type": "condiments", "id": "5" },
128
+ { "type": "condiments", "id": "6" }
129
+ ]
130
+ },
131
+ "restaurant": {
132
+ "data": {
133
+ "type": "restaurants",
134
+ "id": "2"
135
+ }
136
+ }
137
+ }
138
+ }
139
+ }
100
140
  ```
101
141
 
102
- This tells Caprese that when rendering requests for products, it should only include the `product.title` and `product.description` in the response.
142
+ *NOTE:* Caprese only includes resource identifiers (`type` and `id`) for the `condiments` and `restaurant` of the sandwich, or any other relationship for that matter. It does not include the fields (`attributes` and `relationships`) of these resources unless the user specifically requests them (see [this section of JSON API format](http://jsonapi.org/format/#fetching) for details).
103
143
 
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.
144
+ ### Bring the tomato and mozzarella together onto a sandwich or salad (controllers)
105
145
 
106
- From there, add a route:
146
+ The bread of a sandwich or the leaves of a salad are what bring the entire Caprese dish together. Controllers are the same way - alongside tomatoes they are the "bite" of our application. When someone asks for a sandwich from our API, a controller fulfills the request, providing a necessary platform for that user to consume our tomatoes and mozzarella (the serialized resources). Let's bring our sandwich endpoint together with a controller, configuring it so it understands what information to use when creating a sandwich requested by a user:
107
147
 
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
148
+ ```ruby
149
+ class SandwichesController < Caprese::Controller
150
+ def permitted_create_params
151
+ [
152
+ :size, :condiments, :restaurant
153
+ ]
115
154
  end
116
155
  end
117
156
  ```
118
157
 
119
- With just that, you've just created fully functioning endpoints for `index`, `show`, and `destroy`:
158
+ This means that when a user requests a sandwich, we will use the `size` of the sandwich, any `condiments`, as well as the `restaurant` that the user specified in order to create a new sandwich. Note that we don't include `price` and `description` - we don't want the user to be able to change these. The request that the user makes will look something like this:
120
159
 
160
+ ```json
161
+ {
162
+ "data": {
163
+ "type": "sandwiches",
164
+ "attributes": {
165
+ "size": "small"
166
+ },
167
+ "relationships": {
168
+ "condiments": {
169
+ "data": [
170
+ { "type": "condiments", "id": "5" },
171
+ { "type": "condiments", "id": "6" },
172
+ ]
173
+ },
174
+ "restaurant": {
175
+ "data": {
176
+ "type": "restaurants",
177
+ "id": "1"
178
+ }
179
+ }
180
+ }
181
+ }
182
+ }
121
183
  ```
122
- GET /api/v1/products
123
- GET /api/v1/products/:id
124
- DELETE /api/v1/products/:id
184
+
185
+ You could also let the user create *new* condiments that aren't on the menu and put them onto their sandwich. Your controller would have to look like this:
186
+
187
+ ```ruby
188
+ class SandwichesController < Caprese::Controller
189
+ def permitted_create_params
190
+ [
191
+ :size, :restaurant,
192
+ condiments: [:name, :serving_size]
193
+ ]
194
+ end
195
+ end
125
196
  ```
126
197
 
127
- Make a request to any of these endpoints, and your response will be product(s) with their `title` and `description`.
198
+ Now, the controller will look at the `name` and `serving_size` attributes of each condiment when creating the sandwich, and add each new condiment to the end result. The request the user would make would look like this:
128
199
 
129
- You can also modify the requests using JSON API query parameters like `filter`, `sort`, `page`, `limit`, `offset`, `fields`, and `includes`.
200
+ ```json
201
+ {
202
+ "data": {
203
+ "type": "sandwiches",
204
+ "attributes": {
205
+ "size": "small"
206
+ },
207
+ "relationships": {
208
+ "condiments": {
209
+ "data": [
210
+ {
211
+ "type": "condiments",
212
+ "attributes": {
213
+ "name": "Dragon Blood",
214
+ "serving_size": "2"
215
+ }
216
+ },
217
+ {
218
+ "type": "condiments",
219
+ "attributes": {
220
+ "name": "Deep Fried Pickles",
221
+ "serving_size": "10"
222
+ }
223
+ }
224
+ ]
225
+ },
226
+ "restaurant": {
227
+ "data": {
228
+ "type": "restaurants",
229
+ "id": "1"
230
+ }
231
+ }
232
+ }
233
+ }
234
+ }
235
+ ```
130
236
 
131
- You might ask: What about `create` and `update`? Well, those require some configuration in order to work:
237
+ The response (outlined below) would contain the created sandwich along with any newly created condiments. Note that the attributes of the condiments that the user specified are not returned; remember that Caprese does not respond with `attributes` and `relationships` of related resources unless specifically told to do so.
132
238
 
239
+ ```json
240
+ {
241
+ "data": {
242
+ "type": "sandwiches",
243
+ "id": "1",
244
+ "attributes": {
245
+ "price": 5.0,
246
+ "description": "Tomato, mozzarella, and basil pesto between two pieces of bread.",
247
+ "size": "small"
248
+ },
249
+ "relationships": {
250
+ "condiments": {
251
+ "data": [
252
+ { "type": "condiments", "id": "10" },
253
+ { "type": "condiments", "id": "11" },
254
+ ]
255
+ },
256
+ "restaurant": {
257
+ "data": {
258
+ "type": "restaurants",
259
+ "id": "1"
260
+ }
261
+ }
262
+ }
263
+ }
264
+ }
133
265
  ```
134
- # app/controllers/api/v1/products_controller.rb
135
- module API
136
- module V1
137
- class ProductsController < ApplicationController
138
266
 
139
- def permitted_create_params
140
- [:title, :description]
141
- end
267
+ If you want users to be able to update sandwiches they've already created, you must also specify what they are allowed to update in the same manner as create:
142
268
 
143
- def permitted_update_params
144
- [:description]
145
- end
269
+ ```ruby
270
+ class SandwichesController < Caprese::Controller
271
+ def permitted_create_params
272
+ [
273
+ :size, :restaurant,
274
+ condiments: [:name, :serving_size]
275
+ ]
276
+ end
146
277
 
147
- end
278
+ # Only allow users to change the condiments of their sandwich
279
+ # 1. Don't let them update the sandwich by creating new condiments, only specifying existing ones
280
+ # 2. Don't let them change the size or the restaurant
281
+ def permitted_update_params
282
+ [
283
+ :condiments
284
+ ]
148
285
  end
149
286
  end
150
287
  ```
151
288
 
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`.
289
+ ### Complete with a dollop of basil pesto (routes)
153
290
 
154
- That's it. You now have five fully functioning endpoints:
291
+ All that's left to complete our sandwich API is to add routes for `index`, `show`, `create`, `update`, and `destroy`:
155
292
 
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
293
+ ```ruby
294
+ Rails.application.routes.draw do
295
+ caprese_resources :sandwiches
296
+ end
162
297
  ```
163
298
 
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:
299
+ With that, you'll now be able to make requests to any of the following URLs, and assuming you provide the necessary data, each one will provide a working response.
167
300
 
168
301
  ```
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
302
+ GET /sandwiches
303
+ GET /sandwiches/:id
304
+ POST /sandwiches
305
+ PATCH/PUT /sandwiches/:id
306
+ DELETE /sandwiches/:id
177
307
  ```
178
308
 
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:
309
+ Additionally, Caprese provides four routes that can be used to manage the relationships of the sandwich directly:
182
310
 
183
311
  ```
184
- def permitted_create_params
185
- [:title, :description]
186
- end
187
-
188
- def permitted_update_params
189
- [:description, :orders]
190
- end
312
+ GET /sandwiches/:id/:relationship
313
+ GET /sandwiches/:id/relationships/:relationship
314
+ PATCH/PUT /sandwiches/:id/relationships/:relationship
315
+ DELETE /sandwiches/:id/relationships/:relationship
191
316
  ```
192
317
 
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
318
+ For example, one could make a request to `GET /sandwiches/1/condiments` and the response would be like so:
319
+
320
+ ```json
321
+ {
322
+ "data": [
323
+ {
324
+ "type": "condiments",
325
+ "id": "5",
326
+ "attributes": {
327
+ "name": "Ketchup",
328
+ "serving_size": "2"
329
+ },
330
+ "relationships": {
331
+ "sandwich": {
332
+ "data": { "type": "sandwiches", "id": "1" }
333
+ }
334
+ }
335
+ },
336
+ {
337
+ "type": "condiments",
338
+ "id": "6",
339
+ "attributes": {
340
+ "name": "Mustard",
341
+ "serving_size": "1"
342
+ },
343
+ "relationships": {
344
+ "sandwich": {
345
+ "data": { "type": "sandwiches", "id": "1" }
346
+ }
347
+ }
348
+ }
349
+ ]
350
+ }
197
351
  ```
198
352
 
199
- ### Scoping resources to customize behavior
353
+ For all the details about using relationship endpoints, see [this section](http://jsonapi.org/format/#fetching-relationships) and [this section](http://jsonapi.org/format/#crud-updating-relationships) of the JSON API format.
200
354
 
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`:
355
+ ## Customizing the sandwich further
202
356
 
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
- ```
357
+ ### Scoping resources
218
358
 
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:
359
+ Let's say your sandwich API can create sandwiches for users from 5 different restaurants. Each restaurant has its own condiments, and you want to ensure that a customer cannot request a condiment from a restaurant if the restaurant does not have it.
360
+
361
+ By default, when `SandwichesController` looks for `condiments`, it uses `Condiment.all` as a starting point. This means that your user making a request could definitely request a condiment that does not exist at the restaurant they're ordering from. To fix this, we use a helper called `record_scope`:
220
362
 
221
363
  ```
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)
364
+ class SandwichesController < ApplicationController
365
+ def record_scope(type)
366
+ case type
367
+ when :condiments
368
+ Condiment.where(restaurant_id: data[:relationships][:restaurant][:data][:id])
369
+ else
370
+ super
371
+ end
228
372
  end
229
373
  end
230
374
  ```
231
375
 
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`.
376
+ ### Scoping relationships
377
+
378
+ Let's say you've created endpoints for `restaurants` as well, using the steps outlined above. This means that a user could make a request like `GET /restaurants/1/sandwiches` and the response would be all the sandwiches that the restaurant has created.
233
379
 
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`:
380
+ What if, instead, you wanted this endpoint to only return sandwiches that the restaurant had created in the last week alone. Simple, use `relationship_scope`:
235
381
 
236
382
  ```
237
- def relationship_scope(name, scope)
238
- case name
239
- when :orders
240
- scope.where('created_at < ?', 1.week.ago)
383
+ class RestaurantsController < ApplicationController
384
+ def relationship_scope(name, scope)
385
+ case name
386
+ when :sandwiches
387
+ scope.where('created_at < ?', 1.week.ago)
388
+ else
389
+ super
390
+ end
241
391
  end
242
392
  end
243
393
  ```
244
394
 
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
395
  ### Modifying control flow with callbacks
248
396
 
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:
397
+ You may want to customize the behavior of an action like `create`, `update`, or `delete`, 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 these actions:
283
398
 
284
399
  ```
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)
400
+ after_initialize
288
401
  before_create (alias for after_initialize)
289
- after_create (rest are self explanatory)
402
+ after_create
290
403
  before_update
291
404
  after_update
292
- before_save
293
- after_save
405
+ before_save (called before `create` and `update`)
406
+ after_save (ditto, but after)
294
407
  before_destroy
295
408
  after_destroy
296
409
  ```
297
410
 
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.
411
+ To implement one of these callbacks, simply define a callback method and add it to a callback list:
299
412
 
300
- ### Creating nested associations
413
+ ```
414
+ class SandwichesController < ApplicationController
415
+ before_create :cut_bread
416
+ before_save :calculate_price_from_special_condiments
417
+ after_update :refund_payment_method_if_moldy
301
418
 
302
- You can also use Caprese to created nested associations when creating their owners. For example, if I have two models:
419
+ private
303
420
 
304
- ```
305
- class Order < ActiveRecord::Base
306
- has_many :line_items, autosave: true
421
+ # Call custom method Sandwich#cut_bread before creating the sandwich
422
+ def cut_bread(sandwich)
423
+ sandwich.cut_bread
424
+ end
425
+
426
+ # If any of the condiments is avocado, add extra price when creating and updating sandwiches
427
+ def calculate_price_from_special_condiments(sandwich)
428
+ if(avocado = sandwich.condiments.detect { |c| c.is_a?(Avocado) })
429
+ sandwich.price += avocado.special_price
430
+ end
431
+ end
432
+
433
+ # If the customer updates us and says the sandwich is moldy, refund the sandwich
434
+ def refund_payment_method_if_moldy(sandwich)
435
+ sandwich.refund if sandwich.moldy?
436
+ end
307
437
  end
438
+ ```
439
+
440
+ ### Handling errors
441
+
442
+ Errors in Caprese come in two forms: model errors, and controller errors.
443
+
444
+ #### Model Errors
308
445
 
309
- class LineItem < ActiveRecord::Base
310
- belongs_to :order
446
+ Model errors are created when a record does not pass validation. [Validators are defined in the model using standard Rails.](http://guides.rubyonrails.org/active_record_validations.html) For example:
447
+
448
+ ```
449
+ class Sandwich < ApplicationRecord
450
+ validates_presence_of :size
451
+ validates_length_of :condiments, minimum: 2
311
452
  end
312
453
  ```
313
454
 
314
- I can send a resource document to `POST /api/v1/orders` that looks like this:
455
+ If a user were to make a request like so:
315
456
 
316
457
  ```json
317
458
  {
318
459
  "data": {
319
- "type": "orders",
320
- "attributes": {
321
- "some_order_attribute": "..."
322
- },
460
+ "type": "sandwiches",
323
461
  "relationships": {
324
- "line_items": {
462
+ "condiments": {
325
463
  "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
- }
464
+ { "type": "condiments", "id": "1" }
338
465
  ]
339
466
  }
340
467
  }
@@ -342,48 +469,20 @@ I can send a resource document to `POST /api/v1/orders` that looks like this:
342
469
  }
343
470
  ```
344
471
 
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:
472
+ The server would respond with `422 Unprocessable Entity`, with a response body like so:
369
473
 
370
474
  ```json
371
475
  {
372
476
  "errors": [
373
477
  {
374
- "source": { "pointer": "/data/attributes/price" },
375
- "code": "blank",
376
- "detail": "Price cannot be blank."
377
- },
378
- {
379
- "source": { "pointer": "/data/relationships/line_items" },
478
+ "source": { "pointer": "/data/attributes/size" },
380
479
  "code": "blank",
381
- "detail": "Line items cannot be blank."
480
+ "detail": "Size cannot be blank."
382
481
  },
383
482
  {
384
- "source": { "pointer": "/data/relationships/line_items/data/attributes/price" },
483
+ "source": { "pointer": "/data/relationships/condiments" },
385
484
  "code": "blank",
386
- "detail": "Price cannot be blank."
485
+ "detail": "Condiments must be of length 2 or more."
387
486
  }
388
487
  ]
389
488
  }
@@ -391,33 +490,37 @@ Model errors are returned from the server looking like this:
391
490
 
392
491
  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
492
 
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
- ```
493
+ 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 form and other user interfaces. But using the same layperson user-friendly error message to a third party API developer is kinda weird, and maybe not so useful.
401
494
 
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.
495
+ To use your own errors, set `Caprese.config.i18n_scope = '[YOUR_SCOPE]'`
403
496
 
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'`
497
+ You can define your own set of translations specifically for your API: `en.[YOUR_SCOPE].models.product.title.blank = 'Custom error message'`. This requires some configuration on your part.
405
498
 
406
499
  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
500
 
408
501
  ```
409
502
  # 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]
503
+ [YOUR_SCOPE].models.[model_name].[field].[code]
504
+ [YOUR_SCOPE].field.[code]
505
+ [YOUR_SCOPE].[code]
413
506
 
414
507
  # for errors on base
415
- api.v1.errors.models.[model_name].[code]
416
- api.v1.errors.[code]
508
+ [YOUR_SCOPE].models.[model_name].[code]
509
+ [YOUR_SCOPE].[code]
417
510
  ```
418
511
 
419
512
  #### Controller errors
420
513
 
514
+ Caprese provides a method to 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:
515
+
516
+ ```
517
+ fail error(
518
+ field: :filter,
519
+ code: :invalid,
520
+ t: { ... } # translation interpolation variables to use in the error message
521
+ )
522
+ ```
523
+
421
524
  Controller errors are returned from the server looking like this:
422
525
 
423
526
  ```json
@@ -432,45 +535,16 @@ Controller errors are returned from the server looking like this:
432
535
  }
433
536
  ```
434
537
 
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:
538
+ Caprese will search for controller errors in the following order:
456
539
 
457
540
  ```
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
- )
541
+ [YOUR_SCOPE].controllers.[controller].[action].[field].[code]
542
+ [YOUR_SCOPE].controllers.[controller].[action].[code]
543
+ [YOUR_SCOPE].[code]
466
544
  ```
467
545
 
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
546
  ### Configuration
471
547
 
472
- As a guide to configuring Caprese, here is the portion of `lib/caprese.rb` that stores the defaults:
473
-
474
548
  ```
475
549
  # Defines the primary key to use when querying records
476
550
  config.resource_primary_key ||= :id
@@ -34,15 +34,11 @@ module Caprese
34
34
  values = value.to_s.split('.')
35
35
  last_index = values.count - 1
36
36
 
37
+ klass_iteratee = record.class
37
38
  values.each_with_index.inject('') do |pointer, (v, i)|
38
39
  pointer +
39
- if record.class.reflections.key?(v)
40
- record =
41
- if record.class.reflections[v].collection?
42
- record.send(v).first
43
- else
44
- record.send(v)
45
- end
40
+ if ref = (klass_iteratee.reflect_on_association(v) || klass_iteratee.reflect_on_association(klass_iteratee.caprese_unalias_field(v)))
41
+ klass_iteratee = ref.klass
46
42
 
47
43
  if i == last_index
48
44
  format(POINTERS[:relationship_base], v)
@@ -5,6 +5,31 @@ module Caprese
5
5
  module Aliasing
6
6
  extend ActiveSupport::Concern
7
7
 
8
+ # Records all of the field aliases engaged by the API request (called in `assign_record_attributes` using comparison)
9
+ # so that when the response is returned, the appropriate alias is used in reference to fields
10
+ #
11
+ # Success: @todo
12
+ # Errors: @see ErrorSerializer
13
+ #
14
+ # @example
15
+ # {
16
+ # aliased_attribute: true, # used aliased attribute name instead of unaliased one
17
+ #
18
+ # aliased_relationship: { # used aliased relationship name
19
+ # aliased_attribute: true # and aliased attribute names
20
+ # aliased_attribute_2: true
21
+ # },
22
+ #
23
+ # aliased_relationship: {}, # used aliased relationship name but no aliased attribute names
24
+ #
25
+ # unaliased_relationship: { # used unaliased relationship name
26
+ # aliased_attribute: true # and aliased attribute name for that relationship
27
+ # },
28
+ # }
29
+ def engaged_field_aliases
30
+ @__engaged_field_aliases ||= {}
31
+ end
32
+
8
33
  # Specifies specific resource models that have types that are aliased.
9
34
  # @note The `caprese_type` class variable of the model should also be set to the new type
10
35
  # @example
@@ -19,7 +19,7 @@ module Caprese
19
19
  end
20
20
 
21
21
  rescue_from ActiveRecord::RecordInvalid do |e|
22
- rescue_with_handler RecordInvalidError.new(e.record)
22
+ rescue_with_handler RecordInvalidError.new(e.record, engaged_field_aliases)
23
23
  end
24
24
 
25
25
  rescue_from ActiveRecord::RecordNotDestroyed do |e|
@@ -46,7 +46,7 @@ module Caprese
46
46
  execute_before_create_callbacks(record)
47
47
  execute_before_save_callbacks(record)
48
48
 
49
- fail RecordInvalidError.new(record) if record.errors.any?
49
+ fail RecordInvalidError.new(record, engaged_field_aliases) if record.errors.any?
50
50
 
51
51
  record.save!
52
52
 
@@ -75,7 +75,7 @@ module Caprese
75
75
  execute_before_update_callbacks(queried_record)
76
76
  execute_before_save_callbacks(queried_record)
77
77
 
78
- fail RecordInvalidError.new(queried_record) if queried_record.errors.any?
78
+ fail RecordInvalidError.new(queried_record, engaged_field_aliases) if queried_record.errors.any?
79
79
 
80
80
  queried_record.save!
81
81
 
@@ -219,17 +219,34 @@ module Caprese
219
219
  # @param [ActiveRecord::Base] record the record to build attribute into
220
220
  # @param [Array] permitted_params the permitted params for the action
221
221
  # @param [Parameters] data the data sent to the server to construct and assign to the record
222
- def assign_record_attributes(record, permitted_params, data)
223
- attributes = data[:attributes].try(:permit, *permitted_params).try(:inject, {}) do |out, (attr, val)|
224
- out[actual_field(attr, record.class)] = val
222
+ # @option [String] parent_relationship_name the parent relationship assigning these attributes to the record, used to determine
223
+ # engaged aliases @see concerns/aliasing
224
+ def assign_record_attributes(record, permitted_params, data, parent_relationship_name: nil)
225
+ # TODO: Make safe by enforcing that only a single alias/unalias can be engaged at once
226
+ engaged_field_aliases_object = parent_relationship_name ? (engaged_field_aliases[parent_relationship_name] ||= {}) : engaged_field_aliases
227
+
228
+ attributes = data[:attributes].try(:permit, *permitted_params).try(:inject, {}) do |out, (attribute_name, val)|
229
+ attribute_name = attribute_name.to_sym
230
+ actual_attribute_name = actual_field(attribute_name, record.class)
231
+
232
+ if attribute_name != actual_attribute_name
233
+ engaged_field_aliases_object[attribute_name] = true
234
+ end
235
+
236
+ out[actual_attribute_name] = val
225
237
  out
226
238
  end || {}
227
239
 
228
240
  data[:relationships]
229
241
  .try(:slice, *flattened_keys_for(permitted_params))
230
242
  .try(:each) do |relationship_name, relationship_data|
243
+ relationship_name = relationship_name.to_sym
231
244
  actual_relationship_name = actual_field(relationship_name, record.class)
232
245
 
246
+ if relationship_name != actual_relationship_name
247
+ engaged_field_aliases_object[relationship_name] = {}
248
+ end
249
+
233
250
  # TODO: Add checkme for relationship_name to ensure that format is correct (not Array when actually Record, vice versa)
234
251
  # No relationship exists as well
235
252
 
@@ -261,19 +278,24 @@ module Caprese
261
278
  ref = record_for_relationship(owner, relationship_name, relationship_data_item)
262
279
 
263
280
  if ref && contains_constructable_data?(relationship_data_item)
264
- assign_record_attributes(ref, permitted_params, relationship_data_item)
281
+ assign_record_attributes(ref, permitted_params, relationship_data_item, parent_relationship_name: relationship_name)
265
282
  end
266
283
 
267
284
  ref
268
285
  end
269
- else
286
+ elsif relationship_data[:data].present?
270
287
  ref = record_for_relationship(owner, relationship_name, relationship_data[:data])
271
288
 
272
289
  if ref && contains_constructable_data?(relationship_data[:data])
273
- assign_record_attributes(ref, permitted_params, relationship_data[:data])
290
+ assign_record_attributes(ref, permitted_params, relationship_data[:data], parent_relationship_name: relationship_name)
274
291
  end
275
292
 
276
293
  ref
294
+ else
295
+ raise Error.new(
296
+ field: "/data/relationships/#{relationship_name}/data",
297
+ code: :blank
298
+ )
277
299
  end
278
300
  end
279
301
 
@@ -314,19 +336,6 @@ module Caprese
314
336
  end
315
337
  end
316
338
 
317
- # Assigns permitted attributes for a record in a relationship, for a given action
318
- # like create/update
319
- #
320
- # @param [ActiveRecord::Base] record the relationship record
321
- # @param [String] relationship_name the name of the relationship
322
- # @param [Symbol] action the action that is calling this method (create, update)
323
- # @param [Hash] resource_identifier the resource identifier
324
- def assign_relationship_record_attributes(record, relationship_name, action, attributes)
325
- record.assign_attributes(
326
- attributes.permit(nested_permitted_params_for(action, relationship_name))
327
- )
328
- end
329
-
330
339
  # Indicates whether or not :attributes or :relationships keys are in a resource identifier,
331
340
  # thus allowing us to construct this data into the final record
332
341
  #
@@ -24,6 +24,25 @@ module Caprese
24
24
  scope
25
25
  end
26
26
 
27
+ # Allows selection of serializer for any relationship serialized by get_relationship_data
28
+ #
29
+ # @note Returns nil by default because Caprese::Controller#render will determine the serializer if nil
30
+ #
31
+ # @example
32
+ # def relationship_serializer(name)
33
+ # case name
34
+ # when :answer
35
+ # AnswerSerializer
36
+ # else
37
+ # super
38
+ # end
39
+ # end
40
+ # @param [String] name the name of the relationship
41
+ # @return [Serializer,Nil] the serializer for the relationship or nil if none specified
42
+ def relationship_serializer(name)
43
+ nil
44
+ end
45
+
27
46
  # Retrieves the data for a relationship, not just the definition/resource identifier
28
47
  # @note Resource Identifier = { id: '...', type: '....' }
29
48
  # @note Resource = Resource Identifier + { attributes: { ... } }
@@ -44,7 +63,7 @@ module Caprese
44
63
  def get_relationship_data
45
64
  target =
46
65
  if queried_association.reflection.collection?
47
- scope = relationship_scope(params[:relationship], queried_association.reader)
66
+ scope = relationship_scope(params[:relationship].to_sym, queried_association.reader)
48
67
 
49
68
  if params[:relation_primary_key_value].present?
50
69
  get_record!(scope, self.class.config.resource_primary_key, params[:relation_primary_key_value])
@@ -57,18 +76,23 @@ module Caprese
57
76
 
58
77
  links = { self: request.original_url }
59
78
 
60
- if !target.respond_to?(:to_ary) &&
61
- url_helpers.respond_to?(related_url = version_name("#{params[:relationship].singularize}_url"))
79
+ if target.respond_to?(:to_ary)
80
+ serializer_type = :each_serializer
81
+ else
82
+ serializer_type = :serializer
62
83
 
63
- links[:related] =
64
- url_helpers.send(
65
- related_url,
66
- target.read_attribute(self.config.resource_primary_key),
67
- host: caprese_default_url_options_host
68
- )
84
+ if url_helpers.respond_to?(related_url = version_name("#{params[:relationship].singularize}_url"))
85
+ links[:related] =
86
+ url_helpers.send(
87
+ related_url,
88
+ target.read_attribute(self.config.resource_primary_key),
89
+ host: caprese_default_url_options_host
90
+ )
91
+ end
69
92
  end
70
93
 
71
94
  render(
95
+ serializer_type => relationship_serializer(params[:relationship].to_sym),
72
96
  json: target,
73
97
  fields: query_params[:fields],
74
98
  include: query_params[:include],
@@ -231,7 +255,7 @@ module Caprese
231
255
  when :has_one
232
256
  if request.patch?
233
257
  queried_record.send("#{relationship_name}=", relationship_resources[0])
234
- objects[0].save
258
+ relationship_resources[0].save if relationship_resources[0].present?
235
259
  end
236
260
  when :belongs_to
237
261
  if request.patch?
@@ -50,7 +50,7 @@ module Caprese
50
50
  invalid_typed_record = controller_record_class.new
51
51
  invalid_typed_record.errors.add(:type)
52
52
 
53
- fail RecordInvalidError.new(invalid_typed_record)
53
+ fail RecordInvalidError.new(invalid_typed_record, engaged_field_aliases)
54
54
  end
55
55
  end
56
56
  end
@@ -5,11 +5,12 @@ module Caprese
5
5
  #
6
6
  # @param [ActiveRecord::Base] record the record that is invalid
7
7
  class RecordInvalidError < Error
8
- attr_reader :record
8
+ attr_reader :record, :aliases
9
9
 
10
- def initialize(record)
10
+ def initialize(record, engaged_field_aliases = {})
11
11
  super()
12
12
  @record = record
13
+ @aliases = engaged_field_aliases
13
14
  @header = { status: :unprocessable_entity }
14
15
  end
15
16
 
@@ -24,7 +24,7 @@ module Caprese
24
24
 
25
25
  e = Error.new(
26
26
  model: @base.class.name.underscore.downcase,
27
- field: attribute == :base ? nil : @base.class.caprese_alias_field(attribute),
27
+ field: attribute == :base ? nil : attribute,
28
28
  code: code,
29
29
  t: options[:t]
30
30
  )
@@ -56,7 +56,7 @@ module Caprese
56
56
  (get(attribute) || []).map { |e| e.full_message }
57
57
  end
58
58
 
59
- # @note Overriden because traditionally to_a is an alias for `full_messages`, because in Rails standard there is
59
+ # @note Overridden because traditionally to_a is an alias for `full_messages`, because in Rails standard there is
60
60
  # no difference between an error and a full message, an error is that full message. With our API, an error is a
61
61
  # model that can render full messages, but it is still a distinct model. `to_a` thus has a different meaning here
62
62
  # than in Rails standard.
@@ -11,12 +11,14 @@ module Caprese
11
11
  # @note Overrides the AMS default since the default does not do namespaced lookup
12
12
  #
13
13
  # @param [ActiveRecord::Base] record the record to get a serializer for
14
- # @param [Hash] options options for `super` to use when getting the serializer
14
+ # @param [Hash] options options to use when getting the serializer
15
15
  # @return [Serializer,Nil] the serializer for the given record
16
16
  def serializer_for(record, options = {})
17
17
  return ActiveModel::Serializer::CollectionSerializer if record.respond_to?(:to_ary)
18
18
 
19
- get_serializer_for(record.class) if valid_for_serialization(record)
19
+ if valid_for_serialization(record)
20
+ options.fetch(:serializer) { get_serializer_for(record.class) }
21
+ end
20
22
  end
21
23
 
22
24
  # Indicates whether or not the record specified can be serialized by Caprese
@@ -3,11 +3,68 @@ require 'caprese/serializer'
3
3
  module Caprese
4
4
  class Serializer
5
5
  class ErrorSerializer < ActiveModel::Serializer::ErrorSerializer
6
- delegate :as_json, to: :object
7
-
8
6
  def resource_errors?
9
7
  object.try(:record).present?
10
8
  end
9
+
10
+ # Applies aliases to fields of RecordInvalid record's errors if aliases have been applied
11
+ # @see controller/concerns/aliasing#engaged_field_aliases
12
+ # Otherwise returns normal error fields as_json hash
13
+ def as_json
14
+ json = object.as_json
15
+
16
+ record = object.try(:record)
17
+ aliases = object.try(:aliases)
18
+
19
+ if record.present? && aliases.try(:any?)
20
+ aliased_json = {}
21
+
22
+ json.each do |k, v|
23
+ # Iterate over engaged_field_aliases object and see if each segment of the name split by '.' is in it (meaning
24
+ # that segment should be aliased for the error output since that is what the user is expecting)
25
+ klass_iterator = record.class
26
+ alias_iterator = aliases
27
+
28
+ field_iteratee = k.to_s.split('.')
29
+ new_error_field_name =
30
+ field_iteratee.map do |field|
31
+ field_alias = klass_iterator.caprese_alias_field(field)
32
+
33
+ if i = alias_iterator.try(:[], field_alias)
34
+ alias_iterator = i
35
+
36
+ # If != true, will be an object (relationship) to traverse, find the relationship klass so we can use its aliases
37
+ # for the next segment of alias_iterator
38
+ if alias_iterator != true && (ref = klass_iterator.reflect_on_association(field)).present?
39
+ klass_iterator = ref.klass
40
+ end
41
+
42
+ field_alias
43
+ elsif i = alias_iterator.try(:[], field)
44
+ alias_iterator = i
45
+
46
+ # If != true, will be an object (relationship) to traverse, find the relationship klass so we can use its aliases
47
+ # for the next segment of alias_iterator
48
+ if alias_iterator != true && (ref = klass_iterator.reflect_on_association(field)).present?
49
+ klass_iterator = ref.klass
50
+ end
51
+
52
+ field
53
+ else
54
+ alias_iterator = {}
55
+
56
+ field
57
+ end
58
+ end
59
+
60
+ aliased_json[new_error_field_name.join('.')] = v
61
+ end
62
+
63
+ aliased_json
64
+ else
65
+ json
66
+ end
67
+ end
11
68
  end
12
69
  end
13
70
  end
@@ -1,3 +1,3 @@
1
1
  module Caprese
2
- VERSION = '0.3.25'
2
+ VERSION = '0.3.26'
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.3.25
4
+ version: 0.3.26
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: 2017-05-30 00:00:00.000000000 Z
13
+ date: 2017-08-02 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: active_model_serializers