jsonapi-consumer 0.1.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rspec +2 -0
  4. data/CHANGELOG.md +12 -0
  5. data/Gemfile +7 -0
  6. data/LICENSE.txt +176 -0
  7. data/README.md +37 -0
  8. data/Rakefile +12 -0
  9. data/jsonapi-consumer.gemspec +32 -0
  10. data/lib/jsonapi/consumer/errors.rb +98 -0
  11. data/lib/jsonapi/consumer/middleware/parse_json.rb +32 -0
  12. data/lib/jsonapi/consumer/middleware/raise_error.rb +21 -0
  13. data/lib/jsonapi/consumer/middleware/request_timeout.rb +9 -0
  14. data/lib/jsonapi/consumer/middleware.rb +5 -0
  15. data/lib/jsonapi/consumer/parser.rb +75 -0
  16. data/lib/jsonapi/consumer/query/base.rb +34 -0
  17. data/lib/jsonapi/consumer/query/create.rb +9 -0
  18. data/lib/jsonapi/consumer/query/delete.rb +10 -0
  19. data/lib/jsonapi/consumer/query/find.rb +16 -0
  20. data/lib/jsonapi/consumer/query/new.rb +15 -0
  21. data/lib/jsonapi/consumer/query/update.rb +11 -0
  22. data/lib/jsonapi/consumer/query.rb +5 -0
  23. data/lib/jsonapi/consumer/resource/association_concern.rb +203 -0
  24. data/lib/jsonapi/consumer/resource/attributes_concern.rb +70 -0
  25. data/lib/jsonapi/consumer/resource/connection_concern.rb +94 -0
  26. data/lib/jsonapi/consumer/resource/finders_concern.rb +28 -0
  27. data/lib/jsonapi/consumer/resource/object_build_concern.rb +28 -0
  28. data/lib/jsonapi/consumer/resource/serializer_concern.rb +64 -0
  29. data/lib/jsonapi/consumer/resource.rb +88 -0
  30. data/lib/jsonapi/consumer/version.rb +5 -0
  31. data/lib/jsonapi/consumer.rb +40 -0
  32. data/spec/fixtures/.gitkeep +0 -0
  33. data/spec/fixtures/resources.rb +33 -0
  34. data/spec/fixtures/responses.rb +51 -0
  35. data/spec/jsonapi/consumer/associations_spec.rb +141 -0
  36. data/spec/jsonapi/consumer/attributes_spec.rb +27 -0
  37. data/spec/jsonapi/consumer/connection_spec.rb +101 -0
  38. data/spec/jsonapi/consumer/error_handling_spec.rb +37 -0
  39. data/spec/jsonapi/consumer/object_build_spec.rb +20 -0
  40. data/spec/jsonapi/consumer/parser_spec.rb +41 -0
  41. data/spec/jsonapi/consumer/resource_spec.rb +62 -0
  42. data/spec/jsonapi/consumer/serializer_spec.rb +41 -0
  43. data/spec/spec_helper.rb +97 -0
  44. data/spec/support/.gitkeep +0 -0
  45. data/spec/support/load_fixtures.rb +4 -0
  46. metadata +242 -0
@@ -0,0 +1,141 @@
1
+ RSpec.describe 'Associations', 'has_many' do
2
+ let(:user_class) do
3
+ User ||= Class.new do
4
+ include JSONAPI::Consumer::Resource
5
+
6
+ has_many :posts, class_name: 'Post'
7
+ end
8
+ end
9
+
10
+ let!(:post_class) do
11
+ Post ||= Class.new do
12
+ include JSONAPI::Consumer::Resource
13
+ end
14
+ end
15
+
16
+ subject(:user_instance) { user_class.new(username: 'foobar') }
17
+
18
+ it 'lists association names in #association_names' do
19
+ expect(user_instance.association_names).to eql([:posts])
20
+ end
21
+
22
+ describe 'defined accessors for assocation' do
23
+ it { is_expected.to respond_to(:posts) }
24
+ it { is_expected.to respond_to(:posts=) }
25
+ end
26
+
27
+ describe 'adding objects to array' do
28
+ it 'can be added as a single value' do
29
+ expect {
30
+ user_instance.posts = '1'
31
+ }.to change{user_instance.post_ids}.from(nil).to(['1'])
32
+ end
33
+
34
+ it 'can be added as a list of items' do
35
+ expect {
36
+ user_instance.posts = ['1','2','3']
37
+ }.to change{user_instance.post_ids}.from(nil).to(['1','2','3'])
38
+ end
39
+
40
+ it 'can be blanked out' do
41
+ user_instance.posts = '1'
42
+ expect {
43
+ user_instance.posts = nil
44
+ }.to change{user_instance.post_ids}.from(['1']).to(nil)
45
+ end
46
+ end
47
+
48
+ describe 'the links payload' do
49
+ subject(:payload_hash) { user_instance.serializable_hash }
50
+
51
+ it 'has links in output' do
52
+ expect(payload_hash).to have_key(:links)
53
+ end
54
+ end
55
+ end
56
+
57
+ RSpec.describe 'Associations', 'belongs_to' do
58
+ let(:comment_class) do
59
+ Comment ||= Class.new do
60
+ include JSONAPI::Consumer::Resource
61
+
62
+ # belongs_to :comment, class_name: 'Comment'
63
+ belongs_to :user, class_name: 'User'
64
+ end
65
+ end
66
+
67
+ let!(:user_class) do
68
+ User ||= Class.new do
69
+ include JSONAPI::Consumer::Resource
70
+ end
71
+ end
72
+
73
+ subject(:comment_instance) { comment_class.new }
74
+
75
+ it 'lists association names in #association_names' do
76
+ expect(comment_instance.association_names).to eql([:user])
77
+ end
78
+
79
+ describe 'defined accessors for assocation' do
80
+ it { is_expected.to respond_to(:user) }
81
+ it { is_expected.to respond_to(:user=) }
82
+ end
83
+
84
+ describe 'adding objects to belongs_to relationship' do
85
+ it 'can be added as a single value' do
86
+ expect {
87
+ comment_instance.user = '1'
88
+ }.to change{comment_instance.user_id}.from(nil).to('1')
89
+ end
90
+
91
+ it 'can be blanked out' do
92
+ comment_instance.user = '1'
93
+ expect {
94
+ comment_instance.user = nil
95
+ }.to change{comment_instance.user_id}.from('1').to(nil)
96
+ end
97
+ end
98
+ end
99
+
100
+ RSpec.describe 'Associations', 'has_one' do
101
+ let(:post_class) do
102
+ Poster ||= Class.new do
103
+ include JSONAPI::Consumer::Resource
104
+
105
+ has_one :author, class_name: 'Customer'
106
+ end
107
+ end
108
+
109
+ let!(:user_class) do
110
+ Customer ||= Class.new do
111
+ include JSONAPI::Consumer::Resource
112
+ end
113
+ end
114
+
115
+ subject(:post_instance) { post_class.new }
116
+
117
+ it 'lists association names in #association_names' do
118
+ expect(post_instance.association_names).to eql([:author])
119
+ end
120
+
121
+ describe 'defined accessors for assocation' do
122
+ it { is_expected.to respond_to(:author) }
123
+ it { is_expected.to respond_to(:author=) }
124
+ end
125
+
126
+ describe 'adding objects to belongs_to relationship' do
127
+ it 'can be added as a single value' do
128
+ expect {
129
+ post_instance.author = '1'
130
+ }.to change{post_instance.author_id}.from(nil).to('1')
131
+ end
132
+
133
+ it 'can be blanked out' do
134
+ post_instance.author = '1'
135
+ expect {
136
+ post_instance.author = nil
137
+ }.to change{post_instance.author_id}.from('1').to(nil)
138
+ end
139
+ end
140
+ end
141
+
@@ -0,0 +1,27 @@
1
+ RSpec.describe 'Attributes' do
2
+ let(:test_class) do
3
+ Class.new do
4
+ include JSONAPI::Consumer::Resource
5
+ end
6
+ end
7
+
8
+ subject(:obj) { test_class.new }
9
+
10
+ its(:primary_key) { is_expected.to eql(:id) }
11
+
12
+ describe 'changing the primary key' do
13
+ it 'is updatable on the class' do
14
+ expect {
15
+ obj.class.primary_key = :name
16
+ }.to change{obj.primary_key}.to(:name)
17
+ end
18
+ end
19
+
20
+ describe '#persisted?' do
21
+ it 'uses the primary key to decide' do
22
+ expect {
23
+ obj.id = '8'
24
+ }.to change{obj.persisted?}.from(false).to(true)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,101 @@
1
+ RSpec.describe 'Connection' do
2
+ let(:test_class) do
3
+ Record ||= Class.new do
4
+ include JSONAPI::Consumer::Resource
5
+ self.host = 'http://localhost:3000/api/'
6
+
7
+ has_one :domain
8
+ end
9
+ end
10
+
11
+ let(:obj) { test_class.new(name: 'jsonapi.example') }
12
+
13
+ describe '.all' do
14
+ it 'returns all results as objects' do
15
+ stub_request(:get, "http://localhost:3000/api/records")
16
+ .to_return(headers: {content_type: "application/json"}, body: {
17
+ records: [
18
+ {id: '1', name: "foo.example"},
19
+ {id: '2', name: "bar.example"},
20
+ {id: '3', name: "baz.example"}
21
+ ]
22
+ }.to_json)
23
+
24
+ records = test_class.all
25
+ expect(records.size).to eql(3)
26
+
27
+ record = records.first
28
+ expect(record).to be_a(Record)
29
+ end
30
+ end
31
+
32
+ describe '.find' do
33
+ it 'returns proper objects' do
34
+ stub_request(:get, "http://localhost:3000/api/records/1")
35
+ .to_return(headers: {content_type: "application/json"}, body: {
36
+ records: [
37
+ {id: '1', name: "foobar.example"}
38
+ ]
39
+ }.to_json)
40
+
41
+ records = test_class.find(1)
42
+ expect(records.size).to eql(1)
43
+
44
+ record = records.first
45
+ expect(record).to be_a(Record)
46
+ expect(record.id).to eql('1')
47
+ expect(record.name).to eql('foobar.example')
48
+ end
49
+ end
50
+
51
+ describe '#save' do
52
+ it 'can save successfully if called on a new item' do
53
+ stub_request(:post, "http://localhost:3000/api/records")
54
+ .to_return(headers: {content_type: "application/json"}, status: 201, body: {
55
+ records: [
56
+ {id: '1', name: "foobar.example", created_at: "2014-10-16T18:49:40Z", updated_at: "2014-10-18T18:59:40Z"}
57
+ ]
58
+ }.to_json)
59
+
60
+ expect(obj.save).to eql(true)
61
+
62
+ expect(obj.id).to eql('1')
63
+ expect(obj.name).to eql('foobar.example')
64
+
65
+ expect(obj.created_at).to eql('2014-10-16T18:49:40Z')
66
+ expect(obj.updated_at).to eql('2014-10-18T18:59:40Z')
67
+
68
+ expect(obj).to respond_to(:created_at)
69
+ expect(obj).to respond_to(:updated_at)
70
+ expect(obj.persisted?).to eql(true)
71
+ end
72
+
73
+ it 'can update when called on an existing item' do
74
+ stub_request(:put, "http://localhost:3000/api/records/1")
75
+ .to_return(headers: {content_type: "application/json"}, body: {
76
+ records: [
77
+ {id: '1', name: "foobar.example", created_at: "2014-10-16T18:49:40Z", updated_at: "2016-10-18T18:59:40Z"}
78
+ ]
79
+ }.to_json)
80
+
81
+ obj.id = '1'
82
+ obj.updated_at = "2014-10-18T18:59:40Z"
83
+ expect(obj.updated_at).to eql("2014-10-18T18:59:40Z")
84
+
85
+ expect(obj.save).to eql(true)
86
+ expect(obj.updated_at).to eql("2016-10-18T18:59:40Z")
87
+ end
88
+ end
89
+
90
+ describe '#destroy' do
91
+ before { obj.id = '1' }
92
+
93
+ it 'returns true when successful' do
94
+ stub_request(:delete, "http://localhost:3000/api/records/1")
95
+ .to_return(status: 204, body: nil)
96
+
97
+ expect(obj.destroy).to eql(true)
98
+ end
99
+ end
100
+ end
101
+
@@ -0,0 +1,37 @@
1
+ RSpec.describe 'Error handling' do
2
+ let(:test_class) do
3
+ Record ||= Class.new do
4
+ include JSONAPI::Consumer::Resource
5
+ self.host = 'http://localhost:3000/api/'
6
+
7
+ has_one :domain
8
+ end
9
+ end
10
+
11
+ let(:obj) { test_class.new(name: 'jsonapi.example') }
12
+
13
+ context 'on model' do
14
+ it 'adds to the errors object on the model' do
15
+ stub_request(:post, "http://localhost:3000/api/records")
16
+ .to_return(headers: {content_type: "application/json"}, status: 400, body: {
17
+ errors: [
18
+ {title: 'cannot be blank', path: "/name", detail: 'name cannot be blank'},
19
+ {title: 'is invalid', path: '/type', detail: 'type is invalid'}
20
+ ]
21
+ }.to_json)
22
+
23
+ expect(obj.save).to eql(false)
24
+ expect(obj.is_valid?).to eql(false)
25
+ end
26
+ end
27
+
28
+ context 'in general' do
29
+ it 'handles timeout errors' do
30
+ stub_request(:any, "http://localhost:3000/api/records").to_timeout
31
+
32
+ expect {
33
+ obj.save
34
+ }.to raise_error(JSONAPI::Consumer::Errors::ServerNotResponding)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,20 @@
1
+ RSpec.describe 'Object building' do
2
+ let(:class_name) { BuildRequest }
3
+
4
+ subject(:obj) { class_name.build }
5
+
6
+ it 'returns an object with populated items' do
7
+ stub_request(:get, "http://localhost:3000/api/build_requests/new")
8
+ .to_return(headers: {content_type: "application/json"}, body: {
9
+ build_requests: [
10
+ {name: "", title: "default value"}
11
+ ]
12
+ }.to_json)
13
+
14
+ expect(obj).to respond_to(:name)
15
+ expect(obj).to respond_to(:title)
16
+
17
+ expect(obj.name).to be_blank
18
+ expect(obj.title).to eql("default value")
19
+ end
20
+ end
@@ -0,0 +1,41 @@
1
+ RSpec.describe 'Response Parsing' do
2
+ let(:response) { OpenStruct.new(body: Responses.sideload) }
3
+ let(:parser) { JSONAPI::Consumer::Parser.new(Blog::Post, response) }
4
+
5
+ subject(:results) { parser.build }
6
+
7
+ it 'can handle linked associations' do
8
+ stub_request(:get, 'http://localhost:3000/api/comments/9c9ba83b-024c-4d4c-9573-9fd41b95fc14')
9
+ .to_return(headers: {content_type: "application/json"}, body: {
10
+ comments: [
11
+ {
12
+ id: '9c9ba83b-024c-4d4c-9573-9fd41b95fc14',
13
+ content: "i found this useful."
14
+ }
15
+ ]
16
+ }.to_json)
17
+
18
+ stub_request(:get, 'http://localhost:3000/api/comments/27fcf6e8-24b0-41db-94b1-812046a10f54')
19
+ .to_return(headers: {content_type: "application/json"}, body: {
20
+ comments: [
21
+ {
22
+ id: '27fcf6e8-24b0-41db-94b1-812046a10f54',
23
+ content: "i found this useful too."
24
+ }
25
+ ]
26
+ }.to_json)
27
+
28
+ # puts results.inspect
29
+ expect(results.size).to eql(2)
30
+
31
+ result = results.first
32
+ expect(result.comments.size).to eql(2)
33
+
34
+ last = results.last
35
+ expect(result.comments.size).to eql(2)
36
+
37
+ expect(last.comments.first).to be_a(Blog::Comment)
38
+ expect(last.comments.first.id).to eql("9c9ba83b-024c-4d4c-9573-9fd41b95fc14")
39
+ expect(last.comments.first.content).to eql("i found this useful.")
40
+ end
41
+ end
@@ -0,0 +1,62 @@
1
+ RSpec.describe 'Resource' do
2
+ let(:sample_attrs) {
3
+ {
4
+ id: SecureRandom.uuid,
5
+ name: 'foo',
6
+ content: 'bar'
7
+ }
8
+ }
9
+
10
+ subject(:test_class) do
11
+ Paper ||= Class.new do
12
+ include JSONAPI::Consumer::Resource
13
+ end
14
+ end
15
+
16
+ it 'it errors on undefined methods' do
17
+ obj = test_class.new
18
+ expect {
19
+ obj.paper
20
+ }.to raise_error(NoMethodError)
21
+ end
22
+
23
+ describe 'accepts any passed in params through #attributes=' do
24
+ subject(:instance) { test_class.new }
25
+
26
+ before do
27
+ instance.attributes = sample_attrs
28
+ end
29
+
30
+ it { is_expected.to respond_to(:id) }
31
+ it { is_expected.to respond_to(:name) }
32
+ it { is_expected.to respond_to(:content) }
33
+ end
34
+
35
+ describe 'accepts attributes through .new' do
36
+ subject(:instance) { test_class.new(sample_attrs) }
37
+
38
+ it { is_expected.to respond_to(:id) }
39
+ it { is_expected.to respond_to(:name) }
40
+ it { is_expected.to respond_to(:content) }
41
+ end
42
+
43
+ describe '#serializable_hash' do
44
+ subject(:obj_hash) { test_class.new(sample_attrs).serializable_hash }
45
+
46
+ it 'has proper keys' do
47
+ expect(obj_hash).to have_key(:id)
48
+ expect(obj_hash).to have_key(:name)
49
+ expect(obj_hash).to have_key(:content)
50
+ end
51
+ end
52
+
53
+ describe '#to_json' do
54
+ subject(:obj_hash) { test_class.new(sample_attrs).to_json }
55
+
56
+ it 'has all attributes root key' do
57
+ json_hash = JSON.parse(obj_hash)
58
+ expect(json_hash.keys).to eql(['id', 'name', 'content'])
59
+ end
60
+ end
61
+
62
+ end
@@ -0,0 +1,41 @@
1
+ RSpec.describe 'Serializer' do
2
+ let(:test_class) do
3
+ SerializerTestClass ||= Class.new do
4
+ include JSONAPI::Consumer::Resource
5
+
6
+ has_many :users, class_name: 'Account'
7
+ has_one :owner, class_name: 'Owner'
8
+ end
9
+ end
10
+
11
+ let(:owner) {
12
+ Owner ||= Class.new do
13
+ include JSONAPI::Consumer::Resource
14
+
15
+ def to_param
16
+ 1
17
+ end
18
+ end
19
+ }
20
+
21
+ let(:user) {
22
+ Account ||= Class.new do
23
+ include JSONAPI::Consumer::Resource
24
+ end
25
+ }
26
+
27
+ subject(:obj_hash) { test_class.new(id: '76', owner: owner.new, users: [user.new(id: 'a'), user.new(id: 'b')]).serializable_hash }
28
+
29
+ it 'outputs the associated has_one' do
30
+ expect(obj_hash).to have_key(:links)
31
+ expect(obj_hash[:links]).to have_key(:owner)
32
+ expect(obj_hash[:links][:owner]).to eql(1)
33
+ end
34
+
35
+ it 'outputs the associated has_many' do
36
+ expect(obj_hash).to have_key(:links)
37
+ expect(obj_hash[:links]).to have_key(:users)
38
+ expect(obj_hash[:links][:users]).to eql(['a', 'b'])
39
+ end
40
+
41
+ end
@@ -0,0 +1,97 @@
1
+ require 'jsonapi/consumer'
2
+
3
+ require 'rspec/its'
4
+ require 'webmock/rspec'
5
+
6
+ # Load support files
7
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
8
+
9
+ # This file was generated by the `rspec --init` command. Conventionally, all
10
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
11
+ # The generated `.rspec` file contains `--require spec_helper` which will cause this
12
+ # file to always be loaded, without a need to explicitly require it in any files.
13
+ #
14
+ # Given that it is always loaded, you are encouraged to keep this file as
15
+ # light-weight as possible. Requiring heavyweight dependencies from this file
16
+ # will add to the boot time of your test suite on EVERY test run, even for an
17
+ # individual file that may not need all of that loaded. Instead, consider making
18
+ # a separate helper file that requires the additional dependencies and performs
19
+ # the additional setup, and require it from the spec files that actually need it.
20
+ #
21
+ # The `.rspec` file also contains a few flags that are not defaults but that
22
+ # users commonly want.
23
+ #
24
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
25
+ RSpec.configure do |config|
26
+ # rspec-expectations config goes here. You can use an alternate
27
+ # assertion/expectation library such as wrong or the stdlib/minitest
28
+ # assertions if you prefer.
29
+ config.expect_with :rspec do |expectations|
30
+ # This option will default to `true` in RSpec 4. It makes the `description`
31
+ # and `failure_message` of custom matchers include text for helper methods
32
+ # defined using `chain`, e.g.:
33
+ # be_bigger_than(2).and_smaller_than(4).description
34
+ # # => "be bigger than 2 and smaller than 4"
35
+ # ...rather than:
36
+ # # => "be bigger than 2"
37
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
38
+ end
39
+
40
+ # rspec-mocks config goes here. You can use an alternate test double
41
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
42
+ config.mock_with :rspec do |mocks|
43
+ # Prevents you from mocking or stubbing a method that does not exist on
44
+ # a real object. This is generally recommended, and will default to
45
+ # `true` in RSpec 4.
46
+ mocks.verify_partial_doubles = true
47
+ end
48
+
49
+ # The settings below are suggested to provide a good initial experience
50
+ # with RSpec, but feel free to customize to your heart's content.
51
+ =begin
52
+ # These two settings work together to allow you to limit a spec run
53
+ # to individual examples or groups you care about by tagging them with
54
+ # `:focus` metadata. When nothing is tagged with `:focus`, all examples
55
+ # get run.
56
+ config.filter_run :focus
57
+ config.run_all_when_everything_filtered = true
58
+
59
+ # Limits the available syntax to the non-monkey patched syntax that is recommended.
60
+ # For more details, see:
61
+ # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
62
+ # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
63
+ # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching
64
+ config.disable_monkey_patching!
65
+
66
+ # This setting enables warnings. It's recommended, but in some cases may
67
+ # be too noisy due to issues in dependencies.
68
+ config.warnings = true
69
+
70
+ # Many RSpec users commonly either run the entire suite or an individual
71
+ # file, and it's useful to allow more verbose output when running an
72
+ # individual spec file.
73
+ if config.files_to_run.one?
74
+ # Use the documentation formatter for detailed output,
75
+ # unless a formatter has already been configured
76
+ # (e.g. via a command-line flag).
77
+ config.default_formatter = 'doc'
78
+ end
79
+
80
+ # Print the 10 slowest examples and example groups at the
81
+ # end of the spec run, to help surface which specs are running
82
+ # particularly slow.
83
+ config.profile_examples = 10
84
+
85
+ # Run specs in random order to surface order dependencies. If you find an
86
+ # order dependency and want to debug it, you can fix the order by providing
87
+ # the seed, which is printed after each run.
88
+ # --seed 1234
89
+ config.order = :random
90
+
91
+ # Seed global randomization in this process using the `--seed` CLI option.
92
+ # Setting this allows you to use `--seed` to deterministically reproduce
93
+ # test failures related to randomization by passing the same `--seed` value
94
+ # as the one that triggered the failure.
95
+ Kernel.srand config.seed
96
+ =end
97
+ end
File without changes
@@ -0,0 +1,4 @@
1
+ require File.expand_path('../../fixtures/resources', __FILE__)
2
+ require File.expand_path('../../fixtures/responses', __FILE__)
3
+
4
+ Responses.sideload