syncano 3.1.1.beta5 → 3.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: de0d4d181523b5d081bcfffad1a726be2a515cf3
4
- data.tar.gz: 7bf73f1a965e8d7ad82c97dc43fa1b3e41fe3f7a
3
+ metadata.gz: 9c3a289c247f6654d64d2a9510acb205bce778a1
4
+ data.tar.gz: 23f7bc994f710dd3b4e25be3d2847358506b173d
5
5
  SHA512:
6
- metadata.gz: ca1c6352decade66e64eecf110f83d9de1ff852c263468c233ce98acd1b74cc9f02deafef4091ee7579823d795fe37d49959cf897576d86048e03f23d3da929f
7
- data.tar.gz: 542c9ecbb11261a19e8d49b7c17b81b281c929eafa2e4b28fa00aaaea57a4154c2f9518c8bcd12b2deb1283db56b7cfb2ab46ecbf9019721199cc9d0b12caac8
6
+ metadata.gz: ffc0eea9c73a2d85d04999f7d2cb7cd3cb3bd199655fa929b8360abbf76f587bacb66983f69ca8986d0879d236f417470a714fa7e00091c4861af8a55a66404f
7
+ data.tar.gz: 51d2312ec5ba3115266634e9e17742b6501c05232fa4cef96edb292ba95728ceb128f576502bdd1d80ab2e0f20536bf7e7a12f3ed69ba2a97a125793473c3663
data/README.md CHANGED
@@ -10,7 +10,7 @@ Click here to learn more about [Syncano](http://www.syncano.com) or [create an a
10
10
 
11
11
  Add this line to your application's Gemfile:
12
12
 
13
- gem 'syncano', '~> 3.1.1.beta5'
13
+ gem 'syncano', '~> 3.1.1'
14
14
 
15
15
  And then execute:
16
16
 
@@ -18,7 +18,7 @@ And then execute:
18
18
 
19
19
  Or install it yourself as:
20
20
 
21
- $ gem install syncano -v 3.1.1.beta5 --pre
21
+ $ gem install syncano -v 3.1.1
22
22
 
23
23
  At the end generate initializer with api key and api instance name:
24
24
 
@@ -337,6 +337,168 @@ This library does not implement any validations. All errors from the api will ca
337
337
  It is thought that user will create his own validation mechanisms specific not only for restrictions imposed by the Syncano, but also for his own logic.
338
338
  It can be compared to the exceptions after violating constraints in the MySQL database.
339
339
 
340
+ ### Integration with Ruby on Rails
341
+
342
+ Syncano gem provides handy class for implementing model with ActiveRecord pattern. See example below:
343
+
344
+ ```ruby
345
+ class Category < Syncano::ActiveRecord::Base
346
+ has_many :articles
347
+
348
+ attribute :name, type: String
349
+ validates :name, presence: true
350
+ end
351
+
352
+ class Article < Syncano::ActiveRecord::Base
353
+ belongs_to :category
354
+
355
+ attribute :title, type: String
356
+ attribute :text, type: String
357
+ attribute :promoted, type: Integer, filterable: :data1
358
+ validates :title, presence: true
359
+ validates :text, presence: true
360
+
361
+ scope :promoted, -> { where('promoted = ?', 1) }
362
+
363
+ before_save :sanitize_content
364
+
365
+ private
366
+
367
+ def sanitize_content
368
+ self.title = Sanitize.clean(title)
369
+ self.text = Sanitize.clean(text)
370
+ end
371
+ end
372
+ ```
373
+
374
+ #### Attributes
375
+
376
+ As you can see above every attribute has to be declared with a type. Every ActiveRecord class has also three standard attributes:
377
+ - :id, type: Integer
378
+ - :created_at, type: DateTime
379
+ - :updated_at, type: DateTime
380
+
381
+ There can be up to three filterable attributes (mapped to the Syncano data1, data2, data3 attributes), which can be used in where and order clauses. They always should have Integer type.
382
+
383
+ Attributes can be validated like in standard ActiveRecord.
384
+
385
+ #### Scopes and query building
386
+
387
+ You can sort and filter by filterable attributes:
388
+
389
+ ```ruby
390
+ where('attribute1 > ? AND attribute2 <= ?', 0, 30).order('attribute3 ASC').where('attribute 2 > ?', 5)
391
+ ```
392
+
393
+ As you can see methods can be chained.
394
+
395
+ #### Callbacks
396
+
397
+ There are available ten different callbacks fired in the following sequence:
398
+
399
+ 1. before_validation
400
+ 2. after_validation
401
+ 3. before_save
402
+ 4. before_create / before_update
403
+ 5. after_create / after_save
404
+ 6. after_save
405
+
406
+ 1. before_destroy
407
+ 2. after_destroy
408
+
409
+ #### Associations
410
+
411
+ There are three types of relations (belongs_to, has_one, has_many) which are based on Syncano parent - child mechanism.
412
+
413
+ ##### belongs_to :category
414
+
415
+ Adds following methods:
416
+
417
+ ```ruby
418
+ self.category
419
+ self.category = Category.first
420
+ self.category_id
421
+ self.category_id = Category.first.id
422
+ ```
423
+
424
+ Remember to always declare belongs_to association! It creates proper attribute in model.
425
+
426
+ ##### has_one :article
427
+
428
+ ```ruby
429
+ self.article
430
+ self.article = Article.first # this method updates article object
431
+ self.build_article(article_attributes)
432
+ self.create_article(article_attributes)
433
+ ```
434
+
435
+ ##### has_many :articles
436
+
437
+ ```ruby
438
+ self.articles
439
+ self.articles = Article.first(5) # this method updates each article object
440
+ self.articles << Article.first # this method updates article object
441
+ self.articles.build(article_attributes)
442
+ self.articles.create(article_attributes)
443
+ ```
444
+
445
+ You can also call scope builder methods on has_many collection:
446
+
447
+ ```ruby
448
+ self.articles.promoted.all
449
+ ```
450
+
451
+ #### Other useful methods in examples
452
+
453
+ ```ruby
454
+ Article.promoted.find(id)
455
+ Article.where('promoted = ?', 0).count
456
+ Article.since(min_id).before(max_id)
457
+ Article.since(Time.now - 10.days)
458
+ ```
459
+
460
+ For all methods please see reference for Syncano::ActiveRecord::Base class.
461
+
462
+ #### Collection and folders
463
+
464
+ Classes which inherit from Syncano::ActiveRecord::Base needs a collection and a folders in Syncano.
465
+
466
+ Collection can be configured as constant in initializer:
467
+
468
+ ```ruby
469
+ SYNCANO_ACTIVERECORD_COLLECTION = Syncano.client.project.first.collection.first
470
+ ```
471
+
472
+ or it can be overwritten in selected model:
473
+
474
+ ```ruby
475
+ class Article < Syncano::ActiveRecord::Base
476
+
477
+ private
478
+
479
+ def self.collection
480
+ Syncano.client.project.first.collection.first
481
+ end
482
+ end
483
+ ```
484
+
485
+ Folders are used as classes - each model as his own folder (ie. Articles). Folder is automatically created and used without any additional configuration, but you can customize convention by overwriting folder_name or folder method:
486
+
487
+ ```ruby
488
+ class Article < Syncano::ActiveRecord::Base
489
+
490
+ private
491
+
492
+ def self.folder_name
493
+ 'Posts'
494
+ end
495
+
496
+ def self.folder
497
+ collection.folders.find_by_name(folder_name)
498
+ end
499
+ end
500
+ ```
501
+
340
502
  ## Contributing
341
503
 
342
504
  1. Fork it
@@ -0,0 +1,40 @@
1
+ require 'active_support'
2
+ require 'active_model/dirty'
3
+ require 'active_attr'
4
+
5
+ # Overwritting ActiveAttr module
6
+ module ActiveAttr
7
+ # Overwritting ActiveAttr::Dirty module
8
+ module Dirty
9
+ extend ActiveSupport::Concern
10
+ include ActiveModel::Dirty
11
+
12
+ # Class methods for ActiveAttr::Dirty module
13
+ module ClassMethods
14
+ # Overwritten attribute! method
15
+ # @param [Symbol] name
16
+ # @param [Hash] options
17
+ def attribute!(name, options={})
18
+ super(name, options)
19
+ define_method("#{name}=") do |value|
20
+ send("#{name}_will_change!") unless value == read_attribute(name)
21
+ super(value)
22
+ end
23
+ end
24
+ end
25
+
26
+ # Overwritten constructor
27
+ # @param [Hash] attributes
28
+ # @param [Hash] options
29
+ def initialize(attributes = nil, options = {})
30
+ super(attributes, options)
31
+ (@changed_attributes || {}).clear unless new_record?
32
+ end
33
+
34
+ # Overwritten save method
35
+ def save
36
+ @previously_changed = changes
37
+ @changed_attributes.clear
38
+ end
39
+ end
40
+ end
@@ -1,4 +1,7 @@
1
1
  # Syncano instance name
2
2
  SYNCANO_INSTANCE_NAME = 'instance_name'
3
3
  # Syncano api key
4
- SYNCANO_API_KEY = 'api_key'
4
+ SYNCANO_API_KEY = 'api_key'
5
+
6
+ # Collection used for Syncano::ActiveRecord
7
+ # SYNCANO_ACTIVERECORD_COLLECTION = Syncano.client.projects.first.collections.first
data/lib/syncano.rb CHANGED
@@ -59,6 +59,16 @@ require 'active_support/core_ext/object/blank.rb'
59
59
  require 'active_support/json/decoding.rb'
60
60
  require 'active_support/json/encoding.rb'
61
61
  require 'active_support/time_with_zone.rb'
62
+ require 'active_support/concern'
63
+ require 'active_support/inflector/inflections'
64
+
65
+ # ActiveModel
66
+ require 'active_model/forbidden_attributes_protection'
67
+ require 'active_model/dirty'
68
+
69
+ # ActiveAttr
70
+ require 'active_attr/model'
71
+ require 'active_attr/dirty'
62
72
 
63
73
  # Syncano
64
74
  require 'syncano/errors'
@@ -92,4 +102,5 @@ require 'syncano/resources/notifications/base'
92
102
  require 'syncano/resources/notifications/create'
93
103
  require 'syncano/resources/notifications/update'
94
104
  require 'syncano/resources/notifications/destroy'
95
- require 'syncano/resources/notifications/message'
105
+ require 'syncano/resources/notifications/message'
106
+ require 'syncano/active_record/base'
@@ -0,0 +1,38 @@
1
+ require 'syncano/active_record/scope_builder'
2
+
3
+ class Syncano
4
+ module ActiveRecord
5
+ # Module with associations functionality for Syncano::ActiveRecord
6
+ module Association
7
+ # Base class for all associations
8
+ class Base
9
+ # Constructor for association
10
+ # @param [Class] source_model
11
+ # @param [Symbol] name
12
+ def initialize(source_model, name)
13
+ self.source_model = source_model
14
+ self.associated_model = name.to_s.classify.constantize
15
+ self.foreign_key = source_model.name.foreign_key
16
+ end
17
+
18
+ # Checks if association is belongs_to type
19
+ # @return [FalseClass]
20
+ def belongs_to?
21
+ false
22
+ end
23
+
24
+ # Checks if association is has_one type
25
+ # @return [FalseClass]
26
+ def has_one?
27
+ false
28
+ end
29
+
30
+ # Checks if association is has_many type
31
+ # @return [FalseClass]
32
+ def has_many?
33
+ false
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,30 @@
1
+ require 'syncano/active_record/association/base'
2
+
3
+ class Syncano
4
+ module ActiveRecord
5
+ module Association
6
+ # Class for belongs to association
7
+ class BelongsTo < Syncano::ActiveRecord::Association::Base
8
+ attr_reader :associated_model, :foreign_key, :source_model
9
+
10
+ # Constructor for belongs_to association
11
+ # @param [Class] source_model
12
+ # @param [Symbol] name
13
+ def initialize(source_model, name)
14
+ super
15
+ self.foreign_key = associated_model.name.foreign_key
16
+ end
17
+
18
+ # Checks if association is belongs_to type
19
+ # @return [TrueClass]
20
+ def belongs_to?
21
+ true
22
+ end
23
+
24
+ private
25
+
26
+ attr_writer :associated_model, :foreign_key, :source_model
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,69 @@
1
+ require 'syncano/active_record/association/base'
2
+
3
+ class Syncano
4
+ module ActiveRecord
5
+ module Association
6
+ # Class for has many association
7
+ class HasMany < Syncano::ActiveRecord::Association::Base
8
+ attr_reader :associated_model, :foreign_key, :source_model
9
+
10
+ # Checks if association is has_many type
11
+ # @return [TrueClass]
12
+ def has_many?
13
+ true
14
+ end
15
+
16
+ # Returns new associaton object with source object set
17
+ # @param [Object] source
18
+ # @return [Syncano::ActiveRecord::Association::HasMany]
19
+ def scope_builder(source)
20
+ association = self.dup
21
+ association.source = source
22
+ association
23
+ end
24
+
25
+ # Builds new associated object
26
+ # @return [Object]
27
+ def build
28
+ associated_model.new(foreign_key => source.id)
29
+ end
30
+
31
+ # Creates new associated object
32
+ # @return [Object]
33
+ def create
34
+ associated_model.create(foreign_key => source.id)
35
+ end
36
+
37
+ # Adds object to the related collection by setting foreign key
38
+ # @param [Object] object
39
+ # @return [Object]
40
+ def <<(object)
41
+ object.send("#{foreign_key}=", source.id)
42
+ object.save unless object.new_record?
43
+ object
44
+ end
45
+
46
+ protected
47
+
48
+ attr_accessor :source
49
+
50
+ private
51
+
52
+ attr_writer :associated_model, :foreign_key, :source_model
53
+
54
+ # Overwritten method_missing for handling scope methods
55
+ # @param [String] name
56
+ # @param [Array] args
57
+ def method_missing(name, *args)
58
+ scope_builder = Syncano::ActiveRecord::ScopeBuilder.new(associated_model).by_parent_id(source.id)
59
+
60
+ if scope_builder.respond_to?(name) || !source.scopes[name].nil?
61
+ scope_builder.send(name, *args)
62
+ else
63
+ super
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,22 @@
1
+ require 'syncano/active_record/association/base'
2
+
3
+ class Syncano
4
+ module ActiveRecord
5
+ module Association
6
+ # Class for has one association
7
+ class HasOne < Syncano::ActiveRecord::Association::Base
8
+ attr_reader :associated_model, :foreign_key, :source_model
9
+
10
+ # Checks if association is has_one type
11
+ # @return [TrueClass]
12
+ def has_one?
13
+ true
14
+ end
15
+
16
+ private
17
+
18
+ attr_writer :associated_model, :foreign_key, :source_model
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,112 @@
1
+ require 'syncano/active_record/association/belongs_to'
2
+ require 'syncano/active_record/association/has_many'
3
+ require 'syncano/active_record/association/has_one'
4
+
5
+ class Syncano
6
+ module ActiveRecord
7
+ # Module with associations functionality for Syncano::ActiveRecord
8
+ module Associations
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ private
13
+
14
+ class_attribute :_associations
15
+ end
16
+
17
+ # Class methods for Syncano::ActiveRecord::Associations module
18
+ module ClassMethods
19
+ # Lists hash with associations
20
+ # @return [HashWithIndifferentAccess]
21
+ def associations
22
+ self._associations ||= HashWithIndifferentAccess.new
23
+ end
24
+
25
+ private
26
+
27
+ # Setter for associations
28
+ def associations=(hash)
29
+ self._associations = hash
30
+ end
31
+
32
+ # Defines belongs_to association
33
+ # @param [Symbol] object_name
34
+ def belongs_to(object_name)
35
+ association = Syncano::ActiveRecord::Association::BelongsTo.new(self, object_name)
36
+ associations[object_name] = association
37
+
38
+ attribute association.foreign_key
39
+ validates association.foreign_key, numericality: { only_integer: true, allow_nil: true }
40
+
41
+ define_method(object_name) do
42
+ id = send(self.class.associations[object_name].foreign_key)
43
+ scope = scope_builder(self.class.associations[object_name].associated_model)
44
+ scope.find(id) if id.present?
45
+ end
46
+
47
+ define_method("#{object_name}=") do |object|
48
+ unless object.is_a?(self.class.associations[object_name].associated_model)
49
+ "Object should be an instance of #{self.class.associations[object_name].associated_model} class"
50
+ end
51
+ send("#{self.class.associations[object_name].foreign_key}=", object.try(:id))
52
+ end
53
+ end
54
+
55
+ # Defines has_one association
56
+ # @param [Symbol] object_name
57
+ def has_one(object_name)
58
+ association = Syncano::ActiveRecord::Association::HasOne.new(self, object_name)
59
+ associations[object_name] = association
60
+
61
+ define_method(object_name) do
62
+ scope = scope_builder(self.class.associations[object_name].associated_model)
63
+ scope.by_parent_id(id).first if id
64
+ end
65
+
66
+ define_method("#{object_name}=") do |object|
67
+ object.send("#{self.class.associations[object_name].foreign_key}=", id)
68
+ object.save unless object.new_record?
69
+ object
70
+ end
71
+
72
+ define_method("build_#{object_name}") do |attributes = {}|
73
+ self.class.associations[object_name].associated_model.new(attributes)
74
+ end
75
+
76
+ define_method("create_#{object_name}") do |attributes = {}|
77
+ self.class.associations[object_name].associated_model.create(attributes)
78
+ end
79
+ end
80
+
81
+ # Defines has_many association
82
+ # @param [Symbol] collection_name
83
+ def has_many(collection_name)
84
+ association = Syncano::ActiveRecord::Association::HasMany.new(self, collection_name)
85
+ associations[collection_name] = association
86
+
87
+ define_method(collection_name) do
88
+ self.class.associations[collection_name].scope_builder(self)
89
+ end
90
+
91
+ define_method("#{collection_name}=") do |collection|
92
+ association = self.class.associations[collection_name]
93
+
94
+ collection.each do |object|
95
+ "Object should be an instance of #{association.associated_class} class" unless object.is_a?(association.associated_class)
96
+ end
97
+
98
+ send(collection_name).all.each do |object|
99
+ object.send("#{association.foreign_key}=", nil)
100
+ object.save
101
+ end
102
+
103
+ collection.each do |object|
104
+ object.send("#{association.foreign_key}=", id)
105
+ object.save
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,318 @@
1
+ require 'syncano/active_record/scope_builder'
2
+ require 'syncano/active_record/associations'
3
+ require 'syncano/active_record/callbacks'
4
+
5
+ class Syncano
6
+ # Scope for modules and classes integrating ActiveRecord functionality
7
+ module ActiveRecord
8
+ # Class for integrating ActiveRecord functionality
9
+ class Base
10
+ include ActiveAttr::Model
11
+ include ActiveAttr::Dirty
12
+ include ActiveModel::ForbiddenAttributesProtection
13
+ include Syncano::ActiveRecord::Associations
14
+ include Syncano::ActiveRecord::Callbacks
15
+
16
+ attribute :id, type: Integer
17
+ attribute :created_at, type: DateTime
18
+ attribute :updated_at, type: DateTime
19
+
20
+ # Gets collection with all objects
21
+ # @return [Array]
22
+ def self.all
23
+ scope_builder.all
24
+ end
25
+
26
+ # Counts all objects
27
+ # @return [Integer]
28
+ def self.count
29
+ scope_builder.count
30
+ end
31
+
32
+ # Returns first object or collection of first x objects
33
+ # @param [Integer] amount
34
+ # @return [Object, Array]
35
+ def self.first(amount = nil)
36
+ scope_builder.first(amount)
37
+ end
38
+
39
+ # Returns last object or collection of last x objects
40
+ # @param [Integer] amount
41
+ # @return [Object, Array]
42
+ def self.last(amount = nil)
43
+ scope_builder.last(amount)
44
+ end
45
+
46
+ # Returns scope builder with condition passed as arguments
47
+ # @param [String] condition
48
+ # @param [Array] params
49
+ # @return [Syncano::ActiveRecord::ScopeBuilder]
50
+ def self.where(condition, *params)
51
+ scope_builder.where(condition, *params)
52
+ end
53
+
54
+ # Returns scope builder with order passed as first argument
55
+ # @param [String] order
56
+ # @return [Syncano::ActiveRecord::ScopeBuilder]
57
+ def self.order(order)
58
+ scope_builder.order(order)
59
+ end
60
+
61
+ # Returns one object found by id
62
+ # @param [Integer] id
63
+ # @return [Object]
64
+ def self.find(id)
65
+ scope_builder.find(id)
66
+ end
67
+
68
+ # Creates new object with specified attributes
69
+ # @param [Hash] attributes
70
+ # @return [Object]
71
+ def self.create(attributes)
72
+ new_object = self.new(attributes)
73
+ new_object.save
74
+ new_object
75
+ end
76
+
77
+ # Returns scope builder with filtering by ids newer than provided
78
+ # @param [Integer] id
79
+ # @return [Syncano::ActiveRecord::ScopeBuilder]
80
+ def self.since(id)
81
+ scope_builder.since(id)
82
+ end
83
+
84
+ # Returns scope builder with filtering by ids older than provided
85
+ # @param [Integer] id
86
+ # @return [Syncano::ActiveRecord::ScopeBuilder]
87
+ def self.before(id)
88
+ scope_builder.before(id)
89
+ end
90
+
91
+ # Returns corresponding Syncano folder
92
+ # @return [Syncano::Resources::Folder]
93
+ def self.folder
94
+ begin
95
+ folder = collection.folders.find_by_name(folder_name)
96
+ rescue Syncano::ApiError => e
97
+ if e.message.starts_with?('DoesNotExist')
98
+ folder = collection.folders.create(name: folder_name)
99
+ else
100
+ raise e
101
+ end
102
+ end
103
+ folder
104
+ end
105
+
106
+ # Returns scope builder with limit parameter set to parameter
107
+ # @param [Integer] amount
108
+ # @return [Syncano::ActiveRecord::ScopeBuilder]
109
+ def self.limit(amount)
110
+ scope_builder.limit(amount)
111
+ end
112
+
113
+ # Returns hash with filterable attributes
114
+ # @return [HashWithIndifferentAccess]
115
+ def self.filterable_attributes
116
+ self._filterable_attributes ||= HashWithIndifferentAccess.new
117
+ end
118
+
119
+ # Returns hash with scopes
120
+ # @return [HashWithIndifferentAccess]
121
+ def self.scopes
122
+ self._scopes ||= HashWithIndifferentAccess.new
123
+ end
124
+
125
+ # Maps syncano attributes to corresponding model attributes
126
+ # @param [Hash] attributes
127
+ # @return [HashWithIndifferentAccess]
128
+ def self.map_from_syncano_attributes(attributes = {})
129
+ mappings = HashWithIndifferentAccess.new(filterable_attributes.invert)
130
+ HashWithIndifferentAccess[attributes.map {|k, v| [mappings[k] || k, v] }]
131
+ end
132
+
133
+ # Maps model attributes to corresponding syncano attributes
134
+ # @param [Hash] attributes
135
+ # @return [HashWithIndifferentAccess]
136
+ def self.map_to_syncano_attributes(attributes = {})
137
+ mappings = filterable_attributes
138
+ HashWithIndifferentAccess[attributes.map {|k, v| [mappings[k] || k, v] }]
139
+ end
140
+
141
+ # Maps one model attribute to corresponding syncano attribute
142
+ # @param [Symbol, String] attribute
143
+ # @return [String]
144
+ def self.map_to_syncano_attribute(attribute)
145
+ mappings = filterable_attributes
146
+ mappings[attribute] || attribute
147
+ end
148
+
149
+ # Constructor for model
150
+ # @param [Hash] params
151
+ def initialize(params = {})
152
+ if params.is_a?(Syncano::Resources::DataObject)
153
+ super(self.class.map_from_syncano_attributes(params.attributes).merge(id: params.id))
154
+ else
155
+ params.delete(:id)
156
+ super(self.class.map_from_syncano_attributes(params))
157
+ end
158
+ end
159
+
160
+ # Overwritten equality operator
161
+ # @param [Object] object
162
+ # @return [TrueClass, FalseClass]
163
+ def ==(object)
164
+ self.class == object.class && self.id == object.id
165
+ end
166
+
167
+ # Updates object with specified attributes
168
+ # @param [Hash] attributes
169
+ # @return [TrueClass, FalseClass]
170
+ def update_attributes(attributes)
171
+ self.attributes = attributes
172
+ self.save
173
+ end
174
+
175
+ # Performs validations
176
+ # @return [TrueClass, FalseClass]
177
+ def valid?
178
+ process_callbacks(:before_validation)
179
+ process_callbacks(:after_validation) if result = super
180
+ result
181
+ end
182
+
183
+ # Saves object in Syncano
184
+ # @return [TrueClass, FalseClass]
185
+ def save
186
+ saved = false
187
+
188
+ if valid?
189
+ process_callbacks(:before_save)
190
+ process_callbacks(persisted? ? :before_update : :before_create)
191
+
192
+ data_object = persisted? ? self.class.folder.data_objects.find(id) : self.class.folder.data_objects.new
193
+ data_object.attributes = self.class.map_to_syncano_attributes(attributes.except(:id, :created_at, :updated_at))
194
+ data_object.save
195
+
196
+ if data_object.saved?
197
+ self.updated_at = data_object[:updated_at]
198
+
199
+ if persisted?
200
+ process_callbacks(:after_update)
201
+ else
202
+ self.id = data_object.id
203
+ self.created_at = data_object[:created_at]
204
+ process_callbacks(:after_create)
205
+ end
206
+
207
+ self.class.associations.values.select{ |association| association.belongs_to? }.each do |association|
208
+ change = changes[association.foreign_key]
209
+
210
+ if change.present?
211
+ if change.last.nil? || association.associated_model.find(change.last).present?
212
+ data_object.remove_parent(change.first) unless change.first.nil?
213
+ data_object.add_parent(change.last) unless change.last.nil?
214
+ end
215
+ end
216
+ end
217
+
218
+ super
219
+
220
+ process_callbacks(:after_save)
221
+ saved = true
222
+ end
223
+ end
224
+
225
+ saved
226
+ end
227
+
228
+ # Deletes object from Syncano
229
+ # @return [TrueClass, FalseClass]
230
+ def destroy
231
+ process_callbacks(:before_destroy)
232
+ data_object = self.class.folder.data_objects.find(id)
233
+ data_object.destroy
234
+ process_callbacks(:after_destroy) if data_object.destroyed
235
+ data_object.destroyed
236
+ end
237
+
238
+ # Checks if object has not been saved in Syncano yet
239
+ # @return [TrueClass, FalseClass]
240
+ def new_record?
241
+ !persisted?
242
+ end
243
+
244
+ # Checks if object has been already saved in Syncano
245
+ # @return [TrueClass, FalseClass]
246
+ def persisted?
247
+ id.present?
248
+ end
249
+
250
+ private
251
+
252
+ class_attribute :_filterable_attributes, :_scopes
253
+
254
+ # Returns Syncano collection for storing Syncano::ActiveRecord objects
255
+ # @return [Syncano::Resource::Collection]
256
+ def self.collection
257
+ SYNCANO_ACTIVERECORD_COLLECTION
258
+ end
259
+
260
+ # Returns Syncano collection for storing Syncano::ActiveRecord objects
261
+ # @return [Syncano::Resource::Collection]
262
+ def self.folder_name
263
+ name.pluralize
264
+ end
265
+
266
+ # Setter for filterable_attributes attribute
267
+ def self.filterable_attributes=(hash)
268
+ self._filterable_attributes = hash
269
+ end
270
+
271
+ # Setter for scopes attribute
272
+ def self.scopes=(hash)
273
+ self._scopes = hash
274
+ end
275
+
276
+ # Returns scope builder for current model
277
+ # @return [Syncano::ActiveRecord::ScopeBuilder]
278
+ def self.scope_builder
279
+ Syncano::ActiveRecord::ScopeBuilder.new(self)
280
+ end
281
+
282
+ # Defines model attribute
283
+ # @param [Symbol] name
284
+ # @param [Hash] options
285
+ def self.attribute(name, options = {})
286
+ if options[:filterable].present?
287
+ self.filterable_attributes = HashWithIndifferentAccess.new if filterable_attributes.nil?
288
+ filterable_attributes[name] = options.delete(:filterable)
289
+ end
290
+ super(name, options)
291
+ end
292
+
293
+ # Defines model scope
294
+ # @param [Symbol] name
295
+ # @param [Proc] procedure
296
+ def self.scope(name, procedure)
297
+ scopes[name] = procedure
298
+ end
299
+
300
+ # Overwritten method_missing for handling calling defined scopes
301
+ # @param [String] name
302
+ # @param [Array] args
303
+ def self.method_missing(name, *args)
304
+ if scopes[name].nil?
305
+ super
306
+ else
307
+ scope_builder.send(name.to_sym, *args)
308
+ end
309
+ end
310
+
311
+ # Returns scope builder for specified class
312
+ # @return [Syncano::ActiveRecord::ScopeBuilder]
313
+ def scope_builder(object_class)
314
+ Syncano::ActiveRecord::ScopeBuilder.new(object_class)
315
+ end
316
+ end
317
+ end
318
+ end
@@ -0,0 +1,46 @@
1
+ class Syncano
2
+ module ActiveRecord
3
+ # Module with callbacks functionality for Syncano::ActiveRecord
4
+ module Callbacks
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ # Defines chains for all types of callbacks
9
+ [:validation, :save, :create, :update, :destroy].each do |action|
10
+ [:before, :after].each do |type|
11
+ chain_name = "#{type}_#{action}_callbacks"
12
+ class_attribute chain_name
13
+ send("#{chain_name}=", [])
14
+ end
15
+ end
16
+ end
17
+
18
+ # Class methods for Syncano::ActiveRecord::Callbacks module
19
+ module ClassMethods
20
+ private
21
+
22
+ [:validation, :save, :create, :update, :destroy].each do |action|
23
+ [:before, :after].each do |type|
24
+ define_method("prepend_#{type}_#{action}") do |argument|
25
+ send("#{type}_#{action}_callbacks").unshift(argument)
26
+ end
27
+
28
+ define_method("#{type}_#{action}") do |argument|
29
+ send("#{type}_#{action}_callbacks") << argument
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ # Processes callbacks with specified type
36
+ # @param [Symbol, String] type
37
+ def process_callbacks(type)
38
+ if respond_to?("#{type}_callbacks")
39
+ send("#{type}_callbacks").each do |callback_name|
40
+ send(callback_name)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,174 @@
1
+ class Syncano
2
+ module ActiveRecord
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::ActiveRecord::Base' unless model <= Syncano::ActiveRecord::Base
9
+
10
+ self.model = model
11
+ self.parameters = {}
12
+ end
13
+
14
+ # Returns collection of objects
15
+ # @return [Array]
16
+ def all
17
+ folder.data_objects.all(parameters).collect do |data_object|
18
+ model.new(data_object)
19
+ end
20
+ end
21
+
22
+ # Returns count of objects
23
+ # @return [Integer]
24
+ def count
25
+ folder.data_objects.all(parameters).count
26
+ end
27
+
28
+ # Returns one object found by id
29
+ # @param [Integer] id
30
+ # @return [Object]
31
+ def find(id)
32
+ parameters[:data_ids] = [id]
33
+ all.first
34
+ end
35
+
36
+ # Returns first object or collection of first x objects
37
+ # @param [Integer] amount
38
+ # @return [Object, Array]
39
+ def first(amount = nil)
40
+ objects = all.first(amount || 1)
41
+ amount.nil? ? objects.first : objects
42
+ end
43
+
44
+ # Returns last object or last x objects
45
+ # @param [Integer] amount
46
+ # @return [Object, Array]
47
+ def last(amount)
48
+ objects = all.last(amount || 1)
49
+ amount.nil? ? objects.first : objects
50
+ end
51
+
52
+ # Adds to the current scope builder condition to the scope builder
53
+ # @param [String] condition
54
+ # @param [Array] params
55
+ # @return [Syncano::ActiveRecord::ScopeBuilder]
56
+ def where(condition, *params)
57
+ raise 'Invalid params count in where clause!' unless condition.count('?') == params.count
58
+
59
+ params.each do |param|
60
+ condition.sub!('?', param.to_s)
61
+ end
62
+
63
+ conditions = condition.gsub(/\s+/, ' ').split(/and/i)
64
+
65
+ conditions.each do |condition|
66
+ attribute, operator, value = condition.split(' ')
67
+
68
+ raise 'Invalid attribute in where clause!' unless model.attributes.keys.include?(attribute)
69
+ raise 'Invalid operator in where clause!' unless self.class.where_mapping.keys.include?(operator)
70
+ raise 'Parameter in where clause is not an integer!' if !(value =~ /\A[-+]?[0-9]+\z/)
71
+
72
+ method_name = "#{model.filterable_attributes[attribute]}__#{self.class.where_mapping[operator]}"
73
+ parameters[method_name] = value
74
+ end
75
+
76
+ self
77
+ end
78
+
79
+ # Adds to the current scope builder condition for filtering by parent_id
80
+ # @param [Integer] parent_id
81
+ # @return [Syncano::ActiveRecord::ScopeBuilder]
82
+ def by_parent_id(parent_id)
83
+ parameters[:parent_ids] = parent_id
84
+ self
85
+ end
86
+
87
+ # Adds to the current scope builder order clause
88
+ # @param [String] order
89
+ # @return [Syncano::ActiveRecord::ScopeBuilder]
90
+ def order(order)
91
+ attribute, order_type = order.gsub(/\s+/, ' ').split(' ')
92
+ raise 'Invalid attribute in order clause' unless (model.attributes.keys + ['id', 'created_at']).include?(attribute)
93
+
94
+ attribute = model.map_to_syncano_attribute(attribute)
95
+ order_type = order_type.to_s.downcase == 'desc' ? 'DESC' : 'ASC'
96
+
97
+ self.parameters.merge!({ order_by: attribute, order: order_type })
98
+
99
+ self
100
+ end
101
+
102
+ # Adds to the current scope builder condition for filtering by ids newer than provided
103
+ # @param [Integer, String] id - id or datetime
104
+ # @return [Syncano::ActiveRecord::ScopeBuilder]
105
+ def since(id)
106
+ if !(id =~ /\A[-+]?[0-9]+\z/)
107
+ self.parameters[:since] = id
108
+ else
109
+ self.parameters[:since_time] = id.to_time
110
+ end
111
+ self
112
+ end
113
+
114
+ # Adds to the current scope builder condition for filtering by ids older than provided
115
+ # @param [Integer] id
116
+ # @return [Syncano::ActiveRecord::ScopeBuilder]
117
+ def before(id)
118
+ self.parameters[:max_id] = id
119
+ self
120
+ end
121
+
122
+ # Adds to the current scope builder limit clause
123
+ # @param [Integer] amount
124
+ # @return [Syncano::ActiveRecord::ScopeBuilder]
125
+ def limit(amount)
126
+ self.parameters[:limit] = amount
127
+ self
128
+ end
129
+
130
+ private
131
+
132
+ attr_accessor :parameters, :model, :scopes
133
+
134
+ # Returns folder for current model
135
+ # @return [Syncano::Resources::Folder]
136
+ def folder
137
+ model.folder
138
+ end
139
+
140
+ # Returns scopes for current model
141
+ # @return [HashWithIndifferentAccess]
142
+ def scopes
143
+ model.scopes
144
+ end
145
+
146
+ # Returns mapping for operators
147
+ # @return [Hash]
148
+ def self.where_mapping
149
+ { '=' => 'eq', '!=' => 'neq', '<>' => 'neq', '>=' => 'gte', '>' => 'gt', '<=' => 'lte', '<' => 'lt' }
150
+ end
151
+
152
+ # Applies scope to the current scope builder
153
+ # @param [Symbol] name
154
+ # @param [Array] args
155
+ # @return [Syncano::ActiveRecord::ScopeBuilder]
156
+ def execute_scope(name, *args)
157
+ procedure = scopes[name]
158
+ instance_exec(*args, &procedure)
159
+ self
160
+ end
161
+
162
+ # Overwritten method_missing for handling calling defined scopes
163
+ # @param [String] name
164
+ # @param [Array] args
165
+ def method_missing(name, *args)
166
+ if scopes[name].nil?
167
+ super
168
+ else
169
+ execute_scope(name, *args)
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
@@ -126,16 +126,18 @@ class Syncano
126
126
  # @return [Syncano::Resources::Base]
127
127
  def save
128
128
  response = perform_save(nil)
129
+ response_data = ActiveSupport::HashWithIndifferentAccess.new(response.data)
129
130
 
130
131
  if new_record?
131
- response_data = ActiveSupport::HashWithIndifferentAccess.new(response.data)
132
132
  created_object = self.class.new(client, self.class.map_to_scope_parameters(attributes).merge(response_data))
133
133
 
134
134
  self.id = created_object.id
135
135
  self.attributes.merge!(created_object.attributes)
136
- mark_as_saved!
136
+ else
137
+ self[:updated_at] = response_data[:updated_at]
137
138
  end
138
139
 
140
+ mark_as_saved!
139
141
  self
140
142
  end
141
143
 
@@ -211,6 +211,11 @@ class Syncano
211
211
  end
212
212
  end
213
213
 
214
+ if attributes.keys.map(&:to_sym).include?(:folders) && !attributes.keys.map(&:to_sym).include?(:folder)
215
+ attributes[:folder] = attributes[:folders]
216
+ attributes.delete(:folders)
217
+ end
218
+
214
219
  attributes.delete(:user)
215
220
  attributes.delete(:created_at)
216
221
  attributes.delete(:updated_at)
@@ -5,7 +5,7 @@ class Syncano
5
5
  # Association has_many :data_objects
6
6
  # @return [Syncano::QueryBuilder] query builder for resource Syncano::Resources::DataObject
7
7
  def data_objects
8
- ::Syncano::QueryBuilder.new(client, ::Syncano::Resources::DataObject, scope_parameters.merge(folder: @saved_attributes[:name]))
8
+ ::Syncano::QueryBuilder.new(client, ::Syncano::Resources::DataObject, scope_parameters.merge(folders: @saved_attributes[:name]))
9
9
  end
10
10
 
11
11
  # Wrapper for api "get_one" method with folder_name as a key
@@ -1,4 +1,4 @@
1
1
  class Syncano
2
2
  # Syncano version number
3
- VERSION = '3.1.1.beta5'
3
+ VERSION = '3.1.1'
4
4
  end
data/syncano.gemspec CHANGED
@@ -20,8 +20,10 @@ Gem::Specification.new do |spec|
20
20
 
21
21
  spec.add_dependency 'jimson-client'
22
22
  spec.add_dependency 'activesupport'
23
+ spec.add_dependency 'activemodel'
23
24
  spec.add_dependency 'multi_json', '~> 1.10'
24
25
  spec.add_dependency 'eventmachine'
26
+ spec.add_dependency 'active_attr'
25
27
 
26
28
  spec.add_development_dependency 'bundler', '~> 1.6'
27
29
  spec.add_development_dependency 'rake'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: syncano
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.1.beta5
4
+ version: 3.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Piotr Zadrożny
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-06-11 00:00:00.000000000 Z
11
+ date: 2014-07-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jimson-client
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: activemodel
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: multi_json
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -66,6 +80,20 @@ dependencies:
66
80
  - - ">="
67
81
  - !ruby/object:Gem::Version
68
82
  version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: active_attr
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
69
97
  - !ruby/object:Gem::Dependency
70
98
  name: bundler
71
99
  requirement: !ruby/object:Gem::Requirement
@@ -163,9 +191,18 @@ files:
163
191
  - LICENSE.txt
164
192
  - README.md
165
193
  - Rakefile
194
+ - lib/active_attr/dirty.rb
166
195
  - lib/generators/syncano/install_generator.rb
167
196
  - lib/generators/syncano/templates/initializers/syncano.rb
168
197
  - lib/syncano.rb
198
+ - lib/syncano/active_record/association/base.rb
199
+ - lib/syncano/active_record/association/belongs_to.rb
200
+ - lib/syncano/active_record/association/has_many.rb
201
+ - lib/syncano/active_record/association/has_one.rb
202
+ - lib/syncano/active_record/associations.rb
203
+ - lib/syncano/active_record/base.rb
204
+ - lib/syncano/active_record/callbacks.rb
205
+ - lib/syncano/active_record/scope_builder.rb
169
206
  - lib/syncano/batch_queue.rb
170
207
  - lib/syncano/batch_queue_element.rb
171
208
  - lib/syncano/clients/base.rb
@@ -227,9 +264,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
227
264
  version: '0'
228
265
  required_rubygems_version: !ruby/object:Gem::Requirement
229
266
  requirements:
230
- - - ">"
267
+ - - ">="
231
268
  - !ruby/object:Gem::Version
232
- version: 1.3.1
269
+ version: '0'
233
270
  requirements: []
234
271
  rubyforge_project:
235
272
  rubygems_version: 2.2.2
@@ -248,3 +285,4 @@ test_files:
248
285
  - spec/spec_helper.rb
249
286
  - spec/sync_resources_spec.rb
250
287
  - spec/syncano_spec.rb
288
+ has_rdoc: