active_remote 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.rvmrc +1 -0
- data/Gemfile +7 -0
- data/LICENSE +22 -0
- data/README.md +86 -0
- data/Rakefile +21 -0
- data/active_remote.gemspec +35 -0
- data/lib/active_remote.rb +15 -0
- data/lib/active_remote/association.rb +152 -0
- data/lib/active_remote/attributes.rb +29 -0
- data/lib/active_remote/base.rb +49 -0
- data/lib/active_remote/bulk.rb +143 -0
- data/lib/active_remote/dirty.rb +70 -0
- data/lib/active_remote/dsl.rb +141 -0
- data/lib/active_remote/errors.rb +24 -0
- data/lib/active_remote/persistence.rb +226 -0
- data/lib/active_remote/rpc.rb +71 -0
- data/lib/active_remote/search.rb +131 -0
- data/lib/active_remote/serialization.rb +40 -0
- data/lib/active_remote/serializers/json.rb +18 -0
- data/lib/active_remote/serializers/protobuf.rb +100 -0
- data/lib/active_remote/version.rb +3 -0
- data/lib/core_ext/date.rb +7 -0
- data/lib/core_ext/date_time.rb +7 -0
- data/lib/core_ext/integer.rb +19 -0
- data/lib/protobuf_extensions/base_field.rb +18 -0
- data/spec/core_ext/date_time_spec.rb +10 -0
- data/spec/lib/active_remote/association_spec.rb +80 -0
- data/spec/lib/active_remote/base_spec.rb +10 -0
- data/spec/lib/active_remote/bulk_spec.rb +74 -0
- data/spec/lib/active_remote/dsl_spec.rb +73 -0
- data/spec/lib/active_remote/persistence_spec.rb +266 -0
- data/spec/lib/active_remote/rpc_spec.rb +94 -0
- data/spec/lib/active_remote/search_spec.rb +98 -0
- data/spec/lib/active_remote/serialization_spec.rb +57 -0
- data/spec/lib/active_remote/serializers/json_spec.rb +32 -0
- data/spec/lib/active_remote/serializers/protobuf_spec.rb +95 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/support/definitions/author.proto +29 -0
- data/spec/support/definitions/post.proto +33 -0
- data/spec/support/definitions/support/protobuf/category.proto +29 -0
- data/spec/support/definitions/support/protobuf/error.proto +6 -0
- data/spec/support/definitions/tag.proto +29 -0
- data/spec/support/helpers.rb +37 -0
- data/spec/support/models.rb +5 -0
- data/spec/support/models/author.rb +14 -0
- data/spec/support/models/category.rb +14 -0
- data/spec/support/models/message_with_options.rb +11 -0
- data/spec/support/models/post.rb +16 -0
- data/spec/support/models/tag.rb +12 -0
- data/spec/support/protobuf.rb +4 -0
- data/spec/support/protobuf/author.pb.rb +54 -0
- data/spec/support/protobuf/category.pb.rb +54 -0
- data/spec/support/protobuf/error.pb.rb +21 -0
- data/spec/support/protobuf/post.pb.rb +58 -0
- data/spec/support/protobuf/tag.pb.rb +54 -0
- 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,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,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
|