easy_app_helper 0.0.9 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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