syncano 4.0.0.alpha4 → 4.0.0.pre

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/README.md +1 -13
  4. data/circle.yml +1 -1
  5. data/lib/active_attr/dirty.rb +26 -0
  6. data/lib/active_attr/typecasting/hash_typecaster.rb +34 -0
  7. data/lib/active_attr/typecasting_override.rb +29 -0
  8. data/lib/syncano.rb +9 -55
  9. data/lib/syncano/api.rb +2 -20
  10. data/lib/syncano/connection.rb +47 -48
  11. data/lib/syncano/model/associations.rb +121 -0
  12. data/lib/syncano/model/associations/base.rb +38 -0
  13. data/lib/syncano/model/associations/belongs_to.rb +30 -0
  14. data/lib/syncano/model/associations/has_many.rb +75 -0
  15. data/lib/syncano/model/associations/has_one.rb +22 -0
  16. data/lib/syncano/model/base.rb +257 -0
  17. data/lib/syncano/model/callbacks.rb +49 -0
  18. data/lib/syncano/model/scope_builder.rb +158 -0
  19. data/lib/syncano/query_builder.rb +7 -11
  20. data/lib/syncano/resources/base.rb +66 -91
  21. data/lib/syncano/schema.rb +159 -10
  22. data/lib/syncano/schema/attribute_definition.rb +0 -75
  23. data/lib/syncano/schema/resource_definition.rb +2 -24
  24. data/lib/syncano/version.rb +1 -1
  25. data/spec/integration/syncano_spec.rb +26 -268
  26. data/spec/spec_helper.rb +1 -3
  27. data/spec/unit/connection_spec.rb +74 -34
  28. data/spec/unit/query_builder_spec.rb +2 -2
  29. data/spec/unit/resources_base_spec.rb +64 -125
  30. data/spec/unit/schema/resource_definition_spec.rb +3 -24
  31. data/spec/unit/schema_spec.rb +55 -5
  32. data/spec/unit/syncano_spec.rb +9 -45
  33. data/syncano.gemspec +0 -5
  34. metadata +14 -87
  35. data/lib/syncano/api/endpoints.rb +0 -17
  36. data/lib/syncano/poller.rb +0 -55
  37. data/lib/syncano/resources.rb +0 -158
  38. data/lib/syncano/resources/paths.rb +0 -48
  39. data/lib/syncano/resources/resource_invalid.rb +0 -15
  40. data/lib/syncano/response.rb +0 -55
  41. data/lib/syncano/schema/endpoints_whitelist.rb +0 -40
  42. data/lib/syncano/upload_io.rb +0 -7
  43. data/spec/unit/resources/paths_spec.rb +0 -21
  44. data/spec/unit/response_spec.rb +0 -75
  45. data/spec/unit/schema/attribute_definition_spec.rb +0 -18
@@ -0,0 +1,158 @@
1
+ module Syncano
2
+ module Model
3
+ # ScopeBuilder class allows for creating and chaining more complex queries
4
+ class ScopeBuilder
5
+ # Constructor for ScopeBuilder
6
+ # @param [Class] model
7
+ def initialize(model)
8
+ raise 'Model should be a class extending module Syncano::Model::Base' unless model <= Syncano::Model::Base
9
+
10
+ self.model = model
11
+ self.query = HashWithIndifferentAccess.new
12
+ end
13
+
14
+ # Returns collection of objects
15
+ # @return [Array]
16
+ def all
17
+ model.syncano_class.objects.all(parameters).collect do |data_object|
18
+ model.new(data_object)
19
+ end
20
+ end
21
+
22
+ # Returns one object found by id
23
+ # @param [Integer] id
24
+ # @return [Object]
25
+ def find(id)
26
+ data_object = model.syncano_class.objects.find(id)
27
+ data_object.present? ? model.new(data_object) : nil
28
+ end
29
+
30
+ # Returns first object or collection of first x objects
31
+ # @param [Integer] amount
32
+ # @return [Object, Array]
33
+ def first(amount = nil)
34
+ objects = all.first(amount || 1)
35
+ amount.nil? ? objects.first : objects
36
+ end
37
+
38
+ # Returns last object or last x objects
39
+ # @param [Integer] amount
40
+ # @return [Object, Array]
41
+ def last(amount)
42
+ objects = all.last(amount || 1)
43
+ amount.nil? ? objects.first : objects
44
+ end
45
+
46
+ # Adds to the current scope builder condition to the scope builder
47
+ # @param [String] condition
48
+ # @param [Array] params
49
+ # @return [Syncano::ActiveRecord::ScopeBuilder]
50
+ def where(conditions, *params)
51
+ raise 'Invalid params count in where clause!' unless conditions.count('?') == params.count
52
+
53
+ params = params.dup
54
+
55
+ conditions.gsub(/\s+/, ' ').split(/and/i).each do |condition|
56
+ if condition.ends_with?('?')
57
+ value = params.shift
58
+ condition.gsub!('?', '').strip!
59
+ else
60
+ value = true
61
+ end
62
+
63
+ attribute, operator = condition.split(' ', 2)
64
+ operator.upcase!
65
+
66
+ raise 'Invalid attribute in where clause!' unless model.attributes.keys.include?(attribute)
67
+ raise 'Invalid operator in where clause!' unless self.class.where_mapping.keys.include?(operator)
68
+
69
+ operator = self.class.where_mapping[operator]
70
+
71
+ query[attribute] = HashWithIndifferentAccess.new if query[attribute].nil?
72
+ query[attribute][operator] = value
73
+ end
74
+
75
+ self
76
+ end
77
+
78
+ # Adds to the current scope builder order clause
79
+ # @param [String] order
80
+ # @return [Syncano::ActiveRecord::ScopeBuilder]
81
+ def order(order)
82
+ if order.is_a?(Hash)
83
+ attribute = order.keys.first
84
+ order_type = order[attribute]
85
+ else
86
+ attribute, order_type = order.gsub(/\s+/, ' ').split(' ')
87
+ end
88
+
89
+ raise 'Invalid attribute in order clause' unless (model.attributes.keys).include?(attribute)
90
+
91
+ self.order_clause = order_type.to_s.downcase == 'desc' ? "-#{attribute}" : attribute
92
+
93
+ self
94
+ end
95
+
96
+ # # Adds to the current scope builder limit clause
97
+ # # @param [Integer] amount
98
+ # # @return [Syncano::ActiveRecord::ScopeBuilder]
99
+ # def limit(amount)
100
+ # self.parameters[:limit] = amount
101
+ # self
102
+ # end
103
+ #
104
+ private
105
+
106
+ attr_accessor :order_clause, :query, :model, :scopes
107
+
108
+ # Returns Syncano::Resource class for current model
109
+ # @return [Syncano::Resources::Folder]
110
+ def syncano_class
111
+ model.syncano_class
112
+ end
113
+
114
+ # Returns scopes for current model
115
+ # @return [HashWithIndifferentAccess]
116
+ def scopes
117
+ model.scopes
118
+ end
119
+
120
+ def parameters
121
+ params = {}
122
+
123
+ params[:order_by] = order_clause if order_clause.present?
124
+ params[:query] = query.to_json if query.present?
125
+
126
+ params
127
+ end
128
+
129
+ # Returns mapping for operators
130
+ # @return [Hash]
131
+ def self.where_mapping
132
+ { '=' => '_eq', '!=' => '_neq', '<>' => '_neq', '>=' => '_gte', '>' => '_gt',
133
+ '<=' => '_lte', '<' => '_lt', 'IS NOT NULL' => '_exists', 'IN' => '_in' }
134
+ end
135
+
136
+ # Applies scope to the current scope builder
137
+ # @param [Symbol] name
138
+ # @param [Array] args
139
+ # @return [Syncano::ActiveRecord::ScopeBuilder]
140
+ def execute_scope(name, *args)
141
+ procedure = scopes[name]
142
+ instance_exec(*args, &procedure)
143
+ self
144
+ end
145
+
146
+ # Overwritten method_missing for handling calling defined scopes
147
+ # @param [String] name
148
+ # @param [Array] args
149
+ def method_missing(name, *args)
150
+ if scopes[name].nil?
151
+ super
152
+ else
153
+ execute_scope(name, *args)
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
@@ -6,17 +6,17 @@ module Syncano
6
6
  self.scope_parameters = scope_parameters
7
7
  end
8
8
 
9
- def all(query_params = {})
10
- query_params[:query] = query_params[:query].to_json if query_params[:query].try(:any?)
11
- resource_class.all(connection, scope_parameters, query_params)
9
+ def all(filter_attributes = {})
10
+ filter_attributes[:query] = filter_attributes[:query].to_json if filter_attributes[:query].try(:any?)
11
+ resource_class.all(connection, scope_parameters, filter_attributes)
12
12
  end
13
13
 
14
- def first(query_params = {})
15
- resource_class.first(connection, scope_parameters, query_params)
14
+ def first
15
+ resource_class.first(connection, scope_parameters)
16
16
  end
17
17
 
18
- def last(query_params = {})
19
- resource_class.last(connection, scope_parameters, query_params)
18
+ def last
19
+ resource_class.last(connection, scope_parameters)
20
20
  end
21
21
 
22
22
  def find(key = nil)
@@ -31,10 +31,6 @@ module Syncano
31
31
  resource_class.create(connection, scope_parameters, attributes)
32
32
  end
33
33
 
34
- def destroy(primary_key)
35
- resource_class.destroy connection, scope_parameters, primary_key
36
- end
37
-
38
34
  def space(at, options = {})
39
35
  Syncano::Resources::Space.new(at, self, options)
40
36
  end
@@ -2,25 +2,25 @@ module Syncano
2
2
  module Resources
3
3
  class Base
4
4
  include ActiveAttr::Model
5
- include ActiveModel::Dirty
5
+ include ActiveAttr::Dirty
6
6
 
7
7
  PARAMETER_REGEXP = /\{([^}]+)\}/
8
8
 
9
9
  class << self
10
- def all(connection, scope_parameters, query_params = {})
10
+ def all(connection, scope_parameters, filter_attributes = {})
11
11
  check_resource_method_existance!(:index)
12
12
 
13
- response = connection.request(:get, collection_path(scope_parameters), query_params)
13
+ response = connection.request(:get, collection_path(scope_parameters), filter_attributes)
14
14
  scope = Syncano::Scope.new(connection, scope_parameters)
15
15
  Syncano::Resources::Collection.from_database(response, scope, self)
16
16
  end
17
17
 
18
- def first(connection, scope_parameters, query_params = {})
19
- all(connection, scope_parameters, query_params).first
18
+ def first(connection, scope_parameters)
19
+ all(connection, scope_parameters).first
20
20
  end
21
21
 
22
- def last(connection, scope_parameters, query_params = {})
23
- all(connection, scope_parameters, query_params).last
22
+ def last(connection, scope_parameters)
23
+ all(connection, scope_parameters).last
24
24
  end
25
25
 
26
26
  def find(connection, scope_parameters, pk)
@@ -34,21 +34,7 @@ module Syncano
34
34
  def create(connection, scope_parameters, attributes)
35
35
  check_resource_method_existance!(:create)
36
36
 
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!
46
- end
47
-
48
- def destroy(connection, scope_parameters, pk)
49
- check_resource_method_existance! :destroy
50
-
51
- connection.request :delete, member_path(pk, scope_parameters)
37
+ new(connection, scope_parameters, attributes).save
52
38
  end
53
39
 
54
40
  def map_attributes_values(attributes)
@@ -103,7 +89,7 @@ module Syncano
103
89
  end
104
90
 
105
91
  def new_record?
106
- new_record
92
+ primary_key.blank?
107
93
  end
108
94
 
109
95
  def saved?
@@ -119,31 +105,21 @@ module Syncano
119
105
  end
120
106
 
121
107
  def save
122
- return false unless valid?
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
108
+ # TODO: Call validation here
109
+ apply_forced_defaults!
132
110
 
133
- def commit_save
134
111
  if new_record?
135
112
  response = connection.request(:post, collection_path, select_create_attributes)
136
113
  else
137
- response = connection.request(:patch, self_path, select_changed_attributes)
114
+ response = connection.request(:patch, member_path, select_update_attributes)
138
115
  end
139
116
 
140
117
  initialize!(response, true)
141
-
142
118
  end
143
119
 
144
120
  def destroy
145
121
  check_resource_method_existance!(:destroy)
146
- connection.request(:delete, self_path)
122
+ connection.request(:delete, member_path)
147
123
  mark_as_destroyed!
148
124
  end
149
125
 
@@ -154,65 +130,31 @@ module Syncano
154
130
  def reload!
155
131
  raise(Syncano::Error.new('record is not saved')) if new_record?
156
132
 
157
- response = connection.request(:get, self_path)
133
+ response = connection.request(:get, member_path)
158
134
  initialize!(response)
159
135
  end
160
136
 
161
- def attribute_definitions
162
- self.class.resource_definition.attributes
163
- end
164
-
165
- def attribute_definitions_map
166
- Hash[ attribute_definitions.map { |attr| [attr.name, attr] } ]
167
- end
168
-
169
137
  def select_create_attributes
170
- attributes = self.attributes.select { |name, _|
171
- begin
172
- attribute_definitions_map[name].writable?
173
- rescue NoMethodError
174
- if custom_attributes.has_key?(name)
175
- true
176
- else
177
- raise
178
- end
179
- end
180
- }
181
- attributes = custom_attributes.merge(attributes) if respond_to?(:custom_attributes)
138
+ attributes = self.attributes.select { |name, _value| self.class.create_writable_attributes.include?(name.to_sym) }
139
+ attributes.merge!(custom_attributes) if respond_to?(:custom_attributes) && custom_attributes.is_a?(Hash)
182
140
  self.class.map_attributes_values(attributes)
183
141
  end
184
142
 
185
143
  def select_update_attributes
186
- attributes = updatable_attributes
187
- attributes = custom_attributes.merge(attributes) if respond_to?(:custom_attributes)
144
+ attributes = self.attributes.select{ |name, _value| self.class.update_writable_attributes.include?(name.to_sym) }
145
+ attributes.merge!(custom_attributes) if respond_to?(:custom_attributes) && custom_attributes.is_a?(Hash)
188
146
  self.class.map_attributes_values(attributes)
189
147
  end
190
148
 
191
- def select_changed_attributes
192
- updatable_attributes
193
- end
194
-
195
- def updatable_attributes
196
- attributes = self.attributes.select do |name, _value|
197
- self.class.update_writable_attributes.include?(name.to_sym)
198
- end
199
- self.class.map_attributes_values attributes
200
- end
201
-
202
149
  private
203
150
 
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
151
+ class_attribute :resource_definition, :create_writable_attributes, :update_writable_attributes
152
+ attr_accessor :connection, :association_paths, :member_path, :scope_parameters, :destroyed
208
153
 
209
154
  def initialize!(attributes = {}, from_database = false)
210
155
  attributes = HashWithIndifferentAccess.new(attributes)
211
156
 
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
-
157
+ initialize_routing(attributes)
216
158
  initialize_associations(attributes)
217
159
 
218
160
  self.attributes.clear
@@ -239,8 +181,37 @@ module Syncano
239
181
  end
240
182
  end
241
183
 
242
- def map_collection_name_to_resource_class(name)
243
- ::Syncano::Resources::Paths.instance.collections.match(association_paths[name])
184
+ def initialize_routing(attributes)
185
+ self.member_path = attributes[:links].try(:[], :self)
186
+ end
187
+
188
+ def self.map_member_name_to_resource_class(name)
189
+ name = 'code_box' if name == 'codebox'
190
+ "::Syncano::Resources::#{name.camelize}".constantize
191
+ end
192
+
193
+ def self.map_collection_name_to_resource_class(name)
194
+ name = case name
195
+ when 'codeboxes'
196
+ 'code_boxes'
197
+ when 'traces'
198
+ case self.name
199
+ when 'Syncano::Resources::CodeBox'
200
+ 'code_box_traces'
201
+ end
202
+ else
203
+ name
204
+ end
205
+
206
+ map_member_name_to_resource_class(name.singularize)
207
+ end
208
+
209
+ def apply_forced_defaults!
210
+ self.class.attributes.each do |attr_name, attr_definition|
211
+ if read_attribute(attr_name).blank? && attr_definition[:force_default]
212
+ write_attribute(attr_name, attr_definition[:default].is_a?(Proc) ? attr_definition[:default].call : attr_definition[:default])
213
+ end
214
+ end
244
215
  end
245
216
 
246
217
  def mark_as_saved!
@@ -259,26 +230,26 @@ module Syncano
259
230
  # TODO Implement QueryBuilders without scope parameters and adding objects to the association
260
231
  raise(Syncano::Error.new('record not saved')) if new_record?
261
232
 
262
- resource_class = map_collection_name_to_resource_class(name)
233
+ resource_class = self.class.map_collection_name_to_resource_class(name)
263
234
  scope_parameters = resource_class.extract_scope_parameters(association_paths[name])
264
235
 
265
236
  ::Syncano::QueryBuilder.new(connection, resource_class, scope_parameters)
266
237
  end
267
238
 
239
+ def belongs_to_association(name)
240
+ resource_class = self.class.map_member_name_to_resource_class(name)
241
+ scope_parameters = resource_class.extract_scope_parameters(association_paths[name])
242
+ pk = resource_class.extract_primary_key(association_paths[name])
243
+
244
+ ::Syncano::QueryBuilder.new(connection, resource_class, scope_parameters).find(pk)
245
+ end
246
+
268
247
  def custom_method(method_name, config)
269
248
  connection.request self.class.custom_method_http_method(method_name),
270
249
  self.class.custom_method_path(method_name, primary_key, scope_parameters),
271
250
  config
272
251
  end
273
252
 
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
-
282
253
  def self.custom_method_http_method(method_name)
283
254
  custom_method_definition(method_name)[:http_methods].first.to_sym
284
255
  end
@@ -359,6 +330,10 @@ module Syncano
359
330
  self.class.collection_path(scope_parameters)
360
331
  end
361
332
 
333
+ def member_path
334
+ self.class.member_path(primary_key, scope_parameters)
335
+ end
336
+
362
337
  def check_resource_method_existance!(method_name)
363
338
  self.class.check_resource_method_existance!(method_name)
364
339
  end
@@ -367,7 +342,7 @@ module Syncano
367
342
  index: { type: :collection, method: :get },
368
343
  create: { type: :collection, method: :post },
369
344
  show: { type: :member, method: :get },
370
- update: { type: :member, method: :patch },
345
+ update: { type: :member, method: :put },
371
346
  destroy: { type: :member, method: :delete }
372
347
  }.each do |name, parameters|
373
348