cli_application 0.1.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,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
+