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