Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 9c7690b
Showing
15 changed files
with
516 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
debug.log | ||
Gemfile.lock | ||
*.gem |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
|
||
= 3.2.0 | ||
|
||
* Initial release. Works with ActiveRecord 3.2.x | ||
|
||
|
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
source :rubygems | ||
gemspec |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
|
||
# StoreConfigurable | ||
|
||
A zero-configuration recursive hash for storing anything in a serialized ActiveRecord column. | ||
|
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
require 'bundler' | ||
require 'rake/testtask' | ||
|
||
Bundler::GemHelper.install_tasks | ||
|
||
desc 'Test the StoreConfigurable gem.' | ||
Rake::TestTask.new do |t| | ||
t.libs = ['lib','test'] | ||
t.test_files = Dir.glob("test/**/*_test.rb").sort | ||
t.verbose = true | ||
end | ||
|
||
task :default => [:test] |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
require 'active_record' | ||
require 'store_configurable/version' | ||
require 'store_configurable/dirty_options' | ||
require 'store_configurable/object' | ||
require 'store_configurable/serialization' | ||
require 'store_configurable/read' | ||
require 'store_configurable/base' |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
require 'active_support/concern' | ||
|
||
module StoreConfigurable | ||
module Base | ||
|
||
extend ActiveSupport::Concern | ||
|
||
module ClassMethods | ||
|
||
# To use StoreConfigurable, you must create create a +_config+ colun in the mdoel's | ||
# table. Make sure that you declare this column as a text, so there's plenty of room. | ||
# | ||
# class AddStoreConfigurableField < ActiveRecord::Migration | ||
# def up | ||
# add_column :users, :_config, :text | ||
# end | ||
# def down | ||
# remove_column :users, :_config | ||
# end | ||
# end | ||
# | ||
# Next declare that your model uses StoreConfigurable like so `store_configurable`. | ||
# Please read the +config+ documentation for usage examples. | ||
# | ||
# class User < ActiveRecord::Base | ||
# store_configurable | ||
# end | ||
def store_configurable | ||
serialize '_config', StoreConfigurable::Object | ||
include Read | ||
end | ||
|
||
end | ||
|
||
end | ||
end | ||
|
||
ActiveSupport.on_load(:active_record) { include StoreConfigurable::Base } |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
require 'active_support/ordered_options' | ||
|
||
module StoreConfigurable | ||
|
||
# The heart of StoreConfigurable's data store is this subclass of ActiveSupport's OrderedOptions. | ||
# They are the heart of Rails' configurations and allow you to dynamically set and get hash keys | ||
# and values using dot property notation vs the +[]+ hash accessors. | ||
# | ||
# However, instances of DirtyTrackingOrderedOptions use a recursive lambda via Hash's block | ||
# initialization syntax so that you get a dynamic and endless scope on config data. Instances of | ||
# DirtyTrackingOrderedOptions also make sure that every sub instance of it self also has a handle | ||
# back to your store's owner. In this way when config attributes are added or values change, | ||
# we can mark your ActiveRecord object as dirty/changed. | ||
class DirtyTrackingOrderedOptions < ::ActiveSupport::OrderedOptions | ||
|
||
Recursive = lambda { |h,k| h[k] = h.class.new(h.__store_configurable_owner__) } | ||
|
||
attr_accessor :__store_configurable_owner__ | ||
|
||
def initialize(owner) | ||
@__store_configurable_owner__ = owner | ||
super(&Recursive) | ||
end | ||
|
||
def []=(key, value) | ||
_config_may_change!(key, value) | ||
super | ||
end | ||
|
||
def delete(key) | ||
name = key.to_sym | ||
_config_will_change! if has_key?(name) | ||
super | ||
end | ||
|
||
def delete_if | ||
_with_config_keys_may_change! { super } | ||
end | ||
|
||
def dup | ||
raise NotImplementedError, 'the StoreConfigurable::Object does not support making a copy' | ||
end | ||
|
||
def reject! | ||
_with_config_keys_may_change! { super } | ||
end | ||
|
||
def clear | ||
_config_will_change! | ||
super | ||
end | ||
|
||
|
||
protected | ||
|
||
def _with_config_keys_may_change! | ||
starting_keys = keys.dup | ||
yield | ||
_config_will_change! if starting_keys != keys | ||
self | ||
end | ||
|
||
def _config_may_change!(key, value) | ||
name = key.to_sym | ||
_config_will_change! unless has_key?(name) && self[name] == value | ||
end | ||
|
||
def _config_will_change! | ||
__store_configurable_owner__._config_will_change! | ||
end | ||
|
||
end | ||
|
||
end |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
require 'active_support/basic_object' | ||
|
||
module StoreConfigurable | ||
|
||
# The is the object returned by the +config+ method. It does nothing more than delegate | ||
# all calls to a tree of +DirtyTrackingOrderedOptions+ objects which are basically hashes. | ||
class Object < ::ActiveSupport::BasicObject | ||
|
||
# Class methods so the +StoreConfigurable::Object+ responds to +dump+ and +load+ which | ||
# allows it to conform to ActiveRecord's coder requirement via its serialize method | ||
# that we use. | ||
# | ||
# The +dump+ method serializes the raw data behind the +StoreConfigurable::Object+ proxy | ||
# object. This means that we only storing pure ruby primitives in the datbase, not our | ||
# object's proxy. | ||
# | ||
# The +load+ method mimics +ActiveRecord::Coders::YAMLColumn+ internals by retuning a | ||
# new object when needed as well as making sure that the yaml we are process if of the | ||
# same type. When reconstituting a +StoreConfigurable::Object+ we must set the store's | ||
# owner as this does. That way as our recursive lambda loader regenerates the tree of | ||
# config data, we always have a handle for each +DirtyTrackingOrderedOptions+ object to | ||
# report state changes back to the owner. Finally, after each load we make sure to clear | ||
# out changes so reloaded objects are not marked as dirty. | ||
module Coding | ||
|
||
def dump(value) | ||
YAML.dump value.__config__ | ||
end | ||
|
||
def load(yaml, owner) | ||
return StoreConfigurable::Object.new if yaml.blank? | ||
return yaml unless yaml.is_a?(String) && yaml =~ /^---/ | ||
stored_data = YAML.load(yaml) | ||
unless stored_data.is_a?(Hash) | ||
raise ActiveRecord::SerializationTypeMismatch, | ||
"Attribute was supposed to be a Hash, but was a #{stored_data.class}" | ||
end | ||
config = StoreConfigurable::Object.new | ||
config.__store_configurable_owner__ = owner | ||
loader = lambda do |options, key, value| | ||
value.is_a?(Hash) ? value.each { |k,v| loader.call(options.send(key), k, v) } : | ||
options.send("#{key}=", value) | ||
end | ||
stored_data.each { |k,v| loader.call(config, k, v) } | ||
owner.changed_attributes.delete('_config') | ||
config | ||
end | ||
|
||
end | ||
|
||
# Instance methods for +StoreConfigurable::Object+ defined and included in a module so | ||
# that if you ever wanted to, you could redefine these methods and +super+ up. | ||
module Behavior | ||
|
||
attr_accessor :__store_configurable_owner__ | ||
|
||
def __config__ | ||
@__config__ ||= DirtyTrackingOrderedOptions.new(@__store_configurable_owner__) | ||
end | ||
|
||
def inspect | ||
"#<StoreConfigurable::Object:#{object_id}>" | ||
end | ||
|
||
private | ||
|
||
def method_missing(method, *args, &block) | ||
__config__.__send__ method, *args, &block | ||
end | ||
|
||
end | ||
|
||
extend Coding | ||
include Behavior | ||
|
||
end | ||
|
||
end | ||
|
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
module StoreConfigurable | ||
module Read | ||
|
||
# Our main syntatic interface to the underlying +_config+ store. This method ensures that | ||
# +self+, the store's owner, will allways be set in the config object. Hence allowing all | ||
# other recursive options to get a handle back to the owner. | ||
# | ||
# The config object can be treated as a Hash but in actuality is an enhanced subclass of | ||
# +ActiveSupport::OrderedOptions+ that does two important things. First, it allows you to | ||
# dynamically define any nested namespace property. Second, it hooks back into your parent | ||
# object to notify it of change via ActiveRecord's dirty support. | ||
# | ||
# Example: | ||
# | ||
# class User < ActiveRecord::Base | ||
# store_configurable | ||
# end | ||
# | ||
# user = User.find(42) | ||
# user.config.remember_me = true | ||
# user.config.sortable_tables.products.sort_on = 'created_at' | ||
# user.config.sortable_tables.products.direction = 'asc' | ||
# user.changed? # => true | ||
# user.config_changed? # => true | ||
def config | ||
_config.__store_configurable_owner__ = self | ||
_config | ||
end | ||
|
||
# Simple delegation to the underlying data attribute's changed query method. | ||
def config_changed? | ||
_config_changed? | ||
end | ||
|
||
# Simple delegation to the underlying data attribute's change array. | ||
def config_change | ||
_config_change | ||
end | ||
|
||
# An override to ActiveRecord's accessor for the sole purpoes of injecting +Serialization+ | ||
# behavior so that we can set the context of this owner and ensure we pass that down to | ||
# the YAML coder. Doing this on a per instance basis keeps us from trumping all other | ||
# +ActiveRecord::AttributeMethods::Serialization::Attribute+ objects. | ||
def _config | ||
attrib = @attributes['_config'] | ||
unless attrib.respond_to?(:__store_configurable_owner__) | ||
attrib.extend Serialization | ||
attrib.__store_configurable_owner__ = self | ||
end | ||
super | ||
end | ||
|
||
end | ||
end |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
module StoreConfigurable | ||
|
||
# This module's behavior is injected into +ActiveRecord::AttributeMethods::Serialization::Attribute+ | ||
# class which is a mini state machine for serialized objects. It allows us to both set the store's | ||
# owner as well as overwrite the +unserialize+ method to give the coder both the YAML and owner | ||
# context. This is done via the +_config+ attribute reader override. | ||
module Serialization | ||
|
||
attr_accessor :__store_configurable_owner__ | ||
|
||
def unserialize | ||
self.state = :unserialized | ||
self.value = coder.load(value, __store_configurable_owner__) | ||
end | ||
|
||
end | ||
|
||
end |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
module StoreConfigurable | ||
|
||
# We track ActiveRecord's major and minor version and follow semantic versioning. | ||
VERSION = '3.2.0' | ||
|
||
end |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
$:.push File.expand_path("../lib", __FILE__) | ||
require "store_configurable/version" | ||
|
||
Gem::Specification.new do |s| | ||
s.name = 'store_configurable' | ||
s.version = StoreConfigurable::VERSION | ||
s.platform = Gem::Platform::RUBY | ||
s.authors = ['Ken Collins'] | ||
s.email = ['ken@metaskills.net'] | ||
s.homepage = 'http://github.com/Decisiv/store_configurable/' | ||
s.summary = 'A small engine around ActiveRecord::Store.' | ||
s.description = 'Includes a #config method and syntatic sugar hooks for our classes.' | ||
s.files = `git ls-files`.split("\n") - ["store_configurable.gemspec"] | ||
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") | ||
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } | ||
s.require_paths = ['lib'] | ||
s.rdoc_options = ['--charset=UTF-8'] | ||
s.add_runtime_dependency 'activerecord', '~> 3.2.0' | ||
s.add_development_dependency 'sqlite3', '~> 1.3' | ||
s.add_development_dependency 'rake', '~> 0.9.2' | ||
s.add_development_dependency 'minitest', '~> 2.8.1' | ||
end |
Oops, something went wrong.