configmanager 0.0.1 → 0.0.2

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,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: []