Skip to content

Commit

Permalink
Merge branch 'release/0.0.9'
Browse files Browse the repository at this point in the history
  • Loading branch information
Vito Botta committed Jul 26, 2011
2 parents 36fe3ec + 7b5ca0a commit 6ab3ca6
Show file tree
Hide file tree
Showing 8 changed files with 88 additions and 18 deletions.
2 changes: 1 addition & 1 deletion Gemfile.lock
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
tableless_model (0.0.7)
tableless_model (0.0.9)
validatable

GEM
Expand Down
21 changes: 21 additions & 0 deletions README.md
Expand Up @@ -204,6 +204,24 @@ For boolean attributes (or also truthy/falsy ones) you can also make calls to sp
=> true
```

## Encryption

If for some reason you'd like to encrypt the data serialised with the tableless model, so that it cannot be read in clear when looking directly at the contents of the database, you can enable encryption by simply specifying an encryption key:

``` ruby
class SomeModel < ActiveRecord::Base

has_tableless :seo => EncryptedTablelessModel, :encryption_key => "a398bbfaac38c79e60a6e398efba8571"

end
```

In the database, the encrypted tableless model will look similar to this:

``` ruby
Fxq8TU8syHgWBk1ndGt6U5pXZDbDVs3+lLFcxWkvc3b9OONp02RBf+vkmwl2O5VIZpPVqkMiM3Y3zsv5B2N9sXP1eb7Erskq5T3A3oclHoiXvvPizbKbe0T+pulUdbd+GWka8UYHT1FE/vRNAb6o+F83plL6m8ctWTWafM/skqzVXing1FBqpK0Iv+9H8cK3rOjdxdaWT4RMqvRG//MAsAl8gBor2dIdwbg2iap9j42JYSqua8RlGuPKzr/I4dwSYYO1ldg+gDYHRXLIJ//law==--ohQLUAuE4RU+btUyOibx7g==
```


## Validations

Expand Down Expand Up @@ -266,6 +284,9 @@ x.valid?

## Change log

26.07.2011
- Added support for encryption

24.07.2011
- Added support for passing Proc/lamba when defining the default attribute of a value
- Added shortcuts to call getters/setters of attributes defined in a tableless model, from
Expand Down
34 changes: 22 additions & 12 deletions lib/activerecord/base/class_methods.rb
Expand Up @@ -14,42 +14,50 @@ module ClassMethods
#
# class Parent < ActiveRecord::Base
#
# has_tableless :settings
# has_tableless :settings => ParentSettings
#
# # or...
#
# has_tableless :settings => ParentSettings
# has_tableless :settings => ParentSettings, :encryption_key => "secret"
#
# end
#
#
# NOTE: the serialized column is expected to be of type string or text in the database
#
def has_tableless(column)
column_name = column.class == Hash ? column.collect{|k,v| k}.first.to_sym : column
encryption_key = column.delete(:encryption_key)

column_name, class_type = column.to_a.flatten

@tableless_models ||= []
@tableless_models << column_name


# if only the column name is given, the tableless model's class is expected to have that name, classified, as class name
class_type = column.class == Hash ? column.collect{|k,v| v}.last : column.to_s.classify.constantize


# injecting in the parent object a getter and a setter for the
# attribute that will store an instance of a tableless model
class_eval do

# Telling AR that the column has to store an instance of the given tableless model in
# YAML serialized format
serialize column_name
# YAML serialized format; if encryption is enabled, then serialisation/deserialisation will
# be handled when encrypting/decrypting, rather than automatically by ActiveRecord,
# otherwise the different object id would cause a different serialisation string each time

serialize column_name unless encryption_key

# Adding getter for the serialized column,
# making sure it always returns an instance of the specified tableless
# model and not just a normal hash or the value of the attribute in the database,
# which is plain text
define_method column_name.to_s do
instance = class_type.new(read_attribute(column_name.to_sym) || {})
serialised = read_attribute(column_name)

value = if encryption_key
serialised ? YAML::load(ActiveSupport::MessageEncryptor.new(encryption_key).decrypt(serialised)) : serialised
else
serialised || {}
end

instance = class_type.new(value)

instance.__owner_object = self
instance.__serialized_attribute = column_name
Expand All @@ -61,7 +69,9 @@ def has_tableless(column)
# making sure it always stores in it an instance of
# the specified tableless model (as the argument may also be a regular hash)
define_method "#{column_name.to_s}=" do |value|
super class_type.new(value)
v = class_type.new(value)
v = encryption_key ? ActiveSupport::MessageEncryptor.new(encryption_key).encrypt(YAML::dump(v)) : v
super v
end
end
end
Expand Down
2 changes: 2 additions & 0 deletions lib/tableless_model.rb
@@ -1,6 +1,8 @@
$LOAD_PATH.unshift(File.dirname(__FILE__))

require "validatable"
require "active_record"
require "active_support"
require "activerecord/base/class_methods"
require "activerecord/base/instance_methods"
require "tableless_model/class_methods"
Expand Down
2 changes: 1 addition & 1 deletion lib/tableless_model/version.rb
@@ -1,3 +1,3 @@
module TablelessModel
VERSION = "0.0.8"
VERSION = "0.0.9"
end
35 changes: 35 additions & 0 deletions spec/encryption_spec.rb
@@ -0,0 +1,35 @@
require "spec_helper"

class ModelSettings < ActiveRecord::TablelessModel
attribute :some_attribute, :default => "default value"
end

class SomeModel < ActiveRecord::Base
has_tableless :settings => ModelSettings, :encryption_key => "a398bbfaac38c79e60a6e398efba8571"
end

describe SomeModel do
context "when an encryption key has been specified for the tableless-based column" do
let(:mock_encryptor) { mock(ActiveSupport::MessageEncryptor).as_null_object }

before(:each) do
ActiveSupport::MessageEncryptor.stub(:new).with("a398bbfaac38c79e60a6e398efba8571").and_return(mock_encryptor)
end

it "encrypts a tableless-based attribute when writing its attribute" do
mock_encryptor.stub(:encrypt).and_return("encrypted...")

subject.settings = ModelSettings.new(:some_attribute => "non default value")
subject.read_attribute("settings").should == "encrypted..."
end

it "descripts a tableless-based attribute when reading its attribute" do
mock_result = { :some_attribute => "bla bla bla" }

subject.should_receive(:read_attribute).with(:settings).and_return("encrypted...")
mock_encryptor.stub(:decrypt).with("encrypted...").and_return("serialised...")
YAML.should_receive(:load).with("serialised...").and_return(mock_result)
subject.settings.should == mock_result
end
end
end
5 changes: 5 additions & 0 deletions spec/spec_helper.rb
Expand Up @@ -5,3 +5,8 @@
require "timecop"
require "tableless_model"

ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
ActiveRecord::Base.connection.execute(" create table test_models (id integer, options varchar(50)) ")
ActiveRecord::Base.connection.execute(" create table some_models (id integer, settings varchar(50)) ")


5 changes: 1 addition & 4 deletions spec/tableless_model_spec.rb
@@ -1,8 +1,5 @@
require "spec_helper"

ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
ActiveRecord::Base.connection.execute(" create table test_models (id integer, options varchar(50)) ")

class ModelOptions < ActiveRecord::TablelessModel
attribute :no_default_value_no_type_attribute
attribute :no_default_value_typed_attribute, :type => :integer
Expand Down Expand Up @@ -112,7 +109,7 @@ class TestModel < ActiveRecord::Base
end
end
end

describe "#options" do
it "is an instance of the tableless model" do
options.should be_instance_of ModelOptions
Expand Down

0 comments on commit 6ab3ca6

Please sign in to comment.