Last active
December 24, 2015 02:19
-
-
Save rmw/6730353 to your computer and use it in GitHub Desktop.
Serialized Hash with getters/setters, which allows you to add validation and change tracking while keeping your model flexible for your needs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module HasSerializedHash | |
extend ActiveSupport::Concern | |
include ActiveModel::Dirty | |
def render_html(text) | |
@markdown ||= Redcarpet::Markdown.new(Redcarpet::Render::HTML, autolink: true) | |
@markdown.render(text || '').html_safe | |
end | |
def strip_serialized_attributes(key_name) | |
self.class.serialized_hash_keys(key_name).each do |attr_name| | |
attr_value = send(attr_name) | |
send("#{attr_name}=", attr_value.strip) if attr_value.present? | |
end | |
end | |
module ClassMethods | |
def serialized_hashes | |
@serialized_hashes ||= {} | |
end | |
def serialized_hash_keys(attr) | |
serialized_hashes[attr] || [] | |
end | |
def has_serialized_hash(attribute, *keys) | |
serialize(attribute, Hash) | |
serialized_hashes[attribute] = keys | |
attr_accessible(*keys) | |
keys.each { |key| setup_methods(attribute, key) } | |
define_all_exists_predicate(attribute, *keys) | |
end | |
private | |
def setup_methods(attribute, key) | |
define_getter(attribute, key) | |
define_html_getter(attribute, key) | |
define_setter(attribute, key) | |
define_existence_predicate(attribute, key) | |
define_changed_predicate(key) | |
end | |
def define_getter(attribute, key) | |
define_method key do | |
hash = self.send(attribute) | |
hash[key] | |
end | |
end | |
def define_html_getter(attribute, key) | |
define_method "#{key}_html" do | |
value = self.send(attribute)[key] | |
self.render_html(value.to_s) | |
end | |
end | |
def define_setter(attribute, key) | |
define_method "#{key}=" do |value| | |
hash = self.send(attribute) | |
attribute_will_change!(key) unless value == hash[key] | |
hash[key] = value | |
end | |
end | |
def define_existence_predicate(attribute, key) | |
define_method "#{key}?" do | |
hash = self.send(attribute) | |
hash[key].present? | |
end | |
end | |
def define_changed_predicate(key) | |
define_method "#{key}_changed?" do | |
changed.include?(key) | |
end | |
end | |
def define_all_exists_predicate(attribute, *keys) | |
define_method "all_#{attribute}_exist?" do | |
hash = self.send(attribute) | |
keys.all? { |key| hash.key?(key) } | |
end | |
end | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'spec_helper' | |
class MyDummyClass | |
include ActiveRecord::AttributeMethods::Serialization | |
include ActiveModel::MassAssignmentSecurity | |
include HasSerializedHash | |
attr_accessor :my_hash | |
has_serialized_hash :my_hash, :key1, :key2 | |
def initialize | |
@my_hash = {} | |
@hash1 = {} | |
end | |
end | |
describe MyDummyClass do | |
subject { MyDummyClass.new } | |
let(:markdown) { "This is *awesome*. www.howaboutwe.com" } | |
def should_render_markdown_to_html(rendered) | |
rendered.should include("<em>awesome</em>") | |
rendered.should include("http://www.howaboutwe.com") | |
end | |
it "translates text to markdown via #render_html" do | |
rendered = subject.render_html(markdown) | |
should_render_markdown_to_html(rendered) | |
end | |
describe "initialization" do | |
context "the given hash's value is nil" do | |
its(:my_hash) { should == {} } | |
end | |
context "the given hash's value is not nil" do | |
it "returns the value" do | |
subject.my_hash = "foobeedoo" | |
subject.my_hash.should == "foobeedoo" | |
end | |
end | |
end | |
describe "getters" do | |
it { should respond_to(:key1) } | |
it { should respond_to(:key2) } | |
it "gets the key's value" do | |
subject.my_hash[:key2] = "hiii" | |
subject.key2.should == "hiii" | |
end | |
describe "html getter" do | |
it { should respond_to(:key1_html) } | |
it { should respond_to(:key2_html) } | |
it "translates markdown to html" do | |
subject.key1 = markdown | |
rendered = subject.key1_html | |
should_render_markdown_to_html(rendered) | |
end | |
it "converts the hash value to a string before rendering" do | |
val = "blah" | |
val.stub(:to_s) { "qqyyzz" } | |
subject.key1 = val | |
subject.key1_html.should =~ /qqyyzz/ | |
end | |
end | |
end | |
describe "setters" do | |
it { should respond_to(:key1=) } | |
it { should respond_to(:key2=) } | |
it "sets the value" do | |
subject.key1 = "hello" | |
subject.my_hash[:key1].should == "hello" | |
end | |
context "when the value has changed" do | |
it "calls attribute_will_change!" do | |
subject.should_receive(:attribute_will_change!).with(:key1) | |
subject.key1 = "hello" | |
end | |
end | |
context "when the value hasn't changed" do | |
it "doesn't call attribute_will_change!" do | |
subject.should_not_receive(:attribute_will_change!).with(:key1) | |
subject.key1 = subject.key1 | |
end | |
end | |
end | |
describe "existence predicate methods" do | |
it { should respond_to(:key1?) } | |
it { should respond_to(:key2?) } | |
before { subject.key1 = "exist" } | |
its(:key1) { should be } | |
its(:key2) { should_not be } | |
describe "#all_my_hash_exist?" do | |
it { should respond_to(:all_my_hash_exist?) } | |
context "every key in the given hash is not set" do | |
its(:all_my_hash_exist?) { should be_false } | |
end | |
context "every key in the given hash is set" do | |
before do | |
subject.key1 = "hello" | |
subject.key2 = "hii" | |
end | |
it "is true" do | |
subject.all_my_hash_exist?.should be_true | |
end | |
end | |
end | |
end | |
describe "changed predicate methods" do | |
it { should respond_to(:key1_changed?) } | |
it { should respond_to(:key2_changed?) } | |
it "returns true if the attribute has changed" do | |
subject.key1_changed?.should be_false | |
subject.key2_changed?.should be_false | |
subject.key1 = "hello" | |
subject.key1_changed?.should be_true | |
subject.key2_changed?.should be_false | |
end | |
end | |
describe ".serialized_hash_keys" do | |
it "includes every key in the hash" do | |
MyDummyClass.serialized_hash_keys(:my_hash).should include(:key1, :key2) | |
end | |
end | |
describe "mass assigninment" do | |
it "makes the serialized hash keys accessible" do | |
MyDummyClass.should_receive(:attr_accessible).with(:key1, :key2) | |
MyDummyClass.has_serialized_hash :my_hash, :key1, :key2 | |
end | |
end | |
describe "strip_serialized_attributes" do | |
it "strips whitespace from all attributes for a key" do | |
dummy = MyDummyClass.new | |
dummy.key1 = " some_text \r\n" | |
dummy.key2 = "\rother_text " | |
dummy.strip_serialized_attributes(:my_hash) | |
dummy.key1.should == "some_text" | |
dummy.key2.should == "other_text" | |
end | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
- SerializedHashModel.serialized_hash_keys(:text_attributes).each do |attr| | |
= form.input attr, as: :text |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
RSpec::Matchers.define :have_serialized_hash do |attribute| | |
match do |actual| | |
return false unless actual.class.respond_to?(:serialized_hashes) | |
serialized_hashes = actual.class.serialized_hashes | |
result = serialized_hashes.key?(attribute) | |
if result && @serialized_hash_key.present? | |
result &&= serialized_hashes[attribute].include?(@serialized_hash_key) | |
end | |
result | |
end | |
chain :for do |key| | |
@serialized_hash_key = key | |
end | |
failure_message_for_should do |actual| | |
"expected that #{actual.class.name} would have a key for #{expected}" | |
end | |
failure_message_for_should_not do |actual| | |
"expected that #{actual.class.name} would not have a key for #{expected}" | |
end | |
description do | |
"serialized hash key for #{expected}" | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class SerializedHashModel < ActiveRecord::Base | |
include HasSerializedHash | |
has_serialized_hash :text_attributes, | |
:one_of_a_kind_copy, | |
:on_the_date_copy1, :on_the_date_copy2, :on_the_date_copy3, :on_the_date_copy4, | |
:details_copy, :location_copy, :location_copy2, | |
:value, :email_details, :booking_link | |
has_serialized_hash :number_attributes, | |
:map_lat, :map_long, | |
:map_lat2, :map_long2 | |
validates :details_copy, length: { maximum: 60 }, presence: true | |
before_validation :strip_text_attributes | |
private | |
def strip_text_attributes | |
strip_serialized_attributes(:text_attributes) | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment