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
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ *.gem
2
+ *.swp
3
+ .DS_Store
4
+ .bundle
5
+ .config
6
+ .yardoc
7
+ Gemfile.lock
8
+ coverage
9
+ pkg/*
10
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --order rand
2
+ --format RSpec::Pride
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use 1.9.3@active_remote --create
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'http://gems.moneydesktop.com'
2
+ source 'https://rubygems.org'
3
+
4
+ gem 'builder', '~> 3.0.0' # Geminabox will to go to 3.1.3, which causes problems with ActiveModel.
5
+
6
+ # Specify your gem's dependencies in active_remote.gemspec
7
+ gemspec
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