fun_with_json_api 0.0.5 → 0.0.6.pre.alpha.1
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/config/locales/fun_with_json_api.en.yml +3 -0
- data/lib/fun_with_json_api/attributes/relationship.rb +11 -3
- data/lib/fun_with_json_api/attributes/relationship_collection.rb +21 -6
- data/lib/fun_with_json_api/deserializer.rb +12 -1
- data/lib/fun_with_json_api/deserializer_class_methods.rb +27 -17
- data/lib/fun_with_json_api/exceptions/unauthorized_resource.rb +16 -0
- data/lib/fun_with_json_api/find_collection_from_document.rb +9 -33
- data/lib/fun_with_json_api/find_resource_from_document.rb +10 -3
- data/lib/fun_with_json_api/schema_validators/check_collection_has_all_members.rb +67 -0
- data/lib/fun_with_json_api/schema_validators/check_collection_is_authorized.rb +63 -0
- data/lib/fun_with_json_api/schema_validators/check_relationships.rb +2 -2
- data/lib/fun_with_json_api/schema_validators/check_resource_is_authorized.rb +50 -0
- data/lib/fun_with_json_api/version.rb +1 -1
- data/spec/dummy/log/test.log +34269 -0
- data/spec/fun_with_json_api/deserializer_spec.rb +52 -10
- data/spec/fun_with_json_api/find_collection_from_document_spec.rb +82 -4
- data/spec/fun_with_json_api/find_resource_from_document_spec.rb +36 -2
- data/spec/fun_with_json_api/schema_validators/check_relationships_spec.rb +2 -2
- data/spec/fun_with_json_api_spec.rb +68 -0
- metadata +8 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: aa43a36433dec9d40e3a4afaad17becacfad028f
|
4
|
+
data.tar.gz: 468d6bda13144854fa1771a0e113e3d906a223f2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 028179ff069e5a7130e78ca19208113d29b3c93fff1c437e56cf1355e76f53efd465800716502a1c5f1f580a51c4556f45855ed3d0925649d1436270043138c5
|
7
|
+
data.tar.gz: 5f17c1140f3ce92040c395f5f0f303b9ab5eeaac98c1b4bca61f1295aeffb6986f4cf5292b593cadaa8adbaa17d43985e95b353e9b82e4b376ff7151b790f5bf
|
@@ -7,6 +7,7 @@ en:
|
|
7
7
|
illegal_client_generated_identifier: 'Request json_api attempted to set an unsupported client-generated id'
|
8
8
|
invalid_document_type: 'Request json_api data type does not match endpoint'
|
9
9
|
missing_resource: 'Unable to find the requested resource'
|
10
|
+
unauthorized_resource: 'Unable to access the requested resource'
|
10
11
|
invalid_attribute: 'Request json_api attribute data is invalid'
|
11
12
|
unknown_attribute: 'Request json_api attribute is unsupported by the current endpoint'
|
12
13
|
invalid_relationship: 'Request json_api relationship data is invalid'
|
@@ -34,7 +35,9 @@ en:
|
|
34
35
|
invalid_document: "Expected data to be a Hash or null"
|
35
36
|
invalid_document_type: "Expected data type to be a '%{resource}' resource"
|
36
37
|
missing_resource: "Unable to find '%{resource}' with matching id: '%{resource_id}'"
|
38
|
+
unauthorized_resource: "Unable to assign the requested '%{resource}' (%{resource_id}) to the current resource"
|
37
39
|
find_collection_from_document:
|
38
40
|
invalid_document: "Expected data to be a Array of '%{resource}' resources"
|
39
41
|
invalid_document_type: "Expected '%{type}' to be a '%{resource}' resource"
|
40
42
|
missing_resource: "Unable to find '%{resource}' with matching id: '%{resource_id}'"
|
43
|
+
unauthorized_resource: "Unable to assign the requested '%{resource}' (%{resource_id}) to the current resource"
|
@@ -29,9 +29,11 @@ module FunWithJsonApi
|
|
29
29
|
end
|
30
30
|
|
31
31
|
resource = deserializer.load_resource_from_id_value(id_value)
|
32
|
-
|
32
|
+
raise build_missing_relationship_error(id_value) if resource.nil?
|
33
33
|
|
34
|
-
|
34
|
+
check_resource_is_authorized!(resource, id_value)
|
35
|
+
|
36
|
+
resource.id
|
35
37
|
end
|
36
38
|
|
37
39
|
# rubocop:disable Style/PredicateName
|
@@ -52,6 +54,12 @@ module FunWithJsonApi
|
|
52
54
|
|
53
55
|
private
|
54
56
|
|
57
|
+
def check_resource_is_authorized!(resource, id_value)
|
58
|
+
SchemaValidators::CheckResourceIsAuthorised.call(
|
59
|
+
resource, id_value, deserializer, prefix: "/data/relationships/#{name}/data"
|
60
|
+
)
|
61
|
+
end
|
62
|
+
|
55
63
|
def build_deserializer_from_options
|
56
64
|
if @deserializer_class.respond_to?(:call)
|
57
65
|
@deserializer_class.call
|
@@ -72,7 +80,7 @@ module FunWithJsonApi
|
|
72
80
|
def build_missing_relationship_error(id_value, message = nil)
|
73
81
|
message ||= missing_resource_debug_message(id_value)
|
74
82
|
payload = ExceptionPayload.new
|
75
|
-
payload.pointer = "/data/relationships/#{name}/id"
|
83
|
+
payload.pointer = "/data/relationships/#{name}/data/id"
|
76
84
|
payload.detail = "Unable to find '#{deserializer.type}' with matching id"\
|
77
85
|
": #{id_value.inspect}"
|
78
86
|
Exceptions::MissingRelationship.new(message, payload)
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'fun_with_json_api/schema_validators/check_collection_is_authorized'
|
2
|
+
|
1
3
|
module FunWithJsonApi
|
2
4
|
module Attributes
|
3
5
|
class RelationshipCollection < FunWithJsonApi::Attribute
|
@@ -29,11 +31,10 @@ module FunWithJsonApi
|
|
29
31
|
collection = deserializer.load_collection_from_id_values(values)
|
30
32
|
|
31
33
|
# Ensure the collection size matches
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
end
|
34
|
+
check_collection_matches_values!(collection, values)
|
35
|
+
|
36
|
+
# Ensure the user is authorized to access the collection
|
37
|
+
check_collection_is_authorized!(collection, values)
|
37
38
|
|
38
39
|
# Call ActiceRecord#pluck if it is available
|
39
40
|
convert_collection_to_ids(collection)
|
@@ -72,6 +73,20 @@ module FunWithJsonApi
|
|
72
73
|
end
|
73
74
|
end
|
74
75
|
|
76
|
+
def check_collection_matches_values!(collection, values)
|
77
|
+
expected_size = values.size
|
78
|
+
result_size = collection.size
|
79
|
+
if result_size != expected_size
|
80
|
+
raise build_missing_relationship_error_from_collection(collection, values)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def check_collection_is_authorized!(collection, values)
|
85
|
+
SchemaValidators::CheckCollectionIsAuthorised.call(
|
86
|
+
collection, values, deserializer, prefix: "/data/relationships/#{name}/data"
|
87
|
+
)
|
88
|
+
end
|
89
|
+
|
75
90
|
def convert_collection_to_ids(collection)
|
76
91
|
if collection.respond_to? :pluck
|
77
92
|
# Well... pluck+arel doesn't work with SQLite, but select at least is safe
|
@@ -105,7 +120,7 @@ module FunWithJsonApi
|
|
105
120
|
next if collection_ids.include?(resource_id)
|
106
121
|
|
107
122
|
ExceptionPayload.new.tap do |payload|
|
108
|
-
payload.pointer = "/data/relationships/#{name}/#{index}/id"
|
123
|
+
payload.pointer = "/data/relationships/#{name}/data/#{index}/id"
|
109
124
|
payload.detail = "Unable to find '#{deserializer.type}' with matching id"\
|
110
125
|
": \"#{resource_id}\""
|
111
126
|
end
|
@@ -6,6 +6,13 @@ module FunWithJsonApi
|
|
6
6
|
class Deserializer
|
7
7
|
extend FunWithJsonApi::DeserializerClassMethods
|
8
8
|
|
9
|
+
# Fake resource_authorizer that always returns 'authorised'
|
10
|
+
class ResourceAuthorizerDummy
|
11
|
+
def call(_)
|
12
|
+
true
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
9
16
|
# Creates a new instance of a
|
10
17
|
def self.create(options = {})
|
11
18
|
new(options)
|
@@ -16,7 +23,6 @@ module FunWithJsonApi
|
|
16
23
|
|
17
24
|
attr_reader :id_param
|
18
25
|
attr_reader :type
|
19
|
-
attr_reader :resource_class
|
20
26
|
|
21
27
|
attr_reader :attributes
|
22
28
|
|
@@ -25,6 +31,7 @@ module FunWithJsonApi
|
|
25
31
|
@type = options.fetch(:type) { self.class.type }
|
26
32
|
@resource_class = options[:resource_class]
|
27
33
|
@resource_collection = options[:resource_collection] if @type
|
34
|
+
@resource_authorizer = options[:resource_authorizer]
|
28
35
|
load_attributes_from_options(options)
|
29
36
|
load_relationships_from_options(options)
|
30
37
|
end
|
@@ -59,6 +66,10 @@ module FunWithJsonApi
|
|
59
66
|
@resource_collection ||= resource_class
|
60
67
|
end
|
61
68
|
|
69
|
+
def resource_authorizer
|
70
|
+
@resource_authorizer ||= ResourceAuthorizerDummy.new
|
71
|
+
end
|
72
|
+
|
62
73
|
def relationships
|
63
74
|
relationship_lookup.values
|
64
75
|
end
|
@@ -25,9 +25,11 @@ module FunWithJsonApi
|
|
25
25
|
# Attributes
|
26
26
|
|
27
27
|
def attribute(name, options = {})
|
28
|
-
|
29
|
-
|
30
|
-
|
28
|
+
lock.synchronize do
|
29
|
+
Attribute.create(name, options).tap do |attribute|
|
30
|
+
add_parse_attribute_method(attribute)
|
31
|
+
attributes << attribute
|
32
|
+
end
|
31
33
|
end
|
32
34
|
end
|
33
35
|
|
@@ -38,26 +40,30 @@ module FunWithJsonApi
|
|
38
40
|
# Relationships
|
39
41
|
|
40
42
|
def belongs_to(name, deserializer_class_or_callable, options = {})
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
43
|
+
lock.synchronize do
|
44
|
+
Attributes::Relationship.create(
|
45
|
+
name,
|
46
|
+
deserializer_class_or_callable,
|
47
|
+
options
|
48
|
+
).tap do |relationship|
|
49
|
+
add_parse_resource_method(relationship)
|
50
|
+
relationships << relationship
|
51
|
+
end
|
48
52
|
end
|
49
53
|
end
|
50
54
|
|
51
55
|
# rubocop:disable Style/PredicateName
|
52
56
|
|
53
57
|
def has_many(name, deserializer_class_or_callable, options = {})
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
58
|
+
lock.synchronize do
|
59
|
+
Attributes::RelationshipCollection.create(
|
60
|
+
name,
|
61
|
+
deserializer_class_or_callable,
|
62
|
+
options
|
63
|
+
).tap do |relationship|
|
64
|
+
add_parse_resource_method(relationship)
|
65
|
+
relationships << relationship
|
66
|
+
end
|
61
67
|
end
|
62
68
|
end
|
63
69
|
|
@@ -80,6 +86,10 @@ module FunWithJsonApi
|
|
80
86
|
|
81
87
|
private
|
82
88
|
|
89
|
+
def lock
|
90
|
+
@lock ||= Mutex.new
|
91
|
+
end
|
92
|
+
|
83
93
|
def relationships
|
84
94
|
@relationships ||= []
|
85
95
|
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module FunWithJsonApi
|
2
|
+
module Exceptions
|
3
|
+
# Indicates a Resource or Collection item not authorized
|
4
|
+
class UnauthorisedResource < FunWithJsonApi::Exception
|
5
|
+
def initialize(message, payload = ExceptionPayload.new)
|
6
|
+
payload = Array.wrap(payload).each do |unauthorized|
|
7
|
+
unauthorized.code ||= 'unauthorized_resource'
|
8
|
+
unauthorized.title ||=
|
9
|
+
I18n.t('unauthorized_resource', scope: 'fun_with_json_api.exceptions')
|
10
|
+
unauthorized.status ||= '403'
|
11
|
+
end
|
12
|
+
super
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -1,3 +1,6 @@
|
|
1
|
+
require 'fun_with_json_api/schema_validators/check_collection_is_authorized'
|
2
|
+
require 'fun_with_json_api/schema_validators/check_collection_has_all_members'
|
3
|
+
|
1
4
|
module FunWithJsonApi
|
2
5
|
class FindCollectionFromDocument
|
3
6
|
def self.find(*args)
|
@@ -27,6 +30,7 @@ module FunWithJsonApi
|
|
27
30
|
# Load resource from id value
|
28
31
|
deserializer.load_collection_from_id_values(document_ids).tap do |collection|
|
29
32
|
check_collection_contains_all_requested_resources!(collection)
|
33
|
+
check_collection_is_authorised!(collection)
|
30
34
|
end
|
31
35
|
end
|
32
36
|
|
@@ -49,10 +53,11 @@ module FunWithJsonApi
|
|
49
53
|
private
|
50
54
|
|
51
55
|
def check_collection_contains_all_requested_resources!(collection)
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
+
SchemaValidators::CheckCollectionHasAllMembers.call(collection, document_ids, deserializer)
|
57
|
+
end
|
58
|
+
|
59
|
+
def check_collection_is_authorised!(collection)
|
60
|
+
SchemaValidators::CheckCollectionIsAuthorised.call(collection, document_ids, deserializer)
|
56
61
|
end
|
57
62
|
|
58
63
|
def check_document_types_match_deserializer!
|
@@ -83,26 +88,6 @@ module FunWithJsonApi
|
|
83
88
|
Exceptions::InvalidDocumentType.new(message, payload)
|
84
89
|
end
|
85
90
|
|
86
|
-
def build_missing_resources_error(collection_ids)
|
87
|
-
payload = document_ids.each_with_index.map do |resource_id, index|
|
88
|
-
build_missing_resource_payload(collection_ids, resource_id, index)
|
89
|
-
end.reject(&:nil?)
|
90
|
-
|
91
|
-
missing_values = document_ids.reject { |value| collection_ids.include?(value.to_s) }
|
92
|
-
message = "Couldn't find #{resource_class} items with "\
|
93
|
-
"#{id_param} in #{missing_values.inspect}"
|
94
|
-
Exceptions::MissingResource.new(message, payload)
|
95
|
-
end
|
96
|
-
|
97
|
-
def build_missing_resource_payload(collection_ids, resource_id, index)
|
98
|
-
unless collection_ids.include?(resource_id)
|
99
|
-
ExceptionPayload.new(
|
100
|
-
pointer: "/data/#{index}/id",
|
101
|
-
detail: missing_resource_message(resource_id)
|
102
|
-
)
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
91
|
def document_type_does_not_match_endpoint_message(type)
|
107
92
|
I18n.t(
|
108
93
|
:invalid_document_type,
|
@@ -111,14 +96,5 @@ module FunWithJsonApi
|
|
111
96
|
scope: 'fun_with_json_api.find_collection_from_document'
|
112
97
|
)
|
113
98
|
end
|
114
|
-
|
115
|
-
def missing_resource_message(resource_id)
|
116
|
-
I18n.t(
|
117
|
-
:missing_resource,
|
118
|
-
resource: resource_type,
|
119
|
-
resource_id: resource_id,
|
120
|
-
scope: 'fun_with_json_api.find_collection_from_document'
|
121
|
-
)
|
122
|
-
end
|
123
99
|
end
|
124
100
|
end
|
@@ -24,9 +24,7 @@ module FunWithJsonApi
|
|
24
24
|
raise build_invalid_document_type_error unless document_matches_resource_type?
|
25
25
|
|
26
26
|
# Load resource from id value
|
27
|
-
|
28
|
-
raise build_missing_resource_error if resource.nil?
|
29
|
-
end
|
27
|
+
load_resource_and_check!
|
30
28
|
end
|
31
29
|
|
32
30
|
def document_id
|
@@ -57,6 +55,15 @@ module FunWithJsonApi
|
|
57
55
|
|
58
56
|
private
|
59
57
|
|
58
|
+
def load_resource_and_check!
|
59
|
+
deserializer.load_resource_from_id_value(document_id).tap do |resource|
|
60
|
+
raise build_missing_resource_error if resource.nil?
|
61
|
+
FunWithJsonApi::SchemaValidators::CheckResourceIsAuthorised.call(
|
62
|
+
resource, document_id, deserializer
|
63
|
+
)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
60
67
|
def build_invalid_document_error
|
61
68
|
payload = ExceptionPayload.new
|
62
69
|
payload.pointer = '/data'
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'fun_with_json_api/exception'
|
2
|
+
|
3
|
+
module FunWithJsonApi
|
4
|
+
module SchemaValidators
|
5
|
+
class CheckCollectionHasAllMembers
|
6
|
+
def self.call(*args)
|
7
|
+
new(*args).call
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :collection
|
11
|
+
attr_reader :document_ids
|
12
|
+
attr_reader :deserializer
|
13
|
+
attr_reader :prefix
|
14
|
+
|
15
|
+
delegate :id_param, :resource_class, to: :deserializer
|
16
|
+
|
17
|
+
def initialize(collection, document_ids, deserializer, prefix: '/data')
|
18
|
+
@collection = collection
|
19
|
+
@document_ids = document_ids
|
20
|
+
@deserializer = deserializer
|
21
|
+
@prefix = prefix
|
22
|
+
end
|
23
|
+
|
24
|
+
def call
|
25
|
+
if collection.size != document_ids.size
|
26
|
+
collection_ids = deserializer.format_collection_ids(collection)
|
27
|
+
raise build_missing_resources_error(collection_ids)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def resource_type
|
32
|
+
deserializer.type
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def build_missing_resources_error(collection_ids)
|
38
|
+
payload = document_ids.each_with_index.map do |resource_id, index|
|
39
|
+
build_missing_resource_payload(collection_ids, resource_id, index)
|
40
|
+
end.reject(&:nil?)
|
41
|
+
|
42
|
+
missing_values = document_ids.reject { |value| collection_ids.include?(value.to_s) }
|
43
|
+
message = "Couldn't find #{resource_class} items with "\
|
44
|
+
"#{id_param} in #{missing_values.inspect}"
|
45
|
+
Exceptions::MissingResource.new(message, payload)
|
46
|
+
end
|
47
|
+
|
48
|
+
def build_missing_resource_payload(collection_ids, resource_id, index)
|
49
|
+
unless collection_ids.include?(resource_id)
|
50
|
+
ExceptionPayload.new(
|
51
|
+
pointer: "#{prefix}/#{index}/id",
|
52
|
+
detail: missing_resource_message(resource_id)
|
53
|
+
)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def missing_resource_message(resource_id)
|
58
|
+
I18n.t(
|
59
|
+
:missing_resource,
|
60
|
+
resource: resource_type,
|
61
|
+
resource_id: resource_id,
|
62
|
+
scope: 'fun_with_json_api.find_collection_from_document'
|
63
|
+
)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'fun_with_json_api/exception'
|
2
|
+
|
3
|
+
module FunWithJsonApi
|
4
|
+
module SchemaValidators
|
5
|
+
class CheckCollectionIsAuthorised
|
6
|
+
def self.call(*args)
|
7
|
+
new(*args).call
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :collection
|
11
|
+
attr_reader :collection_ids
|
12
|
+
attr_reader :deserializer
|
13
|
+
attr_reader :prefix
|
14
|
+
|
15
|
+
delegate :resource_class,
|
16
|
+
to: :deserializer
|
17
|
+
|
18
|
+
def initialize(collection, collection_ids, deserializer, prefix: '/data')
|
19
|
+
@collection = collection
|
20
|
+
@collection_ids = collection_ids
|
21
|
+
@deserializer = deserializer
|
22
|
+
@prefix = prefix
|
23
|
+
end
|
24
|
+
|
25
|
+
def call
|
26
|
+
payload = collection.each_with_index.map do |resource, index|
|
27
|
+
build_unauthorized_resource_payload(resource, index)
|
28
|
+
end.reject(&:nil?)
|
29
|
+
|
30
|
+
return if payload.empty?
|
31
|
+
|
32
|
+
raise Exceptions::UnauthorisedResource.new(
|
33
|
+
"resource_authorizer method for one or more '#{deserializer.type}' items returned false",
|
34
|
+
payload
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
def resource_type
|
39
|
+
deserializer.type
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def build_unauthorized_resource_payload(resource, index)
|
45
|
+
unless deserializer.resource_authorizer.call(resource)
|
46
|
+
ExceptionPayload.new(
|
47
|
+
pointer: "#{prefix}/#{index}/id",
|
48
|
+
detail: unauthorized_resource_message(collection_ids[index])
|
49
|
+
)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def unauthorized_resource_message(resource_id)
|
54
|
+
I18n.t(
|
55
|
+
:unauthorized_resource,
|
56
|
+
resource: resource_type,
|
57
|
+
resource_id: resource_id,
|
58
|
+
scope: 'fun_with_json_api.find_collection_from_document'
|
59
|
+
)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|