jsonapi-resources 0.7.0 → 0.7.1.beta1
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 +196 -190
- data/lib/generators/jsonapi/USAGE +6 -1
- data/lib/generators/jsonapi/controller_generator.rb +14 -0
- data/lib/generators/jsonapi/templates/jsonapi_controller.rb +4 -0
- data/lib/jsonapi/active_record_operations_processor.rb +4 -3
- data/lib/jsonapi/acts_as_resource_controller.rb +7 -3
- data/lib/jsonapi/error_codes.rb +26 -26
- data/lib/jsonapi/exceptions.rb +124 -53
- data/lib/jsonapi/relationship.rb +8 -0
- data/lib/jsonapi/request.rb +4 -6
- data/lib/jsonapi/resource.rb +37 -13
- data/lib/jsonapi/resource_controller.rb +14 -2
- data/lib/jsonapi/resource_serializer.rb +2 -8
- data/lib/jsonapi/resources/version.rb +1 -1
- data/lib/jsonapi/routing_ext.rb +1 -1
- data/locales/en.yml +80 -0
- data/test/controllers/controller_test.rb +35 -5
- data/test/fixtures/active_record.rb +11 -8
- data/test/fixtures/comments.yml +1 -1
- data/test/fixtures/preferences.yml +0 -4
- data/test/lib/generators/jsonapi/controller_generator_test.rb +25 -0
- data/test/test_helper.rb +3 -0
- data/test/unit/operation/operations_processor_test.rb +3 -3
- data/test/unit/resource/resource_test.rb +20 -0
- data/test/unit/serializer/serializer_test.rb +0 -6
- metadata +10 -5
data/lib/jsonapi/relationship.rb
CHANGED
@@ -25,6 +25,10 @@ module JSONAPI
|
|
25
25
|
@resource_klass = @parent_resource.resource_for(@class_name)
|
26
26
|
end
|
27
27
|
|
28
|
+
def table_name
|
29
|
+
@table_name ||= resource_klass._table_name
|
30
|
+
end
|
31
|
+
|
28
32
|
def relation_name(options)
|
29
33
|
case @relation_name
|
30
34
|
when Symbol
|
@@ -47,6 +51,10 @@ module JSONAPI
|
|
47
51
|
end
|
48
52
|
end
|
49
53
|
|
54
|
+
def belongs_to?
|
55
|
+
false
|
56
|
+
end
|
57
|
+
|
50
58
|
class ToOne < Relationship
|
51
59
|
attr_reader :foreign_key_on
|
52
60
|
|
data/lib/jsonapi/request.rb
CHANGED
@@ -32,11 +32,9 @@ module JSONAPI
|
|
32
32
|
|
33
33
|
@resource_klass ||= Resource.resource_for(params[:controller]) if params[:controller]
|
34
34
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
send(setup_action_method_name, params)
|
39
|
-
end
|
35
|
+
setup_action_method_name = "setup_#{params[:action]}_action"
|
36
|
+
if respond_to?(setup_action_method_name)
|
37
|
+
send(setup_action_method_name, params)
|
40
38
|
end
|
41
39
|
rescue ActionController::ParameterMissing => e
|
42
40
|
@errors.concat(JSONAPI::Exceptions::ParameterMissing.new(e.param).errors)
|
@@ -516,7 +514,7 @@ module JSONAPI
|
|
516
514
|
params.each do |key, value|
|
517
515
|
case key.to_s
|
518
516
|
when 'relationships'
|
519
|
-
value.
|
517
|
+
value.keys.each do |links_key|
|
520
518
|
unless formatted_allowed_fields.include?(links_key.to_sym)
|
521
519
|
params_not_allowed.push(links_key)
|
522
520
|
unless JSONAPI.configuration.raise_if_parameters_not_allowed
|
data/lib/jsonapi/resource.rb
CHANGED
@@ -105,8 +105,9 @@ module JSONAPI
|
|
105
105
|
end
|
106
106
|
end
|
107
107
|
|
108
|
+
# Override this on a resource instance to override the fetchable keys
|
108
109
|
def fetchable_fields
|
109
|
-
self.class.
|
110
|
+
self.class.fields
|
110
111
|
end
|
111
112
|
|
112
113
|
# Override this on a resource to customize how the associated records
|
@@ -119,6 +120,29 @@ module JSONAPI
|
|
119
120
|
_model.errors.messages
|
120
121
|
end
|
121
122
|
|
123
|
+
# Add metadata to validation error objects.
|
124
|
+
#
|
125
|
+
# Suppose `model_error_messages` returned the following error messages
|
126
|
+
# hash:
|
127
|
+
#
|
128
|
+
# {password: ["too_short", "format"]}
|
129
|
+
#
|
130
|
+
# Then to add data to the validation error `validation_error_metadata`
|
131
|
+
# could return:
|
132
|
+
#
|
133
|
+
# {
|
134
|
+
# password: {
|
135
|
+
# "too_short": {"minimum_length" => 6},
|
136
|
+
# "format": {"requirement" => "must contain letters and numbers"}
|
137
|
+
# }
|
138
|
+
# }
|
139
|
+
#
|
140
|
+
# The specified metadata is then be merged into the validation error
|
141
|
+
# object.
|
142
|
+
def validation_error_metadata
|
143
|
+
{}
|
144
|
+
end
|
145
|
+
|
122
146
|
# Override this to return resource level meta data
|
123
147
|
# must return a hash, and if the hash is empty the meta section will not be serialized with the resource
|
124
148
|
# meta keys will be not be formatted with the key formatter for the serializer by default. They can however use the
|
@@ -173,8 +197,9 @@ module JSONAPI
|
|
173
197
|
end
|
174
198
|
|
175
199
|
def _remove
|
176
|
-
@model.destroy
|
177
|
-
|
200
|
+
unless @model.destroy
|
201
|
+
fail JSONAPI::Exceptions::ValidationErrors.new(self)
|
202
|
+
end
|
178
203
|
:completed
|
179
204
|
end
|
180
205
|
|
@@ -440,11 +465,6 @@ module JSONAPI
|
|
440
465
|
end
|
441
466
|
# :nocov:
|
442
467
|
|
443
|
-
# Override in your resource to filter the fetchable keys
|
444
|
-
def fetchable_fields(_context = nil)
|
445
|
-
fields
|
446
|
-
end
|
447
|
-
|
448
468
|
# Override in your resource to filter the updatable keys
|
449
469
|
def updatable_fields(_context = nil)
|
450
470
|
_updatable_relationships | _attributes.keys - [:id]
|
@@ -523,11 +543,11 @@ module JSONAPI
|
|
523
543
|
if filters
|
524
544
|
filters.each do |filter, value|
|
525
545
|
if _relationships.include?(filter)
|
526
|
-
if _relationships[filter].
|
527
|
-
|
528
|
-
records = apply_filter(records, "#{filter}.#{_relationships[filter].primary_key}", value, options)
|
546
|
+
if _relationships[filter].belongs_to?
|
547
|
+
records = apply_filter(records, _relationships[filter].foreign_key, value, options)
|
529
548
|
else
|
530
|
-
|
549
|
+
required_includes.push(filter.to_s)
|
550
|
+
records = apply_filter(records, "#{_relationships[filter].table_name}.#{_relationships[filter].primary_key}", value, options)
|
531
551
|
end
|
532
552
|
else
|
533
553
|
records = apply_filter(records, filter, value, options)
|
@@ -587,7 +607,7 @@ module JSONAPI
|
|
587
607
|
# Override this method if you want to customize the relation for
|
588
608
|
# finder methods (find, find_by_key)
|
589
609
|
def records(_options = {})
|
590
|
-
_model_class
|
610
|
+
_model_class.all
|
591
611
|
end
|
592
612
|
|
593
613
|
def verify_filters(filters, context = nil)
|
@@ -696,6 +716,10 @@ module JSONAPI
|
|
696
716
|
@_primary_key ||= _model_class.respond_to?(:primary_key) ? _model_class.primary_key : :id
|
697
717
|
end
|
698
718
|
|
719
|
+
def _table_name
|
720
|
+
@_table_name ||= _model_class.respond_to?(:table_name) ? _model_class.table_name : _model_name.tableize
|
721
|
+
end
|
722
|
+
|
699
723
|
def _as_parent_key
|
700
724
|
@_as_parent_key ||= "#{_type.to_s.singularize}_id"
|
701
725
|
end
|
@@ -1,5 +1,17 @@
|
|
1
1
|
module JSONAPI
|
2
|
-
class ResourceController < ActionController::
|
3
|
-
|
2
|
+
class ResourceController < ActionController::Metal
|
3
|
+
MODULES = [
|
4
|
+
AbstractController::Rendering,
|
5
|
+
ActionController::Rendering,
|
6
|
+
ActionController::Renderers::All,
|
7
|
+
ActionController::StrongParameters,
|
8
|
+
ActionController::ForceSSL,
|
9
|
+
ActionController::Instrumentation,
|
10
|
+
JSONAPI::ActsAsResourceController
|
11
|
+
].freeze
|
12
|
+
|
13
|
+
MODULES.each do |mod|
|
14
|
+
include mod
|
15
|
+
end
|
4
16
|
end
|
5
17
|
end
|
@@ -93,14 +93,7 @@ module JSONAPI
|
|
93
93
|
# The fields options controls both fields and included links references.
|
94
94
|
def process_primary(source, include_directives)
|
95
95
|
if source.respond_to?(:to_ary)
|
96
|
-
source.each
|
97
|
-
id = resource.id
|
98
|
-
if already_serialized?(resource.class._type, id)
|
99
|
-
set_primary(@primary_class_name, id)
|
100
|
-
end
|
101
|
-
|
102
|
-
add_included_object(id, object_hash(resource, include_directives), true)
|
103
|
-
end
|
96
|
+
source.each { |resource| process_primary(resource, include_directives) }
|
104
97
|
else
|
105
98
|
return {} if source.nil?
|
106
99
|
|
@@ -278,6 +271,7 @@ module JSONAPI
|
|
278
271
|
end
|
279
272
|
|
280
273
|
def link_object_to_many(source, relationship, include_linkage)
|
274
|
+
include_linkage = include_linkage | relationship.always_include_linkage_data
|
281
275
|
link_object_hash = {}
|
282
276
|
link_object_hash[:links] = {}
|
283
277
|
link_object_hash[:links][:self] = self_link(source, relationship)
|
data/lib/jsonapi/routing_ext.rb
CHANGED
@@ -69,7 +69,7 @@ module ActionDispatch
|
|
69
69
|
options[:path] = format_route(@resource_type)
|
70
70
|
|
71
71
|
if res.resource_key_type == :uuid
|
72
|
-
options[:constraints] = {id: /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}
|
72
|
+
options[:constraints] = {id: /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/}
|
73
73
|
end
|
74
74
|
|
75
75
|
if options[:except]
|
data/locales/en.yml
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
en:
|
2
|
+
jsonapi-resources:
|
3
|
+
exceptions:
|
4
|
+
internal_server_error:
|
5
|
+
title: 'Internal Server Error'
|
6
|
+
detail: 'Internal Server Error'
|
7
|
+
invalid_resource:
|
8
|
+
title: 'Invalid resource'
|
9
|
+
detail: "%{resource} is not a valid resource."
|
10
|
+
record_not_found:
|
11
|
+
title: 'Record not found'
|
12
|
+
detail: "The record identified by %{id} could not be found."
|
13
|
+
unsupported_media_type:
|
14
|
+
title: 'Unsupported media type'
|
15
|
+
detail: "All requests that create or update must use the '%{needed_media_type}' Content-Type. This request specified '%{media_type}.'"
|
16
|
+
has_many_relation:
|
17
|
+
title: 'Relation exists'
|
18
|
+
detail: "The relation to %{id} already exists."
|
19
|
+
to_many_set_replacement_forbidden:
|
20
|
+
title: 'Complete replacement forbidden'
|
21
|
+
detail: 'Complete replacement forbidden for this relationship'
|
22
|
+
invalid_filter_syntax:
|
23
|
+
title: 'Invalid filters syntax'
|
24
|
+
detail: "%{filters} is not a valid syntax for filtering."
|
25
|
+
filter_not_allowed:
|
26
|
+
title: "Filter not allowed"
|
27
|
+
detail: "%{filter} is not allowed."
|
28
|
+
invalid_filter_value:
|
29
|
+
title: 'Invalid filter value'
|
30
|
+
detail: "%{value} is not a valid value for %{filter}."
|
31
|
+
invalid_field_value:
|
32
|
+
title: 'Invalid field value'
|
33
|
+
detail: "%{value} is not a valid value for %{field}."
|
34
|
+
invalid_field_format:
|
35
|
+
title: 'Invalid field format'
|
36
|
+
detail: 'Fields must specify a type.'
|
37
|
+
invalid_links_object:
|
38
|
+
title: 'Invalid Links Object'
|
39
|
+
detail: 'Data is not a valid Links Object.'
|
40
|
+
type_mismatch:
|
41
|
+
title: 'Type Mismatch'
|
42
|
+
detail: "%{type} is not a valid type for this operation."
|
43
|
+
invalid_field:
|
44
|
+
title: 'Invalid field'
|
45
|
+
detail: "%{field} is not a valid field for %{type}."
|
46
|
+
invalid_include:
|
47
|
+
title: 'Invalid field'
|
48
|
+
detail: "%{relationship} is not a valid relationship of %{resource}"
|
49
|
+
invalid_sort_criteria:
|
50
|
+
title: 'Invalid sort criteria'
|
51
|
+
detail: "%{sort_criteria} is not a valid sort criteria for %{resource}"
|
52
|
+
parameters_not_allowed:
|
53
|
+
title: 'Param not allowed'
|
54
|
+
detail: "%{param} is not allowed."
|
55
|
+
parameter_missing:
|
56
|
+
title: 'Missing Parameter'
|
57
|
+
detail: "The required parameter, %{param}, is missing."
|
58
|
+
count_mismatch:
|
59
|
+
title: 'Count to key mismatch'
|
60
|
+
detail: 'The resource collection does not contain the same number of objects as the number of keys.'
|
61
|
+
key_not_included_in_url:
|
62
|
+
title: 'Key is not included in URL'
|
63
|
+
detail: "The URL does not support the key %{key}"
|
64
|
+
missing_key:
|
65
|
+
title: 'A key is required'
|
66
|
+
detail: 'The resource object does not contain a key.'
|
67
|
+
record_locked:
|
68
|
+
title: 'Locked resource'
|
69
|
+
save_failed:
|
70
|
+
title: 'Save failed or was cancelled'
|
71
|
+
detail: 'Save failed or was cancelled'
|
72
|
+
invalid_page_object:
|
73
|
+
title: 'Invalid Page Object'
|
74
|
+
detail: 'Invalid Page Object.'
|
75
|
+
page_parameters_not_allowed:
|
76
|
+
title: 'Page parameter not allowed'
|
77
|
+
detail: "%{param} is not an allowed page parameter."
|
78
|
+
invalid_page_value:
|
79
|
+
title: 'Invalid page value'
|
80
|
+
detail: "%{value} is not a valid value for %{page} page parameter."
|
@@ -485,7 +485,7 @@ class PostsControllerTest < ActionController::TestCase
|
|
485
485
|
assert_equal 1, json_response['meta']["warnings"].count
|
486
486
|
assert_equal "Param not allowed", json_response['meta']["warnings"][0]["title"]
|
487
487
|
assert_equal "asdfg is not allowed.", json_response['meta']["warnings"][0]["detail"]
|
488
|
-
assert_equal 105, json_response['meta']["warnings"][0]["code"]
|
488
|
+
assert_equal '105', json_response['meta']["warnings"][0]["code"]
|
489
489
|
ensure
|
490
490
|
JSONAPI.configuration.raise_if_parameters_not_allowed = true
|
491
491
|
end
|
@@ -695,7 +695,7 @@ class PostsControllerTest < ActionController::TestCase
|
|
695
695
|
assert_equal 1, json_response['meta']["warnings"].count
|
696
696
|
assert_equal "Param not allowed", json_response['meta']["warnings"][0]["title"]
|
697
697
|
assert_equal "subject is not allowed.", json_response['meta']["warnings"][0]["detail"]
|
698
|
-
assert_equal 105, json_response['meta']["warnings"][0]["code"]
|
698
|
+
assert_equal '105', json_response['meta']["warnings"][0]["code"]
|
699
699
|
ensure
|
700
700
|
JSONAPI.configuration.raise_if_parameters_not_allowed = true
|
701
701
|
end
|
@@ -866,7 +866,7 @@ class PostsControllerTest < ActionController::TestCase
|
|
866
866
|
assert_equal 1, json_response['meta']["warnings"].count
|
867
867
|
assert_equal "Param not allowed", json_response['meta']["warnings"][0]["title"]
|
868
868
|
assert_equal "subject is not allowed.", json_response['meta']["warnings"][0]["detail"]
|
869
|
-
assert_equal 105, json_response['meta']["warnings"][0]["code"]
|
869
|
+
assert_equal '105', json_response['meta']["warnings"][0]["code"]
|
870
870
|
ensure
|
871
871
|
JSONAPI.configuration.raise_if_parameters_not_allowed = true
|
872
872
|
end
|
@@ -1653,6 +1653,14 @@ class PostsControllerTest < ActionController::TestCase
|
|
1653
1653
|
assert_response :bad_request
|
1654
1654
|
end
|
1655
1655
|
|
1656
|
+
def test_delete_with_validation_error
|
1657
|
+
post = Post.create!(title: "can't destroy me", author: Person.first)
|
1658
|
+
delete :destroy, { id: post.id }
|
1659
|
+
|
1660
|
+
assert_equal "can't destroy me", json_response['errors'][0]['title']
|
1661
|
+
assert_response :unprocessable_entity
|
1662
|
+
end
|
1663
|
+
|
1656
1664
|
def test_delete_single
|
1657
1665
|
initial_count = Post.count
|
1658
1666
|
delete :destroy, {id: '4'}
|
@@ -3273,10 +3281,32 @@ class Api::V7::CustomersControllerTest < ActionController::TestCase
|
|
3273
3281
|
end
|
3274
3282
|
|
3275
3283
|
class Api::V7::CategoriesControllerTest < ActionController::TestCase
|
3276
|
-
def
|
3284
|
+
def test_uncaught_error_in_controller_translated_to_internal_server_error
|
3277
3285
|
|
3278
3286
|
get :show, {id: '1'}
|
3279
3287
|
assert_response 500
|
3280
3288
|
assert_match /Internal Server Error/, json_response['errors'][0]['detail']
|
3281
3289
|
end
|
3282
|
-
|
3290
|
+
|
3291
|
+
def test_not_whitelisted_error_in_controller
|
3292
|
+
original_config = JSONAPI.configuration.dup
|
3293
|
+
JSONAPI.configuration.operations_processor = :error_raising
|
3294
|
+
JSONAPI.configuration.exception_class_whitelist = []
|
3295
|
+
get :show, {id: '1'}
|
3296
|
+
assert_response 500
|
3297
|
+
assert_match /Internal Server Error/, json_response['errors'][0]['detail']
|
3298
|
+
ensure
|
3299
|
+
JSONAPI.configuration = original_config
|
3300
|
+
end
|
3301
|
+
|
3302
|
+
def test_whitelisted_error_in_controller
|
3303
|
+
original_config = JSONAPI.configuration.dup
|
3304
|
+
JSONAPI.configuration.operations_processor = :error_raising
|
3305
|
+
JSONAPI.configuration.exception_class_whitelist = [PostsController::SubSpecialError]
|
3306
|
+
assert_raises PostsController::SubSpecialError do
|
3307
|
+
get :show, {id: '1'}
|
3308
|
+
end
|
3309
|
+
ensure
|
3310
|
+
JSONAPI.configuration = original_config
|
3311
|
+
end
|
3312
|
+
end
|
@@ -282,6 +282,15 @@ class Post < ActiveRecord::Base
|
|
282
282
|
|
283
283
|
validates :author, presence: true
|
284
284
|
validates :title, length: { maximum: 35 }
|
285
|
+
|
286
|
+
before_destroy :destroy_callback
|
287
|
+
|
288
|
+
def destroy_callback
|
289
|
+
if title == "can't destroy me"
|
290
|
+
errors.add(:title, "can't destroy me")
|
291
|
+
return false
|
292
|
+
end
|
293
|
+
end
|
285
294
|
end
|
286
295
|
|
287
296
|
class SpecialPostTag < ActiveRecord::Base
|
@@ -368,8 +377,7 @@ class Crater < ActiveRecord::Base
|
|
368
377
|
end
|
369
378
|
|
370
379
|
class Preferences < ActiveRecord::Base
|
371
|
-
has_one :author, class_name: 'Person'
|
372
|
-
has_many :friends, class_name: 'Person'
|
380
|
+
has_one :author, class_name: 'Person', :inverse_of => 'preferences'
|
373
381
|
end
|
374
382
|
|
375
383
|
class Fact < ActiveRecord::Base
|
@@ -979,9 +987,6 @@ class EmployeeResource < JSONAPI::Resource
|
|
979
987
|
model_name 'Person'
|
980
988
|
end
|
981
989
|
|
982
|
-
class FriendResource < JSONAPI::Resource
|
983
|
-
end
|
984
|
-
|
985
990
|
class BreedResource < JSONAPI::Resource
|
986
991
|
attribute :name, format: :title
|
987
992
|
|
@@ -1053,8 +1058,7 @@ end
|
|
1053
1058
|
class PreferencesResource < JSONAPI::Resource
|
1054
1059
|
attribute :advanced_mode
|
1055
1060
|
|
1056
|
-
has_one :author,
|
1057
|
-
has_many :friends
|
1061
|
+
has_one :author, :foreign_key_on => :related
|
1058
1062
|
|
1059
1063
|
def self.find_by_key(key, options = {})
|
1060
1064
|
new(Preferences.first, nil)
|
@@ -1164,7 +1168,6 @@ module Api
|
|
1164
1168
|
class CraterResource < CraterResource; end
|
1165
1169
|
class PreferencesResource < PreferencesResource; end
|
1166
1170
|
class EmployeeResource < EmployeeResource; end
|
1167
|
-
class FriendResource < FriendResource; end
|
1168
1171
|
class HairCutResource < HairCutResource; end
|
1169
1172
|
class VehicleResource < VehicleResource; end
|
1170
1173
|
class CarResource < CarResource; end
|
data/test/fixtures/comments.yml
CHANGED
@@ -0,0 +1,25 @@
|
|
1
|
+
require File.expand_path('../../../../test_helper', __FILE__)
|
2
|
+
require 'generators/jsonapi/controller_generator'
|
3
|
+
|
4
|
+
module Jsonapi
|
5
|
+
class ControllerGeneratorTest < Rails::Generators::TestCase
|
6
|
+
tests ControllerGenerator
|
7
|
+
destination Rails.root.join('../controllers')
|
8
|
+
setup :prepare_destination
|
9
|
+
teardown :cleanup_destination_root
|
10
|
+
|
11
|
+
def cleanup_destination_root
|
12
|
+
FileUtils.rm_rf destination_root
|
13
|
+
end
|
14
|
+
|
15
|
+
test "controller is created" do
|
16
|
+
run_generator ["post"]
|
17
|
+
assert_file 'app/controllers/posts_controller.rb', /class PostsController < JSONAPI::ResourceController/
|
18
|
+
end
|
19
|
+
|
20
|
+
test "controller is created with namespace" do
|
21
|
+
run_generator ["api/v1/post"]
|
22
|
+
assert_file 'app/controllers/api/v1/posts_controller.rb', /class Api::V1::PostsController < JSONAPI::ResourceController/
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|