syncano 4.0.0.alpha1 → 4.0.0.alpha2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -157
  3. data/circle.yml +1 -1
  4. data/lib/syncano.rb +38 -11
  5. data/lib/syncano/api.rb +20 -2
  6. data/lib/syncano/api/endpoints.rb +17 -0
  7. data/lib/syncano/connection.rb +46 -54
  8. data/lib/syncano/poller.rb +55 -0
  9. data/lib/syncano/resources.rb +48 -16
  10. data/lib/syncano/resources/base.rb +46 -56
  11. data/lib/syncano/resources/paths.rb +48 -0
  12. data/lib/syncano/resources/resource_invalid.rb +15 -0
  13. data/lib/syncano/response.rb +55 -0
  14. data/lib/syncano/schema.rb +10 -29
  15. data/lib/syncano/schema/attribute_definition.rb +2 -2
  16. data/lib/syncano/schema/endpoints_whitelist.rb +40 -0
  17. data/lib/syncano/schema/resource_definition.rb +5 -0
  18. data/lib/syncano/upload_io.rb +7 -0
  19. data/lib/syncano/version.rb +1 -1
  20. data/spec/integration/syncano_spec.rb +220 -15
  21. data/spec/spec_helper.rb +3 -1
  22. data/spec/unit/connection_spec.rb +34 -97
  23. data/spec/unit/resources/paths_spec.rb +21 -0
  24. data/spec/unit/resources_base_spec.rb +77 -16
  25. data/spec/unit/response_spec.rb +75 -0
  26. data/spec/unit/schema/resource_definition_spec.rb +10 -0
  27. data/spec/unit/schema_spec.rb +5 -55
  28. data/syncano.gemspec +4 -0
  29. metadata +69 -13
  30. data/lib/active_attr/dirty.rb +0 -26
  31. data/lib/active_attr/typecasting/hash_typecaster.rb +0 -34
  32. data/lib/active_attr/typecasting_override.rb +0 -29
  33. data/lib/syncano/model/associations.rb +0 -121
  34. data/lib/syncano/model/associations/base.rb +0 -38
  35. data/lib/syncano/model/associations/belongs_to.rb +0 -30
  36. data/lib/syncano/model/associations/has_many.rb +0 -75
  37. data/lib/syncano/model/associations/has_one.rb +0 -22
  38. data/lib/syncano/model/base.rb +0 -257
  39. data/lib/syncano/model/callbacks.rb +0 -49
  40. data/lib/syncano/model/scope_builder.rb +0 -158
@@ -1,35 +1,26 @@
1
1
  require_relative './schema/attribute_definition'
2
2
  require_relative './schema/resource_definition'
3
+ require_relative './schema/endpoints_whitelist'
4
+
5
+ require 'singleton'
3
6
 
4
7
  module Syncano
5
8
  class Schema
6
- SCHEMA_PATH = 'schema/'
7
9
 
8
10
  attr_reader :schema
9
11
 
10
- def initialize(connection)
11
- self.connection = connection
12
- load_schema
12
+ def self.schema_path
13
+ "/#{Syncano::Connection::API_VERSION}/schema/"
13
14
  end
14
15
 
15
- def process!
16
- schema.each do |name, raw_resource_definition|
17
- resource_definition = Syncano::Schema::ResourceDefinition.new(name, raw_resource_definition)
18
- resource_class = ::Syncano::Resources.define_resource_class(resource_definition)
19
-
20
- if resource_definition[:collection].present? && resource_definition[:collection][:path].scan(/\{([^}]+)\}/).empty?
21
- self.class.generate_client_method(name, resource_class)
22
- end
23
- end
16
+ def initialize(connection = ::Syncano::Connection.new)
17
+ self.connection = connection
24
18
  end
25
19
 
26
- private
27
-
28
20
  attr_accessor :connection
29
- attr_writer :schema
30
21
 
31
- def load_schema
32
- raw_schema = connection.request(:get, SCHEMA_PATH)
22
+ def definition
23
+ raw_schema = connection.request(:get, self.class.schema_path)
33
24
  resources = {}
34
25
 
35
26
  raw_schema.each do |resource_schema|
@@ -70,17 +61,7 @@ module Syncano
70
61
  end
71
62
  end
72
63
 
73
- self.schema = resources
74
- end
75
-
76
- class << self
77
- def generate_client_method(resource_name, resource_class)
78
- method_name = resource_name.tableize
79
-
80
- ::Syncano::API.send(:define_method, method_name) do
81
- ::Syncano::QueryBuilder.new(connection, resource_class)
82
- end
83
- end
64
+ resources
84
65
  end
85
66
  end
86
67
  end
@@ -22,8 +22,8 @@ module Syncano
22
22
  set_default
23
23
  end
24
24
 
25
- def force_default?
26
- !default.nil?
25
+ def validate?
26
+ writable? && required?
27
27
  end
28
28
 
29
29
  def writable?
@@ -0,0 +1,40 @@
1
+ module Syncano
2
+ class Schema
3
+ class EndpointsWhitelist
4
+ include Enumerable
5
+
6
+ class SupportedDefinitionPredicate
7
+ attr_accessor :definition
8
+
9
+ def initialize(definition)
10
+ self.definition = definition
11
+ end
12
+
13
+ def call
14
+ path =~ /\A\/v1\/instances/ && path !~ /invitation/
15
+ end
16
+
17
+ private
18
+
19
+ def path
20
+ definition[:collection] && definition[:collection][:path] ||
21
+ definition[:member] && definition[:member][:path]
22
+ end
23
+ end
24
+
25
+ SUPPORTED_DEFINITIONS = -> (definition) {
26
+ SupportedDefinitionPredicate.new(definition).call
27
+ }
28
+
29
+ def initialize(schema)
30
+ @definition = schema.definition
31
+ end
32
+
33
+ def each(&block)
34
+ @definition.select { |_name, definition|
35
+ SUPPORTED_DEFINITIONS === definition
36
+ }.each &block
37
+ end
38
+ end
39
+ end
40
+ end
@@ -20,6 +20,11 @@ module Syncano
20
20
  @raw_definition[key]
21
21
  end
22
22
 
23
+ def top_level?
24
+ @raw_definition[:collection].present? &&
25
+ @raw_definition[:collection][:path].scan(/\{([^}]+)\}/).empty?
26
+ end
27
+
23
28
  private
24
29
 
25
30
  def delete_colliding_links
@@ -0,0 +1,7 @@
1
+ module Syncano
2
+ class UploadIO < Faraday::UploadIO
3
+ def initialize(path)
4
+ super path, File.mime_type?(File.new(path))
5
+ end
6
+ end
7
+ end
@@ -1,3 +1,3 @@
1
1
  module Syncano
2
- VERSION = '4.0.0.alpha1'
2
+ VERSION = '4.0.0.alpha2'
3
3
  end
@@ -4,23 +4,28 @@ WebMock.allow_net_connect!
4
4
 
5
5
  describe Syncano do
6
6
  before(:all) do
7
+ Syncano::API.send :initialized=, false
8
+
7
9
  @api_key = ENV['INTEGRATION_TEST_API_KEY']
8
10
  @api = Syncano.connect(api_key: @api_key)
9
11
  end
10
12
 
11
13
  before(:each) do
12
14
  @api.instances.all.each &:destroy
13
- @instance = @api.instances.create(name: "a#{SecureRandom.hex(24)}")
15
+ @instance = @api.instances.create(name: instance_name )
14
16
  @instance.classes.all.select { |c| c.name != 'user_profile'}.each &:destroy
15
17
  @instance.groups.all.each &:destroy
16
18
  @instance.users.all.each &:delete
17
19
  end
18
20
 
21
+ let(:instance_name) { "a#{SecureRandom.hex(24)}" }
22
+ let(:group) { @instance.groups.create name: 'wheel' }
23
+
19
24
  describe 'working with instances' do
20
25
  subject { @api.instances }
21
26
 
22
27
  it 'should raise an error on not found instance' do
23
- expect { subject.find('kaszanka') }.to raise_error(Syncano::ClientError)
28
+ expect { subject.find('kaszanka') }.to raise_error(Syncano::NotFound)
24
29
  end
25
30
 
26
31
  specify do
@@ -62,7 +67,6 @@ describe Syncano do
62
67
 
63
68
  subject { @class.objects }
64
69
 
65
-
66
70
  specify 'basic operations' do
67
71
  expect { subject.create currency: 'USD', ballance: 1337 }.to create_resource
68
72
 
@@ -79,9 +83,6 @@ describe Syncano do
79
83
  expect(object.currency).to eq('GBP')
80
84
 
81
85
  expect { subject.destroy(object.primary_key) }.to destroy_resource
82
- expect {
83
- subject.destroy(object.primary_key)
84
- }.to raise_error(Syncano::ClientError, /not found/i)
85
86
  end
86
87
 
87
88
 
@@ -161,7 +162,7 @@ describe Syncano do
161
162
 
162
163
 
163
164
  specify 'basic operations' do
164
- expect { subject.create name: 'df', source: 'puts 1337', runtime_name: 'ruby' }.to create_resource
165
+ expect { subject.create label: 'df', source: 'puts 1337', runtime_name: 'ruby' }.to create_resource
165
166
 
166
167
  codebox = subject.first
167
168
  codebox.run
@@ -169,7 +170,7 @@ describe Syncano do
169
170
  codebox.save
170
171
  codebox.run
171
172
 
172
- without_profiling { sleep 5 }
173
+ without_profiling { sleep 10 }
173
174
  traces = codebox.traces.all
174
175
 
175
176
  expect(traces.count).to eq(2)
@@ -177,13 +178,13 @@ describe Syncano do
177
178
  first = traces[1]
178
179
 
179
180
  expect(first.status).to eq('success')
180
- expect(first.result).to eq('1337')
181
+ expect(first.result["stdout"]).to eq('1337')
181
182
 
182
183
  second = traces[0]
183
184
  expect(second.status).to eq('success')
184
- expect(second.result).to eq('123')
185
+ expect(second.result["stdout"]).to eq('123')
185
186
 
186
- expect { @instance.schedules.create name: 'test', interval_sec: 30, codebox: codebox.primary_key }.
187
+ expect { @instance.schedules.create label: 'test', interval_sec: 30, codebox: codebox.primary_key }.
187
188
  to change { @instance.schedules.all.count }.by(1)
188
189
 
189
190
  expect { codebox.destroy }.to destroy_resource
@@ -193,14 +194,218 @@ describe Syncano do
193
194
  describe 'working with webhooks' do
194
195
  subject { @instance.webhooks }
195
196
 
196
- let!(:codebox) { @instance.codeboxes.create name: 'wurst', source: 'puts "currywurst"', runtime_name: 'ruby' }
197
+ describe 'using the gem' do
198
+ let!(:codebox) { @instance.codeboxes.create label: 'wurst', source: 'puts "currywurst"', runtime_name: 'ruby' }
199
+
200
+ specify do
201
+ expect { subject.create name: 'web-wurst', codebox: codebox.primary_key }.to create_resource
202
+
203
+ expect(subject.first.run['result']["stdout"]).to eq('currywurst')
204
+
205
+ expect { subject.first.destroy }.to destroy_resource
206
+ end
207
+ end
208
+
209
+ describe 'using curl' do
210
+ let(:source) {
211
+ <<-SOURCE
212
+ p ARGS["POST"]
213
+ SOURCE
214
+ }
215
+ let!(:codebox) { @instance.codeboxes.create label: 'curl', source: source, runtime_name: 'ruby' }
216
+ let!(:webhook) { subject.create name: 'web-curl', codebox: codebox.primary_key, public: true }
217
+
218
+ specify do
219
+ url = "#{ENV['API_ROOT']}/v1/instances/#{instance_name}/webhooks/p/#{webhook.public_link}/"
220
+ code = %{curl -k --form kiszka=koza -H "kiszonka: 007" -X POST #{url} 2>/dev/null}
221
+ output = JSON.parse(`#{code}`)
222
+
223
+ expect(output["status"]).to eq("success")
224
+ expect(output["result"]["stdout"]).to eq('{"kiszka"=>"koza"}')
225
+ end
226
+ end
227
+ end
228
+
229
+ describe 'working with API keys' do
230
+ subject { @instance.api_keys }
231
+
232
+ specify do
233
+ api_key = nil
234
+
235
+ expect {
236
+ api_key = subject.create allow_user_create: true
237
+ }.to create_resource
238
+
239
+ expect { api_key.destroy }.to destroy_resource
240
+ end
241
+ end
242
+
243
+ describe 'managing users' do
244
+ subject { @instance.users }
245
+
246
+ let(:user_profile) { @instance.classes.find("user_profile") }
247
+
248
+ before do
249
+ user_profile.schema = [{ name: "nickname", type: "text" },
250
+ { name: "resume", type: "file" }]
251
+ user_profile.save
252
+ end
253
+
254
+ specify do
255
+ user = nil
256
+
257
+ expect {
258
+ user = subject.create(username: 'koza', password: 'kiszkakoza')
259
+ }.to create_resource
260
+
261
+ user.update_attributes username: 'kiszka'
262
+ expect(subject.find(user.primary_key).username).to eq('kiszka')
263
+
264
+
265
+ profile = @instance.classes.find("user_profile").objects.find(1)
266
+ profile.nickname = "k0z4"
267
+ profile.resume = Syncano::UploadIO.new(File.absolute_path(__FILE__))
268
+ profile.save
269
+
270
+ expect(profile.nickname).to eq("k0z4")
271
+
272
+ expect { user.destroy }.to destroy_resource
273
+ end
274
+ end
275
+
276
+
277
+ describe 'managing groups' do
278
+ subject { @instance.groups }
279
+
280
+ specify do
281
+ creator = @instance.users.create username: 'content', password: 'creator'
282
+
283
+ content_creators = nil
284
+
285
+ expect {
286
+ content_creators = subject.create label: 'content creators'
287
+ }.to create_resource
288
+
289
+ expect {
290
+ content_creators.users.create user: creator.primary_key
291
+ }.to change { content_creators.users.all.count }.from(0).to(1)
292
+
293
+ expect { content_creators.destroy }.to destroy_resource
294
+ end
295
+ end
296
+
297
+ describe 'managing channels' do
298
+ subject do
299
+ @instance.channels
300
+ end
301
+
302
+ specify do
303
+ channel = nil
304
+ expect { channel = subject.create(name: 'chat') }.to create_resource
305
+ expect { channel.destroy }.to destroy_resource
306
+ end
307
+ end
308
+
309
+
310
+ describe 'subscribing to a channel' do
311
+ before(:each) { Celluloid.boot }
312
+
313
+ after(:each) { Celluloid.shutdown }
314
+
315
+ let!(:notifications) do
316
+ @instance.classes.create(name: 'notifications', schema: [{name: 'message', type: 'string'}]).objects
317
+ end
318
+
319
+ let!(:notifications_channel) do
320
+ @instance.channels.create name: 'system-notifications', other_permissions: 'subscribe'
321
+ end
322
+
323
+ specify do
324
+ poller = notifications_channel.poll
325
+
326
+ notification = notifications.create(message: "A new koza's arrived", channel: 'system-notifications')
327
+
328
+ sleep 20
329
+
330
+ expect(poller.responses.size).to eq(1)
331
+ expect(JSON.parse(poller.responses.last.body)["payload"]["message"]).to eq("A new koza's arrived")
332
+
333
+ notification.message = "A koza's gone"
334
+ notification.save
335
+
336
+ sleep 20
337
+
338
+ expect(poller.responses.size).to eq(2)
339
+ expect(JSON.parse(poller.responses.last.body)["payload"]["message"]).to eq("A koza's gone")
340
+
341
+ poller.terminate
342
+ end
343
+ end
344
+
345
+ describe 'subscribing to a room' do
346
+ before(:each) { Celluloid.boot }
347
+
348
+ after(:each) { Celluloid.shutdown }
349
+
350
+ let!(:house) {
351
+ @instance.channels.create name: 'house', type: 'separate_rooms', other_permissions: 'publish', custom_publish: true
352
+ }
353
+ let!(:shout) {
354
+ @instance.classes.create name: 'shout', schema: [{name: 'message', type: 'string'}]
355
+ }
356
+
357
+ specify do
358
+ shout.objects.create channel: 'house', channel_room: 'bathroom', message: "Where's the water?"
359
+ shout.objects.create channel: 'house', channel_room: 'basement', message: "Where's the light?"
360
+
361
+ expect(house.history.all(room: 'bathroom').count).to eq(1)
362
+ expect(house.history.all(room: 'basement').count).to eq(1)
363
+ expect(house.history.all.count).to eq(2)
364
+ end
365
+ end
366
+
367
+ describe 'using syncano on behalf of the user' do
368
+ let(:user_api_key) { @instance.api_keys.create.api_key }
369
+ let(:user) {
370
+ @instance.users.create username: 'kiszonka', password: 'passwd'
371
+ }
372
+ let(:another_user) {
373
+ @instance.users.create username: 'another', password: 'user'
374
+ }
375
+ let(:user_instance) {
376
+ Syncano.connect(api_key: user_api_key, user_key: user.user_key).
377
+ instances.first
378
+ }
379
+ let(:another_user_instance) {
380
+ Syncano.connect(api_key: user_api_key, user_key: another_user.user_key).
381
+ instances.first
382
+ }
383
+ let(:group) { @instance.groups.create label: 'content creators' }
384
+
385
+ before do
386
+ group.users.create user: user.primary_key
387
+ group.users.create user: another_user.primary_key
388
+
389
+ @instance.classes.create name: 'book',
390
+ schema: [{ name: 'title', type: 'string' }],
391
+ group: group.primary_key,
392
+ group_permissions: 'create_objects'
393
+ end
197
394
 
198
395
  specify do
199
- expect { subject.create slug: 'web-wurst', codebox: codebox.primary_key }.to create_resource
396
+ owner_books = user_instance.classes.find('book').objects
397
+ book = owner_books.create(title: 'Oliver Twist', owner_permissions: 'write')
398
+
399
+ expect(owner_books.all.to_a).to_not be_empty
400
+
401
+ group_member_books = another_user_instance.classes.find('book').objects
402
+ expect(group_member_books.all.to_a).to be_empty
200
403
 
201
- expect(subject.first.run['result']).to eq('currywurst')
404
+ book.group_permissions = 'read'
405
+ book.group = group.primary_key
406
+ book.save
202
407
 
203
- expect { subject.first.destroy }.to destroy_resource
408
+ expect(group_member_books.all.to_a).to_not be_empty
204
409
  end
205
410
  end
206
411
 
@@ -7,6 +7,7 @@ Dotenv.load
7
7
  require 'rspec-prof' if ENV['SPEC_PROFILE']
8
8
  require 'syncano'
9
9
  require 'webmock/rspec'
10
+ require 'celluloid/test'
10
11
 
11
12
  WebMock.disable_net_connect!
12
13
 
@@ -16,4 +17,5 @@ end
16
17
 
17
18
  def endpoint_uri(path)
18
19
  [Syncano::Connection.api_root,"v1", path].join("/")
19
- end
20
+ end
21
+