caprese 0.3.25 → 0.3.26
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.
- checksums.yaml +4 -4
- data/README.md +362 -288
- data/lib/caprese/adapter/json_api/json_pointer.rb +3 -7
- data/lib/caprese/controller/concerns/aliasing.rb +25 -0
- data/lib/caprese/controller/concerns/persistence.rb +31 -22
- data/lib/caprese/controller/concerns/relationships.rb +34 -10
- data/lib/caprese/controller/concerns/typing.rb +1 -1
- data/lib/caprese/errors.rb +3 -2
- data/lib/caprese/record/errors.rb +2 -2
- data/lib/caprese/serializer/concerns/lookup.rb +4 -2
- data/lib/caprese/serializer/error_serializer.rb +59 -2
- data/lib/caprese/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1e98d646da7b21fc84de1e4e9c9954b0b67d0b94
|
4
|
+
data.tar.gz: e98309e9ef3b0d9102103dbd16670c69259793af
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
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
|
-
|
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
|
-
|
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
|
-
##
|
38
|
+
## Building an API for sandwiches
|
39
39
|
|
40
|
-
###
|
40
|
+
### Prep the tomatoes (models)
|
41
41
|
|
42
|
-
|
42
|
+
```ruby
|
43
|
+
class ApplicationRecord < ActiveRecord::Base
|
44
|
+
include Caprese::Record
|
45
|
+
end
|
43
46
|
|
44
|
-
|
45
|
-
#
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
-
#
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
-
|
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
|
-
###
|
90
|
+
### Put on the mozzarella (serializers)
|
65
91
|
|
66
|
-
|
92
|
+
```ruby
|
93
|
+
class SandwichSerializer < Caprese::Serializer
|
94
|
+
attributes :price, :description, :size
|
67
95
|
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
-
|
101
|
+
class RestaurantSerializer < Caprese::Serializer
|
102
|
+
attributes :name
|
103
|
+
end
|
79
104
|
|
80
|
-
|
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
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
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
|
-
|
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
|
-
|
144
|
+
### Bring the tomato and mozzarella together onto a sandwich or salad (controllers)
|
105
145
|
|
106
|
-
|
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
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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
|
-
|
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
|
-
|
123
|
-
|
124
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
144
|
-
|
145
|
-
|
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
|
-
|
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
|
-
|
289
|
+
### Complete with a dollop of basil pesto (routes)
|
153
290
|
|
154
|
-
|
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
|
-
|
158
|
-
|
159
|
-
|
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
|
-
|
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
|
170
|
-
GET
|
171
|
-
POST
|
172
|
-
PATCH/PUT
|
173
|
-
DELETE
|
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
|
-
|
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
|
-
|
185
|
-
|
186
|
-
|
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
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
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
|
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
|
-
|
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
|
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
|
-
|
411
|
+
To implement one of these callbacks, simply define a callback method and add it to a callback list:
|
299
412
|
|
300
|
-
|
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
|
-
|
419
|
+
private
|
303
420
|
|
304
|
-
|
305
|
-
|
306
|
-
|
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
|
-
|
310
|
-
|
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
|
-
|
455
|
+
If a user were to make a request like so:
|
315
456
|
|
316
457
|
```json
|
317
458
|
{
|
318
459
|
"data": {
|
319
|
-
"type": "
|
320
|
-
"attributes": {
|
321
|
-
"some_order_attribute": "..."
|
322
|
-
},
|
460
|
+
"type": "sandwiches",
|
323
461
|
"relationships": {
|
324
|
-
"
|
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
|
-
|
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/
|
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": "
|
480
|
+
"detail": "Size cannot be blank."
|
382
481
|
},
|
383
482
|
{
|
384
|
-
"source": { "pointer": "/data/relationships/
|
483
|
+
"source": { "pointer": "/data/relationships/condiments" },
|
385
484
|
"code": "blank",
|
386
|
-
"detail": "
|
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
|
-
|
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
|
-
|
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.
|
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
|
-
|
411
|
-
|
412
|
-
|
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
|
-
|
416
|
-
|
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
|
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
|
-
|
459
|
-
|
460
|
-
|
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
|
40
|
-
|
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
|
-
|
223
|
-
|
224
|
-
|
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
|
-
|
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
|
61
|
-
|
79
|
+
if target.respond_to?(:to_ary)
|
80
|
+
serializer_type = :each_serializer
|
81
|
+
else
|
82
|
+
serializer_type = :serializer
|
62
83
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
-
|
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
|
data/lib/caprese/errors.rb
CHANGED
@@ -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 :
|
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
|
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
|
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
|
-
|
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
|
data/lib/caprese/version.rb
CHANGED
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.
|
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-
|
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
|