protobuf-activerecord 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ *.db
2
+ *.gem
3
+ .DS_Store
4
+ .bundle
5
+ .config
6
+ .yardoc
7
+ Gemfile.lock
8
+ coverage
9
+ pkg/*
10
+ tmp
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --colour
2
+ --format RSpec::Pride
3
+ --order rand
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use 1.9.3@protobuf-activerecord --create
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'http://gems.moneydesktop.com'
2
+ source 'https://rubygems.org'
3
+
4
+ # Specify your gem's dependencies in protobuf-activerecord.gemspec
5
+ gemspec
6
+
7
+ gem 'builder', '~> 3.0.3'
8
+
9
+ gem 'timecop', '~> 0.3.5', :group => :development
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Adam Hutchison
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,29 @@
1
+ # Protobuf ActiveRecord
2
+
3
+ Protobuf Active Record provides the ability to create Active Record objects from Protocol Buffer messages and vice versa. It adds methods that allow you to create, update, and destroy Active Record objects from protobuf messages. It also provides methods to serialize Active Record objects to protobuf messages.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'protobuf-activerecord'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install protobuf-activerecord
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ desc "Run specs"
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ desc "Run specs (default)"
8
+ task :default => :spec
@@ -0,0 +1,75 @@
1
+ module Protoable
2
+ module Convert
3
+ def self.included(klass)
4
+ klass.extend Protoable::Convert::ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+ def convert_int64_to_time(int64)
9
+ Time.at(int64.to_i)
10
+ end
11
+
12
+ def convert_int64_to_date(int64)
13
+ convert_int64_to_time(int64).to_date
14
+ end
15
+
16
+ def convert_int64_to_datetime(int64)
17
+ convert_int64_to_time(int64).to_datetime
18
+ end
19
+
20
+ def _protobuf_convert_columns_to_fields(key, value)
21
+ value = case
22
+ when _protobuf_column_converters.has_key?(key.to_sym) then
23
+ _protobuf_column_converters[key.to_sym].call(value)
24
+ when _protobuf_date_column?(key) then
25
+ value.to_time.to_i
26
+ when _protobuf_datetime_column?(key) then
27
+ value.to_i
28
+ when _protobuf_time_column?(key) then
29
+ value.to_i
30
+ when _protobuf_timestamp_column?(key) then
31
+ value.to_i
32
+ else
33
+ value
34
+ end
35
+
36
+ return value
37
+ end
38
+
39
+ def _protobuf_convert_fields_to_columns(key, value)
40
+ value = case
41
+ when _protobuf_field_converters.has_key?(key.to_sym) then
42
+ _protobuf_field_converters[key.to_sym].call(value)
43
+ when _protobuf_date_column?(key) then
44
+ convert_int64_to_date(value)
45
+ when _protobuf_datetime_column?(key) then
46
+ convert_int64_to_datetime(value)
47
+ when _protobuf_time_column?(key) then
48
+ convert_int64_to_time(value)
49
+ when _protobuf_timestamp_column?(key) then
50
+ convert_int64_to_time(value)
51
+ else
52
+ value
53
+ end
54
+
55
+ return value
56
+ end
57
+
58
+ def _protobuf_date_column?(key)
59
+ _protobuf_column_types.fetch(:date, false) && _protobuf_column_types[:date].include?(key)
60
+ end
61
+
62
+ def _protobuf_datetime_column?(key)
63
+ _protobuf_column_types.fetch(:datetime, false) && _protobuf_column_types[:datetime].include?(key)
64
+ end
65
+
66
+ def _protobuf_time_column?(key)
67
+ _protobuf_column_types.fetch(:time, false) && _protobuf_column_types[:time].include?(key)
68
+ end
69
+
70
+ def _protobuf_timestamp_column?(key)
71
+ _protobuf_column_types.fetch(:timestamp, false) && _protobuf_column_types[:timestamp].include?(key)
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,23 @@
1
+ module Protoable
2
+
3
+ # = Protoable errors
4
+ #
5
+ # Generic Protoable exception class
6
+ class ProtoableError < StandardError
7
+ end
8
+
9
+ # Raised by Protoable.protobuf_column_convert when the convert method
10
+ # given is not defined, nil, or not callable.
11
+ class ColumnConverterError < ProtoableError
12
+ end
13
+
14
+ # Raised by Protoable.protobuf_column_transform when the transformer method
15
+ # given is not defined, nil, or not callable.
16
+ class ColumnTransformerError < ProtoableError
17
+ end
18
+
19
+ # Raised by Protoable.protobuf_field_convert when the convert method
20
+ # given is not defined, nil, or not callable.
21
+ class FieldConverterError < ProtoableError
22
+ end
23
+ end
@@ -0,0 +1,116 @@
1
+ require 'protoable/inheritable_class_instance_variables'
2
+
3
+ module Protoable
4
+ module Fields
5
+ def self.extended(klass)
6
+ klass.extend Protoable::Fields::ClassMethods
7
+ klass.__send__(:include, Protoable::InheritableClassInstanceVariables)
8
+
9
+ klass.class_eval do
10
+ class << self
11
+ attr_accessor :_protobuf_columns, :_protobuf_column_types,
12
+ :_protobuf_column_transformers, :_protobuf_field_converters
13
+ end
14
+
15
+ @_protobuf_columns = {}
16
+ @_protobuf_column_types = Hash.new { |h,k| h[k] = [] }
17
+ @_protobuf_column_transformers = {}
18
+ @_protobuf_field_converters = {}
19
+
20
+ # NOTE: Make sure each inherited object has the database layout
21
+ inheritable_attributes :_protobuf_columns, :_protobuf_column_types,
22
+ :_protobuf_field_converters, :_protobuf_column_transformers
23
+ end
24
+
25
+ _protobuf_map_columns(klass)
26
+ end
27
+
28
+ # Map out the columns for future reference on type conversion
29
+ #
30
+ def self._protobuf_map_columns(klass)
31
+ return unless klass.table_exists?
32
+ klass.columns.map do |column|
33
+ klass._protobuf_columns[column.name.to_sym] = column
34
+ klass._protobuf_column_types[column.type.to_sym] << column.name.to_sym
35
+ end
36
+ end
37
+
38
+ module ClassMethods
39
+ # Define a field conversion from protobuf to db. Accepts a callable,
40
+ # Symbol, or Hash.
41
+ #
42
+ # When given a callable, it is directly used to convert the field.
43
+ #
44
+ # When a Hash is given, :from and :to keys are expected and expand
45
+ # to extracting a class method in the format of
46
+ # "convert_#{from}_to_#{to}".
47
+ #
48
+ # When a symbol is given, it extracts the method with the same name,
49
+ # if any. When method is not available it is assumed as the "from"
50
+ # data type, and the "to" value is extracted based on the
51
+ # name of the column.
52
+ #
53
+ # Examples:
54
+ # convert_field :created_at, :int64
55
+ # convert_field :public_key, method(:extract_public_key_from_proto)
56
+ # convert_field :public_key, :extract_public_key_from_proto
57
+ # convert_field :status, lambda { |proto_field| ... }
58
+ # convert_field :symmetric_key, :base64
59
+ # convert_field :symmetric_key, :from => :base64, :to => :encoded_string
60
+ # convert_field :symmetric_key, :from => :base64, :to => :raw_string
61
+ #
62
+ def convert_field(field, callable = nil, &blk)
63
+ callable ||= blk
64
+
65
+ if callable.is_a?(Hash)
66
+ callable = :"convert_#{callable[:from]}_to_#{callable[:to]}"
67
+ end
68
+
69
+ if callable.is_a?(Symbol)
70
+ unless self.respond_to?(callable)
71
+ column = _protobuf_columns[field.to_sym]
72
+ callable = :"convert_#{callable}_to_#{column.try(:type)}"
73
+ end
74
+ callable = method(callable) if self.respond_to?(callable)
75
+ end
76
+
77
+ if callable.nil? || !callable.respond_to?(:call)
78
+ raise FieldConverterError, 'Field converters must be a callable or block!'
79
+ end
80
+
81
+ _protobuf_field_converters[field.to_sym] = callable
82
+ end
83
+
84
+ # Define a column transformation from protobuf to db. Accepts a callable,
85
+ # or Symbol.
86
+ #
87
+ # When given a callable, it is directly used to convert the field.
88
+ #
89
+ # When a symbol is given, it extracts the method with the same name.
90
+ #
91
+ # The callable or method must accept a single parameter, which is the
92
+ # proto message.
93
+ #
94
+ # Examples:
95
+ # transform_column :public_key, :extract_public_key_from_proto
96
+ # transform_column :status, lambda { |proto_field| ... }
97
+ #
98
+ def transform_column(field, callable = nil, &blk)
99
+ callable ||= blk
100
+
101
+ if callable.is_a?(Symbol)
102
+ unless self.respond_to?(callable)
103
+ raise ColumnTransformerError, "#{callable} is not defined!"
104
+ end
105
+ callable = method(callable) if self.respond_to?(callable)
106
+ end
107
+
108
+ if callable.nil? || !callable.respond_to?(:call)
109
+ raise ColumnTransformerError, 'Protoable casting needs a callable or block!'
110
+ end
111
+
112
+ _protobuf_column_transformers[field.to_sym] = callable
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,54 @@
1
+ require 'thread'
2
+
3
+ module Protoable
4
+ module InheritableClassInstanceVariables
5
+ def self.included(klass)
6
+ Thread.exclusive do
7
+ klass.extend(Protoable::InheritableClassInstanceVariables::ClassMethods)
8
+
9
+ klass.class_eval do
10
+ @_inheritable_class_instance_variables = [ :_inheritable_class_instance_variables ]
11
+ end
12
+ end
13
+ end
14
+
15
+ module ClassMethods
16
+ def inheritable_attributes(*args)
17
+ Thread.exclusive do
18
+ args.flatten.compact.uniq.each do |class_instance_variable|
19
+ unless @_inheritable_class_instance_variables.include?(class_instance_variable)
20
+ @_inheritable_class_instance_variables << class_instance_variable
21
+ end
22
+ end
23
+
24
+ @_inheritable_class_instance_variables.each do |attr_symbol|
25
+ unless self.respond_to?("#{attr_symbol}")
26
+ class_eval %Q{
27
+ class << self; attr_reader :#{attr_symbol}; end
28
+ }
29
+ end
30
+
31
+ unless self.respond_to?("#{attr_symbol}=")
32
+ class_eval %Q{
33
+ class << self; attr_writer :#{attr_symbol}; end
34
+ }
35
+ end
36
+ end
37
+
38
+ @_inheritable_class_instance_variables
39
+ end
40
+ end
41
+
42
+ def inherited(klass)
43
+ super # ActiveRecord needs the inherited hook to setup fields
44
+
45
+ Thread.exclusive do
46
+ @_inheritable_class_instance_variables.each do |attribute|
47
+ attr_sym = :"@#{attribute}"
48
+ klass.instance_variable_set(attr_sym, self.instance_variable_get(attr_sym))
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,94 @@
1
+ module Protoable
2
+ module Persistence
3
+ def self.included(klass)
4
+ klass.extend Protoable::Persistence::ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+ # Filters accessible attributes that exist in the given protobuf message's
9
+ # fields or have column transformers defined for them.
10
+ #
11
+ # Returns a hash of attribute fields with their respective values.
12
+ #
13
+ def _filter_attribute_fields(proto)
14
+ fields = proto.to_hash
15
+ fields.select! { |key, value| proto.has_field?(key) && !proto.get_field_by_name(key).repeated? }
16
+
17
+ attributes = self.new.attributes.keys - protected_attributes.to_a
18
+
19
+ attribute_fields = attributes.inject({}) do |hash, column_name|
20
+ symbolized_column = column_name.to_sym
21
+
22
+ if fields.has_key?(symbolized_column) ||
23
+ _protobuf_column_transformers.has_key?(symbolized_column)
24
+ hash[symbolized_column] = fields[symbolized_column]
25
+ end
26
+
27
+ hash
28
+ end
29
+
30
+ attribute_fields
31
+ end
32
+
33
+ # Creates a hash of attributes from a given protobuf message.
34
+ #
35
+ # It converts and transforms field values using the field converters and
36
+ # column transformers, ignoring repeated and nil fields.
37
+ #
38
+ def attributes_from_proto(proto)
39
+ attribute_fields = _filter_attribute_fields(proto)
40
+
41
+ attributes = attribute_fields.inject({}) do |hash, (key, value)|
42
+ if _protobuf_column_transformers.has_key?(key)
43
+ hash[key] = _protobuf_column_transformers[key].call(proto)
44
+ else
45
+ hash[key] = _protobuf_convert_fields_to_columns(key, value)
46
+ end
47
+
48
+ hash
49
+ end
50
+
51
+ attributes
52
+ end
53
+
54
+ # Creates an object from the given protobuf message, if it's valid. The
55
+ # newly created object is returned if it was successfully saved or not.
56
+ #
57
+ def create_from_proto(proto)
58
+ attributes = attributes_from_proto(proto)
59
+
60
+ yield(attributes) if block_given?
61
+
62
+ record = self.new(attributes)
63
+
64
+ record.save! if record.valid?
65
+ return record
66
+ end
67
+ end
68
+
69
+ # Calls up to the class version of the method.
70
+ #
71
+ def attributes_from_proto(proto)
72
+ self.class.attributes_from_proto(proto)
73
+ end
74
+
75
+ # Destroys the record. Mainly wrapped to provide a consistent API and
76
+ # a convient way to override protobuf-specific destroy behavior.
77
+ #
78
+ def destroy_from_proto
79
+ destroy
80
+ end
81
+
82
+ # Update a record from a proto message. Accepts an optional block.
83
+ # If block is given, yields the attributes that would be updated.
84
+ #
85
+ def update_from_proto(proto)
86
+ attributes = attributes_from_proto(proto)
87
+
88
+ yield(attributes) if block_given?
89
+
90
+ assign_attributes(attributes)
91
+ return valid? ? save! : false
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,120 @@
1
+ module Protoable
2
+ module Serialization
3
+ def self.included(klass)
4
+ klass.extend Protoable::Serialization::ClassMethods
5
+ klass.__send__(:include, Protoable::InheritableClassInstanceVariables)
6
+
7
+ klass.class_eval do
8
+ class << self
9
+ attr_accessor :_protobuf_column_converters, :protobuf_fields
10
+ end
11
+
12
+ @_protobuf_column_converters = {}
13
+ @protobuf_fields = []
14
+
15
+ # NOTE: Make sure each inherited object has the database layout
16
+ inheritable_attributes :protobuf_fields, :_protobuf_column_converters
17
+ end
18
+ end
19
+
20
+ module ClassMethods
21
+ # Define a column conversion from db to protobuf. Accepts a callable,
22
+ # Symbol, or Hash.
23
+ #
24
+ # When given a callable, it is directly used to convert the field.
25
+ #
26
+ # When a Hash is given, :from and :to keys are expected and expand
27
+ # to extracting a class method in the format of
28
+ # "convert_#{from}_to_#{to}".
29
+ #
30
+ # When a symbol is given, it extracts the method with the same name,
31
+ # if any. When method is not available it is assumed as the "from"
32
+ # data type, and the "to" value is extracted based on the
33
+ # name of the column.
34
+ #
35
+ # Examples:
36
+ # convert_column :created_at, :int64
37
+ # convert_column :public_key, :extract_public_key_from_proto
38
+ # convert_column :public_key, method(:extract_public_key_from_proto)
39
+ # convert_column :status, lambda { |proto_field| ... }
40
+ # convert_column :symmetric_key, :from => :base64, :to => :raw_string
41
+ #
42
+ def convert_column(field, callable = nil, &blk)
43
+ callable ||= blk
44
+
45
+ if callable.is_a?(Hash)
46
+ callable = :"convert_#{callable[:from]}_to_#{callable[:to]}"
47
+ end
48
+
49
+ if callable.is_a?(Symbol)
50
+ unless self.respond_to?(callable)
51
+ column = _protobuf_columns[field.to_sym]
52
+ callable = :"convert_#{callable}_to_#{column.try(:type)}"
53
+ end
54
+ callable = method(callable) if self.respond_to?(callable)
55
+ end
56
+
57
+ if callable.nil? || !callable.respond_to?(:call)
58
+ raise ColumnConverterError, 'Column converters must be a callable or block!'
59
+ end
60
+
61
+ _protobuf_column_converters[field.to_sym] = callable
62
+ end
63
+
64
+ # Define the protobuf message class that should be used to serialize the
65
+ # object to protobuf. Accepts a string or symbol.
66
+ #
67
+ # When protobuf_message is declared, Protoable automatically extracts the
68
+ # fields from the message and automatically adds to_proto and to_proto_hash
69
+ # methods that serialize the object to protobuf.
70
+ #
71
+ # Examples:
72
+ # protobuf_message :user_message
73
+ # protobuf_message "UserMessage"
74
+ # protobuf_message "Namespaced::UserMessage"
75
+ #
76
+ def protobuf_message(message = nil)
77
+ unless message.nil?
78
+ @_protobuf_message = message.to_s.classify.constantize
79
+
80
+ self.protobuf_fields = @_protobuf_message.fields.compact.map do |field|
81
+ field.name.to_sym
82
+ end
83
+
84
+ define_method(:to_proto) do
85
+ self.class.protobuf_message.new(self.to_proto_hash)
86
+ end
87
+
88
+ define_method(:to_proto_hash) do
89
+ protoable_attributes
90
+ end
91
+ end
92
+
93
+ @_protobuf_message
94
+ end
95
+ end
96
+
97
+ # Extracts attributes that correspond to fields on the specified protobuf
98
+ # message, performing any necessary column conversions on them.
99
+ #
100
+ def protoable_attributes
101
+ protoable_attributes = protobuf_fields.inject({}) do |hash, field|
102
+ value = respond_to?(field) ? __send__(field) : nil
103
+ hash[field] = _protobuf_convert_columns_to_fields(field, value)
104
+ hash
105
+ end
106
+
107
+ protoable_attributes
108
+ end
109
+
110
+ private
111
+
112
+ def _protobuf_convert_columns_to_fields(field, value)
113
+ self.class._protobuf_convert_columns_to_fields(field, value)
114
+ end
115
+
116
+ def protobuf_fields
117
+ self.class.protobuf_fields
118
+ end
119
+ end
120
+ end
data/lib/protoable.rb ADDED
@@ -0,0 +1,15 @@
1
+ require 'protoable/convert'
2
+ require 'protoable/errors'
3
+ require 'protoable/fields'
4
+ require 'protoable/persistence'
5
+ require 'protoable/serialization'
6
+
7
+ module Protoable
8
+ def self.included(klass)
9
+ klass.extend Protoable::Fields
10
+
11
+ klass.__send__(:include, Protoable::Convert)
12
+ klass.__send__(:include, Protoable::Persistence)
13
+ klass.__send__(:include, Protoable::Serialization)
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ module Protobuf
2
+ module ActiveRecord
3
+ VERSION = "1.0.1"
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ require 'protoable'
2
+
3
+ require 'protobuf/activerecord/version'
@@ -0,0 +1,37 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'protobuf/activerecord/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "protobuf-activerecord"
8
+ gem.version = Protobuf::ActiveRecord::VERSION
9
+ gem.authors = ["Adam Hutchison"]
10
+ gem.email = ["liveh2o@gmail.com"]
11
+ gem.homepage = "http://github.com/liveh2o/protobuf-activerecord"
12
+ gem.summary = %q{Google Protocol Buffers integration for Active Record}
13
+ gem.description = %q{Provides the ability to create Active Record objects from Protocol Buffer messages and vice versa.}
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ ##
21
+ # Dependencies
22
+ #
23
+ gem.add_dependency "activerecord"
24
+ gem.add_dependency "protobuf", ">= 2.0"
25
+
26
+ ##
27
+ # Development dependencies
28
+ #
29
+ gem.add_development_dependency "rake"
30
+ gem.add_development_dependency "geminabox"
31
+ gem.add_development_dependency "rspec"
32
+ gem.add_development_dependency "rspec-pride"
33
+ gem.add_development_dependency "pry-nav"
34
+ gem.add_development_dependency "simplecov"
35
+ gem.add_development_dependency "sqlite3"
36
+ gem.add_development_dependency "timecop"
37
+ end