jsonapi-resources 0.6.1 → 0.6.2
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 +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 [](http://travis-ci.org/cerebris/jsonapi-resources) [](https://codeclimate.com/github/cerebris/jsonapi-resources)
|
2
2
|
|
3
3
|
[](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
|