easy_app_helper 0.0.9 → 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,123 @@
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
+ def describes_application(app_name: nil, script_filename: nil, app_version: nil, app_description: nil)
60
+ self.app_name = app_name unless app_name.nil?
61
+ self.app_version = app_version unless app_version.nil?
62
+ self.app_description = app_description unless app_description.nil?
63
+ self.script_filename = script_filename unless script_filename.nil?
64
+ end
65
+
66
+ # @return [Hash] This hash built from slop definition correspond to the :command_line layer of internal_configs
67
+ def command_line_config
68
+ @slop_definition.parse
69
+ @slop_definition.to_hash
70
+ end
71
+
72
+ # Yields a slop definition to modify the command line parameters
73
+ # @param [String] title used to insert a slop separator
74
+ def add_command_line_section(title='Script specific')
75
+ raise "Incorrect usage" unless block_given?
76
+ @slop_definition.separator build_separator(title)
77
+ yield @slop_definition
78
+ end
79
+
80
+ # Sets the :command_line layer of internal_configs to the computed {#command_line_config}
81
+ def load_config
82
+ internal_configs[:command_line] = {content: command_line_config, source: 'Command line'}
83
+ end
84
+
85
+ # Any modification done to the config is in fact stored in the :modified layer of internal_configs
86
+ # @param [String] key
87
+ # @param [String] value
88
+ def []=(key,value)
89
+ internal_configs[:modified][:content][key] = value
90
+ end
91
+
92
+ # Reset the :modified layer of internal_configs rolling back any change done to the config
93
+ def reset
94
+ internal_configs[:modified] = {content: {}, source: CHANGED_BY_CODE}
95
+ end
96
+
97
+
98
+ # @return [Array] List of layers
99
+ def layers
100
+ internal_configs.keys
101
+ end
102
+
103
+ private
104
+
105
+ def build_separator(title)
106
+ "-- #{title} ".ljust 80, '-'
107
+ end
108
+
109
+ # Builds common used command line options
110
+ def build_command_line_options
111
+ add_command_line_section('Generic options') do |slop|
112
+ slop.on :auto, 'Auto mode. Bypasses questions to user.', :argument => false
113
+ slop.on :simulate, 'Do not perform the actual underlying actions.', :argument => false
114
+ slop.on :v, :verbose, 'Enable verbose mode.', :argument => false
115
+ slop.on :h, :help, 'Displays this help.', :argument => false
116
+ end
117
+ end
118
+
119
+ def build_banner
120
+ "\nUsage: #{script_filename} [options]\n#{app_name} Version: #{app_version}\n\n#{app_description}"
121
+ end
122
+
123
+ end
@@ -0,0 +1,203 @@
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 (if any) is loaded the
21
+ # 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_config 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
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
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
@@ -0,0 +1,101 @@
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 handing_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.
27
+ class EasyAppHelper::Core::Logger < Logger
28
+ include Singleton
29
+
30
+
31
+ def initialize
32
+ @config = {}
33
+ super(TempLogger.new)
34
+ self.level = Severity::DEBUG
35
+ debug "Temporary initialisation logger created..."
36
+ end
37
+
38
+ # Change the log level while keeping the config in sync.
39
+ def level=(level)
40
+ super
41
+ @config[:'log-level'] = level
42
+ end
43
+
44
+ # Displays the message according to application verbosity and logs it as info.
45
+ def puts_and_logs(msg)
46
+ puts msg if @config[:verbose]
47
+ info(msg)
48
+ end
49
+
50
+ # Reset the logger regarding the config provided
51
+ def set_app_config(config)
52
+ @config = config
53
+ add_cmd_line_options
54
+ @config.load_config
55
+ debug "Config layers:\n#{@config.internal_configs.to_yaml}"
56
+ debug "Merged config:\n#{@config.to_yaml}"
57
+ if config[:'log-file']
58
+ handing_over_to config[:'log-file']
59
+ elsif config[:"debug-on-err"]
60
+ handing_over_to STDERR
61
+ else
62
+ handing_over_to STDOUT
63
+ end
64
+ self.level = config[:'log-level'] ? config[:'log-level'] : Severity::WARN
65
+ self
66
+ end
67
+
68
+ private
69
+
70
+
71
+ def add_cmd_line_options
72
+ @config.add_command_line_section('Debug and logging options') do |slop|
73
+ slop.on :debug, 'Run in debug mode.', :argument => false
74
+ slop.on 'debug-on-err', 'Run in debug mode with output to stderr.', :argument => false
75
+ slop.on 'log-level', "Log level from 0 to 5, default #{Severity::WARN}.", :argument => true, :as => Integer
76
+ slop.on 'log-file', 'File to log to.', :argument => true
77
+ end
78
+ end
79
+
80
+ # This class will act as a temporary logger, actually just keeping the history until the real
81
+ # configuration for the logger is known. Then the history is displayed or not regarding the
82
+ # definitive logger configuration.
83
+ class TempLogger
84
+ attr_reader :history
85
+
86
+ def initialize
87
+ @history = []
88
+ end
89
+
90
+ def write(data)
91
+ @history << data if @history
92
+ end
93
+
94
+ def close
95
+
96
+ end
97
+ end
98
+
99
+ end
100
+
101
+
@@ -0,0 +1,37 @@
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
+ h2.each do |key, v|
15
+ if h1[key] and h1[key].is_a?(Hash)
16
+ # Merges hashes
17
+ h1[key].merge! h2[key]
18
+ else
19
+ # Overrides the rest
20
+ h1[key] = h2[key] unless h2[key].nil?
21
+ end
22
+ end
23
+ h1
24
+ end
25
+
26
+ # Uses the standard "merge!" method
27
+ def simple_merge(h1, h2)
28
+ h1.merge! h2
29
+ end
30
+
31
+ # Brutal override
32
+ def override_merge(h1, h2)
33
+ h1 = nil
34
+ h1 = h2
35
+
36
+ end
37
+ end