configmanager 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,65 @@
1
+ #
2
+ # 20 Jul 2012
3
+ #
4
+
5
+ require "configmanager/configuration"
6
+ require "configmanager/exceptions"
7
+
8
+ module ConfigManager
9
+
10
+ # A configuration class that can merge multiple configuration trees into a single configuration tree.
11
+ # Each addition, overwrites any properties that already existed.
12
+ class CombinedConfiguration < ConfigManager::Configuration
13
+
14
+ # Creates a new empty combined configuration.
15
+ #
16
+ # @param [Hash] options configuration options. Supported keys are:
17
+ # :interpolator - an object that can respond to the 'interpolate' method. Default is the DefaultInterpolator.
18
+ # :use_interpolator - global flag to specify if the interpolator should be used for this configuration. Default is true.
19
+ def initialize(options = {})
20
+ super
21
+
22
+ @configurations = {}
23
+ end
24
+
25
+ # Adds a configuration to be merged into this configuration. All existing properties are overwritten.
26
+ #
27
+ # @param [String] name a unique name for this configuration, it can be retrieved later with this name.
28
+ # @param [ConfigManager::Configuration] configuration the configuration to be merged.
29
+ #
30
+ # @return [NilClass]
31
+ def add_configuration(name, configuration)
32
+ raise ConfigManager::KeyError.new("Cannot add - Configuration '#{name}' already exists!") if @configurations.has_key?(name)
33
+ @configurations[name] = configuration
34
+ merge_configuration(configuration)
35
+
36
+ nil
37
+ end
38
+
39
+ # Get the configuration identified by the specified name. Raises an exception if the configuration does not exist.
40
+ # Modifying the returned configuration does not update this combined configuration.
41
+ #
42
+ # @param [String] name the configuration to get.
43
+ #
44
+ # @return [ConfigManager::Configuration] the configuration.
45
+ def get_configuration(name)
46
+ raise ConfigManager::PropertyNotFoundError("Cannot get - Configuration '#{name}' does not exist!") unless @configurations.has_key?(name)
47
+ @configurations[name]
48
+ end
49
+
50
+ private
51
+
52
+ # Merges the specified configuration with this configuration. Existing nodes are overwritten!
53
+ #
54
+ # @param [ConfigManager::Configuration] configuration the configuration to merge.
55
+ #
56
+ # @return [NilClass]
57
+ def merge_configuration(configuration)
58
+ values = configuration.get_property("")
59
+ at = ""
60
+ add_properties(values, at, :overwrite => true)
61
+ end
62
+
63
+ end
64
+
65
+ end
@@ -0,0 +1,165 @@
1
+ #
2
+ # 20 Jul 2012
3
+ #
4
+
5
+ require "configmanager/exceptions"
6
+ require "configmanager/interpolators"
7
+
8
+ module ConfigManager
9
+
10
+ # Represents a collection of properties stored as a hash.
11
+ class Configuration
12
+
13
+ # Creates a new empty configuration.
14
+ #
15
+ # @param [Hash] options configuration options. Supported keys are:
16
+ # :interpolator - an object that can respond to the 'interpolate' method. Default is the DefaultInterpolator.
17
+ # :use_interpolator - global flag to specify if the interpolator should be used for this configuration. Default is true.
18
+ def initialize(options = {})
19
+ @root = {}
20
+ @use_interpolator = get_option(options, :use_interpolator, false, true)
21
+ @interpolator = get_option(options, :interpolator, false, ConfigManager::DefaultInterpolator)
22
+ end
23
+
24
+ # Adds a hash of properties to this configuration.
25
+ #
26
+ # @param [Hash] values the values to add to this configuration.
27
+ # @param [String] at the node under which to add these properties. Default is the root node.
28
+ # @param [Hash] options configuration options. Supported key are
29
+ # :overwrite - true to overwrite existing properties, false to raise an exception if a property exists. Default is false.
30
+ #
31
+ # @return [NilClass]
32
+ def add_properties(values, at = "", options = {})
33
+ # Add each item in the hash - Nested hashes call this method recursively.
34
+ at_array = at.split(".")
35
+ values.each_pair do |key, value|
36
+ full_key = (at_array + [key]).join(".")
37
+ value.kind_of?(Hash) ? add_properties(value, full_key, options) : add_property(full_key, value, options)
38
+ end
39
+
40
+ nil
41
+ end
42
+
43
+ # Adds a property to this configuration. The key is a '.' separated string that fully qualifies this property.
44
+ # So the property a.b identifies the property 'b' inside the group 'a'. Similarly a.b.c identifies the property
45
+ # 'c' inside the group 'b' which is inside the group 'a'.
46
+ #
47
+ # @param [String] key the unique key to identify this property.
48
+ # @param [Object] value the value of this property.
49
+ # @param [Hash] options configuration options. Supported keys are:
50
+ # :overwrite - true to overwrite existing properties, false to raise an exception if a property exists. Default is false.
51
+ # :at - an optional node's name under which to add this property. Default is the root node.
52
+ #
53
+ # @return [NilClass]
54
+ def add_property(key, value, options = {})
55
+ # Default options.
56
+ at = get_option(options, :at, false, "")
57
+ overwrite = get_option(options, :overwrite, false, false)
58
+ # Create and add the property.
59
+ key_array = key.split(".")
60
+ at_array = at.split(".")
61
+ leaf = key_array[-1]
62
+ at_array = at_array + key_array[0..-2]
63
+ at_node = get_node(at_array, create = true)
64
+ # Got the node, now add the property, unless it already exists.
65
+ raise ConfigManager::KeyError.new("Cannot add - '#{at_array.join(".")}.#{leaf}' already exists!") if at_node.has_key?(leaf) and not overwrite
66
+ at_node[leaf] = value
67
+
68
+ nil
69
+ end
70
+
71
+ # Get the value of the specified property. The key is a '.' separated string that fully qualifies this property.
72
+ # So the property a.b identifies the property 'b' inside the group 'a'. Similarly a.b.c identifies the property
73
+ # 'c' inside the group 'b' which is inside the group 'a'.
74
+ #
75
+ # If enabled, the property's value is interpolated.
76
+ # Finally the list of post-processors are invoked and the value is returned.
77
+ #
78
+ # @param [String] key the property to get.
79
+ # @param [Hash] options configuration options. Supported keys are:
80
+ # :use_interpolator - flag to indicate if the interpolator should be used. Uses the value specified when creating this configuration by default.
81
+ # :interpolator - the interpolator to use. Uses the value specified when creating this configuration by default.
82
+ # :post_processors - an array of callable procs. Each one is invoked with the output of the previous one. The first one is invoked with the fully interpolated value.
83
+ # :replacements - a hash containing replacement values for references. References are looked up in this before being looked up in this configuration.
84
+ # :tolerate_missing_references: a flag to indicate if missing references should raise an exception.
85
+ #
86
+ # Note : Interpolator implementations may support additional options too.
87
+ #
88
+ # @return [Object] the property's value.
89
+ def get_property(key, options = {})
90
+ use_interpolator = get_option(options, :use_interpolator, false, @use_interpolator)
91
+ interpolator = get_option(options, :interpolator, false, @interpolator)
92
+ post_processors = pop_option(options, :post_processors, false, [])
93
+ # Get the required property's raw value.
94
+ key_array = key.split(".")
95
+ create = false
96
+ raw_value = get_node(key_array, create)
97
+ # Interpolate.
98
+ raw_value = interpolator.interpolate(key, raw_value, self, options) if use_interpolator
99
+ # Post-process.
100
+ final_value = raw_value
101
+ post_processors.each { |processor| final_value = processor.call(final_value) }
102
+ # All done.
103
+ final_value
104
+ end
105
+
106
+ # Check to see if the specified property is defined for this configuration. This method will throw an exception if
107
+ # the key is invalid.
108
+ #
109
+ # @param [String] key the property to check.
110
+ #
111
+ # @return [TrueClass, FalseClass] true if the property exists, false otherwise.
112
+ def has_property?(key)
113
+ key_array = key.split(".")
114
+ create = false
115
+ get_node(key_array, create)
116
+ return true
117
+ rescue ConfigManager::PropertyNotFoundError
118
+ return false
119
+ end
120
+
121
+ private
122
+
123
+ # Gets the node identified by the specified key. If the 'create' flag is true, missing nodes will be created.
124
+ #
125
+ # @param [Array] key_array the array representing the full key.
126
+ # @param [TrueClass, FalseClass] create true to create missing nodes, false otherwise.
127
+ #
128
+ # @return [Hash] the node identified by this key.
129
+ def get_node(key_array, create = false)
130
+ return @root if key_array.empty?
131
+
132
+ key_str = key_array.join(".")
133
+ processed_parts = []
134
+ current_node = @root
135
+ # Walk the property tree. What's left at the end, is the required node.
136
+ key_array.each do |part|
137
+ # If the current node is not a hash, it means the key is invalid.
138
+ unless current_node.kind_of?(Hash)
139
+ raise ConfigManager::KeyError.new("Invalid key '#{key_str}' - Reached leaf at '#{processed_parts.join(".")}'!")
140
+ end
141
+
142
+ # If the current node is a hash, then check if the next part of the key is present.
143
+ # If it present, make that the current node.
144
+ # If it isn't present and the create flag is true, create an empty node and make it the current node.
145
+ # If the create flag is false, raise an exception.
146
+ processed_parts << part
147
+ if current_node.has_key?(part)
148
+ current_node = current_node[part]
149
+ elsif create
150
+ current_node = current_node[part] = {}
151
+ else
152
+ raise ConfigManager::PropertyNotFoundError.new("Key '#{processed_parts.join(".")}' does not exist!")
153
+ end
154
+ end
155
+ # The required node.
156
+ current_node
157
+ end
158
+
159
+ alias_method :[], :get_property
160
+ alias_method :[]=, :add_property
161
+ alias_method :set_property, :add_property
162
+
163
+ end
164
+
165
+ end
@@ -0,0 +1,19 @@
1
+ #
2
+ # 20 Jul 2012
3
+ #
4
+
5
+ module ConfigManager
6
+
7
+ # Raised when a property references itself - either directly or indirectly.
8
+ class CircularReferenceError < Exception
9
+ end
10
+
11
+ # Raised when a requested property's key is invalid.
12
+ class KeyError < Exception
13
+ end
14
+
15
+ # Raised when a requested property does not exist.
16
+ class PropertyNotFoundError < Exception
17
+ end
18
+
19
+ end
@@ -0,0 +1,65 @@
1
+ #
2
+ # 20 Jul 2012
3
+ #
4
+
5
+ require "configmanager/configuration"
6
+ require "configmanager/exceptions"
7
+
8
+ module ConfigManager
9
+
10
+ # The default interpolator that will look for ${} property references.
11
+ # The interpolator will only inspect Strings and Strings in Arrays. All other types are returned as is.
12
+ class DefaultInterpolator
13
+
14
+ REFERENCE_REGEX = /\$\{[\w\.]+\}/
15
+
16
+ # Replaces all ${} references with an actual value. Will evaluate any nested references too.
17
+ # If circular references are detected, an exception is raised.
18
+ #
19
+ # @param [String] key the full (. separated) key against which this value was stored.
20
+ # @param [String, Array] raw_value the raw value to interpolate.
21
+ # @param [ConfigManager::Configuration] context the context under which the value should be evaluated.
22
+ # @param [Hash] options extra configuration options. Supported keys are:
23
+ # :replacements - a hash containing replacement values for references. References are replaced with these value on priority.
24
+ # :tolerate_missing_properties - a flag to indicate if an exception should be raised if referenced properties are missing.
25
+ #
26
+ # @return [String] the fully interpolated value.
27
+ def self.interpolate(key, raw_value, context, options = {})
28
+ # Default options.
29
+ replacements = get_option(options, :replacements, false, {})
30
+ tolerate_missing_references = get_option(options, :tolerate_missing_references, false, false)
31
+ interpolating = set_option(options, :interpolating, {}, false)
32
+ # Mark this key
33
+ interpolating[key] = true
34
+ # Check for type.
35
+ # Array - Recursively call this method.
36
+ # Not a String - Return as is.
37
+ # Otherwise, interpolate!
38
+ return raw_value.map { |item| interpolate(key, item, context, options) } if raw_value.kind_of?(Array)
39
+ return raw_value unless raw_value.kind_of?(String)
40
+ raw_value.gsub(REFERENCE_REGEX) do |reference|
41
+ # If the referenced key is still being interpolated, its a circular reference!
42
+ # Otherwise, substitute it with its replacement value (either from the supplied hash, or the provided context)
43
+ key = reference[2..-2]
44
+ if interpolating[key]
45
+ trace = (interpolating.keys + [key]).join(" -> ")
46
+ raise ConfigManager::CircularReferenceError.new("Cannot interpolate - Circular reference detected. Trace: #{trace}")
47
+ elsif replacements.has_key?(key)
48
+ replacements[key]
49
+ elsif context.has_property?(key)
50
+ context.get_property(key, options)
51
+ elsif tolerate_missing_references
52
+ reference
53
+ else
54
+ trace = (interpolating.keys + [key]).join(" -> ")
55
+ raise ConfigManager::PropertyNotFoundError.new("Cannot interpolate - Referenced key '#{key}' does not exist. Trace: #{trace}")
56
+ end
57
+ end
58
+ ensure
59
+ # Done, so mark as done.
60
+ interpolating.delete(key)
61
+ end
62
+
63
+ end
64
+
65
+ end
@@ -0,0 +1,182 @@
1
+ #
2
+ # 20 Jul 2012
3
+ #
4
+
5
+ module ConfigManager
6
+
7
+ # Loads properties from a YAML file.
8
+ class YAMLPropertiesLoader
9
+
10
+ # Loads properties from the specified YAML file. This is the default loader.
11
+ #
12
+ # @param [String] file_path the file to load.
13
+ # @param [Configuration] configuration the configuration to add the loaded properties to.
14
+ #
15
+ # @return [Hash] the properties read from the file.
16
+ def self.load_properties(file_path, configuration)
17
+ require "yaml"
18
+ file_path = File.expand_path(file_path)
19
+ properties = YAML.load_file(file_path)
20
+ files_to_import = properties.delete("import") { [] }
21
+ files_to_import = [files_to_import] unless files_to_import.kind_of?(Array)
22
+ configuration.add_properties(properties)
23
+ # Load any referenced files too.
24
+ file_dir = File.dirname(file_path)
25
+ files_to_import.each { |file_to_import| load_properties(File.expand_path(file_to_import, file_dir), configuration) }
26
+ end
27
+
28
+ end
29
+
30
+ # Loads properties from a Java style property file. The syntax is slightly different from regular property files.
31
+ # Important differences are :
32
+ # 1. Escape characters are not allowed.
33
+ # 2. Properties can be assigned a type (int, float, regex, string, bool).
34
+ # 3. Properties can be collected as arrays.
35
+ #
36
+ # A property would look like :
37
+ # <key><:optional_type> = <value>
38
+ # For arrays, add a [] to the property :
39
+ # <key><:optional_type>[] = <value separated by ','>
40
+ class JavaPropertiesLoader
41
+
42
+ # Raised if a property file is not valid.
43
+ class SyntaxError < Exception
44
+ end
45
+
46
+ LINE_REGEX = /^([\w\.]+)(:(\w+))?(\[\])?\s*([\s=])\s*(.*)$/
47
+ COMMAND_REGEX = /^@(\w+)\s+(.*)$/
48
+
49
+ TRUE_VALUES = %w(true yes on)
50
+ FALSE_VALUES = %w(false no off)
51
+ TYPE_CONVERTERS = {"bool" => :type_bool, "int" => :type_int, "float" => :type_float, "regex" => :type_regex,
52
+ "string" => :type_string, nil => :type_string}
53
+ COMMANDS = {"import" => :command_import}
54
+
55
+ # Executes the import command.
56
+ #
57
+ # @param [String] file_to_import the file that needs to be imported.
58
+ # @param [String] file_path the file that contained the command.
59
+ # @param [Integer] line_number the line number where the command existed.
60
+ # @param [ConfigManager::Configuration] configuration the configuration to load the properties into.
61
+ #
62
+ # @return [NilClass]
63
+ def self.command_import(file_to_import, file_path, line_number, configuration)
64
+ file_to_import = file_to_import[1..-2]
65
+ file_dir = File.dirname(file_path)
66
+ file_to_import = File.expand_path(file_to_import, file_dir)
67
+ load_properties(file_to_import, configuration)
68
+ nil
69
+ end
70
+
71
+ # Loads properties from the specified Java style property file.
72
+ #
73
+ # @param [String] file_path the file to load properties from.
74
+ # @param [ConfigManager::Configuration] configuration the configuration to load the properties into.
75
+ #
76
+ # @return [NilClass]
77
+ def self.load_properties(file_path, configuration)
78
+ file_path = File.expand_path(file_path)
79
+ lines = File.readlines(file_path)
80
+ lines.each_with_index do |line, line_number|
81
+ line = line.chomp.strip
82
+ # Empty lines, ignore.
83
+ # Comment lines, ignore.
84
+ # Command - Call the command's method.
85
+ next if line.empty?
86
+ next if line.start_with?("#")
87
+ match = line.match(COMMAND_REGEX)
88
+ if match
89
+ command_name = match[1]
90
+ command_arg = match[2]
91
+ raise SyntaxError.new("Unrecognized command '#{command_name}' #(#{file_path}:#{line_number})") unless COMMANDS.has_key?(command_name)
92
+ send(COMMANDS[command_name], command_arg, file_path, line_number, configuration)
93
+ next
94
+ end
95
+ # Other lines, parse as a property.
96
+ # Raises an exception if the line was not valid.
97
+ match = line.match(LINE_REGEX)
98
+ raise SyntaxError.new("Syntax error '#{line}' (#{file_path}:#{line_number})") if match.nil?
99
+ key, type, array, value = match[1], match[3], match[4], match[6]
100
+ # Invalid types raise an exception.
101
+ raise SyntaxError.new("Unrecognized type '#{type}' (#{file_path}:#{line_number})") unless TYPE_CONVERTERS.has_key?(type)
102
+ type_converter = TYPE_CONVERTERS[type]
103
+
104
+ # Split into array if required and apply the type converter.
105
+ if array
106
+ value = value.split(",").map { |item| send(type_converter, item.strip, file_path, line_number) }
107
+ else
108
+ value = send(type_converter, value, file_path, line_number)
109
+ end
110
+
111
+ # Finally, add to configuration.
112
+ configuration.add_property(key, value)
113
+ end
114
+ nil
115
+ end
116
+
117
+ # Converts the specified string value into a boolean (TrueClass, FalseClass).
118
+ #
119
+ # @param [String] value the string value to convert.
120
+ # @param [String] file_path the file that contains the property.
121
+ # @param [Integer] line_number the line that contained this property.
122
+ #
123
+ # @return [TrueClass, FalseClass]
124
+ def self.type_bool(value, file_path, line_number)
125
+ return true if TRUE_VALUES.include?(value)
126
+ return false if FALSE_VALUES.include?(value)
127
+ raise SyntaxError.new("Invalid boolean '#{value}' (#{file_path}:#{line_number})")
128
+ end
129
+
130
+ # Converts the specified string value into float.
131
+ # Invalid values will raise an exception.
132
+ #
133
+ # @param [String] value the string value to convert.
134
+ # @param [String] file_path the file that contains the property.
135
+ # @param [Integer] line_number the line that contained this property.
136
+ #
137
+ # @return [Float]
138
+ def self.type_float(value, file_path, line_number)
139
+ Float(value)
140
+ rescue
141
+ raise SyntaxError.new("Invalid float value '#{value}' (#{file_path}:#{line_number})")
142
+ end
143
+
144
+ # Converts the specified string value into a integer.
145
+ # Invalid values will raise an exception
146
+ #
147
+ # @param [String] value the string value to convert.
148
+ # @param [String] file_path the file that contains the property.
149
+ # @param [Integer] line_number the line that contained this property.
150
+ #
151
+ # @return [Integer]
152
+ def self.type_int(value, file_path, line_number)
153
+ Integer(value)
154
+ rescue
155
+ raise SyntaxError.new("Invalid integer value '#{value}' (#{file_path}:#{line_number})")
156
+ end
157
+
158
+ # Converts the specified string value into a regexp.
159
+ #
160
+ # @param [String] value the string value to convert.
161
+ # @param [String] file_path the file that contains the property.
162
+ # @param [Integer] line_number the line that contained this property.
163
+ #
164
+ # @return [Regexp]
165
+ def self.type_regex(value, file_path, line_number)
166
+ Regexp.new(value)
167
+ end
168
+
169
+ # Returns the value as is.
170
+ #
171
+ # @param [String] value the string value to convert.
172
+ # @param [String] file_path the file that contains the property.
173
+ # @param [Integer] line_number the line that contained this property.
174
+ #
175
+ # @return [String]
176
+ def self.type_string(value, file_path, line_number)
177
+ value
178
+ end
179
+
180
+ end
181
+
182
+ end
@@ -0,0 +1,9 @@
1
+ #
2
+ # 20 Jul 2012
3
+ #
4
+
5
+ module ConfigManager
6
+
7
+ VERSION = "0.0.2"
8
+
9
+ end
@@ -0,0 +1,17 @@
1
+ # Comments are ignored. So are empty lines.
2
+
3
+ # Commands start with '@' character.
4
+ # The import command will import another file into this file. The path is resolved relative to this file.
5
+ @import "test 2.properties"
6
+
7
+ # A property has the following format
8
+ # <key><:optional_type> = <value>
9
+ test.sample_string = Test String
10
+ test.sample_int:int = 1
11
+ test.sample_float:float = 1.0
12
+
13
+ # Arrays can be created by appending [] to a property. Arrays are split using the ',' character.
14
+ test.sample_array[] = first, second, third
15
+
16
+ # Arrays can also be provided with a type - each value is converted to that type.
17
+ test.sample_int_array:int[] = 1, 2, 3
@@ -0,0 +1,14 @@
1
+ # Comments are ignored. So are empty lines.
2
+
3
+ # The import key has special meaning. It will import another file into this file.
4
+ # The path is resolved relative to this file.
5
+ import: 'test 2.yaml'
6
+
7
+ # The format is same as any YAML file.
8
+ test_yaml:
9
+ sample_string: Test String
10
+ sample_int: 1
11
+ sample_float: 1.0
12
+
13
+ sample_array: [first, second, third]
14
+ sample_int_array: [1, 2, 3]
@@ -0,0 +1,9 @@
1
+ # This file imported by test 1.properties
2
+
3
+ # Properties can reference other properties too. References are evaluated only when the
4
+ # property's value is evaluated. The referenced property's type is not used - so this property will
5
+ # evaluate as a String.
6
+ test.sample_reference = ${test.sample_int}
7
+
8
+ # References can be part of an array too.
9
+ test.sample_reference_array[] = ${test.sample_string}, ${test.sample_int}, ${test.sample_float}
@@ -0,0 +1,8 @@
1
+ # This file imported by test 1.yaml
2
+
3
+ # Properties can reference other properties too. References are evaluated only when the
4
+ # property's value is evaluated. The referenced property's type is not used - so this property will
5
+ # evaluate as a String.
6
+ test_yaml:
7
+ sample_reference: ${test_yaml.sample_int}
8
+ sample_reference_array: [${test_yaml.sample_string}, ${test_yaml.sample_int}, ${test_yaml.sample_float}]
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: configmanager
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -20,8 +20,18 @@ executables: []
20
20
  extensions: []
21
21
  extra_rdoc_files: []
22
22
  files:
23
+ - lib/configmanager/combined_configuration.rb
24
+ - lib/configmanager/configuration.rb
25
+ - lib/configmanager/exceptions.rb
26
+ - lib/configmanager/interpolators.rb
27
+ - lib/configmanager/loaders.rb
28
+ - lib/configmanager/version.rb
23
29
  - lib/configmanager.rb
24
30
  - lib/options_arg.rb
31
+ - lib/samples/test 1.properties
32
+ - lib/samples/test 1.yaml
33
+ - lib/samples/test 2.properties
34
+ - lib/samples/test 2.yaml
25
35
  - lib/test_configmanager.rb
26
36
  homepage:
27
37
  licenses: []