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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 885923e290e5ac1db4d49ed616f8f93b978edabe
4
- data.tar.gz: f9ba1571591967f00bd09b274b2aae34b01153e0
3
+ metadata.gz: 233ad0db650881125288636b8fc022ccace40b01
4
+ data.tar.gz: 9c28dc89ef3f227398d6eecb9e4e274bcbd40d59
5
5
  SHA512:
6
- metadata.gz: 305b0ae0b9f37ec829b178769044957f12f90c0272b69339b930deabac8f8f34bf310539597eeb01adc911c3bb897fd9aa60b52b3b077c9d925e926fa9fca157
7
- data.tar.gz: d9716961f3daeb72d56ae12b37c42960f8dcd99042f8cf6d884d67081ceebfc1ae20510eef1d682fe504e726433ce443dc5007c6cd3da0a1065c1eef708948c3
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.png?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)
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
- attribute :title, :body
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
- attribute :title, :body
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
- attribute :title, :body
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
- > __Note__: This gem [uses the filter chain to set up the request](https://github.com/cerebris/jsonapi-resources/issues/458#issuecomment-143297055). In some instances, variables that are set in the filter chain (such as `current_user`) may not be set at the right time. If this happens (i.e. `current_user` is `nil` in `context` but it's set properly everywhere else), you may want to have your authentication occur earlier in the filter chain, using `prepend_before_action` instead of `before_action`.
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 do
8
- before_action :ensure_correct_media_type, only: [:create, :update, :create_relationship, :update_relationship]
9
- append_before_action :setup_request
10
- after_action :setup_response
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
- process_request_operations
13
+ process_request
15
14
  end
16
15
 
17
16
  def show
18
- process_request_operations
17
+ process_request
19
18
  end
20
19
 
21
20
  def show_relationship
22
- process_request_operations
21
+ process_request
23
22
  end
24
23
 
25
24
  def create
26
- process_request_operations
25
+ process_request
27
26
  end
28
27
 
29
28
  def create_relationship
30
- process_request_operations
29
+ process_request
31
30
  end
32
31
 
33
32
  def update_relationship
34
- process_request_operations
33
+ process_request
35
34
  end
36
35
 
37
36
  def update
38
- process_request_operations
37
+ process_request
39
38
  end
40
39
 
41
40
  def destroy
42
- process_request_operations
41
+ process_request
43
42
  end
44
43
 
45
44
  def destroy_relationship
46
- process_request_operations
45
+ process_request
47
46
  end
48
47
 
49
48
  def get_related_resource
50
- process_request_operations
49
+ process_request
51
50
  end
52
51
 
53
52
  def get_related_resources
54
- process_request_operations
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
- append_before_action { add_error_callbacks(callbacks) }
213
+ self.class_variable_set :@@server_error_callbacks, callbacks
215
214
  end
215
+
216
216
  end
217
217
  end
218
218
  end
@@ -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._model.errors.messages
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
@@ -76,7 +76,7 @@ end
76
76
  class DasherizedKeyFormatter < JSONAPI::KeyFormatter
77
77
  class << self
78
78
  def format(key)
79
- super.dasherize
79
+ super.underscore.dasherize
80
80
  end
81
81
 
82
82
  def unformat(formatted_key)
@@ -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
@@ -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
- context: @context,
84
- include_directives: @include_directives)
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
 
@@ -22,7 +22,7 @@ module JSONAPI
22
22
  @include_directives = nil
23
23
  @paginator = nil
24
24
  @id = nil
25
- @server_error_callbacks = []
25
+ @server_error_callbacks = options.fetch(:server_error_callbacks, [])
26
26
 
27
27
  setup_action(@params)
28
28
  end
@@ -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.fields
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
- fail JSONAPI::Exceptions::SaveFailed.new unless saved
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(type)
278
- resource_name = JSONAPI::Resource._resource_name_from_type(type)
279
- resource = resource_name.safe_constantize if resource_name
280
- if resource.nil?
281
- fail NameError, "JSONAPI: Could not find resource '#{type}'. (Class #{resource_name} not found)"
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 resource_for(resource_type_for(model)).new(model, context)
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
- resource_for(resource_type_for(model)).new(model, context)
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 = Resource.resource_for(self.class.resource_type_for(associated_model)) if associated_model
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 = Resource.resource_for(self.class.resource_type_for(record))
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
- # base_url: a string to prepend to generated resource links
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, ''),
@@ -1,5 +1,5 @@
1
1
  module JSONAPI
2
2
  module Resources
3
- VERSION = '0.6.1'
3
+ VERSION = '0.6.2'
4
4
  end
5
5
  end
@@ -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
- @controller.class.on_server_error do
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 PostsController < ActionController::Base
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
@@ -140,6 +140,7 @@ TestApp.routes.draw do
140
140
  jsonapi_resources :vehicles
141
141
  jsonapi_resources :cars
142
142
  jsonapi_resources :boats
143
+ jsonapi_resources :flat_posts
143
144
 
144
145
  namespace :api do
145
146
  namespace :v1 do
@@ -0,0 +1,8 @@
1
+ require File.expand_path('../../../test_helper', __FILE__)
2
+
3
+ class DasherizedKeyFormatterTest < ActiveSupport::TestCase
4
+ def test_dasherize_camelize
5
+ formatted = DasherizedKeyFormatter.format("CarWash")
6
+ assert_equal formatted, "car-wash"
7
+ end
8
+ end
@@ -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.1
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-10-21 00:00:00.000000000 Z
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