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
@@ -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
@@ -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
- const_set resource_definition.name, new_resource_class(resource_definition)
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
- force_default: attribute_definition.force_default?
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
- if attribute_definition.required?
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
- if association_schema['type'] == 'list'
104
- define_method(association_schema['name']) do
105
- has_many_association(association_schema['name'])
106
- end
107
- elsif association_schema['type'] == 'detail' && association_schema['name'] != 'self'
108
- define_method(association_schema['name']) do
109
- belongs_to_association(association_schema['name'])
110
- end
111
- elsif association_schema['type'] == 'run'
112
- define_method(association_schema['name']) do |config = nil|
113
- custom_method association_schema['name'], config
114
- end
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 ActiveAttr::Dirty
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).save
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
- primary_key.blank?
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
- # TODO: Call validation here
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, member_path, select_changed_attributes)
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, member_path)
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, member_path)
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, :update_writable_attributes
187
- attr_accessor :connection, :association_paths, :member_path, :scope_parameters, :destroyed
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
- initialize_routing(attributes)
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 initialize_routing(attributes)
220
- self.member_path = attributes[:links].try(:[], :self)
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 = self.class.map_collection_name_to_resource_class(name)
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: :put },
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