syncano 4.0.0.alpha4 → 4.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
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