active_remote 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. data/.gitignore +10 -0
  2. data/.rspec +2 -0
  3. data/.rvmrc +1 -0
  4. data/Gemfile +7 -0
  5. data/LICENSE +22 -0
  6. data/README.md +86 -0
  7. data/Rakefile +21 -0
  8. data/active_remote.gemspec +35 -0
  9. data/lib/active_remote.rb +15 -0
  10. data/lib/active_remote/association.rb +152 -0
  11. data/lib/active_remote/attributes.rb +29 -0
  12. data/lib/active_remote/base.rb +49 -0
  13. data/lib/active_remote/bulk.rb +143 -0
  14. data/lib/active_remote/dirty.rb +70 -0
  15. data/lib/active_remote/dsl.rb +141 -0
  16. data/lib/active_remote/errors.rb +24 -0
  17. data/lib/active_remote/persistence.rb +226 -0
  18. data/lib/active_remote/rpc.rb +71 -0
  19. data/lib/active_remote/search.rb +131 -0
  20. data/lib/active_remote/serialization.rb +40 -0
  21. data/lib/active_remote/serializers/json.rb +18 -0
  22. data/lib/active_remote/serializers/protobuf.rb +100 -0
  23. data/lib/active_remote/version.rb +3 -0
  24. data/lib/core_ext/date.rb +7 -0
  25. data/lib/core_ext/date_time.rb +7 -0
  26. data/lib/core_ext/integer.rb +19 -0
  27. data/lib/protobuf_extensions/base_field.rb +18 -0
  28. data/spec/core_ext/date_time_spec.rb +10 -0
  29. data/spec/lib/active_remote/association_spec.rb +80 -0
  30. data/spec/lib/active_remote/base_spec.rb +10 -0
  31. data/spec/lib/active_remote/bulk_spec.rb +74 -0
  32. data/spec/lib/active_remote/dsl_spec.rb +73 -0
  33. data/spec/lib/active_remote/persistence_spec.rb +266 -0
  34. data/spec/lib/active_remote/rpc_spec.rb +94 -0
  35. data/spec/lib/active_remote/search_spec.rb +98 -0
  36. data/spec/lib/active_remote/serialization_spec.rb +57 -0
  37. data/spec/lib/active_remote/serializers/json_spec.rb +32 -0
  38. data/spec/lib/active_remote/serializers/protobuf_spec.rb +95 -0
  39. data/spec/spec_helper.rb +17 -0
  40. data/spec/support/definitions/author.proto +29 -0
  41. data/spec/support/definitions/post.proto +33 -0
  42. data/spec/support/definitions/support/protobuf/category.proto +29 -0
  43. data/spec/support/definitions/support/protobuf/error.proto +6 -0
  44. data/spec/support/definitions/tag.proto +29 -0
  45. data/spec/support/helpers.rb +37 -0
  46. data/spec/support/models.rb +5 -0
  47. data/spec/support/models/author.rb +14 -0
  48. data/spec/support/models/category.rb +14 -0
  49. data/spec/support/models/message_with_options.rb +11 -0
  50. data/spec/support/models/post.rb +16 -0
  51. data/spec/support/models/tag.rb +12 -0
  52. data/spec/support/protobuf.rb +4 -0
  53. data/spec/support/protobuf/author.pb.rb +54 -0
  54. data/spec/support/protobuf/category.pb.rb +54 -0
  55. data/spec/support/protobuf/error.pb.rb +21 -0
  56. data/spec/support/protobuf/post.pb.rb +58 -0
  57. data/spec/support/protobuf/tag.pb.rb +54 -0
  58. metadata +284 -0
@@ -0,0 +1,143 @@
1
+ require 'active_remote/persistence'
2
+
3
+ module ActiveRemote
4
+ module Bulk
5
+ def self.included(klass)
6
+ klass.class_eval do
7
+ extend ActiveRemote::Bulk::ClassMethods
8
+ include ActiveRemote::Persistence
9
+ end
10
+ end
11
+
12
+ ##
13
+ # Class methods
14
+ #
15
+ module ClassMethods
16
+
17
+ # Create multiple records at the same time. Returns a collection of active
18
+ # remote objects from the passed records. Records that were not created
19
+ # are returned with error messages indicating what went wrong.
20
+ #
21
+ # ====Examples
22
+ #
23
+ # # A single hash
24
+ # Tag.create_all({ :name => 'foo' })
25
+ #
26
+ # # Hashes
27
+ # Tag.create_all({ :name => 'foo' }, { :name => 'bar' })
28
+ #
29
+ # # Active remote objects
30
+ # Tag.create_all(Tag.new(:name => 'foo'), Tag.new(:name => 'bar'))
31
+ #
32
+ # # Protobuf objects
33
+ # Tag.create_all(Generic::Remote::Tag.new(:name => 'foo'), Generic::Remote::Tag.new(:name => 'bar'))
34
+ #
35
+ # # Bulk protobuf object
36
+ # Tag.create_all(Generic::Remote::Tags.new(:records => [ Generic::Remote::Tag.new(:name => 'foo') ])
37
+ #
38
+ def create_all(*records)
39
+ remote = self.new
40
+ remote.execute(:create_all, parse_records(records))
41
+ remote.serialize_records
42
+ end
43
+
44
+ # Delete multiple records at the same time. Returns a collection of active
45
+ # remote objects from the passed records. Records that were not deleted
46
+ # are returned with error messages indicating what went wrong.
47
+ #
48
+ # ====Examples
49
+ #
50
+ # # A single hash
51
+ # Tag.delete_all({ :guid => 'foo' })
52
+ #
53
+ # # Hashes
54
+ # Tag.delete_all({ :guid => 'foo' }, { :guid => 'bar' })
55
+ #
56
+ # # Active remote objects
57
+ # Tag.delete_all(Tag.new(:guid => 'foo'), Tag.new(:guid => 'bar'))
58
+ #
59
+ # # Protobuf objects
60
+ # Tag.delete_all(Generic::Remote::Tag.new(:guid => 'foo'), Generic::Remote::Tag.new(:guid => 'bar'))
61
+ #
62
+ # # Bulk protobuf object
63
+ # Tag.delete_all(Generic::Remote::Tags.new(:records => [ Generic::Remote::Tag.new(:guid => 'foo') ])
64
+ #
65
+ def delete_all(*records)
66
+ remote = self.new
67
+ remote.execute(:delete_all, parse_records(records))
68
+ remote.serialize_records
69
+ end
70
+
71
+ # Destroy multiple records at the same time. Returns a collection of active
72
+ # remote objects from the passed records. Records that were not destroyed
73
+ # are returned with error messages indicating what went wrong.
74
+ #
75
+ # ====Examples
76
+ #
77
+ # # A single hash
78
+ # Tag.destroy_all({ :guid => 'foo' })
79
+ #
80
+ # # Hashes
81
+ # Tag.destroy_all({ :guid => 'foo' }, { :guid => 'bar' })
82
+ #
83
+ # # Active remote objects
84
+ # Tag.destroy_all(Tag.new(:guid => 'foo'), Tag.new(:guid => 'bar'))
85
+ #
86
+ # # Protobuf objects
87
+ # Tag.destroy_all(Generic::Remote::Tag.new(:guid => 'foo'), Generic::Remote::Tag.new(:guid => 'bar'))
88
+ #
89
+ # # Bulk protobuf object
90
+ # Tag.destroy_all(Generic::Remote::Tags.new(:records => [ Generic::Remote::Tag.new(:guid => 'foo') ])
91
+ #
92
+ def destroy_all(*records)
93
+ remote = self.new
94
+ remote.execute(:destroy_all, parse_records(records))
95
+ remote.serialize_records
96
+ end
97
+
98
+ # Parse given records to get them ready to be built into a request.
99
+ #
100
+ # It handles any object that responds to +to_hash+, so protobuf messages
101
+ # and active remote objects will work just like hashes.
102
+ #
103
+ # Returns +{ :records => records }+.
104
+ #
105
+ def parse_records(*records)
106
+ records.flatten!
107
+ records.collect!(&:to_hash)
108
+
109
+ return records.first if records.first.has_key?(:records)
110
+
111
+ # If we made it this far, build a bulk-formatted hash.
112
+ return { :records => records }
113
+ end
114
+
115
+ # Update multiple records at the same time. Returns a collection of active
116
+ # remote objects from the passed records. Records that were not updated
117
+ # are returned with error messages indicating what went wrong.
118
+ #
119
+ # ====Examples
120
+ #
121
+ # # A single hash
122
+ # Tag.update_all({ :guid => 'foo', :name => 'baz' })
123
+ #
124
+ # # Hashes
125
+ # Tag.update_all({ :guid => 'foo', :name => 'baz' }, { :guid => 'bar', :name => 'qux' })
126
+ #
127
+ # # Active remote objects
128
+ # Tag.update_all(Tag.new(:guid => 'foo', :name => 'baz'), Tag.new(:guid => 'bar', :name => 'qux'))
129
+ #
130
+ # # Protobuf objects
131
+ # Tag.update_all(Generic::Remote::Tag.new(:guid => 'foo', :name => 'baz'), Generic::Remote::Tag.new(:guid => 'bar', :name => 'qux'))
132
+ #
133
+ # # Bulk protobuf object
134
+ # Tag.update_all(Generic::Remote::Tags.new(:records => [ Generic::Remote::Tag.new(:guid => 'foo', :name => 'baz') ])
135
+ #
136
+ def update_all(*records)
137
+ remote = self.new
138
+ remote.execute(:update_all, parse_records(records))
139
+ remote.serialize_records
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,70 @@
1
+ require 'active_model/dirty'
2
+
3
+ # Overrides persistence methods, providing support for dirty tracking.
4
+ #
5
+ module ActiveRemote
6
+ module Dirty
7
+ def self.included(klass)
8
+ klass.class_eval do
9
+ include ActiveModel::Dirty
10
+ end
11
+ end
12
+
13
+ # Override #reload to provide dirty tracking.
14
+ #
15
+ def reload(*)
16
+ super.tap do
17
+ @previously_changed.try(:clear)
18
+ @changed_attributes.clear
19
+ end
20
+ end
21
+
22
+ # Override #save to store changes as previous changes then clear them.
23
+ #
24
+ def save(*)
25
+ if status = super
26
+ @previously_changed = changes
27
+ @changed_attributes.clear
28
+ end
29
+
30
+ status
31
+ end
32
+
33
+ # Override #save to store changes as previous changes then clear them.
34
+ #
35
+ def save!(*)
36
+ super.tap do
37
+ @previously_changed = changes
38
+ @changed_attributes.clear
39
+ end
40
+ end
41
+
42
+ # Override #serialize_records so that we can clear changes after
43
+ # initializing records returned from a search.
44
+ #
45
+ def serialize_records(*)
46
+ if serialized_records = super
47
+ serialized_records.each do |record|
48
+ record.previous_changes.try(:clear)
49
+ record.changed_attributes.try(:clear)
50
+ end
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ # Override ActiveAttr's attribute= method so we can provide support for
57
+ # ActiveModel::Dirty.
58
+ #
59
+ def attribute=(name, value)
60
+ __send__("#{name}_will_change!") unless value == self[name]
61
+ super
62
+ end
63
+
64
+ # Override #update to only send changed attributes.
65
+ #
66
+ def update(*)
67
+ super(changed)
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,141 @@
1
+ require 'active_support/inflector'
2
+
3
+ module ActiveRemote
4
+ module DSL
5
+ def self.included(klass)
6
+ klass.class_eval do
7
+ extend ActiveRemote::DSL::ClassMethods
8
+ include ActiveRemote::DSL::InstanceMethods
9
+ end
10
+ end
11
+
12
+ module ClassMethods
13
+
14
+ # Whitelist enable attributes for serialization purposes.
15
+ #
16
+ # ====Examples
17
+ #
18
+ # # To only publish the :guid and :status attributes:
19
+ # class User < ActiveRemote::Base
20
+ # attr_publishable :guid, :status
21
+ # end
22
+ #
23
+ def attr_publishable(*attributes)
24
+ @publishable_attributes ||= []
25
+ @publishable_attributes += attributes
26
+ end
27
+
28
+ # Set the number of records per page when auto paging.
29
+ #
30
+ # ====Examples
31
+ #
32
+ # class User < ActiveRemote::Base
33
+ # auto_paging_size 100
34
+ # end
35
+ #
36
+ def auto_paging_size(size=nil)
37
+ @auto_paging_size = size unless size.nil?
38
+ @auto_paging_size ||= 1000
39
+ end
40
+
41
+ # Set the namespace for the underlying RPC service class. If no namespace
42
+ # is given, then none will be used.
43
+ #
44
+ # ====Examples
45
+ #
46
+ # # If the user's service class is namespaced (e.g. Acme::UserService):
47
+ # class User < ActiveRemote::Base
48
+ # namespace :acme
49
+ # end
50
+ #
51
+ def namespace(name = false)
52
+ @namespace = name unless name == false
53
+ @namespace
54
+ end
55
+
56
+ # Retrieve the attributes that have been whitelisted for serialization.
57
+ #
58
+ def publishable_attributes
59
+ @publishable_attributes
60
+ end
61
+
62
+ # Set the RPC service class directly. By default, ActiveRemove determines
63
+ # the RPC service by constantizing the namespace and service name.
64
+ #
65
+ # ====Examples
66
+ #
67
+ # class User < ActiveRemote::Base
68
+ # service_class Acme::UserService
69
+ # end
70
+ #
71
+ # # ...is the same as:
72
+ #
73
+ # class User < ActiveRemote::Base
74
+ # namespace :acme
75
+ # service_name :user_service
76
+ # end
77
+ #
78
+ # # ...is the same as:
79
+ #
80
+ # class User < ActiveRemote::Base
81
+ # namespace :acme
82
+ # end
83
+ #
84
+ def service_class(klass = false)
85
+ @service_class = klass unless klass == false
86
+ @service_class ||= _determine_service_class
87
+ end
88
+
89
+ # Set the name of the underlying RPC service class. By default, Active
90
+ # Remote assumes that a User model will have a UserService (making the
91
+ # service name :user_service).
92
+ #
93
+ # ====Examples
94
+ #
95
+ # class User < ActiveRemote::Base
96
+ # service_name :jangly_users
97
+ # end
98
+ #
99
+ def service_name(name = false)
100
+ @service_name = name unless name == false
101
+ @service_name ||= _determine_service_name
102
+ end
103
+
104
+ private
105
+
106
+ # Combine the namespace and service values, constantize them and return
107
+ # the class constant.
108
+ #
109
+ def _determine_service_class
110
+ class_name = [ namespace, service_name ].join("/")
111
+ const_name = class_name.camelize
112
+
113
+ return const_name.present? ? const_name.constantize : const_name
114
+ end
115
+
116
+ def _determine_service_name
117
+ underscored_name = self.name.underscore
118
+ "#{underscored_name}_service".to_sym
119
+ end
120
+ end
121
+
122
+ # Convenience methods for accessing DSL methods in instances.
123
+ #
124
+ module InstanceMethods
125
+
126
+ private
127
+
128
+ def _publishable_attributes
129
+ self.class.publishable_attributes
130
+ end
131
+
132
+ def _service_name
133
+ self.class.service_name
134
+ end
135
+
136
+ def _service_class
137
+ self.class.service_class
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,24 @@
1
+ # TODO: Create more specific errors
2
+ #
3
+ module ActiveRemote
4
+
5
+ # = Active Remote Errors
6
+ #
7
+ # Generic Active Remote exception class.
8
+ class ActiveRemoteError < StandardError
9
+ end
10
+
11
+ # Raised by ActiveRemove::Base.save when the remote record is readonly.
12
+ class ReadOnlyRemoteRecord < ActiveRemoteError
13
+ end
14
+
15
+ # Raised by ActiveRemove::Base.find when remote record is not found when
16
+ # searching with the given arguments.
17
+ class RemoteRecordNotFound < ActiveRemoteError
18
+ end
19
+
20
+ # Raised by ActiveRemove::Base.save! and ActiveRemote::Base.create! methods
21
+ # when remote record cannot be saved because it is invalid.
22
+ class RemoteRecordNotSaved < ActiveRemoteError
23
+ end
24
+ end
@@ -0,0 +1,226 @@
1
+ require 'active_remote/rpc'
2
+
3
+ module ActiveRemote
4
+ module Persistence
5
+ def self.included(klass)
6
+ klass.class_eval do
7
+ extend ActiveRemote::Persistence::ClassMethods
8
+ include ActiveRemote::Persistence::InstanceMethods
9
+ include ActiveRemote::RPC
10
+
11
+ define_model_callbacks :save
12
+ end
13
+ end
14
+
15
+ ##
16
+ # Class methods
17
+ #
18
+ module ClassMethods
19
+
20
+ # Creates a remote record through the service.
21
+ #
22
+ # The service will run any validations and if any of them fail, will return
23
+ # the record error messages indicating what went wrong.
24
+ #
25
+ # The newly created record is returned if it was successfully saved or not.
26
+ #
27
+ def create(attributes)
28
+ remote = self.new(attributes)
29
+ remote.save
30
+ remote
31
+ end
32
+
33
+ # Creates a remote record through the service and if successful, returns
34
+ # the newly created record.
35
+ #
36
+ # The service will run any validations and if any of them fail, will raise
37
+ # an ActiveRemote::RemoteRecordNotSaved exception.
38
+ #
39
+ def create!(attributes)
40
+ remote = self.new(attributes)
41
+ remote.save!
42
+ remote
43
+ end
44
+
45
+ # Mark the class as readonly. Overrides instance-level readonly, making
46
+ # any instance of this class readonly.
47
+ def readonly!
48
+ @readonly = true
49
+ end
50
+
51
+ # Returns true if the class is marked as readonly; otherwise, returns false.
52
+ def readonly?
53
+ @readonly
54
+ end
55
+ end
56
+
57
+ ##
58
+ # Instance methods
59
+ #
60
+ module InstanceMethods
61
+ # Deletes the record from the service (the service determines if the
62
+ # record is hard or soft deleted) and freezes this instance to indicate
63
+ # that no changes should be made (since they can't be persisted). If the
64
+ # record was not deleted, it will have error messages indicating what went
65
+ # wrong. Returns the frozen instance.
66
+ #
67
+ def delete
68
+ raise ReadOnlyRemoteRecord if readonly?
69
+ execute(:delete, attributes.slice("guid"))
70
+ freeze if success?
71
+ end
72
+
73
+ # Deletes the record from the service (the service determines if the
74
+ # record is hard or soft deleted) and freezes this instance to indicate
75
+ # that no changes should be made (since they can't be persisted). If the
76
+ # record was not deleted, an exception will be raised. Returns the frozen
77
+ # instance.
78
+ #
79
+ def delete!
80
+ delete
81
+ raise ActiveRemoteError.new(errors.to_s) if has_errors?
82
+ end
83
+
84
+ # Destroys (hard deletes) the record from the service and freezes this
85
+ # instance to indicate that no changes should be made (since they can't
86
+ # be persisted). If the record was not deleted, it will have error
87
+ # messages indicating what went wrong. Returns the frozen instance.
88
+ #
89
+ def destroy
90
+ raise ReadOnlyRemoteRecord if readonly?
91
+ execute(:destroy, attributes.slice("guid"))
92
+ freeze if success?
93
+ end
94
+
95
+ # Destroys (hard deletes) the record from the service and freezes this
96
+ # instance to indicate that no changes should be made (since they can't
97
+ # be persisted). If the record was not deleted, an exception will be
98
+ # raised. Returns the frozen instance.
99
+ #
100
+ def destroy!
101
+ destroy
102
+ raise ActiveRemoteError.new(errors.to_s) if has_errors?
103
+ end
104
+
105
+ # Returns true if the record has errors; otherwise, returns false.
106
+ #
107
+ def has_errors?
108
+ return respond_to?(:errors) && errors.present?
109
+ end
110
+
111
+ # Returns true if the remote record hasn't been saved yet; otherwise,
112
+ # returns false.
113
+ #
114
+ def new_record?
115
+ return self[:guid].nil?
116
+ end
117
+
118
+ # Returns true if the remote record has been saved; otherwise, returns false.
119
+ #
120
+ def persisted?
121
+ return ! new_record?
122
+ end
123
+
124
+ # Returns true if the remote class or remote record is readonly; otherwise, returns false.
125
+ def readonly?
126
+ self.class.readonly? || @readonly
127
+ end
128
+
129
+ # Saves the remote record.
130
+ #
131
+ # If it is a new record, it will be created through the service, otherwise
132
+ # the existing record gets updated.
133
+ #
134
+ # The service will run any validations and if any of them fail, will return
135
+ # the record with error messages indicating what went wrong.
136
+ #
137
+ # Also runs any before/after save callbacks that are defined.
138
+ #
139
+ def save
140
+ run_callbacks :save do
141
+ create_or_update
142
+ end
143
+ end
144
+
145
+ # Saves the remote record.
146
+ #
147
+ # If it is a new record, it will be created through the service, otherwise
148
+ # the existing record gets updated.
149
+ #
150
+ # The service will run any validations. If any of them fail (e.g. error
151
+ # messages are returned), an ActiveRemote::RemoteRecordNotSaved is raised.
152
+ #
153
+ # Also runs any before/after save callbacks that are defined.
154
+ #
155
+ def save!
156
+ save || raise(RemoteRecordNotSaved)
157
+ end
158
+
159
+ # Returns true if the record doesn't have errors; otherwise, returns false.
160
+ #
161
+ def success?
162
+ return ! has_errors?
163
+ end
164
+
165
+ # Updates the attributes of the remote record from the passed-in hash and
166
+ # saves the remote record. If the object is invalid, it will have error
167
+ # messages and false will be returned.
168
+ #
169
+ def update_attributes(attributes)
170
+ assign_attributes(attributes)
171
+ save
172
+ end
173
+
174
+ # Updates the attributes of the remote record from the passed-in hash and
175
+ # saves the remote record. If the object is invalid, an
176
+ # ActiveRemote::RemoteRecordNotSaved is raised.
177
+ #
178
+ def update_attributes!(attributes)
179
+ assign_attributes(attributes)
180
+ save!
181
+ end
182
+
183
+ private
184
+
185
+ # Handles creating a remote object and serializing it's attributes and
186
+ # errors from the response.
187
+ #
188
+ def create
189
+ new_attributes = attributes.deep_dup
190
+ new_attributes.delete("guid")
191
+
192
+ execute(:create, new_attributes)
193
+
194
+ assign_attributes(last_response.to_hash)
195
+ add_errors_from_response
196
+
197
+ success?
198
+ end
199
+
200
+ # Deterines whether the record should be created or updated. New records
201
+ # are created, existing records are updated. If the record is marked as
202
+ # readonly, an ActiveRemote::ReadOnlyRemoteRecord is raised.
203
+ #
204
+ def create_or_update
205
+ raise ReadOnlyRemoteRecord if readonly?
206
+ new_record? ? create : update
207
+ end
208
+
209
+ # Handles updating a remote object and serializing it's attributes and
210
+ # errors from the response. Only attributes with the given attribute names
211
+ # (plus :guid) will be updated. Defaults to all attributes.
212
+ #
213
+ def update(attribute_names = @attributes.keys)
214
+ updated_attributes = attributes.slice(*attribute_names)
215
+ updated_attributes.merge!("guid" => self[:guid])
216
+
217
+ execute(:update, updated_attributes)
218
+
219
+ assign_attributes(last_response.to_hash)
220
+ add_errors_from_response
221
+
222
+ success?
223
+ end
224
+ end
225
+ end
226
+ end