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.
- checksums.yaml +4 -4
- data/README.md +8 -157
- data/circle.yml +1 -1
- data/lib/syncano.rb +38 -11
- data/lib/syncano/api.rb +20 -2
- data/lib/syncano/api/endpoints.rb +17 -0
- data/lib/syncano/connection.rb +46 -54
- data/lib/syncano/poller.rb +55 -0
- data/lib/syncano/resources.rb +48 -16
- data/lib/syncano/resources/base.rb +46 -56
- data/lib/syncano/resources/paths.rb +48 -0
- data/lib/syncano/resources/resource_invalid.rb +15 -0
- data/lib/syncano/response.rb +55 -0
- data/lib/syncano/schema.rb +10 -29
- data/lib/syncano/schema/attribute_definition.rb +2 -2
- data/lib/syncano/schema/endpoints_whitelist.rb +40 -0
- data/lib/syncano/schema/resource_definition.rb +5 -0
- data/lib/syncano/upload_io.rb +7 -0
- data/lib/syncano/version.rb +1 -1
- data/spec/integration/syncano_spec.rb +220 -15
- data/spec/spec_helper.rb +3 -1
- data/spec/unit/connection_spec.rb +34 -97
- data/spec/unit/resources/paths_spec.rb +21 -0
- data/spec/unit/resources_base_spec.rb +77 -16
- data/spec/unit/response_spec.rb +75 -0
- data/spec/unit/schema/resource_definition_spec.rb +10 -0
- data/spec/unit/schema_spec.rb +5 -55
- data/syncano.gemspec +4 -0
- metadata +69 -13
- data/lib/active_attr/dirty.rb +0 -26
- data/lib/active_attr/typecasting/hash_typecaster.rb +0 -34
- data/lib/active_attr/typecasting_override.rb +0 -29
- data/lib/syncano/model/associations.rb +0 -121
- data/lib/syncano/model/associations/base.rb +0 -38
- data/lib/syncano/model/associations/belongs_to.rb +0 -30
- data/lib/syncano/model/associations/has_many.rb +0 -75
- data/lib/syncano/model/associations/has_one.rb +0 -22
- data/lib/syncano/model/base.rb +0 -257
- data/lib/syncano/model/callbacks.rb +0 -49
- data/lib/syncano/model/scope_builder.rb +0 -158
data/lib/syncano/schema.rb
CHANGED
@@ -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
|
11
|
-
|
12
|
-
load_schema
|
12
|
+
def self.schema_path
|
13
|
+
"/#{Syncano::Connection::API_VERSION}/schema/"
|
13
14
|
end
|
14
15
|
|
15
|
-
def
|
16
|
-
|
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
|
32
|
-
raw_schema = connection.request(:get,
|
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
|
-
|
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
|
@@ -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
|
data/lib/syncano/version.rb
CHANGED
@@ -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:
|
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::
|
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
|
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
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
404
|
+
book.group_permissions = 'read'
|
405
|
+
book.group = group.primary_key
|
406
|
+
book.save
|
202
407
|
|
203
|
-
expect
|
408
|
+
expect(group_member_books.all.to_a).to_not be_empty
|
204
409
|
end
|
205
410
|
end
|
206
411
|
|
data/spec/spec_helper.rb
CHANGED
@@ -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
|
+
|