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.
@@ -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