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