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