easy_app_helper 1.0.14 → 2.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.
@@ -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