syncano 4.0.0.alpha4 → 4.0.0.pre

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 (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