jsonapi-resources 0.6.1 → 0.6.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +52 -7
- data/lib/jsonapi/acts_as_resource_controller.rb +44 -44
- data/lib/jsonapi/exceptions.rb +1 -1
- data/lib/jsonapi/formatter.rb +1 -1
- data/lib/jsonapi/mime_types.rb +3 -1
- data/lib/jsonapi/operation.rb +3 -3
- data/lib/jsonapi/request.rb +1 -1
- data/lib/jsonapi/resource.rb +61 -27
- data/lib/jsonapi/resource_serializer.rb +25 -12
- data/lib/jsonapi/resources/version.rb +1 -1
- data/lib/jsonapi/response_document.rb +2 -1
- data/test/controllers/controller_test.rb +83 -6
- data/test/fixtures/active_record.rb +31 -1
- data/test/integration/requests/namespaced_model_test.rb +13 -0
- data/test/test_helper.rb +1 -0
- data/test/unit/formatters/dasherized_key_formatter_test.rb +8 -0
- data/test/unit/resource/resource_test.rb +24 -0
- data/test/unit/serializer/serializer_test.rb +71 -0
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 233ad0db650881125288636b8fc022ccace40b01
|
4
|
+
data.tar.gz: 9c28dc89ef3f227398d6eecb9e4e274bcbd40d59
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 57bca518364bab67494516f10166275905eeb115709113601eb96c691d6bbf1ee2d1db4aa0c17ec83093f746cc8d38f37d4f8f11e1b59b2a2ae68a35922585ca
|
7
|
+
data.tar.gz: 30c3a7125c2eccbcda03e0daa8adab71ac8542afa599d8787087ee9fa461aa1efaff9e95fc8544f6762b8405000d82cec1911adb6d3ec7e7a1e269f60bf387e2
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# JSONAPI::Resources [![Build Status](https://secure.travis-ci.org/cerebris/jsonapi-resources.
|
1
|
+
# JSONAPI::Resources [![Build Status](https://secure.travis-ci.org/cerebris/jsonapi-resources.svg?branch=master)](http://travis-ci.org/cerebris/jsonapi-resources) [![Code Climate](https://codeclimate.com/github/cerebris/jsonapi-resources/badges/gpa.svg)](https://codeclimate.com/github/cerebris/jsonapi-resources)
|
2
2
|
|
3
3
|
[![Join the chat at https://gitter.im/cerebris/jsonapi-resources](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/cerebris/jsonapi-resources?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
4
4
|
|
@@ -26,6 +26,7 @@ backed by ActiveRecord models or by custom objects.
|
|
26
26
|
* [Filters] (#filters)
|
27
27
|
* [Pagination] (#pagination)
|
28
28
|
* [Included relationships (side-loading resources)] (#included-relationships-side-loading-resources)
|
29
|
+
* [Resource meta] (#resource-meta)
|
29
30
|
* [Callbacks] (#callbacks)
|
30
31
|
* [Controllers] (#controllers)
|
31
32
|
* [Namespaces] (#namespaces)
|
@@ -186,7 +187,7 @@ end
|
|
186
187
|
##### Fetchable Attributes
|
187
188
|
|
188
189
|
By default all attributes are assumed to be fetchable. The list of fetchable attributes can be filtered by overriding
|
189
|
-
the `fetchable_fields` method.
|
190
|
+
the `self.fetchable_fields` method.
|
190
191
|
|
191
192
|
Here's an example that prevents guest users from seeing the `email` field:
|
192
193
|
|
@@ -196,7 +197,7 @@ class AuthorResource < JSONAPI::Resource
|
|
196
197
|
model_name 'Person'
|
197
198
|
has_many :posts
|
198
199
|
|
199
|
-
def fetchable_fields
|
200
|
+
def self.fetchable_fields(context)
|
200
201
|
if (context[:current_user].guest)
|
201
202
|
super - [:email]
|
202
203
|
else
|
@@ -370,7 +371,7 @@ posts:
|
|
370
371
|
|
371
372
|
```ruby
|
372
373
|
class PostResource < JSONAPI::Resource
|
373
|
-
|
374
|
+
attributes :title, :body
|
374
375
|
|
375
376
|
relationship :author, to: :one
|
376
377
|
end
|
@@ -390,7 +391,7 @@ And here's the equivalent resources using the `has_one` and `has_many` methods:
|
|
390
391
|
|
391
392
|
```ruby
|
392
393
|
class PostResource < JSONAPI::Resource
|
393
|
-
|
394
|
+
attributes :title, :body
|
394
395
|
|
395
396
|
has_one :author
|
396
397
|
end
|
@@ -523,7 +524,7 @@ For example to allow a user to only retrieve his own posts you can do the follow
|
|
523
524
|
|
524
525
|
```ruby
|
525
526
|
class PostResource < JSONAPI::Resource
|
526
|
-
|
527
|
+
attributes :title, :body
|
527
528
|
|
528
529
|
def self.records(options = {})
|
529
530
|
context = options[:context]
|
@@ -793,6 +794,33 @@ Will get you the following payload by default:
|
|
793
794
|
}
|
794
795
|
```
|
795
796
|
|
797
|
+
#### Resource Meta
|
798
|
+
|
799
|
+
Meta information can be included for each resource using the meta method in the resource declaration. For example:
|
800
|
+
|
801
|
+
```ruby
|
802
|
+
class BookResource < JSONAPI::Resource
|
803
|
+
attribute :title
|
804
|
+
attribute :isbn
|
805
|
+
|
806
|
+
def meta(options)
|
807
|
+
{
|
808
|
+
copyright: 'API Copyright 2015 - XYZ Corp.',
|
809
|
+
computed_copyright: options[:serialization_options][:copyright]
|
810
|
+
last_updated_at: _model.updated_at
|
811
|
+
}
|
812
|
+
end
|
813
|
+
end
|
814
|
+
|
815
|
+
```
|
816
|
+
|
817
|
+
The `meta` method will be called for each resource instance. Override the `meta` method on a resource class to control
|
818
|
+
the meta information for the resource. If a non empty hash is returned from `meta` this will be serialized. The `meta`
|
819
|
+
method is called with an `options` has. The `options` hash will contain the following:
|
820
|
+
|
821
|
+
* `:serializer` -> the serializer instance
|
822
|
+
* `:serialization_options` -> the contents of the `serialization_options` method on the controller.
|
823
|
+
|
796
824
|
#### Callbacks
|
797
825
|
|
798
826
|
`ActiveSupport::Callbacks` is used to provide callback functionality, so the behavior is very similar to what you may be
|
@@ -907,6 +935,9 @@ end
|
|
907
935
|
|
908
936
|
Of course you are free to extend this as needed and override action handlers or other methods.
|
909
937
|
|
938
|
+
|
939
|
+
###### Context
|
940
|
+
|
910
941
|
The context that's used for serialization and resource configuration is set by the controller's `context` method.
|
911
942
|
|
912
943
|
For example:
|
@@ -925,7 +956,21 @@ class PeopleController < ApplicationController
|
|
925
956
|
end
|
926
957
|
```
|
927
958
|
|
928
|
-
|
959
|
+
###### Serialization Options
|
960
|
+
|
961
|
+
Additional options can be passed to the serializer using the `serialization_options` method.
|
962
|
+
|
963
|
+
For example:
|
964
|
+
|
965
|
+
```ruby
|
966
|
+
class ApplicationController < JSONAPI::ResourceController
|
967
|
+
def serialization_options
|
968
|
+
{copyright: 'Copyright 2015'}
|
969
|
+
end
|
970
|
+
end
|
971
|
+
```
|
972
|
+
|
973
|
+
These `serialization_options` are passed to the `meta` method used to generate resource `meta` values.
|
929
974
|
|
930
975
|
##### ActsAsResourceController
|
931
976
|
|
@@ -2,56 +2,74 @@ require 'csv'
|
|
2
2
|
|
3
3
|
module JSONAPI
|
4
4
|
module ActsAsResourceController
|
5
|
-
extend ActiveSupport::Concern
|
6
5
|
|
7
|
-
included
|
8
|
-
|
9
|
-
|
10
|
-
|
6
|
+
def self.included(base)
|
7
|
+
base.extend ClassMethods
|
8
|
+
base.before_action :ensure_correct_media_type, only: [:create, :update, :create_relationship, :update_relationship]
|
9
|
+
base.cattr_reader :server_error_callbacks
|
11
10
|
end
|
12
11
|
|
13
12
|
def index
|
14
|
-
|
13
|
+
process_request
|
15
14
|
end
|
16
15
|
|
17
16
|
def show
|
18
|
-
|
17
|
+
process_request
|
19
18
|
end
|
20
19
|
|
21
20
|
def show_relationship
|
22
|
-
|
21
|
+
process_request
|
23
22
|
end
|
24
23
|
|
25
24
|
def create
|
26
|
-
|
25
|
+
process_request
|
27
26
|
end
|
28
27
|
|
29
28
|
def create_relationship
|
30
|
-
|
29
|
+
process_request
|
31
30
|
end
|
32
31
|
|
33
32
|
def update_relationship
|
34
|
-
|
33
|
+
process_request
|
35
34
|
end
|
36
35
|
|
37
36
|
def update
|
38
|
-
|
37
|
+
process_request
|
39
38
|
end
|
40
39
|
|
41
40
|
def destroy
|
42
|
-
|
41
|
+
process_request
|
43
42
|
end
|
44
43
|
|
45
44
|
def destroy_relationship
|
46
|
-
|
45
|
+
process_request
|
47
46
|
end
|
48
47
|
|
49
48
|
def get_related_resource
|
50
|
-
|
49
|
+
process_request
|
51
50
|
end
|
52
51
|
|
53
52
|
def get_related_resources
|
54
|
-
|
53
|
+
process_request
|
54
|
+
end
|
55
|
+
|
56
|
+
def process_request
|
57
|
+
@request = JSONAPI::Request.new(params, context: context,
|
58
|
+
key_formatter: key_formatter,
|
59
|
+
server_error_callbacks: (self.class.server_error_callbacks || []))
|
60
|
+
unless @request.errors.empty?
|
61
|
+
render_errors(@request.errors)
|
62
|
+
else
|
63
|
+
operation_results = create_operations_processor.process(@request)
|
64
|
+
render_results(operation_results)
|
65
|
+
end
|
66
|
+
|
67
|
+
if response.body.size > 0
|
68
|
+
response.headers['Content-Type'] = JSONAPI::MEDIA_TYPE
|
69
|
+
end
|
70
|
+
|
71
|
+
rescue => e
|
72
|
+
handle_exceptions(e)
|
55
73
|
end
|
56
74
|
|
57
75
|
# set the operations processor in the configuration or override this to use another operations processor
|
@@ -85,25 +103,15 @@ module JSONAPI
|
|
85
103
|
handle_exceptions(e)
|
86
104
|
end
|
87
105
|
|
88
|
-
def setup_request
|
89
|
-
@request = JSONAPI::Request.new(params, context: context, key_formatter: key_formatter)
|
90
|
-
|
91
|
-
render_errors(@request.errors) unless @request.errors.empty?
|
92
|
-
rescue => e
|
93
|
-
handle_exceptions(e)
|
94
|
-
end
|
95
|
-
|
96
|
-
def setup_response
|
97
|
-
if response.body.size > 0
|
98
|
-
response.headers['Content-Type'] = JSONAPI::MEDIA_TYPE
|
99
|
-
end
|
100
|
-
end
|
101
|
-
|
102
106
|
# override to set context
|
103
107
|
def context
|
104
108
|
{}
|
105
109
|
end
|
106
110
|
|
111
|
+
def serialization_options
|
112
|
+
{}
|
113
|
+
end
|
114
|
+
|
107
115
|
# Control by setting in an initializer:
|
108
116
|
# JSONAPI.configuration.json_key_format = :camelized_key
|
109
117
|
# JSONAPI.configuration.route = :camelized_route
|
@@ -159,17 +167,11 @@ module JSONAPI
|
|
159
167
|
base_meta: base_meta,
|
160
168
|
base_links: base_response_links,
|
161
169
|
resource_serializer_klass: resource_serializer_klass,
|
162
|
-
request: @request
|
170
|
+
request: @request,
|
171
|
+
serialization_options: serialization_options
|
163
172
|
)
|
164
173
|
end
|
165
174
|
|
166
|
-
def process_request_operations
|
167
|
-
operation_results = create_operations_processor.process(@request)
|
168
|
-
render_results(operation_results)
|
169
|
-
rescue => e
|
170
|
-
handle_exceptions(e)
|
171
|
-
end
|
172
|
-
|
173
175
|
# override this to process other exceptions
|
174
176
|
# Note: Be sure to either call super(e) or handle JSONAPI::Exceptions::Error and raise unhandled exceptions
|
175
177
|
def handle_exceptions(e)
|
@@ -183,10 +185,6 @@ module JSONAPI
|
|
183
185
|
end
|
184
186
|
end
|
185
187
|
|
186
|
-
def add_error_callbacks(callbacks)
|
187
|
-
@request.server_error_callbacks = callbacks || []
|
188
|
-
end
|
189
|
-
|
190
188
|
# Pass in a methods or a block to be run when an exception is
|
191
189
|
# caught that is not a JSONAPI::Exceptions::Error
|
192
190
|
# Useful for additional logging or notification configuration that
|
@@ -194,8 +192,9 @@ module JSONAPI
|
|
194
192
|
# Ignores whitelist exceptions from config
|
195
193
|
|
196
194
|
module ClassMethods
|
195
|
+
|
197
196
|
def on_server_error(*args, &callback_block)
|
198
|
-
callbacks
|
197
|
+
callbacks ||= []
|
199
198
|
|
200
199
|
if callback_block
|
201
200
|
callbacks << callback_block
|
@@ -211,8 +210,9 @@ module JSONAPI
|
|
211
210
|
end
|
212
211
|
end.compact
|
213
212
|
callbacks += method_callbacks
|
214
|
-
|
213
|
+
self.class_variable_set :@@server_error_callbacks, callbacks
|
215
214
|
end
|
215
|
+
|
216
216
|
end
|
217
217
|
end
|
218
218
|
end
|
data/lib/jsonapi/exceptions.rb
CHANGED
@@ -297,7 +297,7 @@ module JSONAPI
|
|
297
297
|
attr_reader :error_messages, :resource_relationships
|
298
298
|
|
299
299
|
def initialize(resource)
|
300
|
-
@error_messages = resource.
|
300
|
+
@error_messages = resource.model_error_messages
|
301
301
|
@resource_relationships = resource.class._relationships.keys
|
302
302
|
@key_formatter = JSONAPI.configuration.key_formatter
|
303
303
|
end
|
data/lib/jsonapi/formatter.rb
CHANGED
data/lib/jsonapi/mime_types.rb
CHANGED
@@ -5,5 +5,7 @@ end
|
|
5
5
|
Mime::Type.register JSONAPI::MEDIA_TYPE, :api_json
|
6
6
|
|
7
7
|
ActionDispatch::ParamsParser::DEFAULT_PARSERS[Mime::Type.lookup(JSONAPI::MEDIA_TYPE)] = lambda do |body|
|
8
|
-
JSON.parse(body)
|
8
|
+
data = JSON.parse(body)
|
9
|
+
data = {:_json => data} unless data.is_a?(Hash)
|
10
|
+
data.with_indifferent_access
|
9
11
|
end
|
data/lib/jsonapi/operation.rb
CHANGED
@@ -79,9 +79,9 @@ module JSONAPI
|
|
79
79
|
def apply
|
80
80
|
key = @resource_klass.verify_key(@id, @context)
|
81
81
|
|
82
|
-
resource_record = resource_klass.find_by_key(key,
|
83
|
-
|
84
|
-
|
82
|
+
resource_record = @resource_klass.find_by_key(key,
|
83
|
+
context: @context,
|
84
|
+
include_directives: @include_directives)
|
85
85
|
|
86
86
|
return JSONAPI::ResourceOperationResult.new(:ok, resource_record)
|
87
87
|
|
data/lib/jsonapi/request.rb
CHANGED
data/lib/jsonapi/resource.rb
CHANGED
@@ -107,9 +107,8 @@ module JSONAPI
|
|
107
107
|
end
|
108
108
|
end
|
109
109
|
|
110
|
-
# Override this on a resource instance to override the fetchable keys
|
111
110
|
def fetchable_fields
|
112
|
-
self.class.
|
111
|
+
self.class.fetchable_fields(context)
|
113
112
|
end
|
114
113
|
|
115
114
|
# Override this on a resource to customize how the associated records
|
@@ -118,6 +117,19 @@ module JSONAPI
|
|
118
117
|
_model.public_send relation_name
|
119
118
|
end
|
120
119
|
|
120
|
+
def model_error_messages
|
121
|
+
_model.errors.messages
|
122
|
+
end
|
123
|
+
|
124
|
+
# Override this to return resource level meta data
|
125
|
+
# must return a hash, and if the hash is empty the meta section will not be serialized with the resource
|
126
|
+
# meta keys will be not be formatted with the key formatter for the serializer by default. They can however use the
|
127
|
+
# serializer's format_key and format_value methods if desired
|
128
|
+
# the _options hash will contain the serializer and the serialization_options
|
129
|
+
def meta(_options)
|
130
|
+
{}
|
131
|
+
end
|
132
|
+
|
121
133
|
private
|
122
134
|
|
123
135
|
def save
|
@@ -145,8 +157,14 @@ module JSONAPI
|
|
145
157
|
end
|
146
158
|
|
147
159
|
if defined? @model.save
|
148
|
-
saved = @model.save
|
149
|
-
|
160
|
+
saved = @model.save(validate: false)
|
161
|
+
unless saved
|
162
|
+
if @model.errors.present?
|
163
|
+
fail JSONAPI::Exceptions::ValidationErrors.new(self)
|
164
|
+
else
|
165
|
+
fail JSONAPI::Exceptions::SaveFailed.new
|
166
|
+
end
|
167
|
+
end
|
150
168
|
else
|
151
169
|
saved = true
|
152
170
|
end
|
@@ -274,13 +292,37 @@ module JSONAPI
|
|
274
292
|
check_reserved_resource_name(base._type, base.name)
|
275
293
|
end
|
276
294
|
|
277
|
-
def resource_for(
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
295
|
+
def resource_for(resource_path)
|
296
|
+
unless @@resource_types.key? resource_path
|
297
|
+
klass_name = "#{resource_path.to_s.underscore.singularize}_resource".camelize
|
298
|
+
klass = (klass_name.safe_constantize or
|
299
|
+
fail NameError,
|
300
|
+
"JSONAPI: Could not find resource '#{resource_path}'. (Class #{klass_name} not found)")
|
301
|
+
normalized_path = resource_path.rpartition('/').first
|
302
|
+
normalized_model = klass._model_name.to_s.gsub(/\A::/, '')
|
303
|
+
@@resource_types[resource_path] = {
|
304
|
+
resource: klass,
|
305
|
+
path: normalized_path,
|
306
|
+
model: normalized_model,
|
307
|
+
}
|
308
|
+
end
|
309
|
+
@@resource_types[resource_path][:resource]
|
310
|
+
end
|
311
|
+
|
312
|
+
def resource_for_model_path(model, path)
|
313
|
+
normalized_model = model.class.to_s.gsub(/\A::/, '')
|
314
|
+
normalized_path = path.gsub(/\/\z/, '')
|
315
|
+
resource = @@resource_types.find { |_, h|
|
316
|
+
h[:path] == normalized_path && h[:model] == normalized_model
|
317
|
+
}
|
318
|
+
if resource
|
319
|
+
resource.last[:resource]
|
320
|
+
else
|
321
|
+
#:nocov:#
|
322
|
+
fail NameError,
|
323
|
+
"JSONAPI: Could not find resource for model '#{path}#{normalized_model}'"
|
324
|
+
#:nocov:#
|
282
325
|
end
|
283
|
-
resource
|
284
326
|
end
|
285
327
|
|
286
328
|
attr_accessor :_attributes, :_relationships, :_allowed_filters, :_type, :_paginator
|
@@ -385,6 +427,11 @@ module JSONAPI
|
|
385
427
|
end
|
386
428
|
# :nocov:
|
387
429
|
|
430
|
+
# Override in your resource to filter the fetchable keys
|
431
|
+
def fetchable_fields(_context = nil)
|
432
|
+
fields
|
433
|
+
end
|
434
|
+
|
388
435
|
# Override in your resource to filter the updatable keys
|
389
436
|
def updatable_fields(_context = nil)
|
390
437
|
_updatable_relationships | _attributes.keys - [:id]
|
@@ -503,7 +550,7 @@ module JSONAPI
|
|
503
550
|
|
504
551
|
resources = []
|
505
552
|
records.each do |model|
|
506
|
-
resources.push
|
553
|
+
resources.push resource_for_model_path(model, self.module_path).new(model, context)
|
507
554
|
end
|
508
555
|
|
509
556
|
resources
|
@@ -515,11 +562,7 @@ module JSONAPI
|
|
515
562
|
records = apply_includes(records, options)
|
516
563
|
model = records.where({_primary_key => key}).first
|
517
564
|
fail JSONAPI::Exceptions::RecordNotFound.new(key) if model.nil?
|
518
|
-
|
519
|
-
end
|
520
|
-
|
521
|
-
def resource_type_for(model)
|
522
|
-
self.module_path + model.class.to_s.underscore
|
565
|
+
resource_for_model_path(model, self.module_path).new(model, context)
|
523
566
|
end
|
524
567
|
|
525
568
|
# Override this method if you want to customize the relation for
|
@@ -635,15 +678,6 @@ module JSONAPI
|
|
635
678
|
!@_allowed_filters.nil? ? @_allowed_filters : { id: {} }
|
636
679
|
end
|
637
680
|
|
638
|
-
def _resource_name_from_type(type)
|
639
|
-
class_name = @@resource_types[type]
|
640
|
-
if class_name.nil?
|
641
|
-
class_name = "#{type.to_s.underscore.singularize}_resource".camelize
|
642
|
-
@@resource_types[type] = class_name
|
643
|
-
end
|
644
|
-
return class_name
|
645
|
-
end
|
646
|
-
|
647
681
|
def _paginator
|
648
682
|
@_paginator ||= JSONAPI.configuration.default_paginator
|
649
683
|
end
|
@@ -763,7 +797,7 @@ module JSONAPI
|
|
763
797
|
define_method attr do |options = {}|
|
764
798
|
if relationship.polymorphic?
|
765
799
|
associated_model = public_send(associated_records_method_name)
|
766
|
-
resource_klass =
|
800
|
+
resource_klass = self.class.resource_for_model_path(associated_model, self.class.module_path) if associated_model
|
767
801
|
return resource_klass.new(associated_model, @context) if resource_klass
|
768
802
|
else
|
769
803
|
resource_klass = relationship.resource_klass
|
@@ -816,7 +850,7 @@ module JSONAPI
|
|
816
850
|
end
|
817
851
|
|
818
852
|
return records.collect do |record|
|
819
|
-
resource_klass =
|
853
|
+
resource_klass = self.class.resource_for_model_path(record, self.class.module_path)
|
820
854
|
resource_klass.new(record, @context)
|
821
855
|
end
|
822
856
|
end unless method_defined?(attr)
|
@@ -1,6 +1,9 @@
|
|
1
1
|
module JSONAPI
|
2
2
|
class ResourceSerializer
|
3
3
|
|
4
|
+
attr_reader :url_generator, :key_formatter, :serialization_options, :primary_class_name
|
5
|
+
|
6
|
+
# initialize
|
4
7
|
# Options can include
|
5
8
|
# include:
|
6
9
|
# Purpose: determines which objects will be side loaded with the source objects in a linked section
|
@@ -10,9 +13,7 @@ module JSONAPI
|
|
10
13
|
# relationship ids in the links section for a resource. Fields are global for a resource type.
|
11
14
|
# Example: { people: [:id, :email, :comments], posts: [:id, :title, :author], comments: [:id, :body, :post]}
|
12
15
|
# key_formatter: KeyFormatter class to override the default configuration
|
13
|
-
#
|
14
|
-
|
15
|
-
attr_reader :url_generator
|
16
|
+
# serializer_options: additional options that will be passed to resource meta and links lambdas
|
16
17
|
|
17
18
|
def initialize(primary_resource_klass, options = {})
|
18
19
|
@primary_class_name = primary_resource_klass._type
|
@@ -25,6 +26,7 @@ module JSONAPI
|
|
25
26
|
JSONAPI.configuration.always_include_to_one_linkage_data)
|
26
27
|
@always_include_to_many_linkage_data = options.fetch(:always_include_to_many_linkage_data,
|
27
28
|
JSONAPI.configuration.always_include_to_many_linkage_data)
|
29
|
+
@serialization_options = options.fetch(:serialization_options, {})
|
28
30
|
end
|
29
31
|
|
30
32
|
# Converts a single resource, or an array of resources to a hash, conforming to the JSONAPI structure
|
@@ -74,6 +76,15 @@ module JSONAPI
|
|
74
76
|
url_generator.query_link(query_params)
|
75
77
|
end
|
76
78
|
|
79
|
+
def format_key(key)
|
80
|
+
@key_formatter.format(key)
|
81
|
+
end
|
82
|
+
|
83
|
+
def format_value(value, format)
|
84
|
+
value_formatter = JSONAPI::ValueFormatter.value_formatter_for(format)
|
85
|
+
value_formatter.format(value)
|
86
|
+
end
|
87
|
+
|
77
88
|
private
|
78
89
|
|
79
90
|
# Process the primary source object(s). This will then serialize associated object recursively based on the
|
@@ -119,6 +130,10 @@ module JSONAPI
|
|
119
130
|
relationships = relationship_data(source, include_directives)
|
120
131
|
obj_hash['relationships'] = relationships unless relationships.nil? || relationships.empty?
|
121
132
|
|
133
|
+
meta = source.meta(custom_generation_options)
|
134
|
+
if meta.is_a?(Hash) && !meta.empty?
|
135
|
+
obj_hash['meta'] = meta
|
136
|
+
end
|
122
137
|
obj_hash
|
123
138
|
end
|
124
139
|
|
@@ -144,6 +159,13 @@ module JSONAPI
|
|
144
159
|
end
|
145
160
|
end
|
146
161
|
|
162
|
+
def custom_generation_options
|
163
|
+
{
|
164
|
+
serializer: self,
|
165
|
+
serialization_options: @serialization_options
|
166
|
+
}
|
167
|
+
end
|
168
|
+
|
147
169
|
def relationship_data(source, include_directives)
|
148
170
|
relationships = source.class._relationships
|
149
171
|
requested = requested_fields(source.class)
|
@@ -313,15 +335,6 @@ module JSONAPI
|
|
313
335
|
end
|
314
336
|
end
|
315
337
|
|
316
|
-
def format_key(key)
|
317
|
-
@key_formatter.format(key)
|
318
|
-
end
|
319
|
-
|
320
|
-
def format_value(value, format)
|
321
|
-
value_formatter = JSONAPI::ValueFormatter.value_formatter_for(format)
|
322
|
-
value_formatter.format(value)
|
323
|
-
end
|
324
|
-
|
325
338
|
def generate_link_builder(primary_resource_klass, options)
|
326
339
|
LinkBuilder.new(
|
327
340
|
base_url: options.fetch(:base_url, ''),
|
@@ -36,7 +36,8 @@ module JSONAPI
|
|
36
36
|
fields: @options[:fields],
|
37
37
|
base_url: @options.fetch(:base_url, ''),
|
38
38
|
key_formatter: @key_formatter,
|
39
|
-
route_formatter: @options.fetch(:route_formatter, JSONAPI.configuration.route_formatter)
|
39
|
+
route_formatter: @options.fetch(:route_formatter, JSONAPI.configuration.route_formatter),
|
40
|
+
serialization_options: @options.fetch(:serialization_options, {})
|
40
41
|
)
|
41
42
|
end
|
42
43
|
|
@@ -37,10 +37,10 @@ class PostsControllerTest < ActionController::TestCase
|
|
37
37
|
JSONAPI.configuration.exception_class_whitelist = []
|
38
38
|
|
39
39
|
@controller.class.instance_variable_set(:@callback_message, "none")
|
40
|
-
|
40
|
+
BaseController.on_server_error do
|
41
41
|
@controller.class.instance_variable_set(:@callback_message, "Sent from block")
|
42
42
|
end
|
43
|
-
|
43
|
+
|
44
44
|
get :index
|
45
45
|
assert_equal @controller.class.instance_variable_get(:@callback_message), "Sent from block"
|
46
46
|
|
@@ -54,7 +54,7 @@ class PostsControllerTest < ActionController::TestCase
|
|
54
54
|
def test_on_server_error_method_callback_with_exception
|
55
55
|
original_config = JSONAPI.configuration.dup
|
56
56
|
JSONAPI.configuration.operations_processor = :error_raising
|
57
|
-
JSONAPI.configuration.exception_class_whitelist = []
|
57
|
+
JSONAPI.configuration.exception_class_whitelist = []
|
58
58
|
|
59
59
|
#ignores methods that don't exist
|
60
60
|
@controller.class.on_server_error :set_callback_message, :a_bogus_method
|
@@ -70,7 +70,7 @@ class PostsControllerTest < ActionController::TestCase
|
|
70
70
|
end
|
71
71
|
|
72
72
|
def test_on_server_error_callback_without_exception
|
73
|
-
|
73
|
+
|
74
74
|
callback = Proc.new { @controller.class.instance_variable_set(:@callback_message, "Sent from block") }
|
75
75
|
@controller.class.on_server_error callback
|
76
76
|
@controller.class.instance_variable_set(:@callback_message, "none")
|
@@ -1222,7 +1222,7 @@ class PostsControllerTest < ActionController::TestCase
|
|
1222
1222
|
|
1223
1223
|
#check the relationship was created successfully
|
1224
1224
|
assert_equal 1, Post.find(14).special_tags.count
|
1225
|
-
before_tags = Post.find(14).tags.count
|
1225
|
+
before_tags = Post.find(14).tags.count
|
1226
1226
|
|
1227
1227
|
delete :destroy_relationship, {post_id: 14, relationship: 'special_tags', data: [{type: 'tags', id: 2}]}
|
1228
1228
|
assert_equal 0, Post.find(14).special_tags.count, "Relationship that matches URL relationship not destroyed"
|
@@ -2264,6 +2264,15 @@ class Api::V5::AuthorsControllerTest < ActionController::TestCase
|
|
2264
2264
|
assert_equal nil, json_response['data'][0]['attributes']['email']
|
2265
2265
|
end
|
2266
2266
|
|
2267
|
+
def test_show_person_as_author
|
2268
|
+
get :show, {id: '1'}
|
2269
|
+
assert_response :success
|
2270
|
+
assert_equal '1', json_response['data']['id']
|
2271
|
+
assert_equal 'authors', json_response['data']['type']
|
2272
|
+
assert_equal 'Joe Author', json_response['data']['attributes']['name']
|
2273
|
+
assert_equal nil, json_response['data']['attributes']['email']
|
2274
|
+
end
|
2275
|
+
|
2267
2276
|
def test_get_person_as_author_by_name_filter
|
2268
2277
|
get :index, {filter: {name: 'thor'}}
|
2269
2278
|
assert_response :success
|
@@ -2271,6 +2280,74 @@ class Api::V5::AuthorsControllerTest < ActionController::TestCase
|
|
2271
2280
|
assert_equal '1', json_response['data'][0]['id']
|
2272
2281
|
assert_equal 'Joe Author', json_response['data'][0]['attributes']['name']
|
2273
2282
|
end
|
2283
|
+
|
2284
|
+
def test_meta_serializer_options
|
2285
|
+
JSONAPI.configuration.json_key_format = :camelized_key
|
2286
|
+
|
2287
|
+
Api::V5::AuthorResource.class_eval do
|
2288
|
+
def meta(options)
|
2289
|
+
{
|
2290
|
+
fixed: 'Hardcoded value',
|
2291
|
+
computed: "#{self.class._type.to_s}: #{options[:serializer].url_generator.self_link(self)}",
|
2292
|
+
computed_foo: options[:serialization_options][:foo],
|
2293
|
+
options[:serializer].format_key('test_key') => 'test value'
|
2294
|
+
}
|
2295
|
+
end
|
2296
|
+
end
|
2297
|
+
|
2298
|
+
get :show, {id: '1'}
|
2299
|
+
assert_response :success
|
2300
|
+
assert_equal '1', json_response['data']['id']
|
2301
|
+
assert_equal 'Hardcoded value', json_response['data']['meta']['fixed']
|
2302
|
+
assert_equal 'authors: http://test.host/api/v5/authors/1', json_response['data']['meta']['computed']
|
2303
|
+
assert_equal 'bar', json_response['data']['meta']['computed_foo']
|
2304
|
+
assert_equal 'test value', json_response['data']['meta']['testKey']
|
2305
|
+
|
2306
|
+
ensure
|
2307
|
+
JSONAPI.configuration.json_key_format = :dasherized_key
|
2308
|
+
Api::V5::AuthorResource.class_eval do
|
2309
|
+
def meta(options)
|
2310
|
+
# :nocov:
|
2311
|
+
{ }
|
2312
|
+
# :nocov:
|
2313
|
+
end
|
2314
|
+
end
|
2315
|
+
end
|
2316
|
+
|
2317
|
+
def test_meta_serializer_hash_data
|
2318
|
+
JSONAPI.configuration.json_key_format = :camelized_key
|
2319
|
+
|
2320
|
+
Api::V5::AuthorResource.class_eval do
|
2321
|
+
def meta(options)
|
2322
|
+
{
|
2323
|
+
custom_hash: {
|
2324
|
+
fixed: 'Hardcoded value',
|
2325
|
+
computed: "#{self.class._type.to_s}: #{options[:serializer].url_generator.self_link(self)}",
|
2326
|
+
computed_foo: options[:serialization_options][:foo],
|
2327
|
+
options[:serializer].format_key('test_key') => 'test value'
|
2328
|
+
}
|
2329
|
+
}
|
2330
|
+
end
|
2331
|
+
end
|
2332
|
+
|
2333
|
+
get :show, {id: '1'}
|
2334
|
+
assert_response :success
|
2335
|
+
assert_equal '1', json_response['data']['id']
|
2336
|
+
assert_equal 'Hardcoded value', json_response['data']['meta']['custom_hash']['fixed']
|
2337
|
+
assert_equal 'authors: http://test.host/api/v5/authors/1', json_response['data']['meta']['custom_hash']['computed']
|
2338
|
+
assert_equal 'bar', json_response['data']['meta']['custom_hash']['computed_foo']
|
2339
|
+
assert_equal 'test value', json_response['data']['meta']['custom_hash']['testKey']
|
2340
|
+
|
2341
|
+
ensure
|
2342
|
+
JSONAPI.configuration.json_key_format = :dasherized_key
|
2343
|
+
Api::V5::AuthorResource.class_eval do
|
2344
|
+
def meta(options)
|
2345
|
+
# :nocov:
|
2346
|
+
{ }
|
2347
|
+
# :nocov:
|
2348
|
+
end
|
2349
|
+
end
|
2350
|
+
end
|
2274
2351
|
end
|
2275
2352
|
|
2276
2353
|
class BreedsControllerTest < ActionController::TestCase
|
@@ -3158,4 +3235,4 @@ class VehiclesControllerTest < ActionController::TestCase
|
|
3158
3235
|
}
|
3159
3236
|
end
|
3160
3237
|
end
|
3161
|
-
end
|
3238
|
+
end
|
@@ -530,8 +530,12 @@ end
|
|
530
530
|
class PeopleController < JSONAPI::ResourceController
|
531
531
|
end
|
532
532
|
|
533
|
-
class
|
533
|
+
class BaseController < ActionController::Base
|
534
534
|
include JSONAPI::ActsAsResourceController
|
535
|
+
end
|
536
|
+
|
537
|
+
class PostsController < BaseController
|
538
|
+
|
535
539
|
class SpecialError < StandardError; end
|
536
540
|
class SubSpecialError < PostsController::SpecialError; end
|
537
541
|
|
@@ -683,6 +687,9 @@ module Api
|
|
683
687
|
|
684
688
|
module V5
|
685
689
|
class AuthorsController < JSONAPI::ResourceController
|
690
|
+
def serialization_options
|
691
|
+
{foo: 'bar'}
|
692
|
+
end
|
686
693
|
end
|
687
694
|
|
688
695
|
class PostsController < JSONAPI::ResourceController
|
@@ -1242,6 +1249,15 @@ module Api
|
|
1242
1249
|
|
1243
1250
|
filter :name
|
1244
1251
|
|
1252
|
+
def self.find_by_key(key, options = {})
|
1253
|
+
context = options[:context]
|
1254
|
+
records = records(options)
|
1255
|
+
records = apply_includes(records, options)
|
1256
|
+
model = records.where({_primary_key => key}).first
|
1257
|
+
fail JSONAPI::Exceptions::RecordNotFound.new(key) if model.nil?
|
1258
|
+
self.new(model, context)
|
1259
|
+
end
|
1260
|
+
|
1245
1261
|
def self.find(filters, options = {})
|
1246
1262
|
resources = []
|
1247
1263
|
|
@@ -1369,6 +1385,20 @@ module MyEngine
|
|
1369
1385
|
end
|
1370
1386
|
end
|
1371
1387
|
|
1388
|
+
module Legacy
|
1389
|
+
class FlatPost < ActiveRecord::Base
|
1390
|
+
self.table_name = "posts"
|
1391
|
+
end
|
1392
|
+
end
|
1393
|
+
|
1394
|
+
class FlatPostResource < JSONAPI::Resource
|
1395
|
+
model_name "::Legacy::FlatPost"
|
1396
|
+
attribute :title
|
1397
|
+
end
|
1398
|
+
|
1399
|
+
class FlatPostsController < JSONAPI::ResourceController
|
1400
|
+
end
|
1401
|
+
|
1372
1402
|
### PORO Data - don't do this in a production app
|
1373
1403
|
$breed_data = BreedData.new
|
1374
1404
|
$breed_data.add(Breed.new(0, 'persian'))
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require File.expand_path('../../../test_helper', __FILE__)
|
2
|
+
|
3
|
+
class NamedspacedModelTest < ActionDispatch::IntegrationTest
|
4
|
+
def setup
|
5
|
+
JSONAPI.configuration.json_key_format = :underscored_key
|
6
|
+
end
|
7
|
+
|
8
|
+
def test_get_flat_posts
|
9
|
+
get '/flat_posts'
|
10
|
+
assert_equal 200, status
|
11
|
+
assert_equal "flat_posts", json_response["data"].first["type"]
|
12
|
+
end
|
13
|
+
end
|
data/test/test_helper.rb
CHANGED
@@ -8,6 +8,21 @@ class ArticleResource < JSONAPI::Resource
|
|
8
8
|
end
|
9
9
|
end
|
10
10
|
|
11
|
+
class PostWithBadAfterSave < ActiveRecord::Base
|
12
|
+
self.table_name = 'posts'
|
13
|
+
after_save :do_some_after_save_stuff
|
14
|
+
|
15
|
+
def do_some_after_save_stuff
|
16
|
+
errors[:base] << 'Boom! Error added in after_save callback.'
|
17
|
+
raise ActiveRecord::RecordInvalid.new(self)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class ArticleWithBadAfterSaveResource < JSONAPI::Resource
|
22
|
+
model_name 'PostWithBadAfterSave'
|
23
|
+
attribute :title
|
24
|
+
end
|
25
|
+
|
11
26
|
class NoMatchResource < JSONAPI::Resource
|
12
27
|
end
|
13
28
|
|
@@ -447,4 +462,13 @@ class ResourceTest < ActiveSupport::TestCase
|
|
447
462
|
end
|
448
463
|
assert_match "", err
|
449
464
|
end
|
465
|
+
|
466
|
+
def test_correct_error_surfaced_if_validation_errors_in_after_save_callback
|
467
|
+
post = PostWithBadAfterSave.find(1)
|
468
|
+
post_resource = ArticleWithBadAfterSaveResource.new(post, nil)
|
469
|
+
err = assert_raises JSONAPI::Exceptions::ValidationErrors do
|
470
|
+
post_resource.replace_fields({:attributes => {:title => 'Some title'}})
|
471
|
+
end
|
472
|
+
assert_equal(err.error_messages[:base], ['Boom! Error added in after_save callback.'])
|
473
|
+
end
|
450
474
|
end
|
@@ -1737,6 +1737,77 @@ class SerializerTest < ActionDispatch::IntegrationTest
|
|
1737
1737
|
)
|
1738
1738
|
end
|
1739
1739
|
|
1740
|
+
def test_serializer_resource_meta_fixed_value
|
1741
|
+
Api::V5::AuthorResource.class_eval do
|
1742
|
+
def meta(options)
|
1743
|
+
{
|
1744
|
+
fixed: 'Hardcoded value',
|
1745
|
+
computed: "#{self.class._type.to_s}: #{options[:serializer].url_generator.self_link(self)}"
|
1746
|
+
}
|
1747
|
+
end
|
1748
|
+
end
|
1749
|
+
|
1750
|
+
serialized = JSONAPI::ResourceSerializer.new(
|
1751
|
+
Api::V5::AuthorResource,
|
1752
|
+
include: ['author_detail']
|
1753
|
+
).serialize_to_hash(Api::V5::AuthorResource.new(Person.find(1), nil))
|
1754
|
+
|
1755
|
+
assert_hash_equals(
|
1756
|
+
{
|
1757
|
+
data: {
|
1758
|
+
type: 'authors',
|
1759
|
+
id: '1',
|
1760
|
+
attributes: {
|
1761
|
+
name: 'Joe Author',
|
1762
|
+
},
|
1763
|
+
links: {
|
1764
|
+
self: '/api/v5/authors/1'
|
1765
|
+
},
|
1766
|
+
relationships: {
|
1767
|
+
posts: {
|
1768
|
+
links: {
|
1769
|
+
self: '/api/v5/authors/1/relationships/posts',
|
1770
|
+
related: '/api/v5/authors/1/posts'
|
1771
|
+
}
|
1772
|
+
},
|
1773
|
+
authorDetail: {
|
1774
|
+
links: {
|
1775
|
+
self: '/api/v5/authors/1/relationships/authorDetail',
|
1776
|
+
related: '/api/v5/authors/1/authorDetail'
|
1777
|
+
},
|
1778
|
+
data: {type: 'authorDetails', id: '1'}
|
1779
|
+
}
|
1780
|
+
},
|
1781
|
+
meta: {
|
1782
|
+
fixed: 'Hardcoded value',
|
1783
|
+
computed: 'authors: /api/v5/authors/1'
|
1784
|
+
}
|
1785
|
+
},
|
1786
|
+
included: [
|
1787
|
+
{
|
1788
|
+
type: 'authorDetails',
|
1789
|
+
id: '1',
|
1790
|
+
attributes: {
|
1791
|
+
authorStuff: 'blah blah'
|
1792
|
+
},
|
1793
|
+
links: {
|
1794
|
+
self: '/api/v5/authorDetails/1'
|
1795
|
+
}
|
1796
|
+
}
|
1797
|
+
]
|
1798
|
+
},
|
1799
|
+
serialized
|
1800
|
+
)
|
1801
|
+
ensure
|
1802
|
+
Api::V5::AuthorResource.class_eval do
|
1803
|
+
def meta(options)
|
1804
|
+
# :nocov:
|
1805
|
+
{ }
|
1806
|
+
# :nocov:
|
1807
|
+
end
|
1808
|
+
end
|
1809
|
+
end
|
1810
|
+
|
1740
1811
|
def test_serialize_model_attr
|
1741
1812
|
@make = Make.first
|
1742
1813
|
serialized = JSONAPI::ResourceSerializer.new(
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jsonapi-resources
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.6.
|
4
|
+
version: 0.6.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dan Gebhardt
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2015-
|
12
|
+
date: 2015-11-18 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
@@ -192,11 +192,13 @@ files:
|
|
192
192
|
- test/helpers/functional_helpers.rb
|
193
193
|
- test/helpers/value_matchers.rb
|
194
194
|
- test/helpers/value_matchers_test.rb
|
195
|
+
- test/integration/requests/namespaced_model_test.rb
|
195
196
|
- test/integration/requests/request_test.rb
|
196
197
|
- test/integration/routes/routes_test.rb
|
197
198
|
- test/integration/sti_fields_test.rb
|
198
199
|
- test/lib/generators/jsonapi/resource_generator_test.rb
|
199
200
|
- test/test_helper.rb
|
201
|
+
- test/unit/formatters/dasherized_key_formatter_test.rb
|
200
202
|
- test/unit/jsonapi_request/jsonapi_request_test.rb
|
201
203
|
- test/unit/operation/operations_processor_test.rb
|
202
204
|
- test/unit/pagination/offset_paginator_test.rb
|
@@ -271,11 +273,13 @@ test_files:
|
|
271
273
|
- test/helpers/functional_helpers.rb
|
272
274
|
- test/helpers/value_matchers.rb
|
273
275
|
- test/helpers/value_matchers_test.rb
|
276
|
+
- test/integration/requests/namespaced_model_test.rb
|
274
277
|
- test/integration/requests/request_test.rb
|
275
278
|
- test/integration/routes/routes_test.rb
|
276
279
|
- test/integration/sti_fields_test.rb
|
277
280
|
- test/lib/generators/jsonapi/resource_generator_test.rb
|
278
281
|
- test/test_helper.rb
|
282
|
+
- test/unit/formatters/dasherized_key_formatter_test.rb
|
279
283
|
- test/unit/jsonapi_request/jsonapi_request_test.rb
|
280
284
|
- test/unit/operation/operations_processor_test.rb
|
281
285
|
- test/unit/pagination/offset_paginator_test.rb
|