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
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use 1.9.3@active_remote --create
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 MDev
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
# Active Remote
|
2
|
+
|
3
|
+
Active Remote provides [Active Record](https://github.com/rails/rails/tree/master/activerecord)-like object-relational mapping over RPC. Think of it as Active Record for your platform: within a service, use Active Record to persist objects and between services, use Active Remote.
|
4
|
+
|
5
|
+
Active Remote provides a base class that when subclassed, provides the functionality you need to setup your remote model. Because Active Remote provides model persistence between RPC services, it uses a GUID to retrieve records and establish associations. So Active Remote expects your RPC data format to provide a :guid field that can be used to identify your remote models.
|
6
|
+
|
7
|
+
Unlike Active Record, Active Remote doesn't have access to a database table to create attribute mappings. So you'll need to do a little setup to let Active Remote know how to persist your model*.
|
8
|
+
|
9
|
+
```Ruby
|
10
|
+
# Given a product record that has :guid & :name fields:
|
11
|
+
class Product < ActiveRecord::Base
|
12
|
+
# :guid, :name
|
13
|
+
end
|
14
|
+
|
15
|
+
# Configure your Active Remote model like this:
|
16
|
+
class Product < ActiveRemote::Base
|
17
|
+
attribute :guid
|
18
|
+
attribute :name
|
19
|
+
end
|
20
|
+
```
|
21
|
+
|
22
|
+
_*Using Ruby's inherited hook, you could build an attribute mapper to setup your remote models for you._
|
23
|
+
|
24
|
+
Like Active Record, Active Remote relies heavily on naming conventions and standard CRUD actions. It expects models name to map to it's service (e.g Product => ProductService) and will infer the service name automatically.
|
25
|
+
|
26
|
+
```Ruby
|
27
|
+
# Given a product service that has #search, #create, #update, and #delete endpoints
|
28
|
+
class ProductService < RPCService
|
29
|
+
def search(request)
|
30
|
+
#...
|
31
|
+
end
|
32
|
+
|
33
|
+
def create(request)
|
34
|
+
#...
|
35
|
+
end
|
36
|
+
|
37
|
+
def update(request)
|
38
|
+
#...
|
39
|
+
end
|
40
|
+
|
41
|
+
def delete(request)
|
42
|
+
#...
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Your remote model will just work.
|
47
|
+
class Product < ActiveRemote::Base
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
You can, of course override it if need be:
|
52
|
+
|
53
|
+
```Ruby
|
54
|
+
# If you have a custom service:
|
55
|
+
class CustomProductService < RPCService
|
56
|
+
# CRUD actions
|
57
|
+
end
|
58
|
+
|
59
|
+
# Configure your remote model like this:
|
60
|
+
class Product < ActiveRemote::Base
|
61
|
+
service_name :custom_product_service
|
62
|
+
end
|
63
|
+
```
|
64
|
+
|
65
|
+
## Installation
|
66
|
+
|
67
|
+
Add this line to your application's Gemfile:
|
68
|
+
|
69
|
+
gem 'active_remote'
|
70
|
+
|
71
|
+
And then execute:
|
72
|
+
|
73
|
+
$ bundle
|
74
|
+
|
75
|
+
Or install it yourself as:
|
76
|
+
|
77
|
+
$ gem install active_remote
|
78
|
+
|
79
|
+
|
80
|
+
## Contributing
|
81
|
+
|
82
|
+
1. Fork it
|
83
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
84
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
85
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
86
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
|
5
|
+
desc "Run specs"
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
7
|
+
|
8
|
+
desc "Run specs (default)"
|
9
|
+
task :default, [] => :spec
|
10
|
+
|
11
|
+
desc "Remove protobuf definitions that have been compiled"
|
12
|
+
task :clean do
|
13
|
+
FileUtils.rm(Dir.glob("spec/support/protobuf/**/*.proto"))
|
14
|
+
puts "Cleaned"
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "Compile spec/support protobuf definitions"
|
18
|
+
task :compile, [] => :clean do
|
19
|
+
cmd = "rprotoc --ruby_out=spec/support/protobuf --proto_path=spec/support/definitions spec/support/definitions/*.proto"
|
20
|
+
sh(cmd)
|
21
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "active_remote/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "active_remote"
|
7
|
+
s.version = ActiveRemote::VERSION
|
8
|
+
s.authors = ["Adam Hutchison"]
|
9
|
+
s.email = ["liveh2o@gmail.com"]
|
10
|
+
s.homepage = "https://github.com/liveh2o/active_remote"
|
11
|
+
s.summary = %q{Active Record for your platform}
|
12
|
+
s.description = %q{Active Remote provides Active Record-like object-relational mapping over RPC. It was written for use with Google Protocol Buffers, but could be extended to use any RPC data format.}
|
13
|
+
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
16
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
17
|
+
s.require_paths = ["lib"]
|
18
|
+
|
19
|
+
##
|
20
|
+
# Dependencies
|
21
|
+
#
|
22
|
+
s.add_dependency "activemodel"
|
23
|
+
s.add_dependency "active_attr"
|
24
|
+
s.add_dependency "protobuf", ">= 2.0"
|
25
|
+
|
26
|
+
##
|
27
|
+
# Development Dependencies
|
28
|
+
#
|
29
|
+
s.add_development_dependency "rake"
|
30
|
+
s.add_development_dependency "rspec"
|
31
|
+
s.add_development_dependency "rspec-pride"
|
32
|
+
s.add_development_dependency "pry-nav"
|
33
|
+
s.add_development_dependency "protobuf-rspec"
|
34
|
+
s.add_development_dependency "simplecov"
|
35
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'active_attr'
|
2
|
+
require 'active_model'
|
3
|
+
require 'active_support/core_ext/array'
|
4
|
+
require 'active_support/core_ext/hash'
|
5
|
+
require 'active_support/inflector'
|
6
|
+
require 'active_support/json'
|
7
|
+
|
8
|
+
require 'core_ext/date_time'
|
9
|
+
require 'core_ext/date'
|
10
|
+
require 'core_ext/integer'
|
11
|
+
|
12
|
+
require 'active_remote/base'
|
13
|
+
require 'active_remote/errors'
|
14
|
+
|
15
|
+
require 'active_remote/version'
|
@@ -0,0 +1,152 @@
|
|
1
|
+
module ActiveRemote
|
2
|
+
module Association
|
3
|
+
def self.included(klass)
|
4
|
+
klass.class_eval do
|
5
|
+
extend ActiveRemote::Association::ClassMethods
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
# Create a `belongs_to` association for a given remote resource.
|
11
|
+
# Specify one or more associations to define. The constantized
|
12
|
+
# class must be loaded into memory already. A method will be defined
|
13
|
+
# with the same name as the association. When invoked, the associated
|
14
|
+
# remote model will issue a `search` for the :guid with the associated
|
15
|
+
# guid's attribute (e.g. read_attribute(:client_guid)) and return the first
|
16
|
+
# remote object from the result, or nil.
|
17
|
+
#
|
18
|
+
# A `belongs_to` association should be used when the associating remote
|
19
|
+
# contains the guid to the associated model. For example, if a User model
|
20
|
+
# `belongs_to` Client, the User model would have a client_guid field that is
|
21
|
+
# used to search the Client service. The Client model would have no
|
22
|
+
# reference to the user.
|
23
|
+
#
|
24
|
+
# ====Examples
|
25
|
+
#
|
26
|
+
# class User
|
27
|
+
# belongs_to :client
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# An equivalent code snippet without a `belongs_to` declaration would be:
|
31
|
+
#
|
32
|
+
# ====Examples
|
33
|
+
#
|
34
|
+
# class User
|
35
|
+
# def client
|
36
|
+
# Client.search(:guid => self.client_guid).first
|
37
|
+
# end
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
def belongs_to(*klass_names)
|
41
|
+
klass_names.flatten.compact.uniq.each do |klass_name|
|
42
|
+
|
43
|
+
define_method(klass_name) do
|
44
|
+
value = instance_variable_get(:"@#{klass_name}")
|
45
|
+
|
46
|
+
unless value
|
47
|
+
klass = klass_name.to_s.classify.constantize
|
48
|
+
value = klass.search(:guid => read_attribute(:"#{klass_name}_guid")).first
|
49
|
+
instance_variable_set(:"@#{klass_name}", value)
|
50
|
+
end
|
51
|
+
|
52
|
+
return value
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Create a `has_many` association for a given remote resource.
|
58
|
+
# Specify one or more associations to define. The constantized
|
59
|
+
# class must be loaded into memory already. A method will be defined
|
60
|
+
# with the same plural name as the association. When invoked, the associated
|
61
|
+
# remote model will issue a `search` for the :guid with the associated
|
62
|
+
# guid's attribute (e.g. read_attribute(:client_guid)).
|
63
|
+
#
|
64
|
+
# A `has_many` association should be used when the associated model has
|
65
|
+
# a field to identify the associating model, and there can be multiple
|
66
|
+
# remotes associated. For example, if a Client has many Users, the User remote
|
67
|
+
# would have a client_guid field that is searchable. That search would likely
|
68
|
+
# return multiple user records. The client would not
|
69
|
+
# have a field indicating which users are associated.
|
70
|
+
#
|
71
|
+
# ====Examples
|
72
|
+
#
|
73
|
+
# class Client
|
74
|
+
# has_many :users
|
75
|
+
# end
|
76
|
+
#
|
77
|
+
# An equivalent code snippet without a `has_many` declaration would be:
|
78
|
+
#
|
79
|
+
# ====Examples
|
80
|
+
#
|
81
|
+
# class Client
|
82
|
+
# def users
|
83
|
+
# User.search(:client_guid => self.guid)
|
84
|
+
# end
|
85
|
+
# end
|
86
|
+
#
|
87
|
+
def has_many(*klass_names)
|
88
|
+
klass_names.flatten.compact.uniq.each do |plural_klass_name|
|
89
|
+
singular_name = plural_klass_name.to_s.singularize
|
90
|
+
|
91
|
+
define_method(plural_klass_name) do
|
92
|
+
values = instance_variable_get(:"@#{plural_klass_name}")
|
93
|
+
|
94
|
+
unless values
|
95
|
+
klass = plural_klass_name.to_s.classify.constantize
|
96
|
+
values = klass.search(:"#{self.class.name.demodulize.underscore}_guid" => self.guid)
|
97
|
+
instance_variable_set(:"@#{plural_klass_name}", values)
|
98
|
+
end
|
99
|
+
|
100
|
+
return values
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Create a `has_one` association for a given remote resource.
|
106
|
+
# Specify one or more associations to define. The constantized
|
107
|
+
# class must be loaded into memory already. A method will be defined
|
108
|
+
# with the same name as the association. When invoked, the associated
|
109
|
+
# remote model will issue a `search` for the :guid with the associated
|
110
|
+
# guid's attribute (e.g. read_attribute(:client_guid)) and return the first
|
111
|
+
# remote object in the result, or nil.
|
112
|
+
#
|
113
|
+
# A `has_one` association should be used when the associated remote
|
114
|
+
# contains the guid from the associating model. For example, if a User model
|
115
|
+
# `has_one` Client, the Client remote would have a user_guid field that is
|
116
|
+
# searchable. The User model would have no reference to the client.
|
117
|
+
#
|
118
|
+
# ====Examples
|
119
|
+
#
|
120
|
+
# class User
|
121
|
+
# has_one :client
|
122
|
+
# end
|
123
|
+
#
|
124
|
+
# An equivalent code snippet without a `has_one` declaration would be:
|
125
|
+
#
|
126
|
+
# ====Examples
|
127
|
+
#
|
128
|
+
# class User
|
129
|
+
# def client
|
130
|
+
# Client.search(:user_guid => self.guid).first
|
131
|
+
# end
|
132
|
+
# end
|
133
|
+
#
|
134
|
+
def has_one(*klass_names)
|
135
|
+
klass_names.flatten.compact.uniq.each do |klass_name|
|
136
|
+
|
137
|
+
define_method(klass_name) do
|
138
|
+
value = instance_variable_get(:"@#{klass_name}")
|
139
|
+
|
140
|
+
unless value
|
141
|
+
klass = klass_name.to_s.classify.constantize
|
142
|
+
value = klass.search(:"#{self.class.name.demodulize.underscore}_guid" => self.guid).first
|
143
|
+
instance_variable_set(:"@#{klass_name}", value)
|
144
|
+
end
|
145
|
+
|
146
|
+
return value
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module ActiveRemote
|
2
|
+
module Attributes
|
3
|
+
# Read attribute from the attributes hash
|
4
|
+
#
|
5
|
+
def read_attribute(name)
|
6
|
+
name = name.to_s
|
7
|
+
|
8
|
+
if respond_to? name
|
9
|
+
@attributes[name]
|
10
|
+
else
|
11
|
+
raise UnknownAttributeError, "unknown attribute: #{name}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
alias_method :[], :read_attribute
|
15
|
+
|
16
|
+
# Update an attribute in the attributes hash
|
17
|
+
#
|
18
|
+
def write_attribute(name, value)
|
19
|
+
name = name.to_s
|
20
|
+
|
21
|
+
if respond_to? "#{name}="
|
22
|
+
@attributes[name] = typecast_attribute(_attribute_typecaster(name), value)
|
23
|
+
else
|
24
|
+
raise UnknownAttributeError, "unknown attribute: #{name}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
alias_method :[]=, :write_attribute
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'active_remote/association'
|
2
|
+
require 'active_remote/attributes'
|
3
|
+
require 'active_remote/bulk'
|
4
|
+
require 'active_remote/dirty'
|
5
|
+
require 'active_remote/dsl'
|
6
|
+
require 'active_remote/persistence'
|
7
|
+
require 'active_remote/rpc'
|
8
|
+
require 'active_remote/search'
|
9
|
+
require 'active_remote/serialization'
|
10
|
+
|
11
|
+
module ActiveRemote
|
12
|
+
class Base
|
13
|
+
extend ::ActiveModel::Callbacks
|
14
|
+
|
15
|
+
include ::ActiveAttr::Model
|
16
|
+
|
17
|
+
include ::ActiveRemote::Association
|
18
|
+
include ::ActiveRemote::Attributes
|
19
|
+
include ::ActiveRemote::Bulk
|
20
|
+
include ::ActiveRemote::DSL
|
21
|
+
include ::ActiveRemote::Persistence
|
22
|
+
include ::ActiveRemote::RPC
|
23
|
+
include ::ActiveRemote::Search
|
24
|
+
include ::ActiveRemote::Serialization
|
25
|
+
|
26
|
+
# Overrides some methods, providing support for dirty tracking,
|
27
|
+
# so it needs to be included last.
|
28
|
+
include ::ActiveRemote::Dirty
|
29
|
+
|
30
|
+
attr_reader :last_request, :last_response
|
31
|
+
|
32
|
+
define_model_callbacks :initialize, :only => :after
|
33
|
+
|
34
|
+
def initialize(*)
|
35
|
+
run_callbacks :initialize do
|
36
|
+
@attributes ||= {}
|
37
|
+
super
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def freeze
|
42
|
+
@attributes.freeze; self
|
43
|
+
end
|
44
|
+
|
45
|
+
def frozen?
|
46
|
+
@attributes.frozen?
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|