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