syncano 4.0.0.alpha4 → 4.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/README.md +1 -13
  4. data/circle.yml +1 -1
  5. data/lib/active_attr/dirty.rb +26 -0
  6. data/lib/active_attr/typecasting/hash_typecaster.rb +34 -0
  7. data/lib/active_attr/typecasting_override.rb +29 -0
  8. data/lib/syncano.rb +9 -55
  9. data/lib/syncano/api.rb +2 -20
  10. data/lib/syncano/connection.rb +47 -48
  11. data/lib/syncano/model/associations.rb +121 -0
  12. data/lib/syncano/model/associations/base.rb +38 -0
  13. data/lib/syncano/model/associations/belongs_to.rb +30 -0
  14. data/lib/syncano/model/associations/has_many.rb +75 -0
  15. data/lib/syncano/model/associations/has_one.rb +22 -0
  16. data/lib/syncano/model/base.rb +257 -0
  17. data/lib/syncano/model/callbacks.rb +49 -0
  18. data/lib/syncano/model/scope_builder.rb +158 -0
  19. data/lib/syncano/query_builder.rb +7 -11
  20. data/lib/syncano/resources/base.rb +66 -91
  21. data/lib/syncano/schema.rb +159 -10
  22. data/lib/syncano/schema/attribute_definition.rb +0 -75
  23. data/lib/syncano/schema/resource_definition.rb +2 -24
  24. data/lib/syncano/version.rb +1 -1
  25. data/spec/integration/syncano_spec.rb +26 -268
  26. data/spec/spec_helper.rb +1 -3
  27. data/spec/unit/connection_spec.rb +74 -34
  28. data/spec/unit/query_builder_spec.rb +2 -2
  29. data/spec/unit/resources_base_spec.rb +64 -125
  30. data/spec/unit/schema/resource_definition_spec.rb +3 -24
  31. data/spec/unit/schema_spec.rb +55 -5
  32. data/spec/unit/syncano_spec.rb +9 -45
  33. data/syncano.gemspec +0 -5
  34. metadata +14 -87
  35. data/lib/syncano/api/endpoints.rb +0 -17
  36. data/lib/syncano/poller.rb +0 -55
  37. data/lib/syncano/resources.rb +0 -158
  38. data/lib/syncano/resources/paths.rb +0 -48
  39. data/lib/syncano/resources/resource_invalid.rb +0 -15
  40. data/lib/syncano/response.rb +0 -55
  41. data/lib/syncano/schema/endpoints_whitelist.rb +0 -40
  42. data/lib/syncano/upload_io.rb +0 -7
  43. data/spec/unit/resources/paths_spec.rb +0 -21
  44. data/spec/unit/response_spec.rb +0 -75
  45. data/spec/unit/schema/attribute_definition_spec.rb +0 -18
@@ -7,7 +7,6 @@ Dotenv.load
7
7
  require 'rspec-prof' if ENV['SPEC_PROFILE']
8
8
  require 'syncano'
9
9
  require 'webmock/rspec'
10
- require 'celluloid/test'
11
10
 
12
11
  WebMock.disable_net_connect!
13
12
 
@@ -17,5 +16,4 @@ end
17
16
 
18
17
  def endpoint_uri(path)
19
18
  [Syncano::Connection.api_root,"v1", path].join("/")
20
- end
21
-
19
+ end
@@ -7,68 +7,108 @@ describe Syncano::Connection do
7
7
  specify { expect(described_class.new).to_not be_authenticated }
8
8
  end
9
9
 
10
- describe '#authenticate' do
11
- subject { described_class.new email: email, password: password }
10
+ describe '#request' do
11
+ let(:api_key) { '87a7da987da98sd7a98' }
12
12
 
13
- let(:authenticate_uri) { endpoint_uri('account/auth/') }
14
- let(:email) { 'kiszka@koza.com' }
15
- let(:password) { 'kiszonka' }
16
- let(:success_status) { 200 }
17
- let(:unauthorized_status) { 401 }
13
+ subject { described_class.new(api_key: api_key) }
18
14
 
19
- context 'successful' do
15
+ context 'called with unsupported method' do
16
+ specify do
17
+ expect { subject.request :koza, 'fafarafa' }.
18
+ to raise_error(RuntimeError, 'Unsupported method "koza"')
19
+ end
20
+ end
21
+
22
+ context 'called with supported method' do
20
23
  before do
21
- expect(subject).to receive(:request).
22
- with(:post, described_class::AUTH_PATH, email: email, password: password).
23
- and_return('account_key' => 'kEy')
24
+ stub_request(:get, endpoint_uri('somepath/')).
25
+ with(headers: {'X-Api-Key'=>'87a7da987da98sd7a98'}).
26
+ to_return(body: generate_body(some: 'response'))
24
27
  end
25
28
 
26
29
  specify do
27
- expect { subject.authenticate }.to change { subject.authenticated? }
30
+ expect(subject.request(:get, 'somepath/')).to eq('some' => 'response')
28
31
  end
29
32
  end
30
33
 
31
- context 'failed' do
34
+ context 'called with supported method returning an error' do
32
35
  before do
33
- expect(subject).to receive(:request).and_raise('auth failed')
36
+ stub_request(:post, endpoint_uri('instances/')).
37
+ with(body: { 'name' => 'koza' },
38
+ headers: {'X-Api-Key'=>'87a7da987da98sd7a98'}).
39
+ to_return(body: generate_body({name: ['This field can not be "koza"']}),
40
+ status: 400)
41
+ end
34
42
 
43
+ specify do
44
+ expect { subject.request(:post, '/v1/instances/', { name: "koza" }) }.
45
+ to raise_error(Syncano::ClientError)
46
+ end
47
+ end
48
+
49
+ context 'successful returning empty body' do
50
+ before do
51
+ stub_request(:delete, endpoint_uri('instances/kiszonka/')).
52
+ with(headers: {'X-Api-Key'=>'87a7da987da98sd7a98'}).
53
+ to_return(body: nil, status: 204)
35
54
  end
36
55
 
37
56
  specify do
38
- expect { subject.authenticate }.to raise_error('auth failed')
57
+ expect { subject.request(:delete, '/v1/instances/kiszonka/', {}) }.
58
+ to_not raise_error
39
59
  end
60
+
40
61
  end
41
62
  end
42
63
 
43
- describe '#request' do
44
- let(:headers) { { 'X-Api-Key'=>'87a7da987da98sd7a98', 'User-Agent' => "Syncano Ruby Gem #{Syncano::VERSION}" } }
45
- let(:api_key) { '87a7da987da98sd7a98' }
46
- let(:connection_params) { { api_key: api_key, user_key: 'Us3rK3y' } }
47
- let(:raw_response) { { body: 'koza' } }
48
- let(:handled_response) { double :handled_response }
64
+ describe '#authenticate' do
65
+ subject { described_class.new }
49
66
 
50
- subject { described_class.new(connection_params) }
67
+ let(:authenticate_uri) { endpoint_uri('account/auth/') }
68
+ let(:email) { 'kiszka@koza.com' }
69
+ let(:password) { 'kiszonka' }
70
+ let(:success_status) { 200 }
71
+ let(:unauthorized_status) { 401 }
51
72
 
52
- context 'with supported method' do
73
+ context 'successful' do
53
74
  before do
54
- stub_request(:get, endpoint_uri('user/method/')).
55
- with(headers: headers).to_return(raw_response)
75
+ stub_request(:post, authenticate_uri).
76
+ with(body: { 'email' => email, 'password' => password } ).
77
+ to_return(body: successful_body, status: success_status)
78
+ end
56
79
 
57
- expect(Syncano::Response).
58
- to receive(:handle) { |raw_response| expect(raw_response.body).to eq('koza') }.
59
- and_return(handled_response)
80
+ it 'should get an API key' do
81
+ expect { subject.authenticate(email, password) }.to change { subject.authenticated? }
60
82
  end
61
83
 
62
- specify do
63
- expect(subject.request(:get, 'user/method/')).to eq(handled_response)
84
+ def successful_body
85
+ generate_body id: 15,
86
+ email: email,
87
+ first_name: '',
88
+ last_name: '',
89
+ account_key: 'kozakoza123'
64
90
  end
65
91
  end
66
92
 
67
- context 'with unsupported method' do
68
- specify do
69
- expect { subject.request :koza, 'fafarafa' }.
70
- to raise_error(RuntimeError, 'Unsupported method "koza"')
93
+ context 'failed' do
94
+ before do
95
+ stub_request(:post, authenticate_uri).
96
+ with(body: { 'email' => email, 'password' => password }).
97
+ to_return(body: failed_body, status: unauthorized_status)
98
+
99
+ end
100
+
101
+ it 'should raise an exception' do
102
+ expect { subject.authenticate(email, password) }.to raise_error(Syncano::ClientError)
103
+ end
104
+
105
+ def failed_body
106
+ generate_body detail: 'Invalid email or password.'
71
107
  end
72
108
  end
73
109
  end
110
+
111
+ def generate_body(params)
112
+ JSON.generate params
113
+ end
74
114
  end
@@ -23,14 +23,14 @@ describe Syncano::QueryBuilder do
23
23
 
24
24
  describe '.first' do
25
25
  specify do
26
- expect(resource_class).to receive(:first).with(connection, scope_parameters, {})
26
+ expect(resource_class).to receive(:first).with(connection, scope_parameters)
27
27
  subject.first
28
28
  end
29
29
  end
30
30
 
31
31
  describe '.last' do
32
32
  specify do
33
- expect(resource_class).to receive(:last).with(connection, scope_parameters, {})
33
+ expect(resource_class).to receive(:last).with(connection, scope_parameters)
34
34
  subject.last
35
35
  end
36
36
  end
@@ -5,90 +5,90 @@ require 'rspec/expectations'
5
5
 
6
6
  describe Syncano::Resources::Base do
7
7
  subject do
8
- Syncano::Resources.new_resource_class(
9
- Syncano::Schema::ResourceDefinition.new('same_name',
10
- { :attributes => { 'name' => { 'read_only' => false,
8
+ Syncano::Schema.send(:new_resource_class,
9
+ {:attributes => {'name' => {'read_only' => false,
11
10
  'primary_key' => true,
12
11
  'required' => true,
13
12
  'label' => 'name',
14
13
  'max_length' => 64,
15
- 'type' => 'string' },
14
+ 'type' => 'string'},
16
15
 
17
- 'created_at' => { 'read_only' => true,
16
+ 'created_at' => {'read_only' => true,
18
17
  'required' => false,
19
18
  'type' => 'datetime',
20
- 'label' => 'created at' },
21
- 'updated_at' => { 'read_only' => true,
19
+ 'label' => 'created at'},
20
+ 'updated_at' => {'read_only' => true,
22
21
  'required' => false,
23
22
  'type' => 'datetime',
24
- 'label' => 'updated at' },
25
- 'role' => { 'read_only' => true,
23
+ 'label' => 'updated at'},
24
+ 'role' => {'read_only' => true,
26
25
  'required' => false,
27
- 'type' => 'field' },
28
- 'owner' => { 'first_name' => { 'read_only' => false,
26
+ 'type' => 'field'},
27
+ 'owner' => {'first_name' => {'read_only' => false,
29
28
  'max_length' => 35,
30
29
  'required' => false,
31
30
  'type' => 'string',
32
- 'label' => 'first name' },
33
- 'last_name' => { 'read_only' => false,
31
+ 'label' => 'first name'},
32
+ 'last_name' => {'read_only' => false,
34
33
  'max_length' => 35,
35
34
  'required' => false,
36
35
  'type' => 'string',
37
- 'label' => 'last name' },
38
- 'id' => { 'read_only' => true,
36
+ 'label' => 'last name'},
37
+ 'id' => {'read_only' => true,
39
38
  'required' => false,
40
39
  'type' => 'integer',
41
- 'label' => 'ID' },
42
- 'email' => { 'read_only' => false,
40
+ 'label' => 'ID'},
41
+ 'email' => {'read_only' => false,
43
42
  'max_length' => 254,
44
43
  'required' => true,
45
44
  'type' => 'email',
46
- 'label' => 'email address' } },
47
- 'metadata' => { 'read_only' => false,
45
+ 'label' => 'email address'}},
46
+ 'metadata' => {'read_only' => false,
48
47
  'required' => false,
49
48
  'type' => 'field',
50
- 'label' => 'metadata' },
51
- 'description' => { 'read_only' => false,
49
+ 'label' => 'metadata'},
50
+ 'description' => {'read_only' => false,
52
51
  'required' => false,
53
52
  'type' => 'string',
54
- 'label' => 'description' } },
55
- :associations => { 'read_only' => true,
53
+ 'label' => 'description'}},
54
+ :associations => {'read_only' => true,
56
55
  'required' => false,
57
56
  'type' => 'links',
58
- 'links' => [{ 'type' => 'detail',
59
- 'name' => 'self' },
60
- { 'type' => 'list',
61
- 'name' => 'admins' },
62
- { 'type' => 'list',
63
- 'name' => 'classes' },
64
- { 'type' => 'list',
65
- 'name' => 'codeboxes' },
66
- { 'type' => 'list',
67
- 'name' => 'runtimes' },
68
- { 'type' => 'list',
69
- 'name' => 'invitations' },
70
- { 'type' => 'list',
71
- 'name' => 'api_keys' },
72
- { 'type' => 'list',
73
- 'name' => 'triggers' },
74
- { 'type' => 'list',
75
- 'name' => 'webhooks' }] },
76
- :collection => { :path => '/v1/instances/',
77
- :http_methods => ['post',
57
+ 'links' => [{'type' => 'detail',
58
+ 'name' => 'self'},
59
+ {'type' => 'list',
60
+ 'name' => 'admins'},
61
+ {'type' => 'list',
62
+ 'name' => 'classes'},
63
+ {'type' => 'list',
64
+ 'name' => 'codeboxes'},
65
+ {'type' => 'list',
66
+ 'name' => 'runtimes'},
67
+ {'type' => 'list',
68
+ 'name' => 'invitations'},
69
+ {'type' => 'list',
70
+ 'name' => 'api_keys'},
71
+ {'type' => 'list',
72
+ 'name' => 'triggers'},
73
+ {'type' => 'list',
74
+ 'name' => 'webhooks'}]},
75
+ :collection => {:path => '/v1/instances/',
76
+ :http_methods => ['post',
78
77
  'get'],
79
- :params => [] },
80
- :member => { :path => '/v1/instances/{ name }/',
78
+ :params => []},
79
+ :member => {:path => '/v1/instances/{name}/',
81
80
  :http_methods => ['put',
82
81
  'get',
83
82
  'patch',
84
83
  'delete'],
85
- :params => ['name'] },
86
- :custom_methods => [] })
84
+ :params => ['name']},
85
+ :custom_methods => []},
86
+ 'some_name'
87
87
  )
88
88
  end
89
89
 
90
- let(:connection) { double('connection') }
91
- let(:scope_parameters) { double('scope_parameters') }
90
+ let(:connection) { double('connection')}
91
+ let(:scope_parameters) { double('scope_parameters')}
92
92
 
93
93
  describe '.find' do
94
94
  let(:response) {
@@ -140,7 +140,7 @@ describe Syncano::Resources::Base do
140
140
  end
141
141
 
142
142
  it 'should init attributes' do
143
- resource = subject.new(connection, {}, { name: 'test' })
143
+ resource = subject.new(connection, {}, { name: 'test' }, true)
144
144
  expect(resource.name).to eq('test')
145
145
  end
146
146
 
@@ -153,94 +153,33 @@ describe Syncano::Resources::Base do
153
153
  resource = subject.new(connection, {}, { links: { self: '/v1/instances/test/' }, name: 'test' }, false)
154
154
  expect(resource.changed?).to eq(true)
155
155
  end
156
- end
157
-
158
- describe '#new_record?' do
159
- let(:resource) { subject.new connection, {}, { name: 'asd' }, from_db }
160
-
161
- context 'is true' do
162
- let(:from_db) { false }
163
156
 
164
- specify { expect(resource.new_record?).to eq(true) }
157
+ it 'should mark resource as not new if initialized with self path' do
158
+ resource = subject.new(connection, {}, { links: { self: '/v1/instances/test/' } }, true)
159
+ expect(resource.new_record?).to be(false)
165
160
  end
166
161
 
167
- context 'is false' do
168
- let(:from_db) { true }
169
-
170
- specify { expect(resource.new_record?).to eq(false) }
162
+ it 'should mark resource as new if initialized without self path' do
163
+ resource = subject.new(connection, {}, { name: 'test' }, true)
164
+ expect(resource.new_record?).to be(true)
171
165
  end
172
166
  end
173
167
 
174
- describe "persisting data" do
175
- shared_context "resource invalid" do
176
- before { expect(resource).to receive(:valid?) { false } }
177
- end
178
-
179
- shared_context "resource valid" do
180
- before do
181
- # expect(instance_of(Syncano::Resource::Base)).to receive(:valid?) { true }
182
-
183
- expect(resource).to receive(:valid?) { true }
184
-
185
- expect(connection).to receive(:request).
186
- with(instance_of(Symbol), instance_of(String), instance_of(Hash)).
187
- and_return({})
188
- end
189
- end
190
-
191
- let(:resource) { subject.new connection, {}, {}, false }
168
+ describe '.create' do
169
+ it 'should create new object in Syncano' do
192
170
 
193
- describe ".create!" do
194
- before do
195
- expect(subject).to receive(:new).and_return(resource)
196
- expect(resource).to receive(:save!).and_return(resource)
197
- end
198
-
199
- specify { expect(subject.create!(connection, {}, {})).to eq(resource) }
200
171
  end
172
+ end
201
173
 
202
- describe ".create" do
203
- before do
204
- expect(subject).to receive(:new).and_return(resource)
205
- expect(resource).to receive(:save).and_return(false)
206
- end
207
-
208
- specify { expect(subject.create(connection, {}, {})).to eq(resource) }
209
- specify { expect(subject.create(connection, {}, {}).new_record?).to eq(true) }
210
- end
211
-
212
- describe "#save" do
213
- context "when invalid" do
214
- include_context "resource invalid"
215
-
216
- specify { expect(resource.save).to eq(false) }
217
- end
218
-
219
- context "when valid" do
220
- include_context "resource valid"
174
+ describe '.update_attributes' do
175
+ it 'should create update object\'s attributes in Syncano' do
221
176
 
222
- specify { expect(resource.save).to be_kind_of(Syncano::Resources::Base) }
223
- end
224
177
  end
178
+ end
225
179
 
226
- describe "#save!" do
227
- context "when invalid" do
228
- include_context "resource invalid"
229
-
230
- specify do
231
- expect {
232
- resource.save!
233
- }.to raise_error(Syncano::Resources::ResourceInvalid)
234
- end
235
- end
236
-
237
- context "when valid" do
238
- include_context "resource valid"
180
+ describe '.destroy' do
181
+ it 'should delete object from Syncano' do
239
182
 
240
- specify do
241
- expect(resource.save!).to be_kind_of(Syncano::Resources::Base)
242
- end
243
- end
244
183
  end
245
184
  end
246
185
  end
@@ -1,35 +1,14 @@
1
1
  require_relative '../../spec_helper'
2
2
 
3
3
  describe Syncano::Schema::ResourceDefinition do
4
- let(:attribute) { double 'attribute' }
5
- let(:definition) do
6
- described_class.new(name, attributes: { 'koza' => raw_attribute },
7
- associations: {
8
- 'links' => [{ 'name' => 'koza', 'type' => 'detail' }]
9
- })
10
- end
11
- let(:name) { 'Kiszka' }
12
4
  let(:raw_attribute) { double 'raw attribute' }
5
+ let(:attribute) { double 'attribute' }
13
6
 
14
7
  before do
15
8
  expect(Syncano::Schema::AttributeDefinition).to receive(:new).with('koza', raw_attribute).and_return(attribute)
16
9
  end
17
10
 
18
- specify { expect(definition.attributes).to eq([attribute]) }
19
-
20
- specify { expect(definition.name).to eq(name) }
21
-
22
- it 'should delete colliding links' do
23
- expect(definition[:associations]['links']).to be_empty
24
- end
25
-
26
- describe 'top_level?' do
27
- context "when is top level" do
28
- pending "to be done"
29
- end
30
-
31
- context "when is not top level" do
32
- pending "to be done"
33
- end
11
+ it 'should create AttributeDefinition objects' do
12
+ expect(described_class.new(attributes: { 'koza' => raw_attribute }).attributes).to eq([attribute])
34
13
  end
35
14
  end