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