fun_with_json_api 0.0.2 → 0.0.3

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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +4 -1
  3. data/config/locales/fun_with_json_api.en.yml +29 -2
  4. data/lib/fun_with_json_api.rb +30 -2
  5. data/lib/fun_with_json_api/action_controller_extensions/serialization.rb +18 -0
  6. data/lib/fun_with_json_api/attribute.rb +3 -3
  7. data/lib/fun_with_json_api/attributes/relationship.rb +37 -23
  8. data/lib/fun_with_json_api/attributes/relationship_collection.rb +55 -38
  9. data/lib/fun_with_json_api/attributes/string_attribute.rb +12 -1
  10. data/lib/fun_with_json_api/attributes/uuid_v4_attribute.rb +27 -0
  11. data/lib/fun_with_json_api/controller_methods.rb +1 -1
  12. data/lib/fun_with_json_api/deserializer.rb +61 -8
  13. data/lib/fun_with_json_api/deserializer_class_methods.rb +37 -7
  14. data/lib/fun_with_json_api/exceptions/illegal_client_generated_identifier.rb +17 -0
  15. data/lib/fun_with_json_api/exceptions/invalid_client_generated_identifier.rb +17 -0
  16. data/lib/fun_with_json_api/exceptions/invalid_document_identifier.rb +17 -0
  17. data/lib/fun_with_json_api/exceptions/invalid_document_type.rb +20 -0
  18. data/lib/fun_with_json_api/exceptions/invalid_relationship.rb +5 -3
  19. data/lib/fun_with_json_api/exceptions/invalid_relationship_type.rb +17 -0
  20. data/lib/fun_with_json_api/exceptions/missing_resource.rb +15 -0
  21. data/lib/fun_with_json_api/exceptions/unknown_attribute.rb +15 -0
  22. data/lib/fun_with_json_api/exceptions/unknown_relationship.rb +15 -0
  23. data/lib/fun_with_json_api/find_collection_from_document.rb +124 -0
  24. data/lib/fun_with_json_api/find_resource_from_document.rb +112 -0
  25. data/lib/fun_with_json_api/pre_deserializer.rb +1 -0
  26. data/lib/fun_with_json_api/railtie.rb +30 -1
  27. data/lib/fun_with_json_api/schema_validator.rb +47 -0
  28. data/lib/fun_with_json_api/schema_validators/check_attributes.rb +52 -0
  29. data/lib/fun_with_json_api/schema_validators/check_document_id_matches_resource.rb +96 -0
  30. data/lib/fun_with_json_api/schema_validators/check_document_type_matches_resource.rb +40 -0
  31. data/lib/fun_with_json_api/schema_validators/check_relationships.rb +127 -0
  32. data/lib/fun_with_json_api/version.rb +1 -1
  33. data/spec/dummy/log/test.log +172695 -0
  34. data/spec/fixtures/active_record.rb +6 -0
  35. data/spec/fun_with_json_api/controller_methods_spec.rb +8 -3
  36. data/spec/fun_with_json_api/deserializer_class_methods_spec.rb +14 -6
  37. data/spec/fun_with_json_api/deserializer_spec.rb +155 -40
  38. data/spec/fun_with_json_api/exception_spec.rb +9 -9
  39. data/spec/fun_with_json_api/find_collection_from_document_spec.rb +203 -0
  40. data/spec/fun_with_json_api/find_resource_from_document_spec.rb +100 -0
  41. data/spec/fun_with_json_api/pre_deserializer_spec.rb +26 -26
  42. data/spec/fun_with_json_api/railtie_spec.rb +88 -0
  43. data/spec/fun_with_json_api/schema_validator_spec.rb +94 -0
  44. data/spec/fun_with_json_api/schema_validators/check_attributes_spec.rb +52 -0
  45. data/spec/fun_with_json_api/schema_validators/check_document_id_matches_resource_spec.rb +115 -0
  46. data/spec/fun_with_json_api/schema_validators/check_document_type_matches_resource_spec.rb +30 -0
  47. data/spec/fun_with_json_api/schema_validators/check_relationships_spec.rb +150 -0
  48. data/spec/fun_with_json_api_spec.rb +148 -4
  49. metadata +49 -4
  50. data/spec/example_spec.rb +0 -64
@@ -0,0 +1,88 @@
1
+ require 'spec_helper'
2
+
3
+ describe FunWithJsonApi::Railtie do
4
+ describe 'controller parameters' do
5
+ context 'with a application/vnd.api+json request content-type', type: :controller do
6
+ controller do
7
+ def index
8
+ # Concatinates /data/id and /data/type from a json_api request
9
+ render text: "#{params[:data][:id]}:#{params['data']['type']}"
10
+ end
11
+ end
12
+
13
+ it 'converts the request body into param values' do
14
+ json_api_data = { data: { id: '42', type: 'foobar' } }
15
+
16
+ get :index,
17
+ json_api_data.as_json,
18
+ 'Content-Type' => 'application/vnd.api+json'
19
+ expect(response.body).to eq '42:foobar'
20
+ end
21
+ end
22
+ context 'with an implicit respond_to json_api block', type: :controller do
23
+ controller do
24
+ def index
25
+ respond_to do |format|
26
+ format.json_api { render text: 'passed' }
27
+ format.all { render text: 'failed' }
28
+ end
29
+ end
30
+ end
31
+
32
+ it 'responds to a json_api format request' do
33
+ get :index, format: :json_api
34
+ expect(response.body).to eq 'passed'
35
+ end
36
+ it 'responds to a application/vnd.api+json accept header' do
37
+ request.env['HTTP_ACCEPT'] = 'application/vnd.api+json'
38
+ get :index
39
+ expect(response.body).to eq 'passed'
40
+ end
41
+ end
42
+ end
43
+
44
+ describe 'controller rendering' do
45
+ context 'with an explicit render json_api call', type: :controller do
46
+ controller do
47
+ def index
48
+ render json_api: { data: { id: '42', type: 'foobar' } }
49
+ end
50
+ end
51
+
52
+ it 'renders out the hash as a json_api response' do
53
+ get :index
54
+ expect(response.content_type).to eq 'application/vnd.api+json'
55
+ expect(JSON.parse(response.body)).to eq(
56
+ 'data' => { 'id' => '42', 'type' => 'foobar' }
57
+ )
58
+ end
59
+ end
60
+ context 'with a resource and a serializer', type: :controller do
61
+ controller do
62
+ def index
63
+ author = ARModels::Author.new(id: 42, name: 'Foo Bar')
64
+ render json_api: author, serializer: ARModels::AuthorSerializer
65
+ end
66
+ end
67
+
68
+ it 'renders the resource as a json api document' do
69
+ get :index
70
+ expect(response.content_type).to eq 'application/vnd.api+json'
71
+ expect(JSON.parse(response.body)).to eq(
72
+ 'data' => {
73
+ 'id' => '42',
74
+ 'type' => 'person',
75
+ 'attributes' => {
76
+ 'name' => 'Foo Bar'
77
+ },
78
+ 'relationships' => {
79
+ 'posts' => {
80
+ 'data' => []
81
+ }
82
+ }
83
+ }
84
+ )
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,94 @@
1
+ require 'spec_helper'
2
+
3
+ describe FunWithJsonApi::SchemaValidator do
4
+ let(:document) { { data: { id: '42', type: 'examples' } } }
5
+ let(:deserializer) { instance_double('FunWithJsonApi::Deserializer') }
6
+ let(:resource) { double('Resource') }
7
+ subject(:instance) { described_class.send :new, document, deserializer, resource }
8
+
9
+ describe '.check' do
10
+ subject { described_class.check(document, deserializer, resource) }
11
+
12
+ it 'calls all schema validator checks with an instance of itself' do
13
+ [
14
+ FunWithJsonApi::SchemaValidators::CheckDocumentTypeMatchesResource,
15
+ FunWithJsonApi::SchemaValidators::CheckDocumentIdMatchesResource
16
+ ].each do |validator_check|
17
+ expect(validator_check).to receive(:call).with(kind_of(described_class))
18
+ end
19
+
20
+ converted_document = { 'data' => { 'id' => '42', 'type' => 'examples' } }
21
+ [
22
+ FunWithJsonApi::SchemaValidators::CheckAttributes,
23
+ FunWithJsonApi::SchemaValidators::CheckRelationships
24
+ ].each do |property_check|
25
+ expect(property_check).to receive(:call).with(converted_document, deserializer)
26
+ end
27
+
28
+ subject
29
+ end
30
+ end
31
+
32
+ describe '#document_id' do
33
+ subject { instance.document_id }
34
+
35
+ context 'when the api document has symbolized keys' do
36
+ context 'when the data attribute has an id value' do
37
+ let(:document) { { data: { id: '42', type: 'examples' } } }
38
+
39
+ it 'returns the /data/id value' do
40
+ is_expected.to eq '42'
41
+ end
42
+ end
43
+ context 'when the data attribute does not have an id value' do
44
+ let(:document) { { data: { type: 'examples' } } }
45
+
46
+ it { is_expected.to eq nil }
47
+ end
48
+ end
49
+ context 'when the api document has string keys' do
50
+ context 'when the data attribute has an id value' do
51
+ let(:document) { { 'data' => { 'id' => '42', 'type' => 'examples' } } }
52
+
53
+ it 'returns the /data/id value' do
54
+ is_expected.to eq '42'
55
+ end
56
+ end
57
+ context 'when the data attribute does not have an id value' do
58
+ let(:document) { { 'data' => { 'type' => 'examples' } } }
59
+
60
+ it { is_expected.to eq nil }
61
+ end
62
+ end
63
+ end
64
+
65
+ describe '#resource_id' do
66
+ subject { instance.resource_id }
67
+
68
+ context 'when the deserializer#id_param is :id' do
69
+ before { allow(deserializer).to receive(:id_param).and_return(:id) }
70
+
71
+ it 'returns the resource id as a string' do
72
+ expect(resource).to receive(:id).and_return(42)
73
+ expect(subject).to eq '42'
74
+ end
75
+ end
76
+ context 'when the deserializer#id_param a field value' do
77
+ before { allow(deserializer).to receive(:id_param).and_return(:code) }
78
+
79
+ it 'returns the resource field value as a string' do
80
+ expect(resource).to receive(:code).and_return('foobar')
81
+ expect(subject).to eq 'foobar'
82
+ end
83
+ end
84
+ end
85
+
86
+ describe '#resource_type' do
87
+ subject { instance.resource_type }
88
+
89
+ it 'returns the deserializer type' do
90
+ expect(deserializer).to receive(:type).and_return('examples')
91
+ expect(subject).to eq 'examples'
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,52 @@
1
+ require 'spec_helper'
2
+
3
+ describe FunWithJsonApi::SchemaValidators::CheckAttributes do
4
+ describe '.call' do
5
+ let(:document) do
6
+ {
7
+ 'data' => {
8
+ 'id' => '42',
9
+ 'type' => 'examples',
10
+ 'attributes' => {
11
+ 'foobar' => 'blargh'
12
+ }
13
+ }
14
+ }
15
+ end
16
+ let(:deserializer) { instance_double('FunWithJsonApi::Deserializer', type: 'examples') }
17
+ subject { described_class.call(document, deserializer) }
18
+
19
+ context 'when the document contains an attribute supported by the deserializer' do
20
+ let(:attribute) { instance_double('FunWithJsonApi::Attribute', name: :foobar) }
21
+ before { allow(deserializer).to receive(:attributes).and_return([attribute]) }
22
+
23
+ it 'returns true' do
24
+ expect(subject).to eq true
25
+ end
26
+ end
27
+
28
+ context 'when the document contains an unsupported attribute' do
29
+ before { allow(deserializer).to receive(:attributes).and_return([]) }
30
+
31
+ it 'raises a UnknownAttribute error' do
32
+ expect do
33
+ subject
34
+ end.to raise_error(FunWithJsonApi::Exceptions::UnknownAttribute) do |e|
35
+ expect(e.payload.size).to eq 1
36
+
37
+ payload = e.payload.first
38
+ expect(payload.code).to eq 'unknown_attribute'
39
+ expect(payload.pointer).to eq '/data/attributes/foobar'
40
+ expect(payload.title).to eq(
41
+ 'Request json_api attribute is unsupported by the current endpoint'
42
+ )
43
+ expect(payload.detail).to eq(
44
+ "The provided attribute 'foobar' can not be assigned to a 'examples' resource"\
45
+ ' from the current endpoint'
46
+ )
47
+ expect(payload.status).to eq '422'
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,115 @@
1
+ require 'spec_helper'
2
+
3
+ describe FunWithJsonApi::SchemaValidators::CheckDocumentIdMatchesResource do
4
+ describe '.call' do
5
+ let(:schema_validator) do
6
+ instance_double('FunWithJsonApi::SchemaValidator', resource_type: 'examples')
7
+ end
8
+ subject { described_class.call(schema_validator) }
9
+
10
+ context 'when the resource is persisted' do
11
+ let(:resource) { instance_double('ActiveRecord::Base', persisted?: true) }
12
+ before { allow(schema_validator).to receive(:resource).and_return(resource) }
13
+
14
+ context 'when /data/id does not match the resource id' do
15
+ before do
16
+ allow(schema_validator).to receive(:resource_id).and_return('11')
17
+ allow(schema_validator).to receive(:document_id).and_return('42')
18
+ end
19
+
20
+ it 'raises a InvalidDocumentIdentifier error' do
21
+ expect do
22
+ subject
23
+ end.to raise_error(FunWithJsonApi::Exceptions::InvalidDocumentIdentifier) do |e|
24
+ expect(e.payload.size).to eq 1
25
+
26
+ payload = e.payload.first
27
+ expect(payload.code).to eq 'invalid_document_identifier'
28
+ expect(payload.pointer).to eq '/data/id'
29
+ expect(payload.title).to eq 'Request json_api data id is invalid'
30
+ expect(payload.detail).to eq 'Expected data id to match resource at endpoint: 11'
31
+ expect(payload.status).to eq '409'
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ context 'when the resource is not persisted' do
38
+ let(:resource) { instance_double('ActiveRecord::Base', persisted?: false) }
39
+ before { allow(schema_validator).to receive(:resource).and_return(resource) }
40
+
41
+ context 'when a document_id has been supplied' do
42
+ before { allow(schema_validator).to receive(:document_id).and_return('42') }
43
+
44
+ context 'when the deserializer does not have an id attribute' do
45
+ let(:deserializer) { instance_double('FunWithJsonApi::Deserializer') }
46
+ before do
47
+ allow(schema_validator).to receive(:deserializer).and_return(deserializer)
48
+ allow(deserializer).to receive(:attributes).and_return([])
49
+ end
50
+
51
+ it 'raises a IllegalClientGeneratedIdentifier error' do
52
+ expect do
53
+ subject
54
+ end.to raise_error(FunWithJsonApi::Exceptions::IllegalClientGeneratedIdentifier) do |e|
55
+ expect(e.payload.size).to eq 1
56
+
57
+ payload = e.payload.first
58
+ expect(payload.code).to eq 'illegal_client_generated_identifier'
59
+ expect(payload.pointer).to eq '/data/id'
60
+ expect(payload.title).to eq(
61
+ 'Request json_api attempted to set an unsupported client-generated id'
62
+ )
63
+ expect(payload.detail).to eq(
64
+ "The current endpoint does not allow you to set an id for a new 'examples' resource"
65
+ )
66
+ expect(payload.status).to eq '403'
67
+ end
68
+ end
69
+ end
70
+ context 'when the deserializer has an id attribute' do
71
+ let(:deserializer) { instance_double('FunWithJsonApi::Deserializer') }
72
+ before do
73
+ allow(schema_validator).to receive(:deserializer).and_return(deserializer)
74
+ allow(deserializer).to receive(:attributes).and_return(
75
+ [
76
+ instance_double('FunWithJsonApi::Attribute', name: :id)
77
+ ]
78
+ )
79
+ end
80
+
81
+ context 'when a resource matching id exists' do
82
+ before do
83
+ allow(deserializer).to receive(:load_resource_from_id_value)
84
+ .with('42')
85
+ .and_return(double('existing_resource', id: '24'))
86
+ end
87
+
88
+ it 'raises a InvalidClientGeneratedIdentifier error' do
89
+ expect do
90
+ subject
91
+ end.to raise_error(
92
+ FunWithJsonApi::Exceptions::InvalidClientGeneratedIdentifier
93
+ ) do |e|
94
+ expect(e.payload.size).to eq 1
95
+
96
+ payload = e.payload.first
97
+ expect(payload.code).to eq 'invalid_client_generated_identifier'
98
+ expect(payload.pointer).to eq '/data/id'
99
+ expect(payload.title).to eq(
100
+ 'Request json_api data id has already been used for an existing'\
101
+ ' resource'
102
+ )
103
+ expect(payload.detail).to eq(
104
+ "The provided id for a new 'examples' resource has already been used by another"\
105
+ ' resource: 42'
106
+ )
107
+ expect(payload.status).to eq '409'
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+
3
+ describe FunWithJsonApi::SchemaValidators::CheckDocumentTypeMatchesResource do
4
+ describe '.call' do
5
+ let(:schema_validator) do
6
+ instance_double('FunWithJsonApi::SchemaValidator')
7
+ end
8
+ subject { described_class.call(schema_validator) }
9
+
10
+ context 'when document_type does not match resource_type' do
11
+ before do
12
+ allow(schema_validator).to receive(:document_type).and_return('examples')
13
+ allow(schema_validator).to receive(:resource_type).and_return('foobar')
14
+ end
15
+
16
+ it 'raises a InvalidDocumentType error' do
17
+ expect { subject }.to raise_error(FunWithJsonApi::Exceptions::InvalidDocumentType) do |e|
18
+ expect(e.payload.size).to eq 1
19
+
20
+ payload = e.payload.first
21
+ expect(payload.code).to eq 'invalid_document_type'
22
+ expect(payload.pointer).to eq '/data/type'
23
+ expect(payload.title).to eq 'Request json_api data type does not match endpoint'
24
+ expect(payload.detail).to eq "Expected data type to be a 'foobar' resource"
25
+ expect(payload.status).to eq '409'
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,150 @@
1
+ require 'spec_helper'
2
+
3
+ describe FunWithJsonApi::SchemaValidators::CheckRelationships do
4
+ describe '.call' do
5
+ let(:document) do
6
+ {
7
+ 'data' => {
8
+ 'id' => '42',
9
+ 'type' => 'examples',
10
+ 'relationships' => {
11
+ 'foobar' => {
12
+ 'data' => relationship_data
13
+ }
14
+ }
15
+ }
16
+ }
17
+ end
18
+ let(:relationship_data) { double('relationship_data') }
19
+ let(:deserializer) { instance_double('FunWithJsonApi::Deserializer', type: 'examples') }
20
+ subject { described_class.call(document, deserializer) }
21
+
22
+ context 'with a has-one relationship' do
23
+ let(:relationship) do
24
+ instance_double('FunWithJsonApi::Relationship', name: :foobar, has_many?: false)
25
+ end
26
+ before { allow(deserializer).to receive(:relationships).and_return([relationship]) }
27
+
28
+ context 'when the relationship item is a hash' do
29
+ let(:relationship_data) { { 'id' => '24', 'type' => 'foobars' } }
30
+
31
+ context 'when the type matches the relationship' do
32
+ before { allow(relationship).to receive(:type).and_return('foobars') }
33
+
34
+ it { is_expected.to eq true }
35
+ end
36
+
37
+ context 'when the type does not match the relationship' do
38
+ before { allow(relationship).to receive(:type).and_return('invalid') }
39
+
40
+ it 'raises a InvalidRelationshipType error' do
41
+ expect do
42
+ subject
43
+ end.to raise_error(FunWithJsonApi::Exceptions::InvalidRelationshipType) do |e|
44
+ expect(e.payload.size).to eq 1
45
+
46
+ payload = e.payload.first
47
+ expect(payload.code).to eq 'invalid_relationship_type'
48
+ expect(payload.pointer).to eq '/data/relationships/foobar/type'
49
+ expect(payload.title).to eq(
50
+ 'Request json_api relationship type does not match expected resource'
51
+ )
52
+ expect(payload.detail).to eq(
53
+ "Expected 'foobar' relationship to be null or a 'invalid' resource identifier Hash"
54
+ )
55
+ expect(payload.status).to eq '409'
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ context 'when the relationship item is nil' do
62
+ let(:relationship_data) { nil }
63
+
64
+ it { is_expected.to eq true }
65
+ end
66
+
67
+ context 'when the relationship item is a array' do
68
+ let(:relationship_data) { [{ 'id' => '24', 'type' => 'foobars' }] }
69
+
70
+ it { is_expected.to eq true }
71
+ end
72
+ end
73
+
74
+ context 'with a has-many relationship' do
75
+ let(:relationship) do
76
+ instance_double('FunWithJsonApi::RelationshipCollection', name: :foobar, has_many?: true)
77
+ end
78
+ before { allow(deserializer).to receive(:relationships).and_return([relationship]) }
79
+
80
+ context 'when the relationship item is a array' do
81
+ let(:relationship_data) { [{ 'id' => '24', 'type' => 'foobars' }] }
82
+
83
+ context 'when the type matches the relationship' do
84
+ before { allow(relationship).to receive(:type).and_return('foobars') }
85
+
86
+ it { is_expected.to eq true }
87
+ end
88
+
89
+ context 'when the type does not match the deserializer' do
90
+ before { allow(relationship).to receive(:type).and_return('invalid') }
91
+
92
+ it 'raises a InvalidRelationshipType error' do
93
+ expect do
94
+ subject
95
+ end.to raise_error(FunWithJsonApi::Exceptions::InvalidRelationshipType) do |e|
96
+ expect(e.payload.size).to eq 1
97
+
98
+ payload = e.payload.first
99
+ expect(payload.code).to eq 'invalid_relationship_type'
100
+ expect(payload.pointer).to eq '/data/relationships/foobar/0/type'
101
+ expect(payload.title).to eq(
102
+ 'Request json_api relationship type does not match expected resource'
103
+ )
104
+ expect(payload.detail).to eq(
105
+ "Expected 'foobar' relationship to be an Array of 'invalid' resource identifiers"
106
+ )
107
+ expect(payload.status).to eq '409'
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ context 'when the relationship item is a hash' do
114
+ let(:relationship_data) { { 'id' => '24', 'type' => 'foobars' } }
115
+
116
+ it { is_expected.to eq true }
117
+ end
118
+
119
+ context 'when the relationship item is nil' do
120
+ let(:relationship_data) { nil }
121
+
122
+ it { is_expected.to eq true }
123
+ end
124
+ end
125
+
126
+ context 'when the document contains an unsupported relationships' do
127
+ before { allow(deserializer).to receive(:relationships).and_return([]) }
128
+
129
+ it 'raises a UnknownRelationship error' do
130
+ expect do
131
+ subject
132
+ end.to raise_error(FunWithJsonApi::Exceptions::UnknownRelationship) do |e|
133
+ expect(e.payload.size).to eq 1
134
+
135
+ payload = e.payload.first
136
+ expect(payload.code).to eq 'unknown_relationship'
137
+ expect(payload.pointer).to eq '/data/relationships/foobar'
138
+ expect(payload.title).to eq(
139
+ 'Request json_api relationship is unsupported by the current endpoint'
140
+ )
141
+ expect(payload.detail).to eq(
142
+ "The provided relationship 'foobar' can not be assigned to a 'examples' resource"\
143
+ ' from the current endpoint'
144
+ )
145
+ expect(payload.status).to eq '422'
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end