syncano 4.0.0.alpha1 → 4.0.0.alpha2

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