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.
- checksums.yaml +4 -4
- data/README.md +376 -172
- data/lib/easy_app_helper/core/base.rb +123 -0
- data/lib/easy_app_helper/core/config.rb +203 -0
- data/lib/easy_app_helper/core/logger.rb +101 -0
- data/lib/easy_app_helper/core/merge_policies.rb +37 -0
- data/lib/easy_app_helper/core/places.rb +52 -0
- data/lib/easy_app_helper/module_manager.rb +61 -0
- data/lib/easy_app_helper/version.rb +1 -2
- data/lib/easy_app_helper.rb +8 -13
- data/test/test.yml +7 -0
- data/test/test2_app.rb +33 -0
- data/test/test3_app.rb +90 -0
- data/test/test4_app.rb +36 -0
- data/test/test_app.rb +56 -0
- metadata +19 -8
- data/lib/easy_app_helper/base.rb +0 -211
- data/lib/easy_app_helper/common.rb +0 -88
- data/lib/easy_app_helper/config.rb +0 -129
- data/lib/easy_app_helper/logger.rb +0 -104
- data/lib/easy_app_helper/places.rb +0 -42
@@ -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
|