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.
- 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
@@ -0,0 +1,55 @@
|
|
1
|
+
module Syncano
|
2
|
+
class Poller
|
3
|
+
include Celluloid::IO
|
4
|
+
|
5
|
+
attr_accessor :connection, :method_name, :path, :responses
|
6
|
+
|
7
|
+
|
8
|
+
def initialize(connection, method_name, path)
|
9
|
+
self.connection = connection
|
10
|
+
self.method_name = method_name
|
11
|
+
self.path = path
|
12
|
+
self.responses = []
|
13
|
+
end
|
14
|
+
|
15
|
+
def poll
|
16
|
+
loop do
|
17
|
+
responses << http_fetcher.get(path)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def last_response
|
22
|
+
responses.last
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def http_fetcher
|
28
|
+
HttpFetcher.new connection.api_key, connection.user_key
|
29
|
+
end
|
30
|
+
|
31
|
+
class HttpFetcher
|
32
|
+
attr_accessor :api_key, :user_key
|
33
|
+
|
34
|
+
def initialize(api_key, user_key)
|
35
|
+
self.api_key = api_key
|
36
|
+
self.user_key = user_key
|
37
|
+
end
|
38
|
+
|
39
|
+
def get(path, params = {})
|
40
|
+
url = Syncano::Connection.api_root + path
|
41
|
+
|
42
|
+
response = HTTP.
|
43
|
+
with_headers('X-API-KEY' => api_key,
|
44
|
+
'X-USER-KEY' => user_key,
|
45
|
+
'User-Agent' => "Syncano Ruby Gem #{Syncano::VERSION}").
|
46
|
+
get(url,
|
47
|
+
params: params,
|
48
|
+
ssl_socket_class: Celluloid::IO::SSLSocket,
|
49
|
+
socket_class: Celluloid::IO::TCPSocket)
|
50
|
+
|
51
|
+
response
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/lib/syncano/resources.rb
CHANGED
@@ -3,8 +3,30 @@ require 'dirty_hashy'
|
|
3
3
|
module Syncano
|
4
4
|
module Resources
|
5
5
|
class << self
|
6
|
+
def build_definitions(schema_definition)
|
7
|
+
schema_definition.map do |name, raw_resource_definition|
|
8
|
+
::Syncano::Schema::ResourceDefinition.new(name, raw_resource_definition)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
6
12
|
def define_resource_class(resource_definition)
|
7
|
-
|
13
|
+
resource_class = new_resource_class(resource_definition)
|
14
|
+
|
15
|
+
const_set resource_definition.name, resource_class
|
16
|
+
|
17
|
+
if resource_definition[:collection]
|
18
|
+
resources_paths.collections.define resource_definition[:collection][:path], resource_class
|
19
|
+
end
|
20
|
+
|
21
|
+
if resource_definition[:member]
|
22
|
+
resources_paths.members.define resource_definition[:member][:path], resource_class
|
23
|
+
end
|
24
|
+
|
25
|
+
resource_class
|
26
|
+
end
|
27
|
+
|
28
|
+
def resources_paths
|
29
|
+
::Syncano::Resources::Paths.instance
|
8
30
|
end
|
9
31
|
|
10
32
|
def new_resource_class(definition)
|
@@ -15,12 +37,21 @@ module Syncano
|
|
15
37
|
self.update_writable_attributes = []
|
16
38
|
|
17
39
|
attributes_definitions.each do |attribute_definition|
|
40
|
+
|
18
41
|
attribute attribute_definition.name,
|
19
42
|
type: attribute_definition.type,
|
20
|
-
default: attribute_definition.default
|
21
|
-
|
43
|
+
default: attribute_definition.default
|
44
|
+
|
45
|
+
# TODO extract to a dynamically defined module
|
46
|
+
define_method("#{attribute_definition.name}=") do |new_value|
|
47
|
+
if new_value != read_attribute(attribute_definition.name)
|
48
|
+
send("#{attribute_definition.name}_will_change!")
|
49
|
+
end
|
22
50
|
|
23
|
-
|
51
|
+
super new_value
|
52
|
+
end
|
53
|
+
|
54
|
+
if attribute_definition.validate?
|
24
55
|
validates attribute_definition.name, presence: true
|
25
56
|
end
|
26
57
|
|
@@ -100,18 +131,19 @@ module Syncano
|
|
100
131
|
end
|
101
132
|
|
102
133
|
(definition[:associations]['links'] || []).each do |association_schema|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
134
|
+
case association_schema['type']
|
135
|
+
when 'list'
|
136
|
+
define_method(association_schema['name']) do
|
137
|
+
has_many_association(association_schema['name'])
|
138
|
+
end
|
139
|
+
when 'run'
|
140
|
+
define_method(association_schema['name']) do |config = nil|
|
141
|
+
custom_method association_schema['name'], config
|
142
|
+
end
|
143
|
+
when 'poll'
|
144
|
+
define_method(association_schema['name']) do |config = nil|
|
145
|
+
async_method association_schema['name'], config
|
146
|
+
end
|
115
147
|
end
|
116
148
|
end
|
117
149
|
|
@@ -2,7 +2,7 @@ module Syncano
|
|
2
2
|
module Resources
|
3
3
|
class Base
|
4
4
|
include ActiveAttr::Model
|
5
|
-
include
|
5
|
+
include ActiveModel::Dirty
|
6
6
|
|
7
7
|
PARAMETER_REGEXP = /\{([^}]+)\}/
|
8
8
|
|
@@ -34,7 +34,15 @@ module Syncano
|
|
34
34
|
def create(connection, scope_parameters, attributes)
|
35
35
|
check_resource_method_existance!(:create)
|
36
36
|
|
37
|
-
new(connection, scope_parameters, attributes)
|
37
|
+
record = new(connection, scope_parameters, attributes)
|
38
|
+
record.save
|
39
|
+
record
|
40
|
+
end
|
41
|
+
|
42
|
+
def create!(connection, scope_parameters, attribues)
|
43
|
+
check_resource_method_existance!(:create)
|
44
|
+
|
45
|
+
new(connection, scope_parameters, attributes).save!
|
38
46
|
end
|
39
47
|
|
40
48
|
def destroy(connection, scope_parameters, pk)
|
@@ -95,7 +103,7 @@ module Syncano
|
|
95
103
|
end
|
96
104
|
|
97
105
|
def new_record?
|
98
|
-
|
106
|
+
new_record
|
99
107
|
end
|
100
108
|
|
101
109
|
def saved?
|
@@ -111,21 +119,31 @@ module Syncano
|
|
111
119
|
end
|
112
120
|
|
113
121
|
def save
|
114
|
-
|
122
|
+
return false unless valid?
|
115
123
|
|
124
|
+
commit_save
|
125
|
+
end
|
126
|
+
|
127
|
+
def save!
|
128
|
+
raise Syncano::Resources::ResourceInvalid.new(self) unless valid?
|
129
|
+
|
130
|
+
commit_save
|
131
|
+
end
|
132
|
+
|
133
|
+
def commit_save
|
116
134
|
if new_record?
|
117
|
-
apply_forced_defaults!
|
118
135
|
response = connection.request(:post, collection_path, select_create_attributes)
|
119
136
|
else
|
120
|
-
response = connection.request(:patch,
|
137
|
+
response = connection.request(:patch, self_path, select_changed_attributes)
|
121
138
|
end
|
122
139
|
|
123
140
|
initialize!(response, true)
|
141
|
+
|
124
142
|
end
|
125
143
|
|
126
144
|
def destroy
|
127
145
|
check_resource_method_existance!(:destroy)
|
128
|
-
connection.request(:delete,
|
146
|
+
connection.request(:delete, self_path)
|
129
147
|
mark_as_destroyed!
|
130
148
|
end
|
131
149
|
|
@@ -136,7 +154,7 @@ module Syncano
|
|
136
154
|
def reload!
|
137
155
|
raise(Syncano::Error.new('record is not saved')) if new_record?
|
138
156
|
|
139
|
-
response = connection.request(:get,
|
157
|
+
response = connection.request(:get, self_path)
|
140
158
|
initialize!(response)
|
141
159
|
end
|
142
160
|
|
@@ -183,13 +201,18 @@ module Syncano
|
|
183
201
|
|
184
202
|
private
|
185
203
|
|
186
|
-
class_attribute :resource_definition, :create_writable_attributes,
|
187
|
-
|
204
|
+
class_attribute :resource_definition, :create_writable_attributes,
|
205
|
+
:update_writable_attributes
|
206
|
+
attr_accessor :connection, :association_paths, :member_path,
|
207
|
+
:scope_parameters, :destroyed, :self_path, :new_record
|
188
208
|
|
189
209
|
def initialize!(attributes = {}, from_database = false)
|
190
210
|
attributes = HashWithIndifferentAccess.new(attributes)
|
191
211
|
|
192
|
-
|
212
|
+
self.member_path = attributes[:links].try(:[], :self)
|
213
|
+
self.self_path = attributes[:links].try(:[], :self)
|
214
|
+
self.new_record = !from_database # TODO use from_database of self_path.nil?
|
215
|
+
|
193
216
|
initialize_associations(attributes)
|
194
217
|
|
195
218
|
self.attributes.clear
|
@@ -216,37 +239,8 @@ module Syncano
|
|
216
239
|
end
|
217
240
|
end
|
218
241
|
|
219
|
-
def
|
220
|
-
|
221
|
-
end
|
222
|
-
|
223
|
-
def self.map_member_name_to_resource_class(name)
|
224
|
-
name = 'code_box' if name == 'codebox'
|
225
|
-
"::Syncano::Resources::#{name.camelize}".constantize
|
226
|
-
end
|
227
|
-
|
228
|
-
def self.map_collection_name_to_resource_class(name)
|
229
|
-
name = case name
|
230
|
-
when 'codeboxes'
|
231
|
-
'code_boxes'
|
232
|
-
when 'traces'
|
233
|
-
case self.name
|
234
|
-
when 'Syncano::Resources::CodeBox'
|
235
|
-
'code_box_traces'
|
236
|
-
end
|
237
|
-
else
|
238
|
-
name
|
239
|
-
end
|
240
|
-
|
241
|
-
map_member_name_to_resource_class(name.singularize)
|
242
|
-
end
|
243
|
-
|
244
|
-
def apply_forced_defaults!
|
245
|
-
self.class.attributes.each do |attr_name, attr_definition|
|
246
|
-
if read_attribute(attr_name).blank? && attr_definition[:force_default]
|
247
|
-
write_attribute(attr_name, attr_definition[:default].is_a?(Proc) ? attr_definition[:default].call : attr_definition[:default])
|
248
|
-
end
|
249
|
-
end
|
242
|
+
def map_collection_name_to_resource_class(name)
|
243
|
+
::Syncano::Resources::Paths.instance.collections.match(association_paths[name])
|
250
244
|
end
|
251
245
|
|
252
246
|
def mark_as_saved!
|
@@ -265,26 +259,26 @@ module Syncano
|
|
265
259
|
# TODO Implement QueryBuilders without scope parameters and adding objects to the association
|
266
260
|
raise(Syncano::Error.new('record not saved')) if new_record?
|
267
261
|
|
268
|
-
resource_class =
|
262
|
+
resource_class = map_collection_name_to_resource_class(name)
|
269
263
|
scope_parameters = resource_class.extract_scope_parameters(association_paths[name])
|
270
264
|
|
271
265
|
::Syncano::QueryBuilder.new(connection, resource_class, scope_parameters)
|
272
266
|
end
|
273
267
|
|
274
|
-
def belongs_to_association(name)
|
275
|
-
resource_class = self.class.map_member_name_to_resource_class(name)
|
276
|
-
scope_parameters = resource_class.extract_scope_parameters(association_paths[name])
|
277
|
-
pk = resource_class.extract_primary_key(association_paths[name])
|
278
|
-
|
279
|
-
::Syncano::QueryBuilder.new(connection, resource_class, scope_parameters).find(pk)
|
280
|
-
end
|
281
|
-
|
282
268
|
def custom_method(method_name, config)
|
283
269
|
connection.request self.class.custom_method_http_method(method_name),
|
284
270
|
self.class.custom_method_path(method_name, primary_key, scope_parameters),
|
285
271
|
config
|
286
272
|
end
|
287
273
|
|
274
|
+
def async_method(method_name, params)
|
275
|
+
poller = Poller.new(connection,
|
276
|
+
self.class.custom_method_http_method(method_name),
|
277
|
+
self.class.custom_method_path(method_name, primary_key, scope_parameters))
|
278
|
+
poller.async.poll
|
279
|
+
poller
|
280
|
+
end
|
281
|
+
|
288
282
|
def self.custom_method_http_method(method_name)
|
289
283
|
custom_method_definition(method_name)[:http_methods].first.to_sym
|
290
284
|
end
|
@@ -365,10 +359,6 @@ module Syncano
|
|
365
359
|
self.class.collection_path(scope_parameters)
|
366
360
|
end
|
367
361
|
|
368
|
-
def member_path
|
369
|
-
self.class.member_path(primary_key, scope_parameters)
|
370
|
-
end
|
371
|
-
|
372
362
|
def check_resource_method_existance!(method_name)
|
373
363
|
self.class.check_resource_method_existance!(method_name)
|
374
364
|
end
|
@@ -377,7 +367,7 @@ module Syncano
|
|
377
367
|
index: { type: :collection, method: :get },
|
378
368
|
create: { type: :collection, method: :post },
|
379
369
|
show: { type: :member, method: :get },
|
380
|
-
update: { type: :member, method: :
|
370
|
+
update: { type: :member, method: :patch },
|
381
371
|
destroy: { type: :member, method: :delete }
|
382
372
|
}.each do |name, parameters|
|
383
373
|
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
module Syncano
|
4
|
+
module Resources
|
5
|
+
class Paths
|
6
|
+
include Singleton
|
7
|
+
|
8
|
+
attr_accessor :collections, :members
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
self.collections = self.class::Collection.new
|
12
|
+
self.members = self.class::Member.new
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
class Collection
|
18
|
+
def initialize
|
19
|
+
@map = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def define(path, resource)
|
23
|
+
@map[Regexp.new("\\A#{path.gsub(/{[^}]*}/, '([^\/]+)')}\\z")] = resource
|
24
|
+
end
|
25
|
+
|
26
|
+
def match(path)
|
27
|
+
_, resouce = @map.find { |pattern, _| pattern =~ path }
|
28
|
+
resouce
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class Member
|
33
|
+
def initialize
|
34
|
+
@map = {}
|
35
|
+
end
|
36
|
+
|
37
|
+
def define(path, resource)
|
38
|
+
resource_name = resource.name
|
39
|
+
@map[resource_name] = path
|
40
|
+
end
|
41
|
+
|
42
|
+
def find(resource)
|
43
|
+
@map[resource.name]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Syncano
|
2
|
+
module Resources
|
3
|
+
class ResourceInvalid < StandardError
|
4
|
+
attr_accessor :resource
|
5
|
+
|
6
|
+
def initialize(resource)
|
7
|
+
self.resource = resource
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_s
|
11
|
+
"#{self.class.name} <#{resource.class.name} #{resource.errors.full_messages}>"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Syncano
|
2
|
+
class Response
|
3
|
+
class << self
|
4
|
+
def handle(raw_response)
|
5
|
+
code = unify_code(raw_response)
|
6
|
+
|
7
|
+
case code
|
8
|
+
when Status.no_content
|
9
|
+
when Status.successful
|
10
|
+
parse_response raw_response
|
11
|
+
when Status.not_found
|
12
|
+
raise NotFound.new(raw_response.env.url, raw_response.env.method)
|
13
|
+
when Status.client_error
|
14
|
+
raise ClientError.new raw_response.body, raw_response
|
15
|
+
when Status.server_error
|
16
|
+
raise ServerError.new raw_response.body, raw_response
|
17
|
+
else
|
18
|
+
raise UnsupportedStatusError.new raw_response
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def parse_response(raw_response)
|
23
|
+
JSON.parse(raw_response.body)
|
24
|
+
end
|
25
|
+
|
26
|
+
def unify_code(raw_response)
|
27
|
+
raw_response.status.code
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Status
|
32
|
+
class << self
|
33
|
+
def successful
|
34
|
+
->(code) { (200...300).include? code }
|
35
|
+
end
|
36
|
+
|
37
|
+
def client_error
|
38
|
+
->(code) { (400...500).include? code }
|
39
|
+
end
|
40
|
+
|
41
|
+
def no_content
|
42
|
+
->(code) { code == 204 }
|
43
|
+
end
|
44
|
+
|
45
|
+
def server_error
|
46
|
+
->(code) { code >= 500 }
|
47
|
+
end
|
48
|
+
|
49
|
+
def not_found
|
50
|
+
->(code) { code == 404 }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|