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,71 @@
1
+ require 'active_remote/serializers/protobuf'
2
+
3
+ module ActiveRemote
4
+ module RPC
5
+ def self.included(klass)
6
+ klass.class_eval do
7
+ extend ::ActiveRemote::RPC::ClassMethods
8
+ include ::ActiveRemote::Serializers::Protobuf
9
+ end
10
+ end
11
+
12
+ module ClassMethods
13
+ # Execute an RPC call to the remote service and return the raw response.
14
+ #
15
+ def remote_call(rpc_method, request_args)
16
+ remote = self.new
17
+ remote.execute(rpc_method, request_args)
18
+ remote.last_response
19
+ end
20
+
21
+ # Return a protobuf request object for the given rpc request.
22
+ #
23
+ def request(rpc_method, request_args)
24
+ return request_args unless request_args.is_a?(Hash)
25
+
26
+ message_class = request_type(rpc_method)
27
+ build_message(message_class, request_args)
28
+ end
29
+
30
+ # Return the class applicable to the request for the given rpc method.
31
+ #
32
+ def request_type(rpc_method)
33
+ service_class.rpcs[rpc_method].request_type
34
+ end
35
+ end
36
+
37
+ # Invoke an RPC call to the service for the given rpc method.
38
+ #
39
+ def execute(rpc_method, request_args)
40
+ @last_request = request(rpc_method, request_args)
41
+
42
+ _service_class.client.__send__(rpc_method, @last_request) do |c|
43
+
44
+ # In the event of service failure, raise the error.
45
+ c.on_failure do |error|
46
+ raise ActiveRemoteError, error.message
47
+ end
48
+
49
+ # In the event of service success, assign the response.
50
+ c.on_success do |response|
51
+ @last_response = response
52
+ end
53
+ end
54
+ end
55
+
56
+ # Execute an RPC call to the remote service and return the raw response.
57
+ #
58
+ def remote_call(rpc_method, request_args)
59
+ self.execute(rpc_method, request_args)
60
+ self.last_response
61
+ end
62
+
63
+ private
64
+
65
+ # Return a protobuf request object for the given rpc call.
66
+ #
67
+ def request(rpc_method, attributes)
68
+ self.class.request(rpc_method, attributes)
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,131 @@
1
+ require 'active_remote/persistence'
2
+ require 'active_remote/rpc'
3
+
4
+ module ActiveRemote
5
+ module Search
6
+ def self.included(klass)
7
+ klass.class_eval do
8
+ extend ::ActiveRemote::Search::ClassMethods
9
+ include ::ActiveRemote::Persistence
10
+ include ::ActiveRemote::RPC
11
+
12
+ define_model_callbacks :search
13
+ end
14
+ end
15
+
16
+ module ClassMethods
17
+
18
+ # Tries to load the first record; if it fails, an exception is raised.
19
+ #
20
+ # ====Examples
21
+ #
22
+ # # A single hash
23
+ # Tag.find(:guid => 'foo')
24
+ #
25
+ # # Active remote object
26
+ # Tag.find(Tag.new(:guid => 'foo'))
27
+ #
28
+ # # Protobuf object
29
+ # Tag.find(Generic::Remote::TagRequest.new(:guid => 'foo'))
30
+ #
31
+ def find(args)
32
+ remote = self.search(args).first
33
+ raise RemoteRecordNotFound if remote.nil?
34
+
35
+ return remote
36
+ end
37
+
38
+ # Tries to load the first record; if it fails, then create is called
39
+ # with the same arguments.
40
+ #
41
+ # ====Examples
42
+ #
43
+ # # A single hash
44
+ # Tag.first_or_create(:name => 'foo')
45
+ #
46
+ # # Protobuf object
47
+ # Tag.first_or_create(Generic::Remote::TagRequest.new(:name => 'foo'))
48
+ #
49
+ def first_or_create(attributes)
50
+ remote = self.search(attributes).first
51
+ remote ||= self.create(attributes)
52
+ remote
53
+ end
54
+
55
+ # Tries to load the first record; if it fails, then create! is called
56
+ # with the same arguments.
57
+ #
58
+ def first_or_create!(attributes)
59
+ remote = self.search(attributes).first
60
+ remote ||= self.create!(attributes)
61
+ remote
62
+ end
63
+
64
+ # Tries to load the first record; if it fails, then a new record is
65
+ # initialized with the same arguments.
66
+ #
67
+ # ====Examples
68
+ #
69
+ # # A single hash
70
+ # Tag.first_or_initialize(:name => 'foo')
71
+ #
72
+ # # Protobuf object
73
+ # Tag.first_or_initialize(Generic::Remote::TagRequest.new(:name => 'foo'))
74
+ #
75
+ def first_or_initialize(attributes)
76
+ remote = self.search(attributes).first
77
+ remote ||= self.new(attributes)
78
+ remote
79
+ end
80
+
81
+ # Searches for records with the given arguments. Returns a collection of
82
+ # Active Remote objects.
83
+ #
84
+ # ====Examples
85
+ #
86
+ # # A single hash
87
+ # Tag.search(:name => 'foo')
88
+ #
89
+ # # Protobuf object
90
+ # Tag.search(Generic::Remote::TagRequest.new(:name => 'foo'))
91
+ #
92
+ def search(args)
93
+ args = _active_remote_search_args(args)
94
+
95
+ remote = self.new
96
+ remote._active_remote_search(args)
97
+ remote.serialize_records
98
+ end
99
+
100
+ # :noapi:
101
+ def _active_remote_search_args(args)
102
+ unless args.is_a?(Hash)
103
+ if args.respond_to?(:to_hash)
104
+ args = args.to_hash
105
+ else
106
+ raise "Invalid parameter: #{args}. First parameter must respond to :to_hash."
107
+ end
108
+ end
109
+
110
+ args
111
+ end
112
+ end
113
+
114
+ # Search for the given resource. Auto-paginates (i.e. continues searching
115
+ # for records matching the given search args until all records have been
116
+ # retrieved) if no pagination options are given.
117
+ #
118
+ def _active_remote_search(args)
119
+ run_callbacks :search do
120
+ execute(:search, args)
121
+ end
122
+ end
123
+
124
+ # Reload this record from the remote service.
125
+ #
126
+ def reload
127
+ _active_remote_search(:guid => self.guid)
128
+ assign_attributes(last_response.to_hash)
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,40 @@
1
+ require 'active_remote/serializers/json'
2
+
3
+ module ActiveRemote
4
+ module Serialization
5
+ def self.included(klass)
6
+ klass.class_eval do
7
+ include ::ActiveRemote::Serializers::JSON
8
+ end
9
+ end
10
+
11
+ # Examine the last response and add any errors to our internal errors
12
+ # list.
13
+ #
14
+ def add_errors_from_response
15
+ return unless last_response.respond_to?(:errors)
16
+
17
+ last_response.errors.each do |error|
18
+ if error.respond_to?(:message)
19
+ errors.add(error.field, error.message)
20
+ elsif error.respond_to?(:messages)
21
+ error.messages.each do |message|
22
+ errors.add(error.field, message)
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ # Examine the last response and serialize any records returned into Active
29
+ # Remote objects.
30
+ #
31
+ def serialize_records
32
+ return nil unless last_response.respond_to?(:records)
33
+
34
+ last_response.records.map do |record|
35
+ remote = self.class.new(record.to_hash)
36
+ remote
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,18 @@
1
+ module ActiveRemote
2
+ module Serializers
3
+ module JSON
4
+ # Returns a json representation of the whitelisted publishable attributes.
5
+ def as_json(options = {})
6
+ json_attributes = _publishable_attributes || attributes.keys
7
+
8
+ json_methods = json_attributes.reject { |attribute| attributes.key?(attribute) }
9
+ json_attributes -= json_methods
10
+
11
+ default_options = { :only => json_attributes, :methods => json_methods }
12
+ default_options.merge!(options)
13
+
14
+ super(default_options)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,100 @@
1
+ require 'protobuf_extensions/base_field'
2
+
3
+ module ActiveRemote
4
+ module Serializers
5
+ module Protobuf
6
+ ATTRIBUTE_TYPES = {
7
+ ::Protobuf::Field::DoubleField => Float,
8
+ ::Protobuf::Field::FloatField => Float,
9
+ ::Protobuf::Field::Int32Field => Integer,
10
+ ::Protobuf::Field::Int64Field => Integer,
11
+ ::Protobuf::Field::Uint32Field => Integer,
12
+ ::Protobuf::Field::Uint64Field => Integer,
13
+ ::Protobuf::Field::Sint32Field => Integer,
14
+ ::Protobuf::Field::Sint64Field => Integer,
15
+ ::Protobuf::Field::Fixed32Field => Float,
16
+ ::Protobuf::Field::Fixed64Field => Float,
17
+ ::Protobuf::Field::Sfixed32Field => Float,
18
+ ::Protobuf::Field::Sfixed64Field => Float,
19
+ ::Protobuf::Field::StringField => String,
20
+ ::Protobuf::Field::BytesField => String,
21
+ ::Protobuf::Field::BoolField => ::ActiveAttr::Typecasting::Boolean,
22
+ :bool => ::ActiveAttr::Typecasting::Boolean,
23
+ :double => Float,
24
+ :float => Float,
25
+ :int32 => Integer,
26
+ :int64 => Integer,
27
+ :string => String
28
+ }.freeze
29
+
30
+ def self.included(klass)
31
+ klass.extend ::ActiveRemote::Serializers::Protobuf::ClassMethods
32
+ end
33
+
34
+ module ClassMethods
35
+ # Recursively build messages from a hash of attributes.
36
+ # TODO: Pull this functionality into the protobuf gem.
37
+ #
38
+ def build_message(message_class, attributes)
39
+ attributes.inject(message_class.new) do |message, (key, value)|
40
+ if field = message.get_field_by_name(key)
41
+
42
+ # Override the value based on the field type where issues
43
+ # exist in the protobuf gem.
44
+ #
45
+ if field.repeated?
46
+ collection = [ value ]
47
+ collection.flatten!
48
+ collection.compact!
49
+ collection.map! { |value| coerce(value, field) }
50
+ value = collection
51
+ else
52
+ value = coerce(value, field)
53
+ end
54
+
55
+ if field.message? && field.repeated?
56
+ value = value.map do |attributes|
57
+ attributes.is_a?(Hash) ? build_message(field.type, attributes) : attributes
58
+ end
59
+ elsif field.message?
60
+ value = value.is_a?(Hash) ? build_message(field.type, value) : value
61
+ end
62
+
63
+ message.method("#{key}=").call(value)
64
+ end
65
+
66
+ message
67
+ end
68
+ end
69
+
70
+ def coerce(value, field)
71
+ return value if value.nil?
72
+ return value.to_i if field.enum?
73
+
74
+ protobuf_field_type = ::ActiveRemote::Serializers::Protobuf::ATTRIBUTE_TYPES[field.type]
75
+
76
+ case
77
+ when protobuf_field_type == ::ActiveAttr::Typecasting::Boolean then
78
+ if value == 1
79
+ return true
80
+ elsif value == 0
81
+ return false
82
+ end
83
+ when protobuf_field_type == Integer then
84
+ return value.to_i
85
+ when protobuf_field_type == Float then
86
+ return value.to_f
87
+ when protobuf_field_type == String then
88
+ return value.to_s
89
+ end
90
+
91
+ return value
92
+ end
93
+ end
94
+
95
+ def build_message(message_class, attributes)
96
+ self.class.build_message(message_class, attributes)
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,3 @@
1
+ module ActiveRemote
2
+ VERSION = "1.2.1"
3
+ end
@@ -0,0 +1,7 @@
1
+ Date.class_eval do
2
+ unless instance_methods.include?(:to_i)
3
+ def to_i
4
+ to_time.to_i
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ DateTime.class_eval do
2
+ unless instance_methods.include?(:to_i)
3
+ def to_i
4
+ to_time.to_i
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,19 @@
1
+ Integer.class_eval do
2
+ unless instance_methods.include?(:to_date)
3
+ def to_date
4
+ to_time.to_date
5
+ end
6
+ end
7
+
8
+ unless instance_methods.include?(:to_datetime)
9
+ def to_datetime
10
+ to_time.to_datetime
11
+ end
12
+ end
13
+
14
+ unless instance_methods.include?(:to_time)
15
+ def to_time
16
+ Time.at(self)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,18 @@
1
+ require 'protobuf'
2
+
3
+ # A couple of helper fields.
4
+ # Might make sense to create a patch for Protobuf.
5
+ #
6
+ Protobuf::Field::BaseField.class_eval do
7
+ unless respond_to?(:enum?)
8
+ def enum?
9
+ kind_of?(Protobuf::Field::EnumField)
10
+ end
11
+ end
12
+
13
+ unless respond_to?(:message?)
14
+ def message?
15
+ kind_of?(Protobuf::Field::MessageField)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,10 @@
1
+ require 'spec_helper'
2
+
3
+ describe DateTime do
4
+
5
+ describe "#to_i" do
6
+ it "does not raise errors bj" do
7
+ expect {subject.to_i }.to_not raise_error
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,80 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveRemote::Association do
4
+ let(:record) { double(:record) }
5
+ let(:records) { [ record ] }
6
+
7
+ describe '.belongs_to' do
8
+ let(:author_guid) { 'AUT-123' }
9
+
10
+ subject { Post.new(:author_guid => author_guid) }
11
+ it { should respond_to(:author) }
12
+
13
+ it 'searches the associated model for a single record' do
14
+ Author.should_receive(:search).with(:guid => subject.author_guid).and_return(records)
15
+ subject.author.should eq record
16
+ end
17
+
18
+ it 'memoizes the result record' do
19
+ Author.should_receive(:search).once.with(:guid => subject.author_guid).and_return(records)
20
+ 3.times { subject.author.should eq record }
21
+ end
22
+
23
+ context 'when the search is empty' do
24
+ it 'returns a nil' do
25
+ Author.should_receive(:search).with(:guid => subject.author_guid).and_return([])
26
+ subject.author.should be_nil
27
+ end
28
+ end
29
+ end
30
+
31
+ describe '.has_many' do
32
+ let(:records) { [ record, record, record ] }
33
+ let(:guid) { 'AUT-123' }
34
+
35
+ subject { Author.new(:guid => guid) }
36
+ it { should respond_to(:posts) }
37
+
38
+ it 'searches the associated model for all associated records' do
39
+ Post.should_receive(:search).with(:author_guid => subject.guid).and_return(records)
40
+ subject.posts.should eq records
41
+ end
42
+
43
+ it 'memoizes the result record' do
44
+ Post.should_receive(:search).once.with(:author_guid => subject.guid).and_return(records)
45
+ 3.times { subject.posts.should eq records }
46
+ end
47
+
48
+ context 'when the search is empty' do
49
+ it 'returns the empty set' do
50
+ Post.should_receive(:search).with(:author_guid => subject.guid).and_return([])
51
+ subject.posts.should be_empty
52
+ end
53
+ end
54
+ end
55
+
56
+ describe '.has_one' do
57
+ let(:guid) { 'PST-123' }
58
+
59
+ subject { Post.new(:guid => guid) }
60
+ it { should respond_to(:category) }
61
+
62
+ it 'searches the associated model for all associated records' do
63
+ Category.should_receive(:search).with(:post_guid => subject.guid).and_return(records)
64
+ subject.category.should eq record
65
+ end
66
+
67
+ it 'memoizes the result record' do
68
+ Category.should_receive(:search).once.with(:post_guid => subject.guid).and_return(records)
69
+ 3.times { subject.category.should eq record }
70
+ end
71
+
72
+ context 'when the search is empty' do
73
+ it 'returns a nil value' do
74
+ Category.should_receive(:search).with(:post_guid => subject.guid).and_return([])
75
+ subject.category.should be_nil
76
+ end
77
+ end
78
+ end
79
+
80
+ end