cli_application 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,267 @@
1
+ # CliApplication::App - основной класс - каркас CLI-приложений. Класс обеспечивает контроль
2
+ # аргументов командной строки, управление конфигами и подключениями к базе данных.
3
+
4
+ module CliApplication
5
+ class App
6
+ # Ссылка на класс, который содержит аргменты командной строки или значения по умолчанию
7
+ attr_reader :argv
8
+ # Код завершения приложения. Может быть использован в Bash-скриптах
9
+ attr_reader :exitcode
10
+ # Ссылка на массив, содержащий список директорий в которых исполняется приложение.
11
+ # Основные: folders[:app] - папка из которой запущено приложение, folders[:class] - папка,
12
+ # в которой хранится базовый класс проекта.
13
+ attr_reader :folders
14
+ # Ссылка на класс конфигурации приложения
15
+ attr_reader :config
16
+ # Строка - версия приложения
17
+ attr_reader :version
18
+ # Строка - описание приложения
19
+ attr_reader :description
20
+ # Строка - краткое описание (назначение) приложения
21
+ attr_reader :shortdescription
22
+ # Строка - дата релиза ПО
23
+ attr_reader :releasedate
24
+ # Структура, содержащая конфигурации баз данных
25
+ attr_reader :databases
26
+ # Строка-шаблон, вывод которой происходит после завершения работы приложения
27
+ attr_accessor :footer
28
+
29
+ # Конструктор экземпляра приложения
30
+ #
31
+ # @param [Array] argv аргументы командной строки
32
+ # @param [String] appfolder директория, из которой запущено приложение
33
+ # @param [String] classfolder директория, в которой расположен базовый класс проекта
34
+ # @param [Sym] lang язык работы приложения (реализовано не полностью)
35
+ def initialize(argv, appfolder, classfolder, lang = :ru)
36
+ ::StTools::Setup.setup(lang)
37
+
38
+ @folders = Hash.new
39
+ @folders[:app] = appfolder
40
+ @folders[:class] = classfolder
41
+
42
+ @argv = ::CliApplication::Argv.new(argv)
43
+ @stat = ::CliApplication::Stat.new(@folders)
44
+ @config = ::CliApplication::Config.new(@folders)
45
+
46
+ @databases = ::CliApplication::Databases.new(config.cli.databases)
47
+
48
+ @footer = nil
49
+
50
+ init_app
51
+ end
52
+
53
+ #-------------------------------------------------------------
54
+ #
55
+ # Функции для использования внутри функции main
56
+ #
57
+ #-------------------------------------------------------------
58
+
59
+ # Метод возвращает папку из которой запущено приложение или расположен базовый класс.
60
+ # Базовый класс обычно располагается в фиксированном месте, например, в папке cli корня проекта. Соответственно,
61
+ # если вызвать File.dirname(app.folder(:class)), то можно будет узнать корневую папку проекта
62
+ #
63
+ # @param [Sym] type тип возвращаемой папки
64
+ # @option type [Sym] :app папка, из которой запущено приложение (по умолчанию)
65
+ # @option type [Sym] :class папка, в которой хранится базовый класс
66
+ # @option type [Sym] :stat папка, в которой хранится статистика по приложению
67
+ # @return [String] папка, из которой запущено приложение или расположен базовый класс
68
+ def folder(type = :app)
69
+ warn "Предупреждение: тип папки '#{type.inspect}' неизвестен (допустимо #{@folders.keys.inspect})" unless @folders.keys.include?(type)
70
+ @folders[type]
71
+ end
72
+
73
+ # Метод загружает конфиг и делает его доступным через единый интерфейс настроек конфигурации приложения (CliApplication::Config)
74
+ # При каждом вызове данного метода все конфиги перечитываются заново.
75
+ #
76
+ # @param [Sym] type параметр используется для указания местоположения конфига. Если указано :app или :class,
77
+ # то имя файла с конфигом будет дополнено папкой класса или приложения
78
+ # @option type [Sym] :app папка, из которой запущено приложение
79
+ # @option type [Sym] :class папка, в которой хранится базовый класс
80
+ # @option type [Sym] :absolute указывает на необходимость брать имя файла как задано разработчиком
81
+ # @return [Nil] нет
82
+ def add_config(filename, type)
83
+ @config.add(filename, type)
84
+ end
85
+
86
+ # Метод возвращает имя приложения
87
+ #
88
+ # @return [String] имя приложения без параметров командной строки и пути
89
+ def exename
90
+ ::StTools::System.exename
91
+ end
92
+
93
+ # Метод возвращает число секунд в формате Float с момента запуска приложения. В основном используется для показа
94
+ # времени выполнения приложения, но может быть вызван в любой момент из любого места приложения.
95
+ #
96
+ # @return [Float] число секунд с момента запуска приложения
97
+ # @example Примеры использования
98
+ # puts "С момента запуска прошло #{executed_at} сек." #=> "С момента запуска прошло 23.456435 сек."
99
+ def executed_at
100
+ @executed_at = (::Time.now - @started_at).to_f
101
+ end
102
+
103
+ # Метод устанавливает код, с которым будет завершена работа приложения.
104
+ #
105
+ # @param [Integer] code код завершения приложения, который будет передан в операционную систему (bash)
106
+ def exitcode=(code)
107
+ @exitcode = code
108
+ @stat.exitcode = code
109
+ end
110
+
111
+ # Метод устанавливает текущую версию приложения, которая потом отобразится в файле статистики
112
+ #
113
+ # @param [Integer] val строка с версией приложения
114
+ # @example Примеры использования
115
+ # app = CliApplication.new(ARGV, __dir__)
116
+ # app.version = '2.1'
117
+ def version=(val)
118
+ @version = val
119
+ @stat.version = val
120
+ end
121
+
122
+ # Метод устанавливает описание приложения, которое будет выведено при старте скрипта. Данный метод используется
123
+ # для формирования подсказок пользователю.
124
+ #
125
+ # @param [String] val строка с описанием приложения
126
+ # @example Примеры использования
127
+ # app = CliApplication.new(ARGV, __dir__)
128
+ # app.description = 'Данное приложение обеспечивает.... (c) .... и т.д.'
129
+ def description=(val)
130
+ @description = val
131
+ @stat.description = val
132
+ end
133
+
134
+ # Метод устанавливает краткое описание приложения, которое будет выведено при старте скрипта, а также
135
+ # отображено в файле статистики.
136
+ #
137
+ # @param [String] val строка с кратким описанием приложения
138
+ # @example Примеры использования
139
+ # app = CliApplication.new(ARGV, __dir__)
140
+ # app.shortdescription = 'Утилита форматирования диска'
141
+ def shortdescription=(val)
142
+ @shortdescription = val
143
+ @stat.shortdescription = val
144
+ end
145
+
146
+ # Метод устанавливает дату последнего изменения (выпуска) приложения. Используется в справочных целях
147
+ #
148
+ # @param [String] val строка датой релиза (выпуска) приложения
149
+ # @example Примеры использования
150
+ # app = CliApplication.new(ARGV, __dir__)
151
+ # app.releasedate = '2015-05-11'
152
+ def releasedate=(val)
153
+ @releasedate = val
154
+ @stat.releasedate = val
155
+ end
156
+
157
+ # Метод предназначен для подключения файлов-моделей ActiveRecords. Архитектура CLI-приложения, учитывающая
158
+ # совместимость с Rails-проектами, требует загрузки моделей после чтения файлов конфигурации и, соответственно,
159
+ # иницииации класса приложения. Поэтому объявить require файлов моделей в начале файла не получится, будут
160
+ # выводится ошибки инициализации базы данных.
161
+ #
162
+ # @example Примеры использования
163
+ # def init_active_records
164
+ # require 'offers.rb'
165
+ # require 'params.rb'
166
+ # require 'categories.rb'
167
+ # end
168
+ def init_active_records
169
+
170
+ end
171
+
172
+ # Метод инициализации приложения. Может быть переписан с обязательным вызовом функции super
173
+ #
174
+ # @example Примеры использования
175
+ # def init_app
176
+ # super
177
+ #
178
+ # # Код своего приложения
179
+ # end
180
+ def init_app
181
+ @stat.last_started_at = ::Time.zone.now
182
+ @started_at = ::Time.now
183
+ @exitcode = 0
184
+
185
+ init_active_records
186
+ end
187
+
188
+ # Метод добавления аргумента командной строки. Вызывается при инициализации приложения, служит для определения списка
189
+ # аргументов командной строки, формирвоания подсказок и установки значения по умолчанию. В классе принят не традиционный
190
+ # для Linux формат командной строки. Пример вызова: add_city.rb user_id=123 name=Максим city='Верхние Луки'.
191
+ #
192
+ # Параметры, добавленные данным методом доступны через переменную argv (см. примеры)
193
+ #
194
+ # @param [Sym] action параметр определяет действие, которое надо произвести над параметром командной строки.
195
+ # @param [String] key название ключа, напрмиер 'user_id', 'name', 'city'.
196
+ # @param [Object] default значение по умочланию, "подставляемое" при отсутствии заданного пользователем параметра
197
+ # @param [String] description описание параметра (подсказка)
198
+ #
199
+ # @example Примеры использования
200
+ # app = CliApplication.new(ARGV, __dir__)
201
+ # app.set_argv(:integer, 'user_id', 0, 'Идентификатор пользователя')
202
+ # app.set_argv(:string, 'name', 'Без имени', 'Имя пользователя')
203
+ # app.set_argv(:caps, 'city', 'москВА', 'Город проживания пользователя')
204
+ #
205
+ # def main
206
+ # puts argv.user_id #=> 0
207
+ # puts argv.name #=> 'Без имени'
208
+ # puts argv.city #=> 'Москва'
209
+ # end
210
+ def set_argv(action, key, default, description)
211
+ @argv.set_argv(action, key, default, description)
212
+ end
213
+
214
+ # Основной метод, в котором должен быть размещен код приложения
215
+ # @return [Integer] метод должен возвращать код, который будет транслирован в параметр exitcode
216
+ def main
217
+ warn "ПРЕДУПРЕЖДЕНИЕ: необходимо переопределить функцию 'main' в вашем коде"
218
+ 255
219
+ end
220
+
221
+ # При вызове данного метода начнется выполнение кода приложения (будет осуществен вызов функции main)
222
+ def run
223
+ self.exitcode = main || 255
224
+ self.executed_at = (::Time.now - @started_at).to_f
225
+ puts_footer
226
+ @stat.save
227
+ end
228
+
229
+ # Метод отображает на экране информацию о приложении (версия, дата последнего запуска, дата релиза, и пр.)
230
+ # @param [Syn] type при указании :full выводится полное описание, при других значениях не выводится
231
+ # подсказка по аргументам командной строки
232
+ def help(type = :full)
233
+ last_started_at_human = @stat.last_started_at_human
234
+
235
+ puts ::StTools::System.exename + ' - ' + @shortdescription
236
+ puts "Версия #{@version} (#{@releasedate})"
237
+ puts last_started_at_human
238
+ puts @stat.startes_human
239
+ puts
240
+ puts @description
241
+
242
+ if type == :full
243
+ @argv.help
244
+ puts
245
+ end
246
+ end
247
+
248
+
249
+ private
250
+
251
+
252
+ def puts_footer
253
+ return if @footer.nil?
254
+ line = footer.gsub('{executed_at}', executed_at.to_s)
255
+ line.gsub!('{memory}', StTools::Human.memory)
256
+ line.gsub!('{exitcode}', @exitcode.to_s)
257
+ line.gsub!('{status}', (@exitcode == 0 ? 'SUCCESS' : 'FAIL'))
258
+ puts line
259
+ end
260
+
261
+ def executed_at=(at)
262
+ @executed_at = at
263
+ @stat.executed_at = at
264
+ end
265
+
266
+ end
267
+ end
@@ -0,0 +1,152 @@
1
+ # Данный класс обеспечивает управление аргументами командной строки
2
+
3
+ module CliApplication
4
+ class Argv < OpenStruct
5
+
6
+ # Конструктор. Вызывается при создании класса приложения. Данный класс доступен
7
+ # в главной функции приложения (main) через переменную argv
8
+ #
9
+ # @param [Array] argv аргументы командной строки, введенные пользователем
10
+ # @example Примеры использования
11
+ # puts argv.city #=> 'Москва'
12
+ def initialize(argv)
13
+ @params = Hash.new
14
+ @full = Hash.new
15
+
16
+ argv.each do |one|
17
+ if one.match(/[a-z\_0-9]\=/i)
18
+ pair = one.split('=')
19
+ @params[pair.first.to_s.strip.downcase.to_sym] = pair.last
20
+ else
21
+ warn "WARNING: некорректный ключ параметра командной строки: #{one.inspect} (#{File.basename(__FILE__)} at #{__LINE__})"
22
+ end
23
+ end
24
+ super(@params)
25
+ end
26
+
27
+ # Метод добавления аргумента командной строки. Вызывается при инициализации приложения, служит для определения списка
28
+ # аргументов командной строки, формирвоания подсказок и установки значения по умолчанию. В классе принят не традиционный
29
+ # для Linux формат командной строки. Пример вызова: add_city.rb user_id=123 name=Максим city='Верхние Луки'.
30
+ #
31
+ # Параметры, добавленные данным методом доступны через переменную argv (см. примеры)
32
+ #
33
+ # @param [Sym] action параметр определяет действие, которое надо произвести над параметром командной строки.
34
+ # @param [String] key название ключа, напрмиер 'user_id', 'name', 'city'.
35
+ # @param [Object] default значение по умочланию, "подставляемое" при отсутствии заданного пользователем параметра
36
+ # @param [String] description описание параметра (подсказка)
37
+ #
38
+ # @example Примеры использования
39
+ # app = CliApplication.new(ARGV, __dir__)
40
+ # app.set_argv(:integer, 'user_id', 0, 'Идентификатор пользователя')
41
+ # app.set_argv(:string, 'name', 'Без имени', 'Имя пользователя')
42
+ # app.set_argv(:caps, 'city', 'москВА', 'Город проживания пользователя')
43
+ #
44
+ # def main
45
+ # puts argv.user_id #=> 0
46
+ # puts argv.name #=> 'Без имени'
47
+ # puts argv.city #=> 'Москва'
48
+ # end
49
+ def set_argv(action, key, default, description)
50
+ key = key.downcase.strip.to_sym
51
+ unless @params.keys.include?(key)
52
+ @params[key] = default
53
+ end
54
+
55
+ case action
56
+ when :bool, :boolean
57
+ @params[key] = @params[key].to_s.to_bool
58
+ when :split
59
+ @params[key] = ::StTools::String.split(@params[key].to_s, ',', sort: true)
60
+ when :range
61
+ @params[key] = @params[key].to_s.to_range(sort: true, uniq: true)
62
+ when :range_no_uniq
63
+ @params[key] = @params[key].to_s.to_range(sort: true)
64
+ when :float
65
+ @params[key] = @params[key].to_s.strip.to_f
66
+ when :integer
67
+ @params[key] = @params[key].to_s.strip.to_i
68
+ when :downcase
69
+ @params[key] = @params[key].to_s.downcase
70
+ when :upcase
71
+ @params[key] = @params[key].to_s.upcase
72
+ when :normalize
73
+ @params[key] = @params[key].to_s.normalize
74
+ when :caps
75
+ @params[key] = @params[key].to_s.caps
76
+ when :string
77
+ @params[key] = @params[key].to_s
78
+ else
79
+ end
80
+
81
+ convert_from_hash
82
+ set_full(action, key, default, @params[key], description)
83
+ end
84
+
85
+ # Метод выводит подсказку по аргументам командной строки
86
+ def help
87
+ puts
88
+ puts "Параметры приложения:"
89
+
90
+ screenwidth = ::StTools::System.screen(:width)
91
+ keylen = self.keylen
92
+
93
+ @full.each do |key, data|
94
+ line = get_helpline(key, data[:description], keylen, screenwidth)
95
+ line.each { |x| puts x }
96
+ end
97
+ puts
98
+ end
99
+
100
+ private
101
+
102
+ def set_full(action, key, default, value, description)
103
+ @full[key] = Hash.new
104
+ @full[key][:action] = action
105
+ @full[key][:default] = default
106
+ @full[key][:value] = value
107
+ @full[key][:description] = description + ' ' + human_default(action, value, default)
108
+ end
109
+
110
+ def human_default(action, value, default)
111
+ type = value.class.to_s
112
+ defval = default.inspect
113
+ "(по умолчанию #{defval}:#{type})"
114
+ end
115
+
116
+ def keylen
117
+ keylen = 0
118
+ @full.each do |key, data|
119
+ keylen = key.to_s.length if key.to_s.length > keylen
120
+ end
121
+ keylen
122
+ end
123
+
124
+ def get_helpline(key, line, keylen, screenwidth)
125
+ out = Array.new
126
+ width = screenwidth - 2 - keylen - 3
127
+ chunks = line.chars.each_slice(width).map(&:join)
128
+
129
+ chunks.each do |one|
130
+ if out.count == 0
131
+ tmp = key.to_s.ljust(keylen, ' ') + ' - '
132
+ else
133
+ tmp = ' '.ljust(keylen, ' ') + ' '
134
+ end
135
+ out << " #{tmp}#{one.strip}"
136
+ out
137
+ end
138
+
139
+ out
140
+ end
141
+
142
+ def convert_from_hash
143
+ @params.each do |key, value|
144
+ name = new_ostruct_member(key)
145
+ self[name] = value
146
+ end
147
+ end
148
+
149
+
150
+ end
151
+ end
152
+
@@ -0,0 +1,82 @@
1
+ # Класс обеспечивает чтение различных конфиг-файлов и их объединение в единый интерфейс.
2
+ # Например, при задании конфига вида
3
+ #
4
+ # cli:
5
+ # timezone: 3
6
+ #
7
+ # к указанным переменным можно получить доступ через вызов puts config.cli.timezone #=> 3
8
+ #
9
+
10
+ module CliApplication
11
+ class Config < OpenStruct
12
+ attr_reader :config
13
+
14
+ # Конструктор. Вызывается при создании класса приложения. Данный класс доступен
15
+ # в главной функции приложения (main) через переменную config
16
+ #
17
+ # @param [Array] folders директории, в которых расположены базовый класс проекта и класс приложения
18
+ def initialize(folders)
19
+ super(nil)
20
+ return if folders.nil?
21
+ @folders = folders
22
+ @filenames = Array.new
23
+ @config_filename = File.join([folders[:class], 'config.yml'])
24
+ load_config(@config_filename)
25
+ end
26
+
27
+ # Метод загружает конфиг и делает его доступным через единый интерфейс настроек конфигурации приложения (CliApplication::Config)
28
+ # При каждом вызове данного метода все конфиги перечитываются заново.
29
+ #
30
+ # @param [Sym] type параметр используется для указания местоположения конфига. Если указано :app или :class,
31
+ # то имя файла с конфигом будет дополнено папкой класса или приложения
32
+ # @option type [Sym] :app папка, из которой запущено приложение
33
+ # @option type [Sym] :class папка, в которой хранится базовый класс
34
+ # @option type [Sym] :absolute указывает на необходимость брать имя файла как задано разработчиком
35
+ # @return [Nil] нет
36
+ def add(filename, type)
37
+ if @folders.keys.include?(type)
38
+ load_config(File.join(@folders[type], filename))
39
+ elsif type == :absolute
40
+ load_config(filename)
41
+ else
42
+ warn "Предупреждение: попытка загрузить конфиг неизвестного типа (#{type.inspect}). Допустимы #{@folders.keys.inspect}"
43
+ end
44
+ end
45
+
46
+
47
+ private
48
+
49
+
50
+ def load_config(filename) # :nodoc:
51
+ raise "Внимание!!! Не найден файл конфигурации '#{filename}'" unless File.exist?(filename)
52
+ @filenames << filename
53
+ @filenames.uniq!
54
+ @config = Hash.new
55
+
56
+ @filenames.each do |one|
57
+ tmp = YAML.load_file(one).deep_symbolize_keys rescue Hash.new
58
+ @config.merge!(tmp)
59
+ end
60
+
61
+ tmp = JsonStruct.new(@config)
62
+ tmp.each_pair { |key, value| set_pair(key, value) }
63
+ valid?
64
+
65
+ ::Time.zone = self.cli.timezone
66
+ # todo: добавить установки времени для записей ActiveRecord
67
+ end
68
+
69
+ def set_pair(key, value) # :nodoc:
70
+ name = new_ostruct_member(key)
71
+ self[name] = value
72
+ end
73
+
74
+ def valid? # :nodoc:
75
+ raise "ОШИБКА: не найдена секция 'cli'" if self.cli.nil?
76
+ raise "ОШИБКА: не найдена секция 'cli.tz'" if self.cli.timezone.nil?
77
+ raise "ОШИБКА: не найдена секция 'cli.active_record_tz'" if self.cli.ar_timezone.nil?
78
+ end
79
+
80
+
81
+ end
82
+ end
@@ -0,0 +1,39 @@
1
+ # Класс обесечивает формирвоание конфигураций баз данных в совместимом с Rails формате
2
+
3
+ module CliApplication
4
+ class Databases
5
+
6
+ # Конструктор, который обеспечивает конфигурацию базового класса ActiveRecords::Base,
7
+ # а именно загружает в класс все конфигурации, с которыми должно работать приложение.
8
+ def initialize(config)
9
+ @config = config.to_h || Hash.new
10
+ ar_configuration
11
+ end
12
+
13
+ # Метод возвращает список конфигураций баз данных
14
+ #
15
+ # @return [Array] массив названий конфигураций
16
+ # @example Примеры использования
17
+ # puts databases.list #=> [:default, :stat, :work_instance]
18
+ def list
19
+ @config.keys
20
+ end
21
+
22
+ # Метод возвращает конфигурацию базы данных
23
+ #
24
+ # @param [Sym] ind идентификатор (наименование) конфигурации базы данных
25
+ # @return [Hash] конфигурация базы данных
26
+ def [](ind)
27
+ @config[ind]
28
+ end
29
+
30
+ private
31
+
32
+ def ar_configuration # :nodoc:
33
+ list.each do |cfg_name|
34
+ ActiveRecord::Base.configurations[cfg_name] = @config[cfg_name].symbolize_keys
35
+ end
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,21 @@
1
+ # Расширяем стандартные классы, подмешивая туда функционал StTools
2
+
3
+ class String # :nodoc:
4
+ include ::StTools::Module::String
5
+ end
6
+
7
+ class Integer # :nodoc:
8
+ include ::StTools::Module::Integer
9
+ end
10
+
11
+ class Time # :nodoc:
12
+ include ::StTools::Module::Time
13
+ end
14
+
15
+ class Date # :nodoc:
16
+ include ::StTools::Module::Time
17
+ end
18
+
19
+ # class DateTime
20
+ # include ::StTools::Module::Time
21
+ # end
@@ -0,0 +1,48 @@
1
+ # https://gist.github.com/anonymous/0ea3a14166d24f750bd9
2
+
3
+ class JsonStruct < OpenStruct # :nodoc:
4
+ def initialize(hash=nil)
5
+
6
+ @table = {}
7
+ @hash_table = {}
8
+
9
+ if hash
10
+ recurse = Proc.new do |item|
11
+ values = []
12
+
13
+ item.each do |val|
14
+ if val.is_a?(Hash)
15
+ values.push(self.class.new(val))
16
+ elsif val.is_a?(Array)
17
+ values.push(recurse.call(val))
18
+ else
19
+ values.push(val)
20
+ end
21
+ end
22
+
23
+ item.clear
24
+ item.push(*values)
25
+
26
+ item
27
+ end
28
+
29
+ hash.each do |k, v|
30
+
31
+ if v.is_a?(Array)
32
+ recurse.call(v)
33
+ end
34
+
35
+ @table[k.to_sym] = (v.is_a?(Hash) ? self.class.new(v) : v)
36
+ @hash_table[k.to_sym] = v
37
+ new_ostruct_member(k)
38
+
39
+ end
40
+ end
41
+ end
42
+
43
+ def to_h
44
+ @hash_table
45
+ end
46
+
47
+ end
48
+