store_configurable 3.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/.travis.yml +4 -0
- data/CHANGELOG +6 -0
- data/Gemfile +2 -0
- data/MIT-LICENSE +21 -0
- data/README.md +123 -0
- data/Rakefile +13 -0
- data/lib/store_configurable/base.rb +38 -0
- data/lib/store_configurable/dirty_options.rb +79 -0
- data/lib/store_configurable/object.rb +79 -0
- data/lib/store_configurable/read.rb +54 -0
- data/lib/store_configurable/serialization.rb +18 -0
- data/lib/store_configurable/version.rb +6 -0
- data/lib/store_configurable.rb +7 -0
- data/test/cases/base_test.rb +147 -0
- data/test/helper.rb +56 -0
- metadata +109 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/CHANGELOG
ADDED
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
Copyright (c) 2008-2011 Ken Collins
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
+
|
data/README.md
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
|
2
|
+
# StoreConfigurable
|
3
|
+
|
4
|
+
A zero-configuration recursive Hash for storing a tree of options in a serialized ActiveRecord column. Includes self aware hooks that delegate dirty/changed state to your configs owner.
|
5
|
+
|
6
|
+
<img src="http://cdn.actionmoniker.com/share/recursive_kitty_small.jpg" alt="Recursive Github Kitty" width="260" height="160" style="float:right; margin:-20px 35px 15px 15px; background-color:#fff; padding:13px; -moz-box-shadow: 5px 5px 5px rgba(0,0,0,0.5); -webkit-box-shadow: 5px 5px 5px rgba(0,0,0,0.5); box-shadow: 5px 5px 5px rgba(0,0,0,0.5); -moz-transform: rotate(-2deg); -webkit-transform: rotate(-2deg); transform: rotate(-2deg);">
|
7
|
+
|
8
|
+
[![Build Status](https://secure.travis-ci.org/metaskills/store_configurable.png)](http://travis-ci.org/metaskills/store_configurable)
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
Install the gem with bundler. We follow a semantic versioning format that tracks ActiveRecord's minor version. So this means to use the latest 3.2.x version of StoreConfigurable with any ActiveRecord 3.2 version.
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
gem 'store_configurable', '~> 3.2.0'
|
16
|
+
```
|
17
|
+
|
18
|
+
|
19
|
+
## Setup
|
20
|
+
|
21
|
+
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 type, so there's plenty of room.
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
class AddStoreConfigurableField < ActiveRecord::Migration
|
25
|
+
def up
|
26
|
+
add_column :users, :_config, :text
|
27
|
+
end
|
28
|
+
def down
|
29
|
+
remove_column :users, :_config
|
30
|
+
end
|
31
|
+
end
|
32
|
+
```
|
33
|
+
|
34
|
+
Next declare that your model uses StoreConfigurable with the `store_configurable` method.
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
class User < ActiveRecord::Base
|
38
|
+
store_configurable
|
39
|
+
end
|
40
|
+
```
|
41
|
+
|
42
|
+
|
43
|
+
## Usage
|
44
|
+
|
45
|
+
Our `config` method is your gateway to StoreConfigurable and unlike ActiveRecord's new Store object in 3.2, there is no configuration needed to start using it for any property. It will dynamically expand for every property or namespace. This allows you or other plugins' configurations to be grouped in logical nodes. All examples below assume that StoreConfigurable is being used on a User instance as shown in the setup above.
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
@user.config.remember_me = true
|
49
|
+
@user.config.sortable_tables.column = 'created_at'
|
50
|
+
@user.config.sortable_tables.direction = 'asc'
|
51
|
+
@user.config.you.should.never.need.to.do.this = 'deep_value'
|
52
|
+
@user.save
|
53
|
+
```
|
54
|
+
|
55
|
+
#### Dirty Hooks
|
56
|
+
|
57
|
+
StoreConfigurable is smart enought to let your parent object know when it changes. It is not dumb either. It will only trigger changes if the values you set are different, are new, or change the configs state. Some examples assuming the saved record's data above.
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
@user = User.find(42)
|
61
|
+
@user.config_changed? # => false
|
62
|
+
|
63
|
+
@user.config.remember_me = true # Same value
|
64
|
+
@user.config_changed? # => false
|
65
|
+
|
66
|
+
@user.config.sortable_tables.column = 'updated_at'
|
67
|
+
@user.config.sortable_tables.direction = 'desc'
|
68
|
+
@user.config_changed? # => true
|
69
|
+
```
|
70
|
+
|
71
|
+
#### Hash Syntax
|
72
|
+
|
73
|
+
The StoreConfigurable data objects supports most `Hash` methods with the exception of a few that rely on making a copy of the data, like `dup`. This means you can delete whole branches of data or itterate over your data collection. Again, StoreConfigurable reports all changes to the owner object via ActiveRecord's dirty support.
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
@user.config.sortable_tables.delete # Deletes this node/namespace.
|
77
|
+
@user.config.clear # Hash method to purge.
|
78
|
+
@user.config_changed? # => true
|
79
|
+
```
|
80
|
+
|
81
|
+
#### Choose Your Style
|
82
|
+
|
83
|
+
You can choose to get or set config values via any method or hash key syntax you choose. It really does not matter! This means you can mix and match dot property notation, hash string or symbol syntax and it will just work.
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
@user.config.color = '#c1c1c1'
|
87
|
+
@user.config['remember_me'] = true
|
88
|
+
@user.config[:sortable_tables].direction = 'asc'
|
89
|
+
@user.config.sortable_tables['column'] = 'updated_at'
|
90
|
+
|
91
|
+
@user.config['color'] # => '#c1c1c1'
|
92
|
+
@user.config[:color] # => '#c1c1c1'
|
93
|
+
@user.config.remember_me # => true
|
94
|
+
@user.config.sortable_tables[:direction] # => 'asc'
|
95
|
+
@user.config[:sortable_tables][:column] # => 'updated_at'
|
96
|
+
```
|
97
|
+
|
98
|
+
|
99
|
+
## Stored Data
|
100
|
+
|
101
|
+
StoreConfigurable persists your configuration data in YAML format to the `_config` text column. We use Ruby's `YAML::Omap` type on the backend so we can decouple our datastore from our proxy object manager. This means you can easily load this data via other means if you want to.
|
102
|
+
|
103
|
+
```yaml
|
104
|
+
--- !omap
|
105
|
+
- :remember_me: true
|
106
|
+
- :sortable_tables: !omap
|
107
|
+
- :column: created_at
|
108
|
+
- :direction: asc
|
109
|
+
- :you: !omap
|
110
|
+
- :should: !omap
|
111
|
+
- :never: !omap
|
112
|
+
- :need: !omap
|
113
|
+
- :to: !omap
|
114
|
+
- :do: !omap
|
115
|
+
- :this: deep_value
|
116
|
+
```
|
117
|
+
|
118
|
+
|
119
|
+
## License
|
120
|
+
|
121
|
+
* Released under the MIT license thanks to Decisiv, Inc.
|
122
|
+
* Copyright (c) 2011 Ken Collins
|
123
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
require 'rake/testtask'
|
3
|
+
|
4
|
+
Bundler::GemHelper.install_tasks
|
5
|
+
|
6
|
+
desc 'Test the StoreConfigurable gem.'
|
7
|
+
Rake::TestTask.new do |t|
|
8
|
+
t.libs = ['lib','test']
|
9
|
+
t.test_files = Dir.glob("test/**/*_test.rb").sort
|
10
|
+
t.verbose = true
|
11
|
+
end
|
12
|
+
|
13
|
+
task :default => [:test]
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
module StoreConfigurable
|
4
|
+
module Base
|
5
|
+
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
|
10
|
+
# To use StoreConfigurable, you must create create a +_config+ colun in the mdoel's table.
|
11
|
+
# Make sure that you declare this column as a text type, so there's plenty of room.
|
12
|
+
#
|
13
|
+
# class AddStoreConfigurableField < ActiveRecord::Migration
|
14
|
+
# def up
|
15
|
+
# add_column :users, :_config, :text
|
16
|
+
# end
|
17
|
+
# def down
|
18
|
+
# remove_column :users, :_config
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# Next declare that your model uses StoreConfigurable with the +store_configurable+ method.
|
23
|
+
# Please read the +config+ documentation for usage examples.
|
24
|
+
#
|
25
|
+
# class User < ActiveRecord::Base
|
26
|
+
# store_configurable
|
27
|
+
# end
|
28
|
+
def store_configurable
|
29
|
+
serialize '_config', StoreConfigurable::Object
|
30
|
+
include Read
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
ActiveSupport.on_load(:active_record) { include StoreConfigurable::Base }
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'active_support/ordered_options'
|
2
|
+
|
3
|
+
module StoreConfigurable
|
4
|
+
|
5
|
+
# The heart of StoreConfigurable's data store is this subclass of ActiveSupport's OrderedOptions.
|
6
|
+
# They are the heart of Rails' configurations and allow you to dynamically set and get hash keys
|
7
|
+
# and values using dot property notation vs the +[]+ hash accessors.
|
8
|
+
#
|
9
|
+
# However, instances of DirtyTrackingOrderedOptions use a recursive lambda via Hash's block
|
10
|
+
# initialization syntax so that you get a dynamic and endless scope on config data. Instances of
|
11
|
+
# DirtyTrackingOrderedOptions also make sure that every sub instance of it self also has a handle
|
12
|
+
# back to your store's owner. In this way when config attributes are added or values change,
|
13
|
+
# we can mark your ActiveRecord object as dirty/changed.
|
14
|
+
class DirtyTrackingOrderedOptions < ::ActiveSupport::OrderedOptions
|
15
|
+
|
16
|
+
Recursive = lambda { |h,k| h[k] = h.class.new(h.__store_configurable_owner__) }
|
17
|
+
|
18
|
+
attr_accessor :__store_configurable_owner__
|
19
|
+
|
20
|
+
def initialize(owner)
|
21
|
+
@__store_configurable_owner__ = owner
|
22
|
+
super(&Recursive)
|
23
|
+
end
|
24
|
+
|
25
|
+
def []=(key, value)
|
26
|
+
_config_may_change!(key, value)
|
27
|
+
super
|
28
|
+
end
|
29
|
+
|
30
|
+
def delete(key)
|
31
|
+
name = key.to_sym
|
32
|
+
_config_will_change! if has_key?(name)
|
33
|
+
super
|
34
|
+
end
|
35
|
+
|
36
|
+
def delete_if
|
37
|
+
_with_config_keys_may_change! { super }
|
38
|
+
end
|
39
|
+
|
40
|
+
def dup
|
41
|
+
raise NotImplementedError, 'the StoreConfigurable::Object does not support making a copy'
|
42
|
+
end
|
43
|
+
alias_method :reject, :dup
|
44
|
+
|
45
|
+
def merge(other)
|
46
|
+
dup
|
47
|
+
end
|
48
|
+
|
49
|
+
def reject!
|
50
|
+
_with_config_keys_may_change! { super }
|
51
|
+
end
|
52
|
+
|
53
|
+
def clear
|
54
|
+
_config_will_change!
|
55
|
+
super
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
protected
|
60
|
+
|
61
|
+
def _with_config_keys_may_change!
|
62
|
+
starting_keys = keys.dup
|
63
|
+
yield
|
64
|
+
_config_will_change! if starting_keys != keys
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
def _config_may_change!(key, value)
|
69
|
+
name = key.to_sym
|
70
|
+
_config_will_change! unless has_key?(name) && self[name] == value
|
71
|
+
end
|
72
|
+
|
73
|
+
def _config_will_change!
|
74
|
+
__store_configurable_owner__._config_will_change!
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'active_support/basic_object'
|
2
|
+
|
3
|
+
module StoreConfigurable
|
4
|
+
|
5
|
+
# The is the object returned by the +config+ method. It does nothing more than delegate
|
6
|
+
# all calls to a tree of +DirtyTrackingOrderedOptions+ objects which are basically hashes.
|
7
|
+
class Object < ::ActiveSupport::BasicObject
|
8
|
+
|
9
|
+
# Class methods so the +StoreConfigurable::Object+ responds to +dump+ and +load+ which
|
10
|
+
# allows it to conform to ActiveRecord's coder requirement via its serialize method
|
11
|
+
# that we use.
|
12
|
+
#
|
13
|
+
# The +dump+ method serializes the raw data behind the +StoreConfigurable::Object+ proxy
|
14
|
+
# object. This means that we only store pure ruby primitives in the datbase, not our
|
15
|
+
# proxy object's YAML type.
|
16
|
+
#
|
17
|
+
# The +load+ method mimics +ActiveRecord::Coders::YAMLColumn+ internals by retuning a
|
18
|
+
# new object when needed as well as making sure that the YAML we are process if of the
|
19
|
+
# same type. When reconstituting a +StoreConfigurable::Object+ we must set the store's
|
20
|
+
# owner as this does. That way as our recursive lambda loader regenerates the tree of
|
21
|
+
# config data, we always have a handle for each +DirtyTrackingOrderedOptions+ object to
|
22
|
+
# report state changes back to the owner. Finally, after each load we make sure to clear
|
23
|
+
# out changes so reloaded objects are not marked as dirty.
|
24
|
+
module Coding
|
25
|
+
|
26
|
+
def dump(value)
|
27
|
+
YAML.dump value.__config__
|
28
|
+
end
|
29
|
+
|
30
|
+
def load(yaml, owner)
|
31
|
+
return StoreConfigurable::Object.new if yaml.blank?
|
32
|
+
return yaml unless yaml.is_a?(String) && yaml =~ /^---/
|
33
|
+
stored_data = YAML.load(yaml)
|
34
|
+
unless stored_data.is_a?(Hash)
|
35
|
+
raise ActiveRecord::SerializationTypeMismatch,
|
36
|
+
"Attribute was supposed to be a Hash, but was a #{stored_data.class}"
|
37
|
+
end
|
38
|
+
config = StoreConfigurable::Object.new
|
39
|
+
config.__store_configurable_owner__ = owner
|
40
|
+
loader = lambda do |options, key, value|
|
41
|
+
value.is_a?(Hash) ? value.each { |k,v| loader.call(options.send(key), k, v) } :
|
42
|
+
options.send("#{key}=", value)
|
43
|
+
end
|
44
|
+
stored_data.each { |k,v| loader.call(config, k, v) }
|
45
|
+
owner.changed_attributes.delete('_config')
|
46
|
+
config
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
# Instance methods for +StoreConfigurable::Object+ defined and included in a module so
|
52
|
+
# that if you ever wanted to, you could redefine these methods and +super+ up.
|
53
|
+
module Behavior
|
54
|
+
|
55
|
+
attr_accessor :__store_configurable_owner__
|
56
|
+
|
57
|
+
def __config__
|
58
|
+
@__config__ ||= DirtyTrackingOrderedOptions.new(@__store_configurable_owner__)
|
59
|
+
end
|
60
|
+
|
61
|
+
def inspect
|
62
|
+
"#<StoreConfigurable::Object:#{object_id}>"
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def method_missing(method, *args, &block)
|
68
|
+
__config__.__send__ method, *args, &block
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
extend Coding
|
74
|
+
include Behavior
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module StoreConfigurable
|
2
|
+
module Read
|
3
|
+
|
4
|
+
# Our main syntatic interface to the underlying +_config+ store. This method ensures that
|
5
|
+
# +self+, the store's owner, will allways be set in the config object. Hence allowing all
|
6
|
+
# other recursive options to get a handle back to the owner.
|
7
|
+
#
|
8
|
+
# The config object can be treated as a Hash but in actuality is an enhanced subclass of
|
9
|
+
# +ActiveSupport::OrderedOptions+ that does two important things. First, it allows you to
|
10
|
+
# dynamically define any nested namespace property. Second, it hooks back into your parent
|
11
|
+
# object to notify it of change via ActiveRecord's dirty support.
|
12
|
+
#
|
13
|
+
# Example:
|
14
|
+
#
|
15
|
+
# class User < ActiveRecord::Base
|
16
|
+
# store_configurable
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# user = User.find(42)
|
20
|
+
# user.config.remember_me = true
|
21
|
+
# user.config.sortable_tables.products.sort_on = 'created_at'
|
22
|
+
# user.config.sortable_tables.products.direction = 'asc'
|
23
|
+
# user.changed? # => true
|
24
|
+
# user.config_changed? # => true
|
25
|
+
def config
|
26
|
+
_config.__store_configurable_owner__ = self
|
27
|
+
_config
|
28
|
+
end
|
29
|
+
|
30
|
+
# Simple delegation to the underlying data attribute's changed query method.
|
31
|
+
def config_changed?
|
32
|
+
_config_changed?
|
33
|
+
end
|
34
|
+
|
35
|
+
# Simple delegation to the underlying data attribute's change array.
|
36
|
+
def config_change
|
37
|
+
_config_change
|
38
|
+
end
|
39
|
+
|
40
|
+
# An override to ActiveRecord's accessor for the sole purpoes of injecting +Serialization+
|
41
|
+
# behavior so that we can set the context of this owner and ensure we pass that down to
|
42
|
+
# the YAML coder. Doing this on a per instance basis keeps us from trumping all other
|
43
|
+
# +ActiveRecord::AttributeMethods::Serialization::Attribute+ objects.
|
44
|
+
def _config
|
45
|
+
attrib = @attributes['_config']
|
46
|
+
unless attrib.respond_to?(:__store_configurable_owner__)
|
47
|
+
attrib.extend Serialization
|
48
|
+
attrib.__store_configurable_owner__ = self
|
49
|
+
end
|
50
|
+
super
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module StoreConfigurable
|
2
|
+
|
3
|
+
# This module's behavior is injected into +ActiveRecord::AttributeMethods::Serialization::Attribute+
|
4
|
+
# class which is a mini state machine for serialized objects. It allows us to both set the store's
|
5
|
+
# owner as well as overwrite the +unserialize+ method to give the coder both the YAML and owner
|
6
|
+
# context. This is done via the +_config+ attribute reader override.
|
7
|
+
module Serialization
|
8
|
+
|
9
|
+
attr_accessor :__store_configurable_owner__
|
10
|
+
|
11
|
+
def unserialize
|
12
|
+
self.state = :unserialized
|
13
|
+
self.value = coder.load(value, __store_configurable_owner__)
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class StoreConfigurable::BaseTest < StoreConfigurable::TestCase
|
4
|
+
|
5
|
+
it 'is never blank' do
|
6
|
+
new_user.config.wont_be_nil
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'can set and get root attributes' do
|
10
|
+
new_user.config.foo = 'foo'
|
11
|
+
new_user.config.foo.must_equal 'foo'
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'can set and get adhoc nested options' do
|
15
|
+
options = {:this => 'that'}
|
16
|
+
new_user.config.foo.bar.options = options
|
17
|
+
new_user.config.foo.bar.options.must_equal options
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'can serialize to yaml' do
|
21
|
+
user_ken.config.foo = 'bar'
|
22
|
+
user_ken.config.to_yaml.must_include '--- !omap'
|
23
|
+
user_ken.config.to_yaml.must_include ':foo: bar'
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'wont mark owner as dirty after initial read from database with no existing config' do
|
27
|
+
user_ken.config
|
28
|
+
user_ken.wont_be :config_changed?
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'can use uncool hash syntax if you want with varying techniques of strings, symbols and calls' do
|
32
|
+
user_ken.config.color = 'black'
|
33
|
+
user_ken.config['remember_me'] = true
|
34
|
+
user_ken.config['sortable_tables'].direction = 'asc'
|
35
|
+
user_ken.config.sortable_tables['column'] = 'updated_at'
|
36
|
+
user_ken.save!
|
37
|
+
user_ken.reload
|
38
|
+
user_ken.config['color'].must_equal 'black'
|
39
|
+
user_ken.config[:color].must_equal 'black'
|
40
|
+
user_ken.config.remember_me.must_equal true
|
41
|
+
user_ken.config.sortable_tables[:direction].must_equal 'asc'
|
42
|
+
user_ken.config[:sortable_tables][:column].must_equal 'updated_at'
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'must be mark owner as dirty after missing getter since that inits a new namespace' do
|
46
|
+
user_ken.config.bar
|
47
|
+
user_ken.must_be :config_changed?
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'does not support dup, reject, merge' do
|
51
|
+
lambda{ user_ken.config.dup }.must_raise(NotImplementedError)
|
52
|
+
lambda{ user_ken.config.reject{} }.must_raise(NotImplementedError)
|
53
|
+
lambda{ user_ken.config.merge({}) }.must_raise(NotImplementedError)
|
54
|
+
end
|
55
|
+
|
56
|
+
describe 'existing data' do
|
57
|
+
|
58
|
+
let(:color) { '#c1c1c1' }
|
59
|
+
let(:remember) { true }
|
60
|
+
let(:deep_value) { StorableObject.new('test') }
|
61
|
+
let(:plugin_opts) { Hash[:sort,'asc',:on,true] }
|
62
|
+
|
63
|
+
before do
|
64
|
+
user_ken.config.color = color
|
65
|
+
user_ken.config.remember_me = remember
|
66
|
+
user_ken.config.plugin.options = plugin_opts
|
67
|
+
user_ken.config.you.should.never.need.to.do.this = deep_value
|
68
|
+
user_ken.save!
|
69
|
+
@user = User.find(user_ken.id)
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'wont be dirty after loading' do
|
73
|
+
@user.wont_be :config_changed?
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'can reconsitute saved values' do
|
77
|
+
@user.config.color.must_equal color
|
78
|
+
@user.config.remember_me.must_equal remember
|
79
|
+
@user.config.plugin.options.must_equal plugin_opts
|
80
|
+
@user.config.you.should.never.need.to.do.this.must_equal deep_value
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'wont be dirty after reading saved configs' do
|
84
|
+
@user.config.color
|
85
|
+
@user.config.remember_me
|
86
|
+
@user.config.plugin.options
|
87
|
+
@user.config.you.should.never.need.to.do.this
|
88
|
+
@user.wont_be :config_changed?
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'wont be dirty when setting same config values' do
|
92
|
+
@user.config.color = color
|
93
|
+
@user.config.remember_me = remember
|
94
|
+
@user.config.plugin.options = plugin_opts
|
95
|
+
@user.config.you.should.never.need.to.do.this = deep_value
|
96
|
+
@user.wont_be :config_changed?
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'must be marked dirty when values change' do
|
100
|
+
@user.config.color = 'black'
|
101
|
+
@user.must_be :config_changed?
|
102
|
+
@user.save!
|
103
|
+
@user.config.color.must_equal 'black'
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'must be marked dirty when clearing' do
|
107
|
+
@user.config.clear
|
108
|
+
@user.must_be :config_changed?
|
109
|
+
@user.save!
|
110
|
+
@user.config.must_be :blank?
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'must be marked dirty when deleting a key' do
|
114
|
+
@user.config.delete :color
|
115
|
+
@user.must_be :config_changed?
|
116
|
+
@user.save!
|
117
|
+
@user.config.has_key?(:color).must_equal false
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'wont be marked dirty when deleting a non-existent key' do
|
121
|
+
@user.config.delete :doesnotexist
|
122
|
+
@user.wont_be :config_changed?
|
123
|
+
end
|
124
|
+
|
125
|
+
it 'must be marked dirty when using delete_if' do
|
126
|
+
@user.config.delete_if { |k,v| true }
|
127
|
+
@user.must_be :config_changed?
|
128
|
+
@user.config.must_be :blank?
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'wont be marked dirty when using delete_if and nothing happens' do
|
132
|
+
@user.config.delete_if { |k,v| false }
|
133
|
+
@user.wont_be :config_changed?
|
134
|
+
@user.config.you.should.never.need.to.do.this = deep_value
|
135
|
+
end
|
136
|
+
|
137
|
+
it 'must be marked dirty when using reject! on true' do
|
138
|
+
@user.config.reject! { |k,v| true }
|
139
|
+
@user.must_be :config_changed?
|
140
|
+
@user.config.must_be :blank?
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
|
145
|
+
|
146
|
+
end
|
147
|
+
|
data/test/helper.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
require "bundler/setup"
|
4
|
+
Bundler.require
|
5
|
+
require 'store_configurable'
|
6
|
+
require 'active_record/base'
|
7
|
+
require 'minitest/autorun'
|
8
|
+
require 'logger'
|
9
|
+
|
10
|
+
|
11
|
+
ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__),'debug.log'))
|
12
|
+
ActiveRecord::Base.establish_connection :adapter => 'sqlite3', :database => ':memory:'
|
13
|
+
|
14
|
+
|
15
|
+
module StoreConfigurable
|
16
|
+
class TestCase < MiniTest::Spec
|
17
|
+
|
18
|
+
before { setup_environment }
|
19
|
+
|
20
|
+
let(:new_user) { User.new }
|
21
|
+
let(:user_ken) { User.find_by_email('ken@metaskills.net') }
|
22
|
+
|
23
|
+
def setup_environment
|
24
|
+
setup_database
|
25
|
+
setup_data
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
|
30
|
+
def setup_database
|
31
|
+
ActiveRecord::Base.class_eval do
|
32
|
+
silence do
|
33
|
+
connection.create_table :users, :force => true do |t|
|
34
|
+
t.string :name, :email
|
35
|
+
t.text :_config
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def setup_data
|
42
|
+
User.create :name => 'Ken Collins', :email => 'ken@metaskills.net'
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class StorableObject
|
49
|
+
attr_accessor :value
|
50
|
+
def initialize(value) ; @value = value ; end
|
51
|
+
def ==(other) ; value == other.value ; end
|
52
|
+
end
|
53
|
+
|
54
|
+
class User < ActiveRecord::Base
|
55
|
+
store_configurable
|
56
|
+
end
|
metadata
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: store_configurable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 3.2.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Ken Collins
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-03-10 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activerecord
|
16
|
+
requirement: &70239521805020 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 3.2.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70239521805020
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: sqlite3
|
27
|
+
requirement: &70239521804520 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ~>
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '1.3'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70239521804520
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rake
|
38
|
+
requirement: &70239521804060 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 0.9.2
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70239521804060
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: minitest
|
49
|
+
requirement: &70239521803600 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 2.8.1
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70239521803600
|
58
|
+
description: Grown up ActiveRecord::Store config options!
|
59
|
+
email:
|
60
|
+
- ken@metaskills.net
|
61
|
+
executables: []
|
62
|
+
extensions: []
|
63
|
+
extra_rdoc_files: []
|
64
|
+
files:
|
65
|
+
- .gitignore
|
66
|
+
- .travis.yml
|
67
|
+
- CHANGELOG
|
68
|
+
- Gemfile
|
69
|
+
- MIT-LICENSE
|
70
|
+
- README.md
|
71
|
+
- Rakefile
|
72
|
+
- lib/store_configurable.rb
|
73
|
+
- lib/store_configurable/base.rb
|
74
|
+
- lib/store_configurable/dirty_options.rb
|
75
|
+
- lib/store_configurable/object.rb
|
76
|
+
- lib/store_configurable/read.rb
|
77
|
+
- lib/store_configurable/serialization.rb
|
78
|
+
- lib/store_configurable/version.rb
|
79
|
+
- test/cases/base_test.rb
|
80
|
+
- test/helper.rb
|
81
|
+
homepage: http://github.com/Decisiv/store_configurable/
|
82
|
+
licenses: []
|
83
|
+
post_install_message:
|
84
|
+
rdoc_options:
|
85
|
+
- --charset=UTF-8
|
86
|
+
require_paths:
|
87
|
+
- lib
|
88
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
95
|
+
none: false
|
96
|
+
requirements:
|
97
|
+
- - ! '>='
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '0'
|
100
|
+
requirements: []
|
101
|
+
rubyforge_project:
|
102
|
+
rubygems_version: 1.8.17
|
103
|
+
signing_key:
|
104
|
+
specification_version: 3
|
105
|
+
summary: A zero-configuration recursive Hash for storing a tree of options in a serialized
|
106
|
+
ActiveRecord column.
|
107
|
+
test_files:
|
108
|
+
- test/cases/base_test.rb
|
109
|
+
- test/helper.rb
|