acfs 1.3.3 → 1.6.0

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 (96) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +372 -0
  3. data/LICENSE +22 -0
  4. data/README.md +321 -0
  5. data/acfs.gemspec +38 -0
  6. data/lib/acfs.rb +51 -0
  7. data/lib/acfs/adapter/base.rb +26 -0
  8. data/lib/acfs/adapter/typhoeus.rb +82 -0
  9. data/lib/acfs/collection.rb +28 -0
  10. data/lib/acfs/collections/paginatable.rb +76 -0
  11. data/lib/acfs/configuration.rb +120 -0
  12. data/lib/acfs/errors.rb +147 -0
  13. data/lib/acfs/global.rb +101 -0
  14. data/lib/acfs/location.rb +76 -0
  15. data/lib/acfs/middleware/base.rb +24 -0
  16. data/lib/acfs/middleware/json.rb +31 -0
  17. data/lib/acfs/middleware/logger.rb +23 -0
  18. data/lib/acfs/middleware/msgpack.rb +32 -0
  19. data/lib/acfs/middleware/print.rb +23 -0
  20. data/lib/acfs/middleware/serializer.rb +41 -0
  21. data/lib/acfs/operation.rb +96 -0
  22. data/lib/acfs/request.rb +32 -0
  23. data/lib/acfs/request/callbacks.rb +54 -0
  24. data/lib/acfs/resource.rb +39 -0
  25. data/lib/acfs/resource/attributes.rb +270 -0
  26. data/lib/acfs/resource/attributes/base.rb +29 -0
  27. data/lib/acfs/resource/attributes/boolean.rb +39 -0
  28. data/lib/acfs/resource/attributes/date_time.rb +32 -0
  29. data/lib/acfs/resource/attributes/dict.rb +39 -0
  30. data/lib/acfs/resource/attributes/float.rb +33 -0
  31. data/lib/acfs/resource/attributes/integer.rb +29 -0
  32. data/lib/acfs/resource/attributes/list.rb +36 -0
  33. data/lib/acfs/resource/attributes/string.rb +26 -0
  34. data/lib/acfs/resource/attributes/uuid.rb +48 -0
  35. data/lib/acfs/resource/dirty.rb +37 -0
  36. data/lib/acfs/resource/initialization.rb +31 -0
  37. data/lib/acfs/resource/loadable.rb +35 -0
  38. data/lib/acfs/resource/locatable.rb +135 -0
  39. data/lib/acfs/resource/operational.rb +26 -0
  40. data/lib/acfs/resource/persistence.rb +258 -0
  41. data/lib/acfs/resource/query_methods.rb +266 -0
  42. data/lib/acfs/resource/service.rb +44 -0
  43. data/lib/acfs/resource/validation.rb +49 -0
  44. data/lib/acfs/response.rb +30 -0
  45. data/lib/acfs/response/formats.rb +27 -0
  46. data/lib/acfs/response/status.rb +33 -0
  47. data/lib/acfs/rspec.rb +13 -0
  48. data/lib/acfs/runner.rb +102 -0
  49. data/lib/acfs/service.rb +94 -0
  50. data/lib/acfs/service/middleware.rb +58 -0
  51. data/lib/acfs/service/middleware/stack.rb +65 -0
  52. data/lib/acfs/singleton_resource.rb +85 -0
  53. data/lib/acfs/stub.rb +199 -0
  54. data/lib/acfs/util.rb +22 -0
  55. data/lib/acfs/version.rb +16 -0
  56. data/lib/acfs/yard.rb +6 -0
  57. data/spec/acfs/adapter/typhoeus_spec.rb +55 -0
  58. data/spec/acfs/collection_spec.rb +157 -0
  59. data/spec/acfs/configuration_spec.rb +53 -0
  60. data/spec/acfs/global_spec.rb +140 -0
  61. data/spec/acfs/location_spec.rb +25 -0
  62. data/spec/acfs/middleware/json_spec.rb +79 -0
  63. data/spec/acfs/middleware/msgpack_spec.rb +62 -0
  64. data/spec/acfs/operation_spec.rb +12 -0
  65. data/spec/acfs/request/callbacks_spec.rb +48 -0
  66. data/spec/acfs/request_spec.rb +79 -0
  67. data/spec/acfs/resource/attributes/boolean_spec.rb +58 -0
  68. data/spec/acfs/resource/attributes/date_time_spec.rb +51 -0
  69. data/spec/acfs/resource/attributes/dict_spec.rb +77 -0
  70. data/spec/acfs/resource/attributes/float_spec.rb +61 -0
  71. data/spec/acfs/resource/attributes/integer_spec.rb +36 -0
  72. data/spec/acfs/resource/attributes/list_spec.rb +60 -0
  73. data/spec/acfs/resource/attributes/uuid_spec.rb +42 -0
  74. data/spec/acfs/resource/attributes_spec.rb +179 -0
  75. data/spec/acfs/resource/dirty_spec.rb +49 -0
  76. data/spec/acfs/resource/initialization_spec.rb +36 -0
  77. data/spec/acfs/resource/loadable_spec.rb +22 -0
  78. data/spec/acfs/resource/locatable_spec.rb +118 -0
  79. data/spec/acfs/resource/persistance_spec.rb +322 -0
  80. data/spec/acfs/resource/query_methods_spec.rb +548 -0
  81. data/spec/acfs/resource/validation_spec.rb +129 -0
  82. data/spec/acfs/response/formats_spec.rb +52 -0
  83. data/spec/acfs/response/status_spec.rb +71 -0
  84. data/spec/acfs/runner_spec.rb +95 -0
  85. data/spec/acfs/service/middleware_spec.rb +35 -0
  86. data/spec/acfs/service_spec.rb +48 -0
  87. data/spec/acfs/singleton_resource_spec.rb +17 -0
  88. data/spec/acfs/stub_spec.rb +345 -0
  89. data/spec/acfs_spec.rb +205 -0
  90. data/spec/fixtures/config.yml +14 -0
  91. data/spec/spec_helper.rb +42 -0
  92. data/spec/support/hash.rb +11 -0
  93. data/spec/support/response.rb +12 -0
  94. data/spec/support/service.rb +92 -0
  95. data/spec/support/shared/find_callbacks.rb +50 -0
  96. metadata +159 -26
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Acfs::Collection do
6
+ let(:model) { MyUser }
7
+
8
+ describe 'Pagination' do
9
+ let(:params) { {} }
10
+ let!(:collection) { model.all params }
11
+
12
+ subject { Acfs.run; collection }
13
+
14
+ context 'without explicit page parameter' do
15
+ before do
16
+ stub_request(:get, 'http://users.example.org/users')
17
+ .to_return response([{id: 1, name: 'Anon', age: 12, born_at: 'Berlin'}],
18
+ headers: {
19
+ 'X-Total-Pages' => '2',
20
+ 'X-Total-Count' => '10'
21
+ })
22
+ end
23
+
24
+ its(:total_pages) { should eq 2 }
25
+ its(:current_page) { should eq 1 }
26
+ its(:total_count) { should eq 10 }
27
+ end
28
+
29
+ context 'with page parameter' do
30
+ let(:params) { {page: 2} }
31
+ before do
32
+ stub_request(:get, 'http://users.example.org/users?page=2')
33
+ .to_return response([{id: 1, name: 'Anon', age: 12, born_at: 'Berlin'}],
34
+ headers: {
35
+ 'X-Total-Pages' => '2',
36
+ 'X-Total-Count' => '10'
37
+ })
38
+ end
39
+
40
+ its(:total_pages) { should eq 2 }
41
+ its(:current_page) { should eq 2 }
42
+ its(:total_count) { should eq 10 }
43
+ end
44
+
45
+ context 'with non-numerical page parameter' do
46
+ let(:params) { {page: 'e546f5'} }
47
+ before do
48
+ stub_request(:get, 'http://users.example.org/users?page=e546f5')
49
+ .to_return response([{id: 1, name: 'Anon', age: 12, born_at: 'Berlin'}],
50
+ headers: {
51
+ 'X-Total-Pages' => '2',
52
+ 'X-Total-Count' => '10'
53
+ })
54
+ end
55
+
56
+ its(:total_pages) { should eq 2 }
57
+ its(:current_page) { should eq 'e546f5' }
58
+ its(:total_count) { should eq 10 }
59
+ end
60
+
61
+ describe '#next_page' do
62
+ before do
63
+ stub_request(:get, 'http://users.example.org/users')
64
+ .to_return response([{id: 1, name: 'Anon', age: 12, born_at: 'Berlin'}],
65
+ headers: {
66
+ 'X-Total-Pages' => '2',
67
+ 'Link' => '<http://users.example.org/users?page=2>; rel="next"'
68
+ })
69
+ end
70
+ let!(:req) do
71
+ stub_request(:get, 'http://users.example.org/users?page=2').to_return response([])
72
+ end
73
+ let!(:collection) { model.all }
74
+ subject { Acfs.run; collection.next_page }
75
+
76
+ it { should be_a Acfs::Collection }
77
+
78
+ it 'should have fetched page 2' do
79
+ subject
80
+ Acfs.run
81
+ expect(req).to have_been_requested
82
+ end
83
+ end
84
+
85
+ describe '#prev_page' do
86
+ before do
87
+ stub_request(:get, 'http://users.example.org/users?page=2')
88
+ .to_return response([{id: 2, name: 'Anno', age: 1604, born_at: 'Santa Maria'}],
89
+ headers: {
90
+ 'X-Total-Pages' => '2',
91
+ 'Link' => '<http://users.example.org/users>; rel="prev"'
92
+ })
93
+ end
94
+ let!(:req) do
95
+ stub_request(:get, 'http://users.example.org/users').to_return response([])
96
+ end
97
+ let!(:collection) { model.all page: 2 }
98
+ subject { Acfs.run; collection.prev_page }
99
+
100
+ it { should be_a Acfs::Collection }
101
+
102
+ it 'should have fetched page 1' do
103
+ subject
104
+ Acfs.run
105
+ expect(req).to have_been_requested
106
+ end
107
+ end
108
+
109
+ describe '#first_page' do
110
+ before do
111
+ stub_request(:get, 'http://users.example.org/users?page=2')
112
+ .to_return response([{id: 2, name: 'Anno', age: 1604, born_at: 'Santa Maria'}],
113
+ headers: {
114
+ 'X-Total-Pages' => '2',
115
+ 'Link' => '<http://users.example.org/users>; rel="first"'
116
+ })
117
+ end
118
+ let!(:req) do
119
+ stub_request(:get, 'http://users.example.org/users').to_return response([])
120
+ end
121
+ let!(:collection) { model.all page: 2 }
122
+ subject { Acfs.run; collection.first_page }
123
+
124
+ it { should be_a Acfs::Collection }
125
+
126
+ it 'should have fetched page 1' do
127
+ subject
128
+ Acfs.run
129
+ expect(req).to have_been_requested
130
+ end
131
+ end
132
+
133
+ describe '#last_page' do
134
+ before do
135
+ stub_request(:get, 'http://users.example.org/users?page=2')
136
+ .to_return response([{id: 2, name: 'Anno', age: 1604, born_at: 'Santa Maria'}],
137
+ headers: {
138
+ 'X-Total-Pages' => '2',
139
+ 'Link' => '<http://users.example.org/users?page=12>; rel="last"'
140
+ })
141
+ end
142
+ let!(:req) do
143
+ stub_request(:get, 'http://users.example.org/users?page=12').to_return response([])
144
+ end
145
+ let!(:collection) { model.all page: 2 }
146
+ subject { Acfs.run; collection.last_page }
147
+
148
+ it { should be_a Acfs::Collection }
149
+
150
+ it 'should have fetched page 1' do
151
+ subject
152
+ Acfs.run
153
+ expect(req).to have_been_requested
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Acfs::Configuration do
6
+ let(:cfg) { Acfs::Configuration.new }
7
+ before { @configuration = Acfs::Configuration.current.dup }
8
+ after { Acfs::Configuration.set @configuration }
9
+
10
+ describe 'Acfs.configure' do
11
+ it 'should invoke configure on current configuration' do
12
+ expect(Acfs::Configuration.current).to receive(:configure).once.and_call_original
13
+
14
+ Acfs.configure do |c|
15
+ expect(c).to be_a Acfs::Configuration
16
+ end
17
+ end
18
+ end
19
+
20
+ describe '.load' do
21
+ it 'should be able to load YAML' do
22
+ cfg.configure do
23
+ load 'spec/fixtures/config.yml'
24
+ end
25
+
26
+ expect(cfg.locate(UserService).to_s).to be == 'http://localhost:3001/'
27
+ expect(cfg.locate(CommentService).to_s).to be == 'http://localhost:3002/'
28
+ end
29
+
30
+ context 'with RACK_ENV' do
31
+ before { @env = ENV['RACK_ENV']; ENV['RACK_ENV'] = 'production' }
32
+ after { ENV['RACK_ENV'] = @env }
33
+
34
+ it 'should load ENV block' do
35
+ cfg.configure do
36
+ load 'spec/fixtures/config.yml'
37
+ end
38
+
39
+ expect(cfg.locate(UserService).to_s).to be == 'http://user.example.org/'
40
+ expect(cfg.locate(CommentService).to_s).to be == 'http://comment.example.org/'
41
+ end
42
+ end
43
+ end
44
+
45
+ describe '#adapter' do
46
+ let(:object) { Object.new }
47
+
48
+ it 'should be a accessor' do
49
+ cfg.adapter = object
50
+ expect(cfg.adapter).to eq object
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ class NotificationCollector
6
+ def call(*args)
7
+ events << ActiveSupport::Notifications::Event.new(*args)
8
+ end
9
+
10
+ def events
11
+ @events ||= []
12
+ end
13
+ end
14
+
15
+ describe ::Acfs::Global do
16
+ let(:adapter) { ::NullAdapter.new }
17
+ let(:runner) { double 'runner' }
18
+ let(:collector) { NotificationCollector.new }
19
+ let(:acfs) { Object.new.tap {|o| o.extend ::Acfs::Global } }
20
+
21
+ describe 'instrumentation' do
22
+ before do
23
+ # allow(runner).to receive(:start)
24
+ allow(acfs).to receive(:runner).and_return runner
25
+ end
26
+
27
+ describe '#run' do
28
+ before do
29
+ ::ActiveSupport::Notifications.subscribe 'acfs.run', collector
30
+ end
31
+ it 'should trigger event' do
32
+ Acfs.run
33
+ expect(collector.events).to have(1).items
34
+ end
35
+ end
36
+
37
+ describe '#reset' do
38
+ before do
39
+ ::ActiveSupport::Notifications.subscribe 'acfs.reset', collector
40
+ end
41
+ it 'should trigger event' do
42
+ Acfs.reset
43
+ expect(collector.events).to have(1).items
44
+ end
45
+ end
46
+ end
47
+
48
+ describe '#on' do
49
+ before do
50
+ stub_request(:get, %r{http://users.example.org/users/\d+}).to_return(
51
+ status: 200,
52
+ body: '{}',
53
+ headers: {'Content-Type' => 'application/json'}
54
+ )
55
+ end
56
+
57
+ it 'should invoke when both resources' do
58
+ user1 = MyUser.find 1
59
+ user2 = MyUser.find 2
60
+
61
+ expect do |cb|
62
+ Acfs.on(user1, user2, &cb)
63
+ Acfs.run
64
+ end.to yield_with_args(user1, user2)
65
+ end
66
+
67
+ it 'should invoke when both resources when loaded' do
68
+ user1 = MyUser.find 1
69
+ user2 = MyUser.find 2
70
+
71
+ Acfs.on(user1, user2) do |u1, u2|
72
+ expect(u1).to be_loaded
73
+ expect(u2).to be_loaded
74
+ end
75
+ Acfs.run
76
+ end
77
+
78
+ context 'with an empty result for a find_by call' do
79
+ before do
80
+ stub_request(:get, %r{http://users.example.org/users})
81
+ .with(query: {id: '2'})
82
+ .to_return(
83
+ status: 200,
84
+ body: '{}',
85
+ headers: {'Content-Type' => 'application/json'}
86
+ )
87
+ end
88
+
89
+ it 'invokes once both requests are finished' do
90
+ user1 = MyUser.find 1
91
+ user2 = MyUser.find_by id: 2
92
+
93
+ expect do |cb|
94
+ Acfs.on(user1, user2, &cb)
95
+ Acfs.run
96
+ end.to yield_with_args(user1, be_nil)
97
+ end
98
+
99
+ it 'invokes once remaining requests are finished' do
100
+ user1 = MyUser.find 1
101
+ Acfs.run # Finish the first request
102
+
103
+ user2 = MyUser.find_by id: 2
104
+
105
+ expect do |cb|
106
+ Acfs.on(user1, user2, &cb)
107
+ Acfs.run
108
+ end.to yield_with_args(user1, be_nil)
109
+ end
110
+
111
+ it 'invokes immediately when all requests have already been finished' do
112
+ user1 = MyUser.find 1
113
+ user2 = MyUser.find_by id: 2
114
+ Acfs.run
115
+
116
+ expect do |cb|
117
+ Acfs.on(user1, user2, &cb)
118
+ end.to yield_with_args(user1, be_nil)
119
+ end
120
+ end
121
+ end
122
+
123
+ describe '#runner' do
124
+ it 'returns per-thread runner' do
125
+ runner1 = Thread.new { acfs.runner }.value
126
+ runner2 = Thread.new { acfs.runner }.value
127
+
128
+ expect(runner1).to_not equal runner2
129
+ end
130
+
131
+ it 'uses configurated adapter' do
132
+ adapter = double :adapter
133
+ expect(Acfs::Configuration.current).to receive(:adapter).and_return(-> { adapter })
134
+
135
+ runner = Thread.new { acfs.runner }.value
136
+
137
+ expect(runner.adapter).to equal adapter
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe ::Acfs::Location do
6
+ let(:location) { described_class.new(uri, args) }
7
+ let(:uri) { 'http://localhost/users/:id' }
8
+ let(:args) { {'id' => 4} }
9
+
10
+ describe '#str' do
11
+ subject(:str) { location.str }
12
+
13
+ it 'replaces variables with values' do
14
+ expect(str).to eq 'http://localhost/users/4'
15
+ end
16
+
17
+ context 'with special characters' do
18
+ let(:args) { {'id' => '4 [@(\/!^$'} }
19
+
20
+ it 'escapes special characters' do
21
+ expect(str).to eq 'http://localhost/users/4+%5B%40%28%5C%2F%21%5E%24'
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Acfs::Middleware::JSON do
6
+ let(:data) { [{id: 1, name: 'Anon'}, {id: 2, name: 'John', friends: [1]}] }
7
+ let(:body) { '' }
8
+ let(:headers) { {} }
9
+ let(:request) { Acfs::Request.new 'url', method: 'GET', data: data }
10
+ let(:response) { Acfs::Response.new request, status: 200, headers: headers, body: body }
11
+ let(:decoder) { Acfs::Middleware::JSON.new ->(req) { req } }
12
+
13
+ before do
14
+ decoder.call request
15
+ end
16
+
17
+ describe 'encode' do
18
+ context 'with not serialized request' do
19
+ it 'should set Content-Type' do
20
+ expect(request.headers['Content-Type']).to eq 'application/json'
21
+ end
22
+
23
+ it 'should append Accept header' do
24
+ expect(request.headers['Accept']).to eq 'application/json;q=1'
25
+ end
26
+
27
+ it 'should serialize data to JSON' do
28
+ expect(JSON.parse(request.body)).to eq data.map(&:stringify_keys)
29
+ end
30
+ end
31
+
32
+ context 'with #to_json objects' do
33
+ let(:data) do
34
+ Class.new do
35
+ def to_json(*)
36
+ '{"a": 1, "b": 2}'
37
+ end
38
+ end.new
39
+ end
40
+
41
+ it 'should serialize data with #to_json' do
42
+ expect(JSON.parse(request.body)).to eq 'a' => 1, 'b' => 2
43
+ end
44
+ end
45
+ end
46
+
47
+ describe 'decode' do
48
+ context 'with JSON response' do
49
+ let(:headers) { {'Content-Type' => 'application/json; charset=utf-8'} }
50
+ let(:body) { data.to_json }
51
+
52
+ it 'should decode body data' do
53
+ request.complete! response
54
+
55
+ expect(response.data).to be == data.map(&:stringify_keys)
56
+ end
57
+ end
58
+
59
+ context 'with invalid JSON response' do
60
+ let(:headers) { {'Content-Type' => 'application/json'} }
61
+ let(:body) { data.to_json[4..-4] }
62
+
63
+ it 'should raise an error' do
64
+ expect { request.complete! response }.to raise_error(::JSON::ParserError)
65
+ end
66
+ end
67
+
68
+ context 'without JSON response' do
69
+ let(:headers) { {'Content-Type' => 'application/text'} }
70
+ let(:body) { data.to_json }
71
+
72
+ it 'should not decode non-JSON encoded responses' do
73
+ request.complete! response
74
+
75
+ expect(response.data).to be_nil
76
+ end
77
+ end
78
+ end
79
+ end