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