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