easy_app_helper 1.0.3 → 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -1 +1,8 @@
1
- require "bundler/gem_tasks"
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new
5
+
6
+ task :default => :spec
7
+ task :test => :spec
8
+
@@ -1,26 +1,27 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'easy_app_helper/version'
5
-
6
- Gem::Specification.new do |spec|
7
- spec.name = "easy_app_helper"
8
- spec.version = EasyAppHelper::EASY_APP_HELPER_VERSION
9
- spec.authors = ["L.Briais"]
10
- spec.email = ["lbnetid+rb@gmail.com"]
11
- spec.description = %q{Easy Application Helpers framework}
12
- spec.summary = %q{Provides cool helpers to your application, including configuration and logging features}
13
- spec.homepage = "https://github.com/lbriais/easy_app_helper"
14
- spec.license = "MIT"
15
-
16
- spec.files = `git ls-files`.split($/)
17
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
- spec.require_paths = ["lib"]
20
-
21
- spec.add_development_dependency "bundler"
22
- spec.add_development_dependency "rake"
23
- spec.add_development_dependency "pry"
24
-
25
- spec.add_runtime_dependency "slop"
26
- end
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'easy_app_helper/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "easy_app_helper"
8
+ spec.version = EasyAppHelper::EASY_APP_HELPER_VERSION
9
+ spec.authors = ["L.Briais"]
10
+ spec.email = ["lbnetid+rb@gmail.com"]
11
+ spec.description = %q{Easy Application Helpers framework}
12
+ spec.summary = %q{Provides cool helpers to your application, including configuration and logging features}
13
+ spec.homepage = "https://github.com/lbriais/easy_app_helper"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|rspec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "pry"
24
+ spec.add_development_dependency "rspec"
25
+
26
+ spec.add_runtime_dependency "slop"
27
+ end
@@ -1,20 +1,20 @@
1
- ################################################################################
2
- # EasyAppHelper
3
- #
4
- # Copyright (c) 2013 L.Briais under MIT license
5
- # http://opensource.org/licenses/MIT
6
- ################################################################################
7
-
8
- require 'easy_app_helper/version'
9
-
10
- # When this module is included in any class, it mixes in automatically
11
- # EasyAppHelper::ModuleManager methods both into the
12
- # instance and the class of the instance that includes it.
13
- # Thus to have access to the helper methods, the only requirement is to include
14
- # this module...
15
- module EasyAppHelper
16
- require 'easy_app_helper/module_manager'
17
- include ModuleManager
18
- end
19
-
20
-
1
+ ################################################################################
2
+ # EasyAppHelper
3
+ #
4
+ # Copyright (c) 2013 L.Briais under MIT license
5
+ # http://opensource.org/licenses/MIT
6
+ ################################################################################
7
+
8
+ require 'easy_app_helper/version'
9
+
10
+ # When this module is included in any class, it mixes in automatically
11
+ # EasyAppHelper::ModuleManager methods both into the
12
+ # instance and the class of the instance that includes it.
13
+ # Thus to have access to the helper methods, the only requirement is to include
14
+ # this module...
15
+ module EasyAppHelper
16
+ require 'easy_app_helper/module_manager'
17
+ include ModuleManager
18
+ end
19
+
20
+
@@ -1,142 +1,169 @@
1
- ################################################################################
2
- # EasyAppHelper
3
- #
4
- # Copyright (c) 2013 L.Briais under MIT license
5
- # http://opensource.org/licenses/MIT
6
- ################################################################################
7
-
8
- require 'slop'
9
-
10
- # This class is the base class for the {EasyAppHelper::Core::Config config} object.
11
- # It handles the internal_configs hash that actually contains all configurations read from
12
- # various sources: command line, config files etc...
13
-
14
- class EasyAppHelper::Core::Base
15
- CHANGED_BY_CODE = 'Changed by code'
16
-
17
- attr_reader :script_filename, :app_name, :app_version, :app_description, :internal_configs, :logger
18
-
19
- def initialize(logger)
20
- @app_name = @app_version = @app_description = ""
21
- @script_filename = File.basename $0, '.*'
22
- @internal_configs = {modified: {content: {}, source: CHANGED_BY_CODE}}
23
- @logger = logger
24
- @slop_definition = Slop.new
25
- build_command_line_options
26
- end
27
-
28
-
29
- # @return [String] The formatted command line help
30
- def help
31
- @slop_definition.to_s
32
- end
33
-
34
- # sets the filename while maintaining the slop definition upto date
35
- # @param [String] filename
36
- def script_filename=(filename)
37
- @script_filename = filename
38
- @slop_definition.banner = build_banner
39
- end
40
- # sets the application name used for logging while maintaining the slop definition upto date
41
- # @param [String] fname
42
- def app_name=(name)
43
- @app_name = name
44
- @slop_definition.banner = build_banner
45
- end
46
- # sets the version while maintaining the slop definition upto date
47
- # @param [String] version
48
- def app_version=(version)
49
- @app_version = version
50
- @slop_definition.banner = build_banner
51
- end
52
- # sets the filename while maintaining the slop definition upto date
53
- # @param [String] description
54
- def app_description=(description)
55
- @app_description = description
56
- @slop_definition.banner = build_banner
57
- end
58
-
59
- # helper to add in one command any of the four base properties used
60
- # by the logger and the config objects.
61
- # @param [String] app_name
62
- # @param [String] script_filename
63
- # @param [String] app_version
64
- # @param [String] app_description
65
- def describes_application(app_name: nil, script_filename: nil, app_version: nil, app_description: nil)
66
- self.app_name = app_name unless app_name.nil?
67
- self.app_version = app_version unless app_version.nil?
68
- self.app_description = app_description unless app_description.nil?
69
- self.script_filename = script_filename unless script_filename.nil?
70
- end
71
-
72
- # @return [Hash] This hash built from slop definition correspond to the :command_line layer of internal_configs
73
- def command_line_config
74
- @slop_definition.parse
75
- @slop_definition.to_hash
76
- end
77
-
78
- # Yields a slop definition to modify the command line parameters
79
- # @param [String] title used to insert a slop separator
80
- def add_command_line_section(title='Script specific')
81
- raise "Incorrect usage" unless block_given?
82
- @slop_definition.separator build_separator(title)
83
- yield @slop_definition
84
- end
85
-
86
- # Sets the :command_line layer of internal_configs to the computed {#command_line_config}
87
- def load_config
88
- internal_configs[:command_line] = {content: command_line_config, source: 'Command line'}
89
- end
90
-
91
- # Any modification done to the config is in fact stored in the :modified layer of internal_configs
92
- # @param [String] key
93
- # @param [String] value
94
- def []=(key,value)
95
- internal_configs[:modified][:content][key] = value
96
- end
97
-
98
- # Reset the :modified layer of internal_configs rolling back any change done to the config
99
- def reset
100
- internal_configs[:modified] = {content: {}, source: CHANGED_BY_CODE}
101
- end
102
-
103
-
104
- # @return [Array] List of layers
105
- def layers
106
- internal_configs.keys
107
- end
108
-
109
- # Executes code (block given) unless :simulate is in the config.
110
- # If :simulate specified then display message instead of executing the code (block).
111
- def safely_exec(message, *args)
112
- raise "No block given" unless block_given?
113
- if self[:simulate]
114
- logger.puts_and_logs "SIMULATING: #{message}" unless message.nil?
115
- else
116
- logger.puts_and_logs message
117
- yield(*args)
118
- end
119
- end
120
-
121
-
122
- private
123
-
124
- def build_separator(title)
125
- "-- #{title} ".ljust 80, '-'
126
- end
127
-
128
- # Builds common used command line options
129
- def build_command_line_options
130
- add_command_line_section('Generic options') do |slop|
131
- slop.on :auto, 'Auto mode. Bypasses questions to user.', :argument => false
132
- slop.on :simulate, 'Do not perform the actual underlying actions.', :argument => false
133
- slop.on :v, :verbose, 'Enable verbose mode.', :argument => false
134
- slop.on :h, :help, 'Displays this help.', :argument => false
135
- end
136
- end
137
-
138
- def build_banner
139
- "\nUsage: #{script_filename} [options]\n#{app_name} Version: #{app_version}\n\n#{app_description}"
140
- end
141
-
1
+ ################################################################################
2
+ # EasyAppHelper
3
+ #
4
+ # Copyright (c) 2013 L.Briais under MIT license
5
+ # http://opensource.org/licenses/MIT
6
+ ################################################################################
7
+
8
+ require 'slop'
9
+
10
+ # This class is the base class for the {EasyAppHelper::Core::Config config} object.
11
+ # It handles the internal_configs hash that actually contains all configurations read from
12
+ # various sources: command line, config files etc...
13
+
14
+ class EasyAppHelper::Core::Base
15
+ CHANGED_BY_CODE = 'Changed by code'
16
+
17
+ attr_reader :script_filename, :app_name, :app_version, :app_description, :internal_configs, :logger
18
+
19
+ def initialize(logger)
20
+ @app_name = @app_version = @app_description = ""
21
+ @script_filename = File.basename $0, '.*'
22
+ @internal_configs = {modified: {content: {}, source: CHANGED_BY_CODE}}
23
+ @logger = logger
24
+ @slop_definition = Slop.new
25
+ build_command_line_options
26
+ end
27
+
28
+
29
+ # @return [String] The formatted command line help
30
+ def help
31
+ @slop_definition.to_s
32
+ end
33
+
34
+ # sets the filename while maintaining the slop definition upto date
35
+ # @param [String] filename
36
+ def script_filename=(filename)
37
+ @script_filename = filename
38
+ @slop_definition.banner = build_banner
39
+ end
40
+ # sets the application name used for logging while maintaining the slop definition upto date
41
+ # @param [String] fname
42
+ def app_name=(name)
43
+ @app_name = name
44
+ @slop_definition.banner = build_banner
45
+ end
46
+ # sets the version while maintaining the slop definition upto date
47
+ # @param [String] version
48
+ def app_version=(version)
49
+ @app_version = version
50
+ @slop_definition.banner = build_banner
51
+ end
52
+ # sets the filename while maintaining the slop definition upto date
53
+ # @param [String] description
54
+ def app_description=(description)
55
+ @app_description = description
56
+ @slop_definition.banner = build_banner
57
+ end
58
+
59
+ # helper to add in one command any of the four base properties used
60
+ # by the logger and the config objects.
61
+ # @param [String] app_name
62
+ # @param [String] script_filename
63
+ # @param [String] app_version
64
+ # @param [String] app_description
65
+ def describes_application(app_name: nil, script_filename: nil, app_version: nil, app_description: nil)
66
+ self.app_name = app_name unless app_name.nil?
67
+ self.app_version = app_version unless app_version.nil?
68
+ self.app_description = app_description unless app_description.nil?
69
+ self.script_filename = script_filename unless script_filename.nil?
70
+ end
71
+
72
+ # @return [Hash] This hash built from slop definition correspond to the :command_line layer of internal_configs
73
+ def command_line_config
74
+ @slop_definition.parse
75
+ @slop_definition.to_hash
76
+ end
77
+
78
+ # Yields a slop definition to modify the command line parameters
79
+ # @param [String] title used to insert a slop separator
80
+ def add_command_line_section(title='Script specific')
81
+ raise "Incorrect usage" unless block_given?
82
+ @slop_definition.separator build_separator(title)
83
+ yield @slop_definition
84
+ end
85
+
86
+ # Sets the :command_line layer of internal_configs to the computed {#command_line_config}
87
+ def load_config
88
+ internal_configs[:command_line] = {content: command_line_config, source: 'Command line'}
89
+ end
90
+
91
+ # Any modification done to the config is in fact stored in the :modified layer of internal_configs
92
+ # @param [String] key
93
+ # @param [String] value
94
+ def []=(key,value)
95
+ internal_configs[:modified][:content][key] = value unless check_hardcoded_properties key, value
96
+ end
97
+
98
+ # Reset the :modified layer of internal_configs rolling back any change done to the config
99
+ def reset
100
+ internal_configs[:modified] = {content: {}, source: CHANGED_BY_CODE}
101
+ end
102
+
103
+
104
+ # @return [Array] List of layers
105
+ def layers
106
+ internal_configs.keys
107
+ end
108
+
109
+ def find_layer(key)
110
+ [:modified, :command_line].each do |layer|
111
+ return layer if internal_configs[layer][:content][key]
112
+ end
113
+ nil
114
+ end
115
+
116
+
117
+ # Executes code (block given) unless :simulate is in the config.
118
+ # If :simulate specified then display message instead of executing the code (block).
119
+ def safely_exec(message, *args)
120
+ raise "No block given" unless block_given?
121
+ if self[:simulate]
122
+ logger.puts_and_logs "SIMULATING: #{message}" unless message.nil?
123
+ else
124
+ logger.puts_and_logs message
125
+ yield(*args)
126
+ end
127
+ end
128
+
129
+
130
+ private
131
+
132
+ # Performs actions related the very specific config parameters
133
+ # @param [String] key The parameter to check
134
+ # @param [Object] value The value it expects to be set to
135
+ # @param [Symbol] layer Optional layer, default is :modified
136
+ # @return [Boolean] Whether or not the internal state has been changed
137
+ def check_hardcoded_properties(key, value, layer = :modified)
138
+ processed = false
139
+ case key
140
+ when :'log-level'
141
+ logger.send :level=, value, false
142
+ when :'config-file'
143
+ internal_configs[layer][:content][key] = value
144
+ force_reload
145
+ processed = true
146
+ end
147
+ processed
148
+ end
149
+
150
+ # Builds a nice separator
151
+ def build_separator(title, width = 80, filler = '-')
152
+ "#{filler * 2} #{title} ".ljust width, filler
153
+ end
154
+
155
+ # Builds common used command line options
156
+ def build_command_line_options
157
+ add_command_line_section('Generic options') do |slop|
158
+ slop.on :auto, 'Auto mode. Bypasses questions to user.', :argument => false
159
+ slop.on :simulate, 'Do not perform the actual underlying actions.', :argument => false
160
+ slop.on :v, :verbose, 'Enable verbose mode.', :argument => false
161
+ slop.on :h, :help, 'Displays this help.', :argument => false
162
+ end
163
+ end
164
+
165
+ def build_banner
166
+ "\nUsage: #{script_filename} [options]\n#{app_name} Version: #{app_version}\n\n#{app_description}"
167
+ end
168
+
142
169
  end
@@ -1,203 +1,213 @@
1
- ################################################################################
2
- # EasyAppHelper
3
- #
4
- # Copyright (c) 2013 L.Briais under MIT license
5
- # http://opensource.org/licenses/MIT
6
- ################################################################################
7
-
8
- require 'yaml'
9
- # This is the class that will handle the configuration.
10
- # Configuration is read from different sources:
11
- # - config files (system, global, user, specified on the command line)
12
- # - command line options
13
- # - any extra config you provide programmatically
14
- #
15
- # == Config files:
16
- # system, global, and user config files are searched in the file system according to
17
- # complex rules. First the place where to search them depends on the OS
18
- # (Provided by {EasyAppHelper::Core::Config::Places}), and then multiple file extensions are
19
- # tested ({EasyAppHelper::Core::Config::CONFIG_FILE_POSSIBLE_EXTENSIONS}). This is basically
20
- # performed by the private method {#find_file}. The config specified on command line (if any)
21
- # is loaded the same way.
22
- #
23
- # == Command line:
24
- # Any option can be declared as being callable from the command line. Modules add already some
25
- # command line options, but the application can obviously add its own (see
26
- # {EasyAppHelper::Core::Base#add_command_line_section}).
27
- #
28
- # Each of the config sources are kept in a separated "layer" and addressed this way using the
29
- # #internal_configs attribute reader. But of course the config object provides a "merged" config
30
- # result of the computation of all the sources. See the {#to_hash} method to see the order for the
31
- # merge.
32
- # Any option can be accessed or modified directly using the {#[]} and {#[]=} methods.
33
- # Any change to the global config should be done using the {#[]=} method and is kept in the last separated
34
- # layer called "modified". Therefore the config can be easily reset using the {#reset}
35
- # method.
36
- class EasyAppHelper::Core::Config < EasyAppHelper::Core::Base
37
- end
38
-
39
- require 'easy_app_helper/core/places'
40
- require 'easy_app_helper/core/merge_policies'
41
-
42
-
43
- class EasyAppHelper::Core::Config < EasyAppHelper::Core::Base
44
- include EasyAppHelper::Core::HashesMergePolicies
45
- include EasyAppHelper::Core::Config::Places.get_OS_module
46
-
47
- ADMIN_CONFIG_FILENAME = EasyAppHelper.name
48
-
49
-
50
- # Potential extensions a config file can have
51
- CONFIG_FILE_POSSIBLE_EXTENSIONS = %w(conf yml cfg yaml CFG YML YAML Yaml)
52
- ADMIN_CONFIG_FILENAME
53
-
54
- # @param [EasyAppHelper::Core::Logger] logger
55
- # The logger passed to this constructor should be a temporary logger until the full config is loaded.
56
- def initialize(logger)
57
- super
58
- add_cmd_line_options
59
- load_config
60
- end
61
-
62
- # After calling the super method, triggers a forced reload of the file based config.
63
- # @param [String] name of the config file
64
- # @see Base#script_filename=
65
- def script_filename=(name)
66
- super
67
- force_reload
68
- end
69
-
70
- # Sets the Application name and passes it to the logger.
71
- # @param [String] name
72
- # @see Base#app_name=
73
- def app_name=(name)
74
- super
75
- logger.progname = name
76
- end
77
-
78
- # Loads all config (command line and config files)
79
- # Do not reload a file if already loaded unless forced too.
80
- # It *does not flush the "modified" layer*. Use {#reset} instead
81
- # @param [Boolean] force to force the reload
82
- def load_config(force=false)
83
- super()
84
- load_layer_config :system, ADMIN_CONFIG_FILENAME, force
85
- load_layer_config :global, script_filename, force
86
- load_layer_config :user, script_filename, force
87
- load_layer_config :specific_file, internal_configs[:command_line][:content][:'config-file'], force
88
- end
89
-
90
- # @see #load_config
91
- def force_reload
92
- load_config true
93
- end
94
-
95
-
96
- # This is the main method that provides the config as a hash.
97
- #
98
- # Every layer is kept untouched (and could accessed independently
99
- # using {#internal_configs}), while this methods provides a merged config.
100
- # @return [Hash] The hash of the merged config.
101
- def to_hash
102
- merged_config = [:system, :global, :user].inject({}) do |temp_config, config_level|
103
- hashes_second_level_merge temp_config, internal_configs[config_level][:content]
104
- end
105
- if command_line_config[:'config-file']
106
- if command_line_config[:'config-override']
107
- override_merge merged_config, internal_configs[:specific_file][:content]
108
- else
109
- hashes_second_level_merge merged_config, internal_configs[:specific_file][:content]
110
- end
111
-
112
- end
113
- hashes_second_level_merge merged_config, command_line_config
114
- hashes_second_level_merge merged_config, internal_configs[:modified][:content]
115
- end
116
-
117
- # @param [Object] key: The key to access the data in the merged_config hash (see {#to_hash})
118
- # @return [String] Value for this key in the merged config.
119
- def [](key)
120
- self.to_hash[key]
121
- end
122
-
123
-
124
- # @return [String] The merged config (see {#to_hash}) rendered as Yaml
125
- def to_yaml
126
- to_hash.to_yaml
127
- end
128
-
129
- #############################################################################
130
- private
131
-
132
- # Command line options specific to config manipulation
133
- def add_cmd_line_options
134
- add_command_line_section('Configuration options') do |slop|
135
- slop.on 'config-file', 'Specify a config file.', :argument => true
136
- slop.on 'config-override', 'If specified override all other config.', :argument => false
137
- end
138
- end
139
-
140
- # Tries to find a config file to be loaded into the config layer cake unless cached.
141
- def load_layer_config(layer, filename_or_pattern, force=false)
142
- unless_cached(layer, filename_or_pattern, force) do |layer, filename_or_pattern|
143
- fetch_config_layer layer, filename_or_pattern
144
- end
145
- end
146
-
147
- # Actual loads
148
- def fetch_config_layer(layer, filename_or_pattern)
149
- if filename_or_pattern.nil?
150
- internal_configs[layer] = {content: {}}
151
- filename = nil
152
- else
153
- if File.exists? filename_or_pattern and !File.directory? filename_or_pattern
154
- filename = filename_or_pattern
155
- else
156
- filename = find_file POSSIBLE_PLACES[layer], filename_or_pattern
157
- end
158
- internal_configs[layer] = {content: load_config_file(filename), source: filename, origin: filename_or_pattern}
159
- end
160
- ensure
161
- logger.info "No config file found for layer #{layer}." if filename.nil?
162
- end
163
-
164
- def unless_cached(layer, filename_or_pattern, forced)
165
- cached = false
166
- if internal_configs[layer]
167
- cached = true unless internal_configs[layer][:origin] == filename_or_pattern
168
- end
169
- if forced or not cached
170
- yield layer, filename_or_pattern
171
- end
172
- end
173
-
174
- # Tries to find config files according to places (array) given and possible extensions
175
- def find_file(places, filename)
176
- return nil if places.nil? or filename.nil? or filename.empty?
177
- places.each do |dir|
178
- CONFIG_FILE_POSSIBLE_EXTENSIONS.each do |ext|
179
- filename_with_path = dir + '/' + filename + '.' + ext
180
- if File.exists? filename_with_path and !File.directory? filename_with_path
181
- return filename_with_path
182
- else
183
- logger.debug "Trying \"#{filename_with_path}\" as config file."
184
- end
185
- end
186
- end
187
- nil
188
- end
189
-
190
- def load_config_file(conf_filename)
191
- conf = {}
192
- return conf if conf_filename.nil?
193
-
194
- begin
195
- logger.debug "Loading config file \"#{conf_filename}\""
196
- conf = Hash[YAML::load(open(conf_filename)).map { |k, v| [k.to_sym, v] }]
197
- rescue => e
198
- logger.error "Invalid config file \"#{conf_filename}\". Skipped as not respecting YAML syntax!\n#{e.message}"
199
- end
200
- conf
201
- end
202
-
203
- end
1
+ ################################################################################
2
+ # EasyAppHelper
3
+ #
4
+ # Copyright (c) 2013 L.Briais under MIT license
5
+ # http://opensource.org/licenses/MIT
6
+ ################################################################################
7
+
8
+ require 'yaml'
9
+ # This is the class that will handle the configuration.
10
+ # Configuration is read from different sources:
11
+ # - config files (system, global, user, specified on the command line)
12
+ # - command line options
13
+ # - any extra config you provide programmatically
14
+ #
15
+ # == Config files:
16
+ # system, global, and user config files are searched in the file system according to
17
+ # complex rules. First the place where to search them depends on the OS
18
+ # (Provided by {EasyAppHelper::Core::Config::Places}), and then multiple file extensions are
19
+ # tested ({EasyAppHelper::Core::Config::CONFIG_FILE_POSSIBLE_EXTENSIONS}). This is basically
20
+ # performed by the private method {#find_file}. The config specified on command line (if any)
21
+ # is loaded the same way.
22
+ #
23
+ # == Command line:
24
+ # Any option can be declared as being callable from the command line. Modules add already some
25
+ # command line options, but the application can obviously add its own (see
26
+ # {EasyAppHelper::Core::Base#add_command_line_section}).
27
+ #
28
+ # Each of the config sources are kept in a separated "layer" and addressed this way using the
29
+ # #internal_configs attribute reader. But of course the config object provides a "merged" config
30
+ # result of the computation of all the sources. See the {#to_hash} method to see the order for the
31
+ # merge.
32
+ # Any option can be accessed or modified directly using the {#[]} and {#[]=} methods.
33
+ # Any change to the global config should be done using the {#[]=} method and is kept in the last separated
34
+ # layer called "modified". Therefore the config can be easily reset using the {#reset}
35
+ # method.
36
+ class EasyAppHelper::Core::Config < EasyAppHelper::Core::Base
37
+ end
38
+
39
+ require 'easy_app_helper/core/places'
40
+ require 'easy_app_helper/core/merge_policies'
41
+
42
+
43
+ class EasyAppHelper::Core::Config
44
+ include EasyAppHelper::Core::HashesMergePolicies
45
+ include EasyAppHelper::Core::Config::Places.get_OS_module
46
+
47
+ ADMIN_CONFIG_FILENAME = EasyAppHelper.name
48
+
49
+
50
+ # Potential extensions a config file can have
51
+ CONFIG_FILE_POSSIBLE_EXTENSIONS = %w(conf yml cfg yaml CFG YML YAML Yaml)
52
+ ADMIN_CONFIG_FILENAME
53
+
54
+ # @param [EasyAppHelper::Core::Logger] logger
55
+ # The logger passed to this constructor should be a temporary logger until the full config is loaded.
56
+ def initialize(logger)
57
+ super
58
+ add_cmd_line_options
59
+ load_config
60
+ end
61
+
62
+ # After calling the super method, triggers a forced reload of the file based config.
63
+ # @param [String] name of the config file
64
+ # @see Base#script_filename=
65
+ def script_filename=(name)
66
+ super
67
+ force_reload
68
+ end
69
+
70
+ # Sets the Application name and passes it to the logger.
71
+ # @param [String] name
72
+ # @see Base#app_name=
73
+ def app_name=(name)
74
+ super
75
+ logger.progname = name
76
+ end
77
+
78
+ # Loads all config (command line and config files)
79
+ # Do not reload a file if already loaded unless forced too.
80
+ # It *does not flush the "modified" layer*. Use {#reset} instead
81
+ # @param [Boolean] force to force the reload
82
+ def load_config(force=false)
83
+ super()
84
+ load_layer_config :system, ADMIN_CONFIG_FILENAME, force
85
+ load_layer_config :global, script_filename, force
86
+ load_layer_config :user, script_filename, force
87
+ load_layer_config :specific_file, internal_configs[:command_line][:content][:'config-file'], force
88
+ end
89
+
90
+ # @see #load_config
91
+ def force_reload
92
+ load_config true
93
+ end
94
+
95
+
96
+ # This is the main method that provides the config as a hash.
97
+ #
98
+ # Every layer is kept untouched (and could accessed independently
99
+ # using {#internal_configs}), while this methods provides a merged config.
100
+ # @return [Hash] The hash of the merged config.
101
+ def to_hash
102
+ merged_config = [:system, :global, :user].inject({}) do |temp_config, config_level|
103
+ hashes_second_level_merge temp_config, internal_configs[config_level][:content]
104
+ end
105
+ if internal_configs[:command_line][:content][:'config-file']
106
+ if internal_configs[:command_line][:content][:'config-override']
107
+ override_merge merged_config, internal_configs[:specific_file][:content]
108
+ else
109
+ hashes_second_level_merge merged_config, internal_configs[:specific_file][:content]
110
+ end
111
+
112
+ end
113
+ hashes_second_level_merge merged_config, internal_configs[:command_line][:content]
114
+ hashes_second_level_merge merged_config, internal_configs[:modified][:content]
115
+ end
116
+
117
+ # @param [Object] key: The key to access the data in the merged_config hash (see {#to_hash})
118
+ # @return [String] Value for this key in the merged config.
119
+ def [](key = nil)
120
+ key.nil? ? self.to_hash : self.to_hash[key]
121
+ end
122
+
123
+
124
+ # @return [String] The merged config (see {#to_hash}) rendered as Yaml
125
+ def to_yaml
126
+ to_hash.to_yaml
127
+ end
128
+
129
+ def find_layer(key)
130
+ layer = super
131
+ return layer unless layer.nil?
132
+ [:specific_file, :user, :global, :system].each do |layer|
133
+ return layer if internal_configs[layer][:content][key]
134
+ end
135
+ nil
136
+ end
137
+
138
+
139
+ #############################################################################
140
+ private
141
+
142
+ # Command line options specific to config manipulation
143
+ def add_cmd_line_options
144
+ add_command_line_section('Configuration options') do |slop|
145
+ slop.on 'config-file', 'Specify a config file.', :argument => true
146
+ slop.on 'config-override', 'If specified override all other config.', :argument => false
147
+ end
148
+ end
149
+
150
+ # Tries to find a config file to be loaded into the config layer cake unless cached.
151
+ def load_layer_config(layer, filename_or_pattern, force=false)
152
+ unless_cached(layer, filename_or_pattern, force) do |layer, filename_or_pattern|
153
+ fetch_config_layer layer, filename_or_pattern
154
+ end
155
+ end
156
+
157
+ # Actual loads
158
+ def fetch_config_layer(layer, filename_or_pattern)
159
+ if filename_or_pattern.nil?
160
+ internal_configs[layer] = {content: {}}
161
+ filename = nil
162
+ else
163
+ if File.exists? filename_or_pattern and !File.directory? filename_or_pattern
164
+ filename = filename_or_pattern
165
+ else
166
+ filename = find_file POSSIBLE_PLACES[layer], filename_or_pattern
167
+ end
168
+ internal_configs[layer] = {content: load_config_file(filename), source: filename, origin: filename_or_pattern}
169
+ end
170
+ ensure
171
+ logger.info "No config file found for layer #{layer}." if filename.nil?
172
+ end
173
+
174
+ def unless_cached(layer, filename_or_pattern, forced)
175
+ cached = false
176
+ if internal_configs[layer]
177
+ cached = true unless internal_configs[layer][:origin] == filename_or_pattern
178
+ end
179
+ if forced or not cached
180
+ yield layer, filename_or_pattern
181
+ end
182
+ end
183
+
184
+ # Tries to find config files according to places (array) given and possible extensions
185
+ def find_file(places, filename)
186
+ return nil if places.nil? or filename.nil? or filename.empty?
187
+ places.each do |dir|
188
+ CONFIG_FILE_POSSIBLE_EXTENSIONS.each do |ext|
189
+ filename_with_path = dir + '/' + filename + '.' + ext
190
+ if File.exists? filename_with_path and !File.directory? filename_with_path
191
+ return filename_with_path
192
+ else
193
+ logger.debug "Trying \"#{filename_with_path}\" as config file."
194
+ end
195
+ end
196
+ end
197
+ nil
198
+ end
199
+
200
+ def load_config_file(conf_filename)
201
+ conf = {}
202
+ return conf if conf_filename.nil?
203
+
204
+ begin
205
+ logger.debug "Loading config file \"#{conf_filename}\""
206
+ conf = Hash[YAML::load(open(conf_filename)).map { |k, v| [k.to_sym, v] }]
207
+ rescue => e
208
+ logger.error "Invalid config file \"#{conf_filename}\". Skipped as not respecting YAML syntax!\n#{e.message}"
209
+ end
210
+ conf
211
+ end
212
+
213
+ end