Skip to content

Instantly share code, notes, and snippets.

@rmw
Last active December 24, 2015 02:19
Show Gist options
  • Save rmw/6730353 to your computer and use it in GitHub Desktop.
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.
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
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
- SerializedHashModel.serialized_hash_keys(:text_attributes).each do |attr|
= form.input attr, as: :text
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
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