iron-settings 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.rspec +1 -0
- data/History.txt +9 -0
- data/LICENSE +20 -0
- data/README.rdoc +133 -0
- data/Version.txt +1 -0
- data/db/settings_migration.rb +23 -0
- data/lib/iron/rake_loader.rb +6 -0
- data/lib/iron/settings.rb +161 -0
- data/lib/iron/settings/builder.rb +72 -0
- data/lib/iron/settings/class_level.rb +122 -0
- data/lib/iron/settings/cursor.rb +170 -0
- data/lib/iron/settings/db_store.rb +57 -0
- data/lib/iron/settings/db_value.rb +33 -0
- data/lib/iron/settings/entry.rb +26 -0
- data/lib/iron/settings/group.rb +102 -0
- data/lib/iron/settings/instance_level.rb +98 -0
- data/lib/iron/settings/node.rb +46 -0
- data/lib/iron/settings/root.rb +15 -0
- data/lib/iron/settings/static_store.rb +85 -0
- data/lib/iron/settings/value_store.rb +88 -0
- data/lib/tasks/settings.rake +65 -0
- data/spec/samples/static-test +4 -0
- data/spec/samples/static-test-2 +1 -0
- data/spec/settings/builder_spec.rb +48 -0
- data/spec/settings/class_level_spec.rb +46 -0
- data/spec/settings/cursor_spec.rb +96 -0
- data/spec/settings/db_store_spec.rb +12 -0
- data/spec/settings/db_value_spec.rb +58 -0
- data/spec/settings/entry_spec.rb +34 -0
- data/spec/settings/group_spec.rb +35 -0
- data/spec/settings/instance_level_spec.rb +44 -0
- data/spec/settings/settings_spec.rb +61 -0
- data/spec/settings/static_store_spec.rb +46 -0
- data/spec/settings/value_store_spec.rb +55 -0
- data/spec/spec_helper.rb +50 -0
- metadata +114 -0
@@ -0,0 +1,170 @@
|
|
1
|
+
class Settings
|
2
|
+
|
3
|
+
# Cursors handle navigating the settings hierarchy built by our Builder class, allowing
|
4
|
+
# getting and setting entry values and inspecting the hierarchy itself.
|
5
|
+
class Cursor < DslBuilder
|
6
|
+
|
7
|
+
# Start up our cursor bound to a given group in the settings hierarchy, with
|
8
|
+
# the value store holding the values for the current context.
|
9
|
+
def initialize(group, values, context = nil)
|
10
|
+
@group = group
|
11
|
+
@values = values
|
12
|
+
@context = context
|
13
|
+
end
|
14
|
+
|
15
|
+
# Provides access to the root of the hierarchy, generally not useful
|
16
|
+
# during operations... :-)
|
17
|
+
def root
|
18
|
+
@group.root
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns all entry keys at the cursor's current position, and optionally
|
22
|
+
# including all child keys. If the cursor is at a sub-group node, keys
|
23
|
+
# will be relative to that node.
|
24
|
+
def entry_keys(include_all = true)
|
25
|
+
keys = @group.entries(include_all).collect {|e| e.key }
|
26
|
+
unless @group.key.blank?
|
27
|
+
keys.collect! {|k| k.gsub(@group.key + '.', '') }
|
28
|
+
end
|
29
|
+
keys
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns all group keys
|
33
|
+
def group_keys(include_all = false)
|
34
|
+
keys = @group.entries(include_all).collect {|e| e.key }
|
35
|
+
unless @group.key.blank?
|
36
|
+
keys.collect! {|k| k.gsub(@group.key + '.', '') }
|
37
|
+
end
|
38
|
+
keys
|
39
|
+
end
|
40
|
+
|
41
|
+
# Finds the item (group or entry) in the hierarchy matching the provided
|
42
|
+
# relative key. Raises a RuntimeError on unknown keys.
|
43
|
+
def find_item(key)
|
44
|
+
item = @group
|
45
|
+
key = key.to_s
|
46
|
+
parts = key.split(/\./)
|
47
|
+
until parts.empty?
|
48
|
+
item_key = parts.shift
|
49
|
+
item = item.find_item(item_key)
|
50
|
+
raise RuntimeError.new("Unknown settings group or entry '#{item_key}' in settings path #{[@group.key,key].list_join('.')}") if item.nil?
|
51
|
+
end
|
52
|
+
item
|
53
|
+
end
|
54
|
+
|
55
|
+
# Return Settings::Entry items for entries at this cursor level and optionally below it
|
56
|
+
def find_entries(include_all = true)
|
57
|
+
@group.entries(include_all)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Array-like access to the entry value at the specified key
|
61
|
+
def [](key, &block)
|
62
|
+
item = find_item(key)
|
63
|
+
if item.group?
|
64
|
+
# Got asked for another group, so create a new cursor and do the right thing(tm)
|
65
|
+
cursor = Settings::Cursor.new(item, @values)
|
66
|
+
DslProxy::exec(cursor, &block) if block
|
67
|
+
cursor
|
68
|
+
else
|
69
|
+
item_value(item)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Array-like setter for entry values using the specified key
|
74
|
+
def []=(key, val)
|
75
|
+
item = find_item(key)
|
76
|
+
if item
|
77
|
+
@values.set_value(item.key, Settings.parse(val, item.type))
|
78
|
+
end
|
79
|
+
val
|
80
|
+
end
|
81
|
+
|
82
|
+
# Look for the next item from our current group pointer,
|
83
|
+
# returning a new cursor if the item is a sub-group, or the value
|
84
|
+
# of the requested entry if the item is a leaf in the
|
85
|
+
# hierarchy tree.
|
86
|
+
def method_missing(method, *args, &block)
|
87
|
+
method = method.to_s
|
88
|
+
query = method.ends_with?('?')
|
89
|
+
assignment = method.ends_with?('=')
|
90
|
+
method.gsub!(/[=\?]+/,'')
|
91
|
+
|
92
|
+
# Look up the item
|
93
|
+
item = @group.find_item(method)
|
94
|
+
if item.nil?
|
95
|
+
# Unknown item name, whoops.
|
96
|
+
raise RuntimeError.new("Unknown settings group or entry '#{method}' for settings path #{@group.key}")
|
97
|
+
|
98
|
+
elsif item.group?
|
99
|
+
if query
|
100
|
+
# Yes, this group exists
|
101
|
+
return true
|
102
|
+
else
|
103
|
+
# Got asked for another group, so create a new cursor and do the right thing(tm)
|
104
|
+
cursor = Settings::Cursor.new(item, @values)
|
105
|
+
DslProxy::exec(cursor, &block) if block
|
106
|
+
return cursor
|
107
|
+
end
|
108
|
+
|
109
|
+
elsif item.entry?
|
110
|
+
if query
|
111
|
+
# Return true if the given item has a (non-default) value
|
112
|
+
return item_has_value?(item)
|
113
|
+
else
|
114
|
+
if args.empty?
|
115
|
+
# No args means return the current value (or default if none)
|
116
|
+
return item_value(item)
|
117
|
+
else
|
118
|
+
# With args, we set the current value of the item (if it parses correctly)
|
119
|
+
val = Settings.parse(args.first, item.type)
|
120
|
+
@values.set_value(item.key, val)
|
121
|
+
return args.first
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Counterpart to #method_missing
|
128
|
+
def respond_to_missing?(method, include_private = false)
|
129
|
+
method = method.to_s.gsub(/[=\?]+/,'')
|
130
|
+
item = @group.find_item(method)
|
131
|
+
return !item.nil?
|
132
|
+
end
|
133
|
+
|
134
|
+
# When true, has non-default value set for the given entry
|
135
|
+
def item_has_value?(item)
|
136
|
+
return false if item.group?
|
137
|
+
@values.has_value?(item.key)
|
138
|
+
end
|
139
|
+
|
140
|
+
# Calculates the value of the given entry item given the current value store
|
141
|
+
# and item default value.
|
142
|
+
def item_value(item)
|
143
|
+
return item_default_value(item) unless item_has_value?(item)
|
144
|
+
val = @values.get_value(item.key)
|
145
|
+
Settings.restore(val, item.type)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Calculates the default value for an entry, handling callable defaults.
|
149
|
+
def item_default_value(item)
|
150
|
+
return nil if item.group? || item.default.nil?
|
151
|
+
if item.default.respond_to?(:call)
|
152
|
+
# Callable default, call in context of a root cursor, yielding our context (generally a
|
153
|
+
# model instance) to the block.
|
154
|
+
val = DslProxy.exec(Cursor.new(root, @values), @context, &(item.default))
|
155
|
+
val = Settings.parse(val, item.type)
|
156
|
+
else
|
157
|
+
val = item.default
|
158
|
+
end
|
159
|
+
Settings.restore(val, item.type)
|
160
|
+
end
|
161
|
+
|
162
|
+
def eval_in_context(text) # :nodoc:
|
163
|
+
proc = Proc.new {}
|
164
|
+
binding = proc.binding
|
165
|
+
eval(text, binding)
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
class Settings # :nodoc:
|
2
|
+
|
3
|
+
# Implements database-backed value storage for dynamic settings. This value store is appropriate for both
|
4
|
+
# class-level and instance-level settings storage. This store takes no special options on creation, however
|
5
|
+
# it is critical that the proper :reload option be set for your use-case.
|
6
|
+
#
|
7
|
+
# For class-level settings, the proper option (and default) will often be to use a
|
8
|
+
# synchronization file by specifying :reload => '<some path>' during
|
9
|
+
# initialization. This will cause the value store to touch the given file on saving changes, and
|
10
|
+
# reload values when the timestamp on the file is newer than the last reload.
|
11
|
+
#
|
12
|
+
# For example, in Rails (with long-running, multi-instance operations), this will allow you to have
|
13
|
+
# your settings loaded once on load, then once per instance when changes are saved to the database.
|
14
|
+
#
|
15
|
+
# For instance-level settings, the proper value is often :reload => false, as the values will
|
16
|
+
# be loaded on initilization of the instance, then thrown away with the instance. This will be
|
17
|
+
# ideal for Models in Rails, for example. If you have long-running instances that need reloads,
|
18
|
+
# you can use a shared timestamp as above, causing *all* instances to reload when any instance
|
19
|
+
# changes (feasible for a small number of instances or when settings do not change frequently), or
|
20
|
+
# you can set a timeout in seconds using :reload => <num seconds> to cause instances to reload
|
21
|
+
# after that time has elapsed.
|
22
|
+
class DBStore < Settings::ValueStore
|
23
|
+
|
24
|
+
def initialize(root, context, options = {})
|
25
|
+
options = {
|
26
|
+
:reload => false
|
27
|
+
}.merge(options)
|
28
|
+
@context = context
|
29
|
+
|
30
|
+
# Let our base class at it
|
31
|
+
super(root, options)
|
32
|
+
end
|
33
|
+
|
34
|
+
def load
|
35
|
+
super
|
36
|
+
|
37
|
+
# Load up the values from DB, store in internal values hash by full key
|
38
|
+
Settings::DBValue.for_context(@context).each do |val|
|
39
|
+
@values[val.full_key] = val.value
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def save
|
44
|
+
# Clear any old settings
|
45
|
+
Settings::DBValue.for_context(@context).delete_all
|
46
|
+
|
47
|
+
# Create new settings
|
48
|
+
@values.each_pair do |key, val|
|
49
|
+
Settings::DBValue.create(context: @context, full_key: key, value: val)
|
50
|
+
end
|
51
|
+
|
52
|
+
super
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# Stores a given setting entry's value in the database
|
2
|
+
class Settings::DBValue < ActiveRecord::Base
|
3
|
+
|
4
|
+
# Use a non-standard table name
|
5
|
+
self.table_name = 'settings_values'
|
6
|
+
|
7
|
+
# Scopes for our values
|
8
|
+
scope :for_context, lambda {|context|
|
9
|
+
if context.is_a?(Module)
|
10
|
+
klass = context
|
11
|
+
id = nil
|
12
|
+
else
|
13
|
+
klass = context.class
|
14
|
+
id = context.id
|
15
|
+
end
|
16
|
+
where(:context_type => klass.to_s, :context_id => id)
|
17
|
+
}
|
18
|
+
|
19
|
+
# We serialize our values...
|
20
|
+
serialize :value
|
21
|
+
|
22
|
+
# Set our context
|
23
|
+
def context=(context)
|
24
|
+
if context.is_a?(ActiveRecord::Base)
|
25
|
+
self.context_type = context.class.name
|
26
|
+
self.context_id = context.id
|
27
|
+
else
|
28
|
+
self.context_type = context.to_s
|
29
|
+
self.context_id = nil
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class Settings
|
2
|
+
|
3
|
+
# Represents a leaf in our structure, has a value
|
4
|
+
class Entry < Settings::Node
|
5
|
+
|
6
|
+
attr_accessor :type, :default
|
7
|
+
|
8
|
+
def initialize(parent, type, name, default)
|
9
|
+
super(parent, name)
|
10
|
+
|
11
|
+
@type = type
|
12
|
+
@default = default.respond_to?(:call) ? default : Settings.parse(default, type)
|
13
|
+
end
|
14
|
+
|
15
|
+
def entry?
|
16
|
+
true
|
17
|
+
end
|
18
|
+
|
19
|
+
def default_value(root_cursor, context = nil)
|
20
|
+
return nil if @default.nil?
|
21
|
+
@default.respond_to?(:call) ? Settings.parse(@default.call(context), @type) : DslProxy.exec(root_cursor, @default, context)
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
class Settings
|
2
|
+
|
3
|
+
# Groups contain a set of items - other groups and entries - that
|
4
|
+
# can be traversed by the Cursor to read out or set values.
|
5
|
+
class Group < Settings::Node
|
6
|
+
|
7
|
+
# Create and set up a new group with the given name
|
8
|
+
# and optional parent
|
9
|
+
def initialize(parent, name = nil)
|
10
|
+
super
|
11
|
+
@nodes = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def nodes
|
15
|
+
@nodes
|
16
|
+
end
|
17
|
+
|
18
|
+
def group?
|
19
|
+
true
|
20
|
+
end
|
21
|
+
|
22
|
+
def [](key)
|
23
|
+
find_item(key)
|
24
|
+
end
|
25
|
+
|
26
|
+
# def _add_node(item)
|
27
|
+
# @nodes[item._key] =
|
28
|
+
|
29
|
+
# Add a group to our list of items, and define
|
30
|
+
# a getter to access it by name
|
31
|
+
def add_group(name)
|
32
|
+
# Add getter for the group
|
33
|
+
instance_eval <<-eos
|
34
|
+
def #{name}
|
35
|
+
find_group('#{name}')
|
36
|
+
end
|
37
|
+
eos
|
38
|
+
|
39
|
+
group = Group.new(self, name)
|
40
|
+
@nodes[name] = group
|
41
|
+
group
|
42
|
+
end
|
43
|
+
|
44
|
+
# Simply access a given group by name
|
45
|
+
def find_group(key)
|
46
|
+
group = @nodes[key.to_s]
|
47
|
+
group.is_a?(Group) ? group : nil
|
48
|
+
end
|
49
|
+
|
50
|
+
def add_entry(name, type, default = nil, &block)
|
51
|
+
default = block unless block.nil?
|
52
|
+
entry = Settings::Entry.new(self, type, name, default)
|
53
|
+
@nodes[name] = entry
|
54
|
+
entry
|
55
|
+
end
|
56
|
+
|
57
|
+
def find_entry(name)
|
58
|
+
entry = @nodes[name.to_s]
|
59
|
+
entry.is_a?(Settings::Entry) ? entry : nil
|
60
|
+
end
|
61
|
+
|
62
|
+
def get_entry_val(name)
|
63
|
+
entry = find_entry(name)
|
64
|
+
return nil unless entry
|
65
|
+
entry.value
|
66
|
+
end
|
67
|
+
|
68
|
+
def set_entry_val(name, value)
|
69
|
+
entry = find_entry(name)
|
70
|
+
return unless entry
|
71
|
+
entry.value = value
|
72
|
+
end
|
73
|
+
|
74
|
+
def find_item(key)
|
75
|
+
@nodes[key.to_s]
|
76
|
+
end
|
77
|
+
|
78
|
+
# Returns all child entries for this group, optionally recursing
|
79
|
+
# to extract sub-groups' entries as well
|
80
|
+
def entries(include_children = true)
|
81
|
+
@nodes.values.collect do |item|
|
82
|
+
if item.entry?
|
83
|
+
item
|
84
|
+
elsif include_children
|
85
|
+
item.entries(include_children)
|
86
|
+
else
|
87
|
+
[]
|
88
|
+
end
|
89
|
+
end.flatten
|
90
|
+
end
|
91
|
+
|
92
|
+
# Returns all groups that are children of this group
|
93
|
+
def groups(include_children = false)
|
94
|
+
list = @nodes.values.select {|i| i.group?}
|
95
|
+
if include_children
|
96
|
+
list += list.collect {|i| i.groups}.flatten
|
97
|
+
end
|
98
|
+
list
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
class Settings
|
2
|
+
|
3
|
+
module InstanceLevel
|
4
|
+
|
5
|
+
# Will be bound to Object, provides support for defining settings structure + defaults
|
6
|
+
# at the class level for use in instances of that class. Defaults to database-backed
|
7
|
+
# dynamic storage with a 10 second reload. Requires that ActiveRecord be required
|
8
|
+
# prior to requiring this gem.
|
9
|
+
module ClassMethods
|
10
|
+
|
11
|
+
def instance_settings(options = {}, &block)
|
12
|
+
unless @settings_instance_root
|
13
|
+
@settings_instance_root = Settings::Root.new()
|
14
|
+
@options = {
|
15
|
+
:store => :dynamic
|
16
|
+
}.merge(options)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Set our default reload timing
|
20
|
+
if options[:reload].nil?
|
21
|
+
if options[:store] == :static
|
22
|
+
# Static settings generally don't need reloading
|
23
|
+
options[:reload] = false
|
24
|
+
else
|
25
|
+
# For dynamic, db-backed settings at the instance level, we use
|
26
|
+
# a 10 second timeout by default
|
27
|
+
options[:reload] = 10
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Save off our options
|
32
|
+
@settings_instance_options = options
|
33
|
+
|
34
|
+
# This class now need settings support at the instance level
|
35
|
+
include InstanceMethods
|
36
|
+
|
37
|
+
# Add our save hook if the class is an ActiveRecord model
|
38
|
+
if defined?(ActiveRecord) && self < ActiveRecord::Base
|
39
|
+
after_save :settings_save!
|
40
|
+
end
|
41
|
+
|
42
|
+
# Construct a builder and do the right thing
|
43
|
+
builder = Settings::Builder.new(@settings_instance_root)
|
44
|
+
builder.define(&block) if block
|
45
|
+
builder
|
46
|
+
end
|
47
|
+
|
48
|
+
def settings_instance_options
|
49
|
+
@settings_instance_options
|
50
|
+
end
|
51
|
+
|
52
|
+
def settings_instance_root
|
53
|
+
@settings_instance_root
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
# Set of methods that all instances with instance_settings set will share
|
59
|
+
module InstanceMethods
|
60
|
+
|
61
|
+
# Access settings at instance level
|
62
|
+
def settings(&block)
|
63
|
+
# Ensure we have a value store
|
64
|
+
unless @settings_values
|
65
|
+
settings_reset!
|
66
|
+
end
|
67
|
+
|
68
|
+
# Set up for use, create a cursor to read/write, and we're good to go
|
69
|
+
@settings_values.reload_if_needed
|
70
|
+
cursor = Settings::Cursor.new(self.class.settings_instance_root, @settings_values, self)
|
71
|
+
DslProxy::exec(cursor, &block) if block
|
72
|
+
cursor
|
73
|
+
end
|
74
|
+
|
75
|
+
def settings_save!
|
76
|
+
@settings_values.save if @settings_values
|
77
|
+
end
|
78
|
+
|
79
|
+
# Throw out any unsaved changes
|
80
|
+
def settings_reset!
|
81
|
+
# Create our value store
|
82
|
+
opts = self.class.settings_instance_options
|
83
|
+
@settings_values = opts[:store] == :static ?
|
84
|
+
Settings::StaticStore.new(self.class.settings_instance_root, opts) :
|
85
|
+
Settings::DBStore.new(self.class.settings_instance_root, self, opts)
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
# Install hooks
|
91
|
+
def self.included(base)
|
92
|
+
# Add our class methods
|
93
|
+
base.extend(ClassMethods)
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|