jsonapi-resources 0.7.0 → 0.7.1.beta1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|