easy_app_helper 1.0.14 → 2.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,228 +0,0 @@
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
- INTRODUCED_SORTED_LAYERS = [:modified, :command_line]
17
-
18
- attr_reader :script_filename, :app_name, :app_version, :app_description, :internal_configs, :logger
19
-
20
- def initialize(logger)
21
- @app_name = @app_version = @app_description = ""
22
- @script_filename = File.basename $0, '.*'
23
- @internal_configs = {modified: {content: {}, source: CHANGED_BY_CODE}}
24
- @logger = logger
25
- @slop_definition = Slop.new
26
- build_command_line_options
27
- end
28
-
29
-
30
- # @return [String] The formatted command line help
31
- def help
32
- @slop_definition.to_s
33
- end
34
-
35
- # sets the filename while maintaining the slop definition upto date
36
- # @param [String] filename
37
- def script_filename=(filename)
38
- @script_filename = filename
39
- @slop_definition.banner = build_banner
40
- end
41
- # sets the application name used for logging while maintaining the slop definition upto date
42
- # @param [String] fname
43
- def app_name=(name)
44
- @app_name = name
45
- @slop_definition.banner = build_banner
46
- end
47
- # sets the version while maintaining the slop definition upto date
48
- # @param [String] version
49
- def app_version=(version)
50
- @app_version = version
51
- @slop_definition.banner = build_banner
52
- end
53
- # sets the filename while maintaining the slop definition upto date
54
- # @param [String] description
55
- def app_description=(description)
56
- @app_description = description
57
- @slop_definition.banner = build_banner
58
- end
59
-
60
- # helper to add in one command any of the four base properties used
61
- # by the logger and the config objects.
62
- # @param [String] app_name
63
- # @param [String] script_filename
64
- # @param [String] app_version
65
- # @param [String] app_description
66
- def describes_application(options = {})
67
- app_name = options.fetch(:app_name, nil)
68
- script_filename = options.fetch(:script_filename, nil)
69
- app_version = options.fetch(:app_version, nil)
70
- app_description = options.fetch(:app_description, nil)
71
- self.app_name = app_name unless app_name.nil?
72
- self.app_version = app_version unless app_version.nil?
73
- self.app_description = app_description unless app_description.nil?
74
- self.script_filename = script_filename unless script_filename.nil?
75
- end
76
-
77
- # @return [Hash] This hash built from slop definition correspond to the :command_line layer of internal_configs
78
- def command_line_config
79
- @slop_definition.parse
80
- @slop_definition.to_hash
81
- end
82
-
83
- # Yields a slop definition to modify the command line parameters
84
- # @param [String] title used to insert a slop separator
85
- def add_command_line_section(title='Script specific')
86
- raise "Incorrect usage" unless block_given?
87
- @slop_definition.separator build_separator(title)
88
- yield @slop_definition
89
- ensure
90
- sync!
91
- end
92
-
93
- # Sets the :command_line layer of internal_configs to the computed {#command_line_config}
94
- def load_config
95
- sync!
96
- self
97
- end
98
-
99
- # Convenient method to set a value in a particular layer
100
- # If the layer does not exist it is correctly created and filled in with the key/value couple
101
- def set_value key, value, layer = nil
102
- if layer.nil?
103
- self[key] = value
104
- return
105
- end
106
- unless layers.include? layer
107
- internal_configs[layer] = {content: {}, source: 'Unknown source'}
108
- logger.warn "Trying to modify a non existing config layer: \"#{layer.to_s}\". Automatically creating it..."
109
- end
110
- internal_configs[layer][:content][key] = value
111
- end
112
-
113
- def get_value key, layer = nil
114
- if layer.nil?
115
- return self[key]
116
- end
117
- res = nil
118
- begin
119
- res = internal_configs[layer][:content][key]
120
- rescue => e
121
- logger.warn "Trying to reading from a non existing config layer: \"#{layer}\". Returning nil for the key \"#{key}\"..."
122
- end
123
- res
124
- end
125
-
126
-
127
-
128
- # Any modification done to the config is in fact stored in the :modified layer of internal_configs
129
- # @param [String] key
130
- # @param [String] value
131
- def []=(key,value)
132
- internal_configs[:modified][:content][key] = value unless check_hardcoded_properties key, value
133
- end
134
-
135
- # Reset the :modified layer of internal_configs rolling back any change done to the config
136
- def reset
137
- internal_configs[:modified] = {content: {}, source: CHANGED_BY_CODE}
138
- self
139
- end
140
-
141
-
142
- # @return [Array] List of layers
143
- def layers
144
- res = self.class.layers
145
- internal_configs.keys.each do |layer|
146
- next if res.include? layer
147
- res << layer
148
- end
149
- res
150
- end
151
-
152
- def self.layers
153
- res = []
154
- self.ancestors.each do |klass|
155
- next unless klass.is_a? Class
156
- break if EasyAppHelper::Core::Base < klass
157
- res << klass::INTRODUCED_SORTED_LAYERS.reverse
158
- end
159
- res.flatten.reverse
160
- end
161
-
162
-
163
- def find_layer(key)
164
- layers.each do |layer|
165
- return layer if internal_configs[layer][:content][key]
166
- end
167
- nil
168
- end
169
-
170
-
171
- # Executes code (block given) unless :simulate is in the config.
172
- # If :simulate specified then display message instead of executing the code (block).
173
- def safely_exec(message, *args)
174
- raise "No block given" unless block_given?
175
- if self[:simulate]
176
- logger.puts_and_logs "SIMULATING: #{message}" unless message.nil?
177
- else
178
- logger.puts_and_logs message
179
- yield(*args)
180
- end
181
- end
182
-
183
-
184
- private
185
-
186
- def sync!
187
- internal_configs[:command_line] = {content: command_line_config, source: 'Command line'}
188
- end
189
-
190
-
191
- # Performs actions related the very specific config parameters
192
- # @param [String] key The parameter to check
193
- # @param [Object] value The value it expects to be set to
194
- # @param [Symbol] layer Optional layer, default is :modified
195
- # @return [Boolean] Whether or not the internal state has been changed
196
- def check_hardcoded_properties(key, value, layer = :modified)
197
- processed = false
198
- case key
199
- when :'log-level'
200
- logger.send :level=, value, false
201
- when :'config-file'
202
- set_value key, value, layer
203
- force_reload
204
- processed = true
205
- end
206
- processed
207
- end
208
-
209
- # Builds a nice separator
210
- def build_separator(title, width = 80, filler = '-')
211
- "#{filler * 2} #{title} ".ljust width, filler
212
- end
213
-
214
- # Builds common used command line options
215
- def build_command_line_options
216
- add_command_line_section('Generic options') do |slop|
217
- slop.on :auto, 'Auto mode. Bypasses questions to user.', :argument => false
218
- slop.on :simulate, 'Do not perform the actual underlying actions.', :argument => false
219
- slop.on :v, :verbose, 'Enable verbose mode.', :argument => false
220
- slop.on :h, :help, 'Displays this help.', :argument => false
221
- end
222
- end
223
-
224
- def build_banner
225
- "\nUsage: #{script_filename} [options]\n#{app_name} Version: #{app_version}\n\n#{app_description}"
226
- end
227
-
228
- end
@@ -1,220 +0,0 @@
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
-
46
- ADMIN_CONFIG_FILENAME = EasyAppHelper.name
47
- INTRODUCED_SORTED_LAYERS = [:specific_file, :user, :global, :internal, :system]
48
-
49
- # Potential extensions a config file can have
50
- CONFIG_FILE_POSSIBLE_EXTENSIONS = %w(conf yml cfg yaml CFG YML YAML Yaml)
51
-
52
- # @param [EasyAppHelper::Core::Logger] logger
53
- # The logger passed to this constructor should be a temporary logger until the full config is loaded.
54
- def initialize(logger)
55
- super
56
- add_cmd_line_options
57
- load_config
58
- end
59
-
60
- # After calling the super method, triggers a forced reload of the file based config.
61
- # @param [String] name of the config file
62
- # @see Base#script_filename=
63
- def script_filename=(name)
64
- super
65
- force_reload
66
- end
67
-
68
- # Sets the Application name and passes it to the logger.
69
- # @param [String] name
70
- # @see Base#app_name=
71
- def app_name=(name)
72
- super
73
- logger.progname = name
74
- end
75
-
76
- # Loads all config (command line and config files)
77
- # Do not reload a file if already loaded unless forced too.
78
- # It *does not flush the "modified" layer*. Use {#reset} instead
79
- # @param [Boolean] force to force the reload
80
- def load_config(force=false)
81
- super()
82
- load_layer_config :system, ADMIN_CONFIG_FILENAME, force
83
- load_layer_config :internal, script_filename, force
84
- load_layer_config :global, script_filename, force
85
- load_layer_config :user, script_filename, force
86
- load_layer_config :specific_file, internal_configs[:command_line][:content][:'config-file'], force
87
- self
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
-
103
- merged_config = {}
104
-
105
- # Process any other level as a low priority unmanaged layer
106
- internal_configs.keys.each do |layer|
107
- next if self.class.layers.include? layer
108
- hashes_second_level_merge merged_config, internal_configs[layer][:content]
109
- end
110
-
111
- # Process Config-level layers
112
- merged_config = [:system, :internal, :global, :user].inject(merged_config) do |temp_config, config_level|
113
- hashes_second_level_merge temp_config, internal_configs[config_level][:content]
114
- end
115
- if get_value :'config-file', :command_line
116
- if get_value :'config-override', :command_line
117
- override_merge merged_config, internal_configs[:specific_file][:content]
118
- else
119
- hashes_second_level_merge merged_config, internal_configs[:specific_file][:content]
120
- end
121
-
122
- end
123
-
124
- # Process Base-level layers with highest priority (last processed the highest)
125
- [:command_line, :modified].each { |base_layer| hashes_second_level_merge merged_config, internal_configs[base_layer][:content]}
126
- merged_config
127
-
128
- end
129
-
130
- # @param [Object] key: The key to access the data in the merged_config hash (see {#to_hash})
131
- # @return [String] Value for this key in the merged config.
132
- def [](key = nil)
133
- key.nil? ? to_hash : to_hash[key]
134
- end
135
-
136
-
137
- # @return [String] The merged config (see {#to_hash}) rendered as Yaml
138
- def to_yaml
139
- to_hash.to_yaml
140
- end
141
-
142
- alias_method :to_s, :to_yaml
143
- alias_method :inspect, :internal_configs
144
-
145
- #############################################################################
146
- private
147
-
148
- # Command line options specific to config manipulation
149
- def add_cmd_line_options
150
- add_command_line_section('Configuration options') do |slop|
151
- slop.on 'config-file', 'Specify a config file.', :argument => true
152
- slop.on 'config-override', 'If specified override all other config.', :argument => false
153
- end
154
- end
155
-
156
- # Tries to find a config file to be loaded into the config layer cake unless cached.
157
- def load_layer_config(layer, filename_or_pattern, force=false)
158
- unless_cached(layer, filename_or_pattern, force) do |layer, filename_or_pattern|
159
- fetch_config_layer layer, filename_or_pattern
160
- end
161
- end
162
-
163
- # Actual loads
164
- def fetch_config_layer(layer, filename_or_pattern)
165
- if filename_or_pattern.nil?
166
- internal_configs[layer] = {content: {}}
167
- filename = nil
168
- else
169
- if File.exists? filename_or_pattern and !File.directory? filename_or_pattern
170
- filename = filename_or_pattern
171
- else
172
- places = Places.possible_config_places[layer]
173
- filename = find_file places, filename_or_pattern
174
- end
175
- internal_configs[layer] = {content: load_config_file(filename, layer), source: filename, origin: filename_or_pattern}
176
- end
177
- ensure
178
- logger.info "No config file found for layer #{layer}." if filename.nil?
179
- end
180
-
181
- def unless_cached(layer, filename_or_pattern, forced)
182
- cached = false
183
- if internal_configs[layer]
184
- cached = true unless internal_configs[layer][:origin] == filename_or_pattern
185
- end
186
- if forced or not cached
187
- yield layer, filename_or_pattern
188
- end
189
- end
190
-
191
- # Tries to find config files according to places (array) given and possible extensions
192
- def find_file(places, filename)
193
- return nil if places.nil? or filename.nil? or filename.empty?
194
- places.each do |dir|
195
- CONFIG_FILE_POSSIBLE_EXTENSIONS.each do |ext|
196
- filename_with_path = dir + '/' + filename + '.' + ext
197
- if File.exists? filename_with_path and !File.directory? filename_with_path
198
- return filename_with_path
199
- else
200
- logger.debug "Trying \"#{filename_with_path}\" as config file."
201
- end
202
- end
203
- end
204
- nil
205
- end
206
-
207
- def load_config_file(conf_filename, layer=nil)
208
- conf = {}
209
- return conf if conf_filename.nil?
210
- ext = layer.nil? ? '' : " as layer #{layer}"
211
- begin
212
- logger.debug "Loading config file \"#{conf_filename}\"#{ext}"
213
- conf = Hash[YAML::load(open(conf_filename)).map { |k, v| [k.to_sym, v] }]
214
- rescue => e
215
- logger.error "Invalid config file \"#{conf_filename}\"#{ext}. Skipped as not respecting YAML syntax!\n#{e.message}"
216
- end
217
- conf
218
- end
219
-
220
- end
@@ -1,111 +0,0 @@
1
- ################################################################################
2
- # EasyAppHelper
3
- #
4
- # Copyright (c) 2013 L.Briais under MIT license
5
- # http://opensource.org/licenses/MIT
6
- ################################################################################
7
-
8
- require 'logger'
9
- require 'singleton'
10
-
11
- # Official Ruby Logger re-opened to introduce a method to hand-over the temporary history from a temporary logger
12
- # to the definitive one.
13
- # TODO: Ensure only the messages that are above the current level are displayed when handing over to the definitive logger.
14
- class Logger
15
- def hand_over_to(log)
16
- history = []
17
- history = @logdev.dev.history if @logdev.dev.respond_to? :history
18
- @logdev.close
19
- @logdev = LogDevice.new log
20
- history.each do |msg|
21
- @logdev.write msg if ENV['DEBUG_EASY_MODULES'] or (msg =~ /^[WE]/)
22
- end
23
- end
24
- end
25
-
26
- # This is the logger that will be used by the application and any class that include {EasyAppHelper} module. It is
27
- # configured by the {EasyAppHelper::Core::Config Config} object, and provides a temporary logger until the config
28
- # is fully loaded.
29
- class EasyAppHelper::Core::Logger < Logger
30
- include Singleton
31
-
32
-
33
- def initialize
34
- @config = {}
35
- super(TempLogger.new)
36
- self.level = Severity::DEBUG
37
- debug "Temporary initialisation logger created..."
38
- end
39
-
40
- # Change the log level while keeping the config in sync.
41
- def level=(level, update_config = true)
42
- super(level)
43
- @config[:'log-level'] = level if update_config
44
- end
45
-
46
- # Displays the message according to application verbosity and logs it as info.
47
- def puts_and_logs(msg)
48
- puts msg if @config[:verbose]
49
- info(msg)
50
- end
51
-
52
- # Reset the logger regarding the config provided
53
- def set_app_config(config)
54
- @config = config
55
- add_cmd_line_options
56
- @config.load_config
57
- debug "Config layers:\n#{@config.internal_configs.to_yaml}"
58
- debug "Merged config:\n#{@config.to_yaml}"
59
- if config[:debug]
60
- if config[:'log-file']
61
- hand_over_to config[:'log-file']
62
- elsif config[:"debug-on-err"]
63
- hand_over_to STDERR
64
- else
65
- hand_over_to STDOUT
66
- end
67
- else
68
- close
69
- end
70
- self.level = config[:'log-level'] ? config[:'log-level'] : Severity::WARN
71
- self
72
- end
73
-
74
- private
75
-
76
-
77
- def add_cmd_line_options
78
- @config.add_command_line_section('Debug and logging options') do |slop|
79
- slop.on :debug, 'Run in debug mode.', :argument => false
80
- slop.on 'debug-on-err', 'Run in debug mode with output to stderr.', :argument => false
81
- slop.on 'log-level', "Log level from 0 to 5, default #{Severity::WARN}.", :argument => true, :as => Integer
82
- slop.on 'log-file', 'File to log to.', :argument => true
83
- end
84
- end
85
-
86
- # This class will act as a temporary logger, actually just keeping the history until the real
87
- # configuration for the logger is known. Then the history is displayed or not regarding the
88
- # definitive logger configuration.
89
- class TempLogger
90
- attr_reader :history
91
-
92
- def initialize
93
- @history = []
94
- end
95
-
96
- def write(data)
97
- return if closed?
98
- @history << data if @history
99
- end
100
-
101
- def close
102
- @closed = true
103
- end
104
-
105
- def opened?() not @closed ; end
106
- def closed?() @closed ; end
107
- end
108
-
109
- end
110
-
111
-
@@ -1,40 +0,0 @@
1
- ################################################################################
2
- # EasyAppHelper
3
- #
4
- # Copyright (c) 2013 L.Briais under MIT license
5
- # http://opensource.org/licenses/MIT
6
- ################################################################################
7
-
8
- # This module proposes different merge policies for two hashes.
9
- module EasyAppHelper::Core::HashesMergePolicies
10
-
11
- # Performs a merge at the second level of hashes.
12
- # simple entries and arrays are overridden.
13
- def hashes_second_level_merge(h1, h2)
14
- return [] if h1.nil? && h2.nil?
15
- return h1 if h2.nil?
16
- return h2 if h1.nil?
17
- h2.each do |key, v|
18
- if h1[key] and h1[key].is_a?(Hash)
19
- # Merges hashes
20
- h1[key].merge! h2[key]
21
- else
22
- # Overrides the rest
23
- h1[key] = h2[key] unless h2[key].nil?
24
- end
25
- end
26
- h1
27
- end
28
-
29
- # Uses the standard "merge!" method
30
- def simple_merge(h1, h2)
31
- h1.merge! h2
32
- end
33
-
34
- # Brutal override
35
- def override_merge(h1, h2)
36
- h1 = nil
37
- h1 = h2
38
-
39
- end
40
- end
@@ -1,87 +0,0 @@
1
- ################################################################################
2
- # EasyAppHelper
3
- #
4
- # Copyright (c) 2013 L.Briais under MIT license
5
- # http://opensource.org/licenses/MIT
6
- ################################################################################
7
-
8
- # The goal of this class is to return a module containing the POSSIBLE_PLACES hash
9
- # that provides a list of OS dependant paths.
10
- # The only method that should be used is the #get_os_module method that returns this module.
11
- # TODO: Add equivalent for Mac
12
-
13
- module EasyAppHelper
14
- module Core
15
- class Config
16
- module Places
17
-
18
- OS_FLAVOURS = {
19
- mingw32: :windows,
20
- linux: :unix
21
- }
22
- DEFAULT_OS_FLAVOUR = :unix
23
-
24
- FLAVOUR_PLACES = {
25
- unix: {
26
- internal: [],
27
-
28
- system: ['/etc'],
29
-
30
- # Where could be stored global wide configuration
31
- global: %w(/etc /usr/local/etc),
32
-
33
- # Where could be stored user configuration
34
- user: ["#{ENV['HOME']}/.config"]
35
- },
36
- windows: {
37
- internal: [],
38
-
39
- system: ["#{ENV['systemRoot']}/Config"],
40
-
41
- # Where could be stored global configuration
42
- global: ["#{ENV['systemRoot']}/Config",
43
- "#{ENV['ALLUSERSPROFILE']}/Application Data"],
44
-
45
- # Where could be stored user configuration
46
- user: [ENV['APPDATA']]
47
- }
48
- }
49
-
50
-
51
- def self.os_flavour
52
- flavour = OS_FLAVOURS[RbConfig::CONFIG['target_os'].to_sym]
53
- flavour.nil? ? DEFAULT_OS_FLAVOUR : flavour
54
- end
55
-
56
- def self.gem_root_path(file=__FILE__)
57
- file=__FILE__ if file.nil?
58
- searcher = if Gem::Specification.respond_to? :find
59
- # ruby 2.0
60
- Gem::Specification
61
- elsif Gem.respond_to? :searcher
62
- # ruby 1.8/1.9
63
- Gem.searcher.init_gemspecs
64
- end
65
- spec = unless searcher.nil?
66
- searcher.find do |spec|
67
- File.fnmatch(File.join(spec.full_gem_path,'*'), file)
68
- end
69
- end
70
-
71
- spec.gem_dir
72
- end
73
-
74
-
75
- def self.possible_config_places(file_of_gem=nil)
76
- root = gem_root_path file_of_gem
77
- places = FLAVOUR_PLACES[os_flavour].dup
78
- places[:internal] = %w(etc config).map do |place|
79
- File.join root, place
80
- end
81
- places
82
- end
83
-
84
- end
85
- end
86
- end
87
- end