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.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/README.md +1 -13
- data/circle.yml +1 -1
- data/lib/active_attr/dirty.rb +26 -0
- data/lib/active_attr/typecasting/hash_typecaster.rb +34 -0
- data/lib/active_attr/typecasting_override.rb +29 -0
- data/lib/syncano.rb +9 -55
- data/lib/syncano/api.rb +2 -20
- data/lib/syncano/connection.rb +47 -48
- data/lib/syncano/model/associations.rb +121 -0
- data/lib/syncano/model/associations/base.rb +38 -0
- data/lib/syncano/model/associations/belongs_to.rb +30 -0
- data/lib/syncano/model/associations/has_many.rb +75 -0
- data/lib/syncano/model/associations/has_one.rb +22 -0
- data/lib/syncano/model/base.rb +257 -0
- data/lib/syncano/model/callbacks.rb +49 -0
- data/lib/syncano/model/scope_builder.rb +158 -0
- data/lib/syncano/query_builder.rb +7 -11
- data/lib/syncano/resources/base.rb +66 -91
- data/lib/syncano/schema.rb +159 -10
- data/lib/syncano/schema/attribute_definition.rb +0 -75
- data/lib/syncano/schema/resource_definition.rb +2 -24
- data/lib/syncano/version.rb +1 -1
- data/spec/integration/syncano_spec.rb +26 -268
- data/spec/spec_helper.rb +1 -3
- data/spec/unit/connection_spec.rb +74 -34
- data/spec/unit/query_builder_spec.rb +2 -2
- data/spec/unit/resources_base_spec.rb +64 -125
- data/spec/unit/schema/resource_definition_spec.rb +3 -24
- data/spec/unit/schema_spec.rb +55 -5
- data/spec/unit/syncano_spec.rb +9 -45
- data/syncano.gemspec +0 -5
- metadata +14 -87
- data/lib/syncano/api/endpoints.rb +0 -17
- data/lib/syncano/poller.rb +0 -55
- data/lib/syncano/resources.rb +0 -158
- data/lib/syncano/resources/paths.rb +0 -48
- data/lib/syncano/resources/resource_invalid.rb +0 -15
- data/lib/syncano/response.rb +0 -55
- data/lib/syncano/schema/endpoints_whitelist.rb +0 -40
- data/lib/syncano/upload_io.rb +0 -7
- data/spec/unit/resources/paths_spec.rb +0 -21
- data/spec/unit/response_spec.rb +0 -75
- 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(
|
10
|
-
|
11
|
-
resource_class.all(connection, scope_parameters,
|
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
|
15
|
-
resource_class.first(connection, scope_parameters
|
14
|
+
def first
|
15
|
+
resource_class.first(connection, scope_parameters)
|
16
16
|
end
|
17
17
|
|
18
|
-
def last
|
19
|
-
resource_class.last(connection, scope_parameters
|
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
|
5
|
+
include ActiveAttr::Dirty
|
6
6
|
|
7
7
|
PARAMETER_REGEXP = /\{([^}]+)\}/
|
8
8
|
|
9
9
|
class << self
|
10
|
-
def all(connection, scope_parameters,
|
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),
|
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
|
19
|
-
all(connection, scope_parameters
|
18
|
+
def first(connection, scope_parameters)
|
19
|
+
all(connection, scope_parameters).first
|
20
20
|
end
|
21
21
|
|
22
|
-
def last(connection, scope_parameters
|
23
|
-
all(connection, scope_parameters
|
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
|
-
|
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
|
-
|
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
|
-
|
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,
|
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,
|
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,
|
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
|
-
|
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 =
|
187
|
-
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
|
-
|
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
|
-
|
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
|
243
|
-
|
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: :
|
345
|
+
update: { type: :member, method: :put },
|
371
346
|
destroy: { type: :member, method: :delete }
|
372
347
|
}.each do |name, parameters|
|
373
348
|
|