iron-settings 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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