active_remote 1.2.1

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