chamber 0.0.4 → 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,265 @@
1
+ require 'chamber/namespace_set'
2
+ require 'chamber/file'
3
+ require 'chamber/settings'
4
+
5
+ ###
6
+ # Internal: Represents a set of settings files that should be considered for
7
+ # processing. Whether they actually *are* processed depends on their extension
8
+ # (only *.yml files are processed unless explicitly specified), and whether
9
+ # their namespace matches one of the namespaces passed to the FileSet (text
10
+ # after a dash '-' but before the extension is considered the namespace for the
11
+ # file).
12
+ #
13
+ # When converted to settings, files are always processed in the order of least
14
+ # specific to most specific. So if there are two files:
15
+ #
16
+ # * /tmp/settings.yml
17
+ # * /tmp/settings-blue.yml
18
+ #
19
+ # Then '/tmp/settings.yml' will be processed first and '/tmp/settings-blue.yml'
20
+ # will be processed second (assuming a namespace with the value 'blue' was
21
+ # passed in).
22
+ #
23
+ # If there are multiple namespaces, they will be process in the order that they
24
+ # appear in the passed in hash. So assuming two files:
25
+ #
26
+ # * /tmp/settings-blue.yml
27
+ # * /tmp/settings-green.yml
28
+ #
29
+ # Then:
30
+ #
31
+ # ```ruby
32
+ # FileSet.new files: '/tmp/settings*.yml',
33
+ # namespaces: ['blue', 'green']
34
+ # ```
35
+ #
36
+ # will process in this order:
37
+ #
38
+ # * /tmp/settings-blue.yml
39
+ # * /tmp/settings-green.yml
40
+ #
41
+ # Whereas:
42
+ #
43
+ # ```ruby
44
+ # FileSet.new files: '/tmp/settings*.yml',
45
+ # namespaces: ['green', 'blue']
46
+ # ```
47
+ #
48
+ # will process in this order:
49
+ #
50
+ # * /tmp/settings-green.yml
51
+ # * /tmp/settings-blue.yml
52
+ #
53
+ # Examples:
54
+ #
55
+ # ###
56
+ # # Assuming the following files exist:
57
+ # #
58
+ # # /tmp/settings.yml
59
+ # # /tmp/settings-blue.yml
60
+ # # /tmp/settings-green.yml
61
+ # # /tmp/settings/another.yml
62
+ # # /tmp/settings/another.json
63
+ # # /tmp/settings/yet_another-blue.yml
64
+ # # /tmp/settings/yet_another-green.yml
65
+ # #
66
+ #
67
+ # ###
68
+ # # This will *consider* all files listed but will only process 'settings.yml'
69
+ # # and 'another.yml'
70
+ # #
71
+ # FileSet.new files: ['/tmp/settings.yml',
72
+ # '/tmp/settings']
73
+ #
74
+ # ###
75
+ # # This will all files in the 'settings' directory but will only process
76
+ # # 'another.yml' and 'yet_another-blue.yml'
77
+ # #
78
+ # FileSet.new(files: '/tmp/settings',
79
+ # namespaces: {
80
+ # favorite_color: 'blue' } )
81
+ #
82
+ # ###
83
+ # # Passed in namespaces do not have to be hashes. Hash keys are used only for
84
+ # # human readability.
85
+ # #
86
+ # # This results in the same outcome as the example above.
87
+ # #
88
+ # FileSet.new(files: '/tmp/settings',
89
+ # namespaces: ['blue'])
90
+ #
91
+ # ###
92
+ # # This will process all files listed:
93
+ # #
94
+ # FileSet.new(files: [
95
+ # '/tmp/settings*.yml',
96
+ # '/tmp/settings',
97
+ # ],
98
+ # namespaces: %w{blue green})
99
+ #
100
+ # ###
101
+ # # This is the only way to explicitly specify files which do not end in
102
+ # # a 'yml' extension.
103
+ # #
104
+ # # This is the only example thus far which will process
105
+ # # '/tmp/settings/another.json'
106
+ # #
107
+ # FileSet.new(files: '/tmp/settings/*.json',
108
+ # namespaces: %w{blue green})
109
+ #
110
+ class Chamber
111
+ class FileSet
112
+
113
+ def initialize(options = {})
114
+ self.namespaces = options.fetch(:namespaces, {})
115
+ self.paths = options.fetch(:files)
116
+ end
117
+
118
+ ###
119
+ # Internal: Returns an Array of the ordered list of files that was processed
120
+ # by Chamber in order to get the resulting settings values. This is useful
121
+ # for debugging if a given settings value isn't quite what you anticipated it
122
+ # should be.
123
+ #
124
+ # Returns an Array of file path strings
125
+ #
126
+ def filenames
127
+ @filenames ||= files.map(&:to_s)
128
+ end
129
+
130
+ ###
131
+ # Internal: Converts the FileSet into a Settings object which represents all
132
+ # the settings specified in all of the files in the FileSet.
133
+ #
134
+ # This can be used in one of two ways. You may either specify a block which
135
+ # will be passed each file's settings as they are converted, or you can choose
136
+ # not to pass a block, in which case it will pass back a single completed
137
+ # Settings object to the caller.
138
+ #
139
+ # The reason the block version is used in Chamber.settings is because we want
140
+ # to be able to load each settings file as it's processed so that we can use
141
+ # those already-processed settings in subsequently processed settings files.
142
+ #
143
+ # Examples:
144
+ #
145
+ # ###
146
+ # # Specifying a Block
147
+ # #
148
+ # file_set = FileSet.new files: [ '/path/to/my/settings.yml' ]
149
+ #
150
+ # file_set.to_settings do |settings|
151
+ # # do stuff with each settings
152
+ # end
153
+ #
154
+ #
155
+ # ###
156
+ # # No Block Specified
157
+ # #
158
+ # file_set = FileSet.new files: [ '/path/to/my/settings.yml' ]
159
+ # file_set.to_settings
160
+ #
161
+ # # => <Chamber::Settings>
162
+ #
163
+ def to_settings
164
+ clean_settings = Settings.new(:namespaces => namespaces)
165
+
166
+ files.each_with_object(clean_settings) do |file, settings|
167
+ if block_given?
168
+ yield file.to_settings
169
+ else
170
+ settings.merge!(file.to_settings)
171
+ end
172
+ end
173
+ end
174
+
175
+ protected
176
+
177
+ attr_reader :namespaces,
178
+ :paths
179
+
180
+ ###
181
+ # Internal: Allows the paths for the FileSet to be set. It can either be an
182
+ # object that responds to `#each` like an Array or one that doesn't. In which
183
+ # case it will be considered a single path.
184
+ #
185
+ # All paths will be converted to Pathnames.
186
+ #
187
+ def paths=(raw_paths)
188
+ raw_paths = [raw_paths] unless raw_paths.respond_to? :each
189
+
190
+ @paths = raw_paths.map { |path| Pathname.new(path) }
191
+ end
192
+
193
+ ###
194
+ # Internal: Allows the namespaces for the FileSet to be set. An Array or Hash
195
+ # can be passed; in both cases it will be converted to a NamespaceSet.
196
+ #
197
+ def namespaces=(raw_namespaces)
198
+ @namespaces = NamespaceSet.new(raw_namespaces)
199
+ end
200
+
201
+ ###
202
+ # Internal: The set of files which are considered to be relevant, but with any
203
+ # duplicates removed.
204
+ #
205
+ def files
206
+ @files ||= -> do
207
+ sorted_relevant_files = []
208
+
209
+ file_globs.each do |glob|
210
+ current_glob_files = Pathname.glob(glob)
211
+ relevant_glob_files = relevant_files & current_glob_files
212
+
213
+ relevant_glob_files.map! { |file| File.new( path: file,
214
+ namespaces: namespaces) }
215
+
216
+ sorted_relevant_files += relevant_glob_files
217
+ end
218
+
219
+ sorted_relevant_files.uniq
220
+ end.call
221
+ end
222
+
223
+ private
224
+
225
+ def all_files
226
+ @all_files ||= Pathname.glob(file_globs).sort
227
+ end
228
+
229
+ def non_namespaced_files
230
+ @non_namespaced_files ||= all_files - namespaced_files
231
+ end
232
+
233
+ def relevant_files
234
+ @relevant_files ||= non_namespaced_files + relevant_namespaced_files
235
+ end
236
+
237
+ def file_globs
238
+ @file_globs ||= paths.map do |path|
239
+ if path.directory?
240
+ path + '*.yml'
241
+ else
242
+ path
243
+ end
244
+ end
245
+ end
246
+
247
+ def namespaced_files
248
+ @namespaced_files ||= all_files.select do |file|
249
+ file.fnmatch? '*-*'
250
+ end
251
+ end
252
+
253
+ def relevant_namespaced_files
254
+ file_holder = []
255
+
256
+ namespaces.each do |namespace|
257
+ file_holder << namespaced_files.select do |file|
258
+ file.fnmatch? "*-#{namespace}.???"
259
+ end
260
+ end
261
+
262
+ file_holder.flatten
263
+ end
264
+ end
265
+ end
@@ -0,0 +1,141 @@
1
+ require 'set'
2
+
3
+ ###
4
+ # Internal: Respresents a set of namespaces which will be processed by Chamber
5
+ # at various stages when settings are loaded.
6
+ #
7
+ # The main function that this class provides is the ability to create
8
+ # a NamespaceSet from either an array-like or hash-like object and the ability
9
+ # to allow callables to be passed which will then be executed.
10
+ #
11
+ class Chamber
12
+ class NamespaceSet
13
+ include Enumerable
14
+
15
+ ###
16
+ # Internal: Creates a new NamespaceSet from arrays, hashes and sets.
17
+ #
18
+ def initialize(raw_namespaces = {})
19
+ self.namespaces = raw_namespaces
20
+ end
21
+
22
+ ###
23
+ # Internal: Allows a NamespaceSet to be combined with some other array-like
24
+ # object.
25
+ #
26
+ # It does not mutate the source NamespaceSet but rather creates a new one and
27
+ # returns it.
28
+ #
29
+ # Examples:
30
+ #
31
+ # # Can be an Array
32
+ # namespace_set = NamespaceSet.new ['value_1', 'value_2']
33
+ # namespace_set + ['value_3']
34
+ # # => <NamespaceSet namespaces=['value_1', 'value_2', 'value_3']>
35
+ #
36
+ # # Can be a Set
37
+ # namespace_set = NamespaceSet.new ['value_1', 'value_2']
38
+ # namespace_set + Set['value_3']
39
+ # # => <NamespaceSet namespaces=['value_1', 'value_2', 'value_3']>
40
+ #
41
+ # # Can be a object which is convertable to an Array
42
+ # namespace_set = NamespaceSet.new ['value_1', 'value_2']
43
+ # namespace_set + (1..3)
44
+ # # => <NamespaceSet namespaces=['value_1', 'value_2', '1', '2', '3']>
45
+ #
46
+ # Returns a NamespaceSet
47
+ #
48
+ def +(other)
49
+ NamespaceSet.new namespaces + other.to_a
50
+ end
51
+
52
+ ###
53
+ # Internal: Iterates over each namespace value and allows it to be used in
54
+ # a block.
55
+ #
56
+ def each
57
+ namespaces.each do |namespace|
58
+ yield namespace
59
+ end
60
+ end
61
+
62
+ ###
63
+ # Internal: Converts a NamespaceSet into an Array consisting of the namespace
64
+ # values stored in the set.
65
+ #
66
+ # Returns an Array
67
+ #
68
+ def to_ary
69
+ namespaces.to_a
70
+ end
71
+
72
+ alias_method :to_a, :to_ary
73
+
74
+ ###
75
+ # Internal: Determines whether a NamespaceSet is equal to another array-like
76
+ # object.
77
+ #
78
+ # Returns a Boolean
79
+ #
80
+ def ==(other)
81
+ self.to_ary.eql? other.to_ary
82
+ end
83
+
84
+ ###
85
+ # Internal: Determines whether a NamespaceSet is equal to another
86
+ # NamespaceSet.
87
+ #
88
+ # Returns a Boolean
89
+ #
90
+ def eql?(other)
91
+ other.is_a?( Chamber::NamespaceSet) &&
92
+ self.namespaces == other.namespaces
93
+ end
94
+
95
+ protected
96
+
97
+ def namespaces
98
+ @namespaces ||= Set.new
99
+ end
100
+
101
+ ###
102
+ # Internal: Sets the namespaces for the set from a variety of objects and
103
+ # processes them by checking to see if they can be 'called'.
104
+ #
105
+ # Examples:
106
+ #
107
+ # namespace_set = NamespaceSet.new
108
+ #
109
+ # # Can be set to an array
110
+ # namespace_set.namespaces = %w{namespace_value_1 namespace_value_2}
111
+ # # => ['namespace_value_1', 'namespace_value_2']
112
+ #
113
+ # # Can be set to a hash
114
+ # namespace_set.namespaces = { environment: 'development',
115
+ # hostname: 'my host' }
116
+ # namespace_set.namespaces
117
+ # # => ['development', 'my host']
118
+ #
119
+ # # Can be set to a callable
120
+ # namespace_set.namespaces = { environment: -> { 'called' } }
121
+ # namespace_set.namespaces
122
+ # # => ['called']
123
+ #
124
+ # # Does not allow duplicate items
125
+ # namespace_set.namespaces = %w{namespace_value namespace_value}
126
+ # namespace_set.namespaces
127
+ # # => ['namespace_value']
128
+ #
129
+ def namespaces=(raw_namespaces)
130
+ namespace_values = if raw_namespaces.respond_to? :values
131
+ raw_namespaces.values
132
+ else
133
+ raw_namespaces
134
+ end
135
+
136
+ @namespaces = Set.new namespace_values.map do |value|
137
+ value.respond_to?(:call) ? value.call : value
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,3 @@
1
+ if defined?(::Rails)
2
+ require 'chamber/rails/railtie'
3
+ end
@@ -0,0 +1,11 @@
1
+ class Chamber
2
+ module Rails
3
+ class Railtie < ::Rails::Railtie
4
+ initializer 'chamber.load', before: :load_environment_config do
5
+ Chamber.load( :basepath => ::Rails.root.join('config'),
6
+ :namespaces => {
7
+ :environment => -> { ::Rails.env } })
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,152 @@
1
+ require 'hashie/mash'
2
+ require 'chamber/system_environment'
3
+ require 'chamber/namespace_set'
4
+
5
+ ###
6
+ # Internal: Represents the base settings storage needed for Chamber.
7
+ #
8
+ class Chamber
9
+ class Settings
10
+
11
+ attr_reader :namespaces
12
+
13
+ def initialize(options = {})
14
+ self.namespaces = options.fetch(:namespaces, NamespaceSet.new)
15
+ self.data = options.fetch(:settings, Hashie::Mash.new)
16
+ end
17
+
18
+ ###
19
+ # Internal: Converts a Settings object into a hash that is compatible as an
20
+ # environment variable hash.
21
+ #
22
+ # Example:
23
+ #
24
+ # settings = Settings.new settings: {
25
+ # my_setting: 'my value',
26
+ # my_sub_setting: {
27
+ # my_sub_sub_setting_1: 'my sub value 1',
28
+ # my_sub_sub_setting_2: 'my sub value 2',
29
+ # }
30
+ # settings.to_environment
31
+ # # => {
32
+ # 'MY_SETTING' => 'my value',
33
+ # 'MY_SUB_SETTING_MY_SUB_SUB_SETTING_1' => 'my sub value 1',
34
+ # 'MY_SUB_SETTING_MY_SUB_SUB_SETTING_2' => 'my sub value 2',
35
+ # }
36
+ #
37
+ # Returns a Hash sorted alphabetically by the names of the keys
38
+ #
39
+ def to_environment
40
+ Hash[SystemEnvironment.extract_from(data).sort]
41
+ end
42
+
43
+ ###
44
+ # Internal: Converts a Settings object into a String with a format that will
45
+ # work well when working with the shell.
46
+ #
47
+ # Examples:
48
+ #
49
+ # Settings.new( settings: {
50
+ # my_key: 'my value',
51
+ # my_other_key: 'my other value',
52
+ # } ).to_s
53
+ # # => 'MY_KEY="my value" MY_OTHER_KEY="my other value"'
54
+ #
55
+ def to_s(options = {})
56
+ pair_separator = options[:pair_separator] || ' '
57
+ value_surrounder = options[:value_surrounder] || '"'
58
+ name_value_separator = options[:name_value_separator] || '='
59
+
60
+ pairs = to_environment.to_a.map do |pair|
61
+ %Q{#{pair[0]}#{name_value_separator}#{value_surrounder}#{pair[1]}#{value_surrounder}}
62
+ end
63
+
64
+ pairs.join(pair_separator)
65
+ end
66
+
67
+ ###
68
+ # Internal: Merges a Settings object with another Settings object or
69
+ # a hash-like object.
70
+ #
71
+ # Also, if merging Settings, it will merge the namespaces as well.
72
+ #
73
+ # Example:
74
+ #
75
+ # settings = Settings.new settings: { my_setting: 'my value' }
76
+ # other_settings = Settings.new settings: { my_other_setting: 'my other value' }
77
+ #
78
+ # settings.merge! other_settings
79
+ #
80
+ # settings
81
+ # # => {
82
+ # 'my_setting' => 'my value',
83
+ # 'my_other_setting' => 'my other value',
84
+ # }
85
+ #
86
+ # Returns a Hash
87
+ #
88
+ def merge!(other)
89
+ self.data = data.merge(other.to_hash)
90
+ self.namespaces = (namespaces + other.namespaces) if other.respond_to? :namespaces
91
+ end
92
+
93
+ def eql?(other)
94
+ other.is_a?( Chamber::Settings) &&
95
+ self.data == other.data &&
96
+ self.namespaces == other.namespaces
97
+ end
98
+
99
+ def to_hash
100
+ data
101
+ end
102
+
103
+ def method_missing(name, *args)
104
+ return data.public_send(name, *args) if data.respond_to?(name)
105
+
106
+ super
107
+ end
108
+
109
+ def respond_to_missing?(name, include_private = false)
110
+ data.respond_to?(name, include_private)
111
+ end
112
+
113
+ protected
114
+
115
+ attr_reader :raw_data
116
+ attr_writer :namespaces
117
+
118
+ def data
119
+ @data ||= Hashie::Mash.new
120
+ end
121
+
122
+ def data=(raw_data)
123
+ @raw_data = Hashie::Mash.new(raw_data)
124
+
125
+ namespace_checked_data = if data_is_namespaced?
126
+ namespace_filtered_data
127
+ else
128
+ self.raw_data
129
+ end
130
+
131
+ @data = SystemEnvironment.inject_into(namespace_checked_data)
132
+ end
133
+
134
+ private
135
+
136
+ def data_is_namespaced?
137
+ @data_is_namespaced ||= raw_data.keys.any? { |key| namespaces.include? key }
138
+ end
139
+
140
+ def namespace_filtered_data
141
+ @namespace_filtered_data ||= -> do
142
+ data = Hashie::Mash.new
143
+
144
+ namespaces.each do |namespace|
145
+ data.merge!(raw_data[namespace]) if raw_data[namespace]
146
+ end
147
+
148
+ data
149
+ end.call
150
+ end
151
+ end
152
+ end