flame 4.18.1 → 5.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/bin/flame +7 -62
  3. data/lib/flame.rb +1 -0
  4. data/lib/flame/application.rb +75 -17
  5. data/lib/flame/application/config.rb +6 -0
  6. data/lib/flame/controller.rb +36 -76
  7. data/lib/flame/controller/path_to.rb +39 -0
  8. data/lib/flame/dispatcher.rb +25 -66
  9. data/lib/flame/dispatcher/cookies.rb +10 -2
  10. data/lib/flame/dispatcher/routes.rb +53 -0
  11. data/lib/flame/dispatcher/static.rb +15 -8
  12. data/lib/flame/errors/argument_not_assigned_error.rb +6 -0
  13. data/lib/flame/errors/route_arguments_order_error.rb +6 -0
  14. data/lib/flame/errors/route_extra_arguments_error.rb +10 -0
  15. data/lib/flame/errors/route_not_found_error.rb +10 -4
  16. data/lib/flame/errors/template_not_found_error.rb +6 -0
  17. data/lib/flame/path.rb +63 -33
  18. data/lib/flame/render.rb +21 -8
  19. data/lib/flame/router.rb +112 -66
  20. data/lib/flame/router/route.rb +9 -56
  21. data/lib/flame/router/routes.rb +86 -0
  22. data/lib/flame/validators.rb +7 -1
  23. data/lib/flame/version.rb +1 -1
  24. data/template/.editorconfig +15 -0
  25. data/template/.gitignore +19 -2
  26. data/template/.rubocop.yml +14 -0
  27. data/template/Gemfile +48 -8
  28. data/template/Rakefile +824 -0
  29. data/template/{app.rb.erb → application.rb.erb} +4 -1
  30. data/template/config.ru.erb +62 -10
  31. data/template/config/config.rb.erb +44 -2
  32. data/template/config/database.example.yml +1 -1
  33. data/template/config/deploy.example.yml +2 -0
  34. data/template/config/puma.rb +56 -0
  35. data/template/config/sequel.rb.erb +13 -6
  36. data/template/config/server.example.yml +32 -0
  37. data/template/config/session.example.yml +7 -0
  38. data/template/controllers/{_base_controller.rb.erb → _controller.rb.erb} +5 -4
  39. data/template/controllers/site/_controller.rb.erb +18 -0
  40. data/template/controllers/site/index_controller.rb.erb +12 -0
  41. data/template/filewatchers.yml +12 -0
  42. data/template/server +172 -21
  43. data/template/services/.keep +0 -0
  44. data/template/views/site/index.html.erb.erb +1 -0
  45. data/template/views/site/layout.html.erb.erb +10 -0
  46. metadata +112 -54
  47. data/template/Rakefile.erb +0 -64
  48. data/template/config/thin.example.yml +0 -18
@@ -7,12 +7,18 @@ module Flame
7
7
  module Validators
8
8
  ## Compare arguments from path and from controller's action
9
9
  class RouteArgumentsValidator
10
+ ## Create a new instance of validator
11
+ ## @param ctrl [Flame::Controller] controller of route
12
+ ## @param path [Flame::Path, String] path of route
13
+ ## @param action [Symbol, String] action of route
10
14
  def initialize(ctrl, path, action)
11
15
  @ctrl = ctrl
12
16
  @path = Flame::Path.new(path)
13
17
  @action = action
14
18
  end
15
19
 
20
+ ## Validate
21
+ ## @return [true, false] valid or not
16
22
  def valid?
17
23
  extra_valid? && order_valid?
18
24
  end
@@ -81,7 +87,7 @@ module Flame
81
87
  def first_wrong_ordered_arguments
82
88
  opt_arguments = action_arguments[:opt].zip(path_arguments[:opt])
83
89
  opt_arguments.map! do |args|
84
- args.map { |arg| Flame::Path::PathPart.new(arg, arg: :opt) }
90
+ args.map { |arg| Flame::Path::Part.new(arg, arg: :opt) }
85
91
  end
86
92
  opt_arguments.find do |action_argument, path_argument|
87
93
  action_argument != path_argument
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Flame
4
- VERSION = '4.18.1'
4
+ VERSION = '5.0.0.rc1'
5
5
  end
@@ -0,0 +1,15 @@
1
+ root = true
2
+
3
+ [*]
4
+ indent_style = tab
5
+ indent_size = 2
6
+ end_of_line = lf
7
+ charset = utf-8
8
+ trim_trailing_whitespace = true
9
+ insert_final_newline = true
10
+
11
+ [*.yml]
12
+ indent_style = space
13
+
14
+ [*.md]
15
+ indent_style = space
@@ -1,11 +1,28 @@
1
1
  # configuration
2
- config/*.yml
3
- !config/*example.yml
2
+ config/**/*
3
+ !config/**/*.example*
4
+ !config/**/*.rb
5
+ !config/**/*.erb
6
+ !config/nginx
7
+ !config/nginx/snippets
8
+ !config/nginx/snippets/*.example*
9
+ !config/nginx/snippets/ssl-params.conf
10
+ !config/nginx/sites
11
+ !config/nginx/sites/*.example*
4
12
 
5
13
  # temp
14
+ .sass-cache/
6
15
  log/
7
16
  tmp/
8
17
  *.bak
18
+ *~
19
+
20
+ # uploaded files
21
+ #public/files
9
22
 
10
23
  # dumps
11
24
  *.sql
25
+
26
+ # seo files
27
+ #views/site/sitemap.html
28
+ #public/robots.txt
@@ -0,0 +1,14 @@
1
+ Layout/Tab:
2
+ Enabled: false
3
+ Layout/IndentationWidth:
4
+ Width: 1
5
+ Layout/MultilineMethodCallIndentation:
6
+ EnforcedStyle: indented
7
+
8
+ AllCops:
9
+ TargetRubyVersion: 2.3
10
+
11
+ Metrics/BlockLength:
12
+ Exclude:
13
+ - Rakefile
14
+ - db/migrations/*
@@ -2,14 +2,54 @@
2
2
 
3
3
  source 'https://rubygems.org'
4
4
 
5
- ## Framework
6
- gem 'flame'
7
- # gem 'flame-flash'
8
- # gem 'flame-r18n'
5
+ ## https://github.com/bundler/bundler/issues/4978
6
+ git_source(:github) { |name| "https://github.com/#{name}.git" }
9
7
 
10
- ## Server
11
- gem 'thin'
8
+ ## system
9
+ # gem 'gorilla-patch'
12
10
 
13
- ## DataBase
14
- gem 'sequel'
11
+ ## server
12
+ gem 'flame', github: 'AlexWayfer/flame'
13
+ # gem 'flame-flash', github: 'AlexWayfer/flame-flash'
14
+ gem 'puma'
15
+ # gem 'rack-slashenforce'
16
+ gem 'rack-utf8_sanitizer'
17
+ # gem 'rack_csrf', require: 'rack/csrf'
18
+
19
+ group :development do
20
+ gem 'filewatcher'
21
+ gem 'pry-byebug'
22
+ end
23
+
24
+ group :linter do
25
+ # gem 'rubocop'
26
+ end
27
+
28
+ ## database
15
29
  # gem 'pg'
30
+ # gem 'sequel'
31
+ # gem 'sequel_pg', require: 'sequel'
32
+
33
+ ## translations
34
+ # gem 'flame-r18n', github: 'AlexWayfer/flame-r18n'
35
+ ## for named_variables filter
36
+ # gem 'r18n-rails-api'
37
+
38
+ ## views
39
+ gem 'erubi', require: 'tilt/erubi'
40
+
41
+ ## assets
42
+ # gem 'sass'
43
+
44
+ ## others
45
+ # gem 'faker'
46
+ # gem 'google_currency'
47
+ # gem 'kramdown'
48
+ # gem 'mail'
49
+ # gem 'money'
50
+ # gem 'sentry-raven'
51
+
52
+ ## tools
53
+ gem 'pry'
54
+ gem 'rack-console'
55
+ gem 'rake'
@@ -0,0 +1,824 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ require 'pry-byebug' ## for `binding.pry` debugging
6
+
7
+ def alias_task(name, old_name)
8
+ t = Rake::Task[old_name]
9
+ desc t.full_comment if t.full_comment
10
+ task name, *t.arg_names do |_, args|
11
+ # values_at is broken on Rake::TaskArguments
12
+ args = t.arg_names.map { |a| args[a] }
13
+ t.invoke(*args)
14
+ end
15
+ end
16
+
17
+ def edit_file(filename)
18
+ sh "eval $EDITOR #{filename}"
19
+ end
20
+
21
+ def show_diff(filename, other_filename)
22
+ sh "diff -u --color=always #{filename} #{other_filename} || true"
23
+ puts
24
+ end
25
+
26
+ def env_true?(key)
27
+ %(true yes 1 y).include?(ENV[key.to_s].to_s.downcase)
28
+ end
29
+
30
+ ## Class for questions
31
+ class Question
32
+ def initialize(text, possible_answers)
33
+ @text = text
34
+ @possible_answers = Set.new(possible_answers) << 'quit' << 'help'
35
+ end
36
+
37
+ def answer
38
+ while @answer.nil?
39
+ ask
40
+ @answer = @possible_answers.find do |possible_answer|
41
+ possible_answer.start_with? @real_answer
42
+ end
43
+ print_help if @answer.nil?
44
+ end
45
+ @answer
46
+ end
47
+
48
+ private
49
+
50
+ def print_question
51
+ print "#{@text} [#{@possible_answers.map(&:chr).join(',')}] : "
52
+ end
53
+
54
+ def print_help
55
+ @possible_answers.each do |possible_answer|
56
+ puts "#{possible_answer.chr} - #{possible_answer}"
57
+ end
58
+ end
59
+
60
+ def ask
61
+ print_question
62
+ @real_answer = STDIN.gets.chomp.downcase
63
+ case @real_answer
64
+ when 'h'
65
+ print_help
66
+ return ask
67
+ when 'q'
68
+ exit
69
+ end
70
+ end
71
+ end
72
+
73
+ DB_CONFIG_FILE = File.join(__dir__, 'config', 'database.yml')
74
+
75
+ if File.exist? DB_CONFIG_FILE
76
+ namespace :db do
77
+ ## Require libs and config
78
+ require 'logger'
79
+ require 'sequel'
80
+
81
+ ## Constants for DB directories
82
+
83
+ DB_DIR = File.join(__dir__, 'db')
84
+ DB_MIGRATIONS_DIR = File.join(DB_DIR, 'migrations')
85
+ DB_DUMPS_DIR = File.join(DB_DIR, 'dumps')
86
+
87
+ DB_CONFIG = YAML.load_file DB_CONFIG_FILE
88
+
89
+ env_db_name = ENV['DB_NAME']
90
+ DB_CONFIG[:database] = env_db_name if env_db_name
91
+
92
+ def db_connection
93
+ @db_connection ||= Sequel.connect DB_CONFIG
94
+ end
95
+
96
+ DB_ACCESS = "-U #{DB_CONFIG[:user]} -h #{DB_CONFIG[:host]}"
97
+
98
+ DB_EXTENSIONS = %w[citext pgcrypto].freeze
99
+
100
+ PGPASS_FILE = File.expand_path File.join('~', '.pgpass')
101
+
102
+ PGPASS_LINE =
103
+ DB_CONFIG
104
+ .fetch_values(:host, :port, :database, :user, :password) { |_key| '*' }
105
+ .join(':')
106
+
107
+ def update_pgpass
108
+ pgpass_lines =
109
+ File.exist?(PGPASS_FILE) ? File.read(PGPASS_FILE).split($RS) : []
110
+ return if pgpass_lines&.include? PGPASS_LINE
111
+ File.write PGPASS_FILE, pgpass_lines.push(PGPASS_LINE, nil).join($RS)
112
+ File.chmod(0o600, PGPASS_FILE)
113
+ end
114
+
115
+ # db_connection.loggers << Logger.new($stdout)
116
+
117
+ namespace :migrations do
118
+ ## Migration file
119
+ class MigrationFile
120
+ MIGRATION_CONTENT =
121
+ <<~STR
122
+ # frozen_string_literal: true
123
+
124
+ Sequel.migration do
125
+ change do
126
+ end
127
+ end
128
+ STR
129
+
130
+ DISABLING_EXT = '.bak'
131
+
132
+ def self.find(query, only_one: true, enabled: true, disabled: true)
133
+ filenames = Dir[File.join(DB_MIGRATIONS_DIR, "*#{query}*")]
134
+ filenames.select! { |filename| File.file? filename }
135
+ files = filenames.map { |filename| new filename: filename }.sort!
136
+ files.reject!(&:disabled) unless disabled
137
+ files.select!(&:disabled) unless enabled
138
+ return files unless only_one
139
+ return files.first if files.size < 2
140
+ raise 'More than one file mathes the query'
141
+ end
142
+
143
+ attr_accessor :version, :name, :disabled
144
+
145
+ def initialize(filename: nil, name: nil)
146
+ self.filename = filename
147
+ self.name = name if name
148
+ end
149
+
150
+ ## Accessors
151
+
152
+ def basename
153
+ File.basename(@filename)
154
+ end
155
+
156
+ def filename=(value)
157
+ parse_filename value if value.is_a? String
158
+ @filename = value
159
+ end
160
+
161
+ def name=(value)
162
+ @name = value.tr(' ', '_').downcase
163
+ end
164
+
165
+ def disabled=(value)
166
+ @disabled =
167
+ case value
168
+ when String
169
+ [DISABLING_EXT, DISABLING_EXT[1..-1]].include? value
170
+ else
171
+ value
172
+ end
173
+ end
174
+
175
+ def <=>(other)
176
+ version <=> other.version
177
+ end
178
+
179
+ ## Behavior
180
+
181
+ def print
182
+ datetime = Time.parse(version).strftime('%F %R')
183
+ fullname = name.tr('_', ' ').capitalize
184
+ fullname = "#{fullname} (disabled)" if disabled
185
+ version_color, name_color =
186
+ disabled ? ["\e[37m", "\e[37m- "] : ["\e[36m", '']
187
+ puts "\e[37m[#{version}]\e[0m #{version_color}#{datetime}\e[0m" \
188
+ " #{name_color}#{fullname}\e[0m"
189
+ end
190
+
191
+ def generate
192
+ self.version = new_version
193
+ FileUtils.mkdir_p File.dirname new_filename
194
+ File.write new_filename, MIGRATION_CONTENT
195
+ end
196
+
197
+ def reversion
198
+ rename version: new_version
199
+ end
200
+
201
+ def disable
202
+ abort 'Migration already disabled' if disabled
203
+
204
+ rename disabled: true
205
+ end
206
+
207
+ def enable
208
+ abort 'Migration already enabled' unless disabled
209
+
210
+ rename disabled: false
211
+ end
212
+
213
+ private
214
+
215
+ def parse_filename(value = @filename)
216
+ basename = File.basename value
217
+ self.version, parts = basename.split('_', 2)
218
+ self.name, _ext, self.disabled = parts.split('.')
219
+ end
220
+
221
+ def new_version
222
+ Time.now.strftime('%Y%m%d%H%M')
223
+ end
224
+
225
+ def rename(vars = {})
226
+ vars.each { |key, value| send :"#{key}=", value }
227
+ return unless @filename.is_a? String
228
+ File.rename @filename, new_filename
229
+ self.filename = new_filename
230
+ end
231
+
232
+ def new_filename
233
+ new_basename = "#{version}_#{name}.rb#{DISABLING_EXT if disabled}"
234
+ File.join DB_MIGRATIONS_DIR, new_basename
235
+ end
236
+ end
237
+
238
+ desc 'Run migrations'
239
+ task :run, %i[target current] do |_t, args|
240
+ Rake::Task['db:dump'].invoke
241
+
242
+ Sequel.extension :migration
243
+ Sequel.extension :inflector
244
+ # db_connection.extension :pg_enum
245
+
246
+ options = {
247
+ allow_missing_migration_files: env_true?(:ignore)
248
+ }
249
+ if (target = args[:target])
250
+ if target == '0'
251
+ puts 'Migrating all the way down'
252
+ else
253
+ file = MigrationFile.find target, disabled: false
254
+
255
+ abort 'Migration with this version not found' if file.nil?
256
+
257
+ current = args[:current] || 'current'
258
+ puts "Migrating from #{current} to #{file.basename}"
259
+ target = file.version
260
+ end
261
+ options[:current] = args[:current].to_i
262
+ options[:target] = target.to_i
263
+ else
264
+ puts 'Migrating to latest'
265
+ end
266
+
267
+ db_connection.loggers << Logger.new($stdout)
268
+
269
+ Sequel::Migrator.run(
270
+ db_connection,
271
+ DB_MIGRATIONS_DIR,
272
+ options
273
+ )
274
+ end
275
+
276
+ desc 'Rollback the database N steps'
277
+ task :rollback, :step do |_task, args|
278
+ Rake::Task['db:dump'].invoke
279
+
280
+ step = args[:step] ? Integer(args[:step]).abs : 1
281
+
282
+ file = MigrationFile.find('*', only_one: false)[-1 - step]
283
+
284
+ Rake::Task['db:migrations:run'].invoke(file.version)
285
+
286
+ puts "Rolled back to #{file.basename}"
287
+ end
288
+
289
+ desc 'Create migration'
290
+ task :new, :name do |_t, args|
291
+ abort 'You must specify a migration name' if args[:name].nil?
292
+
293
+ file = MigrationFile.new name: args[:name]
294
+ file.generate
295
+ end
296
+
297
+ desc 'Change version of migration to latest'
298
+ task :reversion, :filename do |_t, args|
299
+ # rubocop:disable Style/IfUnlessModifier
300
+ if args[:filename].nil?
301
+ abort 'You must specify a migration name or version'
302
+ end
303
+
304
+ file = MigrationFile.find args[:filename]
305
+ file.reversion
306
+ end
307
+
308
+ desc 'Disable migration'
309
+ task :disable, :filename do |_t, args|
310
+ if args[:filename].nil?
311
+ abort 'You must specify a migration name or version'
312
+ end
313
+
314
+ file = MigrationFile.find args[:filename]
315
+ file.disable
316
+ end
317
+
318
+ desc 'Enable migration'
319
+ task :enable, :filename do |_t, args|
320
+ if args[:filename].nil?
321
+ abort 'You must specify a migration name or version'
322
+ end
323
+
324
+ file = MigrationFile.find args[:filename]
325
+ file.enable
326
+ end
327
+
328
+ desc 'Show all migrations'
329
+ task :list do |_t, _args|
330
+ files = MigrationFile.find '*', only_one: false
331
+ files.each(&:print)
332
+ end
333
+
334
+ desc 'Check applied migrations'
335
+ task :check do
336
+ applied_names = db_connection[:schema_migrations].select_map(:filename)
337
+ applied = applied_names.map { |one| MigrationFile.new filename: one }
338
+ existing = MigrationFile.find '*', only_one: false, disabled: false
339
+ existing_names = existing.map(&:basename)
340
+ a_not_e = applied.reject { |one| existing_names.include? one.basename }
341
+ e_not_a = existing.reject { |one| applied_names.include? one.basename }
342
+ if a_not_e.any?
343
+ puts 'Applied, but not existing'
344
+ a_not_e.each(&:print)
345
+ puts "\n" if e_not_a.any?
346
+ end
347
+ if e_not_a.any?
348
+ puts 'Existing, but not applied'
349
+ e_not_a.each(&:print)
350
+ end
351
+ end
352
+ end
353
+
354
+ alias_task :migrate, 'migrations:run'
355
+
356
+ desc 'Run seeds'
357
+ task :seed do
358
+ require 'sequel/extensions/seed'
359
+ seeds_dir = File.join(DB_DIR, 'seeds')
360
+
361
+ ## Doesn't support version yet
362
+ puts 'Seeding latest'
363
+ Sequel::Seeder.apply(db_connection, seeds_dir)
364
+ end
365
+
366
+ namespace :dumps do
367
+ ## Class for single DB dump file
368
+ class DumpFile
369
+ DB_DUMP_TIMESTAMP = '%Y-%m-%d_%H-%M'
370
+
371
+ DB_DUMP_TIMESTAMP_REGEXP_MAP = {
372
+ 'Y' => '\d{4}',
373
+ 'm' => '\d{2}',
374
+ 'd' => '\d{2}',
375
+ 'H' => '\d{2}',
376
+ 'M' => '\d{2}'
377
+ }.freeze
378
+
379
+ missing_keys =
380
+ DB_DUMP_TIMESTAMP.scan(/%(\w)/).flatten -
381
+ DB_DUMP_TIMESTAMP_REGEXP_MAP.keys
382
+
383
+ if missing_keys.any?
384
+ raise "`DB_DUMP_TIMESTAMP_REGEXP_MAP` doesn't contain keys" \
385
+ " #{missing_keys} for `DB_DUMP_TIMESTAMP`"
386
+ end
387
+
388
+ DB_DUMP_TIMESTAMP_REGEXP =
389
+ DB_DUMP_TIMESTAMP_REGEXP_MAP
390
+ .each_with_object(DB_DUMP_TIMESTAMP.dup) do |(key, value), result|
391
+ result.gsub! "%#{key}", value
392
+ end
393
+
394
+ DB_DUMP_FORMATS = %w[custom plain].freeze
395
+
396
+ DB_DUMP_EXTENSIONS = {
397
+ 'plain' => '.sql',
398
+ 'custom' => '.dump'
399
+ }.freeze
400
+
401
+ missing_formats = DB_DUMP_FORMATS.reject do |db_dump_format|
402
+ DB_DUMP_EXTENSIONS[db_dump_format]
403
+ end
404
+
405
+ if missing_formats.any?
406
+ raise "`DB_DUMP_EXTENSIONS` has no keys for #{missing_formats}" \
407
+ ' from `DB_DUMP_FORMATS`'
408
+ end
409
+
410
+ regexp_escaped_db_dump_extensions =
411
+ DB_DUMP_EXTENSIONS.values.map do |db_dump_extension|
412
+ Regexp.escape(db_dump_extension)
413
+ end
414
+
415
+ DB_DUMP_REGEXP = /^
416
+ #{DB_DUMPS_DIR}#{Regexp.escape(File::SEPARATOR)}
417
+ #{DB_CONFIG[:database]}_#{DB_DUMP_TIMESTAMP_REGEXP}
418
+ (#{regexp_escaped_db_dump_extensions.join('|')})
419
+ $/xo
420
+
421
+ def self.all
422
+ Dir[File.join(DB_DUMPS_DIR, '*')]
423
+ .select { |file| file.match?(DB_DUMP_REGEXP) }
424
+ .map! { |file| new filename: file }
425
+ .sort!
426
+ end
427
+
428
+ attr_reader :version, :timestamp, :format
429
+
430
+ def initialize(filename: nil, format: 'custom')
431
+ if filename
432
+ @extension = File.extname(filename)
433
+ @format = DB_DUMP_EXTENSIONS.key(@extension)
434
+ self.version = filename[/#{DB_DUMP_TIMESTAMP_REGEXP}/o]
435
+ else
436
+ @format = format
437
+ @extension = DB_DUMP_EXTENSIONS[@format]
438
+ self.timestamp = Time.now
439
+ end
440
+ end
441
+
442
+ def <=>(other)
443
+ timestamp <=> other.timestamp
444
+ end
445
+
446
+ def to_s
447
+ "#{readable_timestamp} #{format}"
448
+ end
449
+
450
+ def print
451
+ puts to_s
452
+ end
453
+
454
+ def path
455
+ File.join(
456
+ DB_DUMPS_DIR, "#{DB_CONFIG[:database]}_#{version}#{@extension}"
457
+ )
458
+ end
459
+
460
+ private
461
+
462
+ def version=(value)
463
+ @version = value
464
+ @timestamp = Time.strptime(version, DB_DUMP_TIMESTAMP)
465
+ end
466
+
467
+ def timestamp=(value)
468
+ @timestamp = value
469
+ @version = timestamp.strftime(DB_DUMP_TIMESTAMP)
470
+ end
471
+
472
+ def readable_timestamp
473
+ datetime = timestamp.strftime('%F %R')
474
+ "\e[36m#{datetime}\e[0m"
475
+ end
476
+ end
477
+
478
+ desc 'Make DB dump'
479
+ task :create, :format do |_task, args|
480
+ dump_format =
481
+ if args[:format]
482
+ DumpFile::DB_DUMP_FORMATS.find do |db_dump_format|
483
+ db_dump_format.start_with? args[:format]
484
+ end
485
+ else
486
+ DumpFile::DB_DUMP_FORMATS.first
487
+ end
488
+
489
+ update_pgpass
490
+
491
+ filename = DumpFile.new(format: dump_format).path
492
+ sh "mkdir -p #{DB_DUMPS_DIR}"
493
+ sh "pg_dump #{DB_ACCESS} -F#{dump_format.chr}" \
494
+ " #{DB_CONFIG[:database]} > #{filename}"
495
+ end
496
+
497
+ desc 'Restore DB dump'
498
+ task :restore, :step do |_task, args|
499
+ step = args[:step] ? Integer(args[:step]) : -1
500
+
501
+ update_pgpass
502
+
503
+ dump_file = DumpFile.all[step]
504
+
505
+ abort 'Dump file not found' unless dump_file
506
+
507
+ if Question.new("Restore #{dump_file} ?", %w[yes no]).answer == 'no'
508
+ abort 'Okay'
509
+ end
510
+
511
+ Rake::Task['db:dump'].invoke
512
+
513
+ case dump_file.format
514
+ when 'custom'
515
+ sh "pg_restore #{DB_ACCESS} -n public -d #{DB_CONFIG[:database]}" \
516
+ " #{dump_file.path} --jobs=4 --clean --if-exists"
517
+ when 'plain'
518
+ Rake::Task['db:drop'].invoke
519
+ Rake::Task['db:create'].invoke
520
+ sh "psql #{DB_ACCESS} #{DB_CONFIG[:database]} < #{dump_file.path}"
521
+ else
522
+ raise 'Unknown DB dump file format'
523
+ end
524
+ end
525
+
526
+ desc 'List DB dumps'
527
+ task :list do
528
+ DumpFile.all.each(&:print)
529
+ end
530
+ end
531
+
532
+ alias_task :dumps, 'dumps:list'
533
+ alias_task :dump, 'dumps:create'
534
+ alias_task :restore, 'dumps:restore'
535
+
536
+ desc 'Create empty DB'
537
+ task :create do
538
+ sh "createdb -U postgres #{DB_CONFIG[:database]} -O #{DB_CONFIG[:user]}"
539
+ DB_EXTENSIONS.each do |db_extension|
540
+ sh "psql -U postgres -c 'CREATE EXTENSION #{db_extension}'" \
541
+ " #{DB_CONFIG[:database]}"
542
+ end
543
+ end
544
+
545
+ desc 'Drop DB'
546
+ task :drop, :force do |_task, args|
547
+ case Question.new("Drop #{DB_CONFIG[:database]} ?", %w[yes no]).answer
548
+ when 'no'
549
+ abort 'OK'
550
+ end
551
+
552
+ Rake::Task['db:dump'].invoke unless args[:force]
553
+ sh "dropdb #{DB_ACCESS} #{DB_CONFIG[:database]}"
554
+ end
555
+ end
556
+ end
557
+
558
+ namespace :locales do
559
+ CROWDIN_CONFIG_FILE = File.join('config', 'crowdin.yml')
560
+
561
+ desc 'Upload files for translation'
562
+ task :upload do
563
+ sh "crowdin --config #{CROWDIN_CONFIG_FILE} upload sources"
564
+ end
565
+
566
+ desc 'Download translated files'
567
+ task :download do
568
+ sh "crowdin --config #{CROWDIN_CONFIG_FILE} download translations"
569
+ end
570
+
571
+ desc 'Check locales'
572
+ task :check do
573
+ require 'yaml'
574
+ require 'json'
575
+
576
+ ## Class for Locale file
577
+ class Locale
578
+ attr_reader :code, :hash
579
+
580
+ EXT = '.yml'
581
+
582
+ def self.load(locales_dir = 'locales')
583
+ Dir[File.join(__dir__, locales_dir, "*#{EXT}")].map do |file|
584
+ new File.basename(file, EXT), YAML.load_file(file)
585
+ end
586
+ end
587
+
588
+ def initialize(code, hash)
589
+ @code = code
590
+ @hash = hash
591
+ end
592
+
593
+ class HashCompare
594
+ def initialize(hash, other_hash)
595
+ @hash = hash
596
+ @other_hash = other_hash
597
+ @diff = {}
598
+ end
599
+
600
+ def different_keys
601
+ @hash.each_pair do |key, value|
602
+ other_value = @other_hash[key]
603
+ if value.is_a?(Hash) && other_value.is_a?(Hash)
604
+ add_differences_in_hash(value, other_value, key)
605
+ elsif value.is_a?(Array) && other_value.is_a?(Array)
606
+ add_differences_in_array(value, other_value, key)
607
+ elsif other_value.nil? || value.class != other_value.class
608
+ add_difference(value, key)
609
+ end
610
+ end
611
+ @diff
612
+ end
613
+
614
+ private
615
+
616
+ def add_difference(difference, key)
617
+ @diff[key] = difference unless difference.empty?
618
+ end
619
+
620
+ def add_differences_in_hash(hash, other_hash, key)
621
+ difference = self.class.new(hash, other_hash).different_keys
622
+ add_difference(difference, key)
623
+ end
624
+
625
+ def add_differences_in_array(array, other_array, key)
626
+ difference =
627
+ if array.size != other_array.size
628
+ array
629
+ else
630
+ differences_in_array(array, other_array)
631
+ end
632
+ add_difference(difference, key)
633
+ end
634
+
635
+ def differences_in_array(array, other_array)
636
+ array.each_with_object([]).with_index do |(object, diff), i|
637
+ other_object = other_array[i]
638
+ if object.is_a?(Hash) && other_object.is_a?(Hash)
639
+ difference = self.class.new(object, other_object).different_keys
640
+ diff << difference unless difference.empty?
641
+ end
642
+ end.compact
643
+ end
644
+ end
645
+
646
+ def diff(other)
647
+ HashCompare.new(hash, other.hash).different_keys
648
+ end
649
+ end
650
+
651
+ locales = Locale.load
652
+
653
+ def compare_locales(locale, other_locale)
654
+ puts "#{locale.code.upcase} -> #{other_locale.code.upcase}:\n\n"
655
+ puts locale.diff(other_locale).to_yaml
656
+ end
657
+
658
+ locales.each_with_index do |locale, ind|
659
+ locales[ind..-1].each do |other_locale|
660
+ next if locale == other_locale
661
+ compare_locales(locale, other_locale)
662
+ compare_locales(other_locale, locale)
663
+ end
664
+ end
665
+ end
666
+ end
667
+
668
+ namespace :static do
669
+ desc 'Check static files'
670
+ task :check do
671
+ Dir[File.join(__dir__, 'public', '**', '*')].each do |file|
672
+ basename = File.basename(file)
673
+ grep_options = '--exclude-dir={\.git,log} --color=always'
674
+ found = `grep -ir '#{basename}' ./ #{grep_options}`
675
+ next unless found.empty? && File.dirname(file) != @skipping_dir
676
+ filename = file.sub(__dir__, '')
677
+ case Question.new("Delete #{filename} ?", %w[yes no skip]).answer
678
+ when 'yes'
679
+ `git rm #{file.gsub(' ', '\ ')}`
680
+ when 'skip'
681
+ @skipping_dir = File.dirname(file)
682
+ end
683
+ end
684
+ end
685
+ end
686
+
687
+ namespace :config do
688
+ desc 'Check config files'
689
+ task :check do
690
+ example_suffix = '.example'
691
+ Dir[
692
+ File.join(__dir__, 'config', '**', "*#{example_suffix}*")
693
+ ].each do |example_filename|
694
+ regular_filename = example_filename.sub(example_suffix, '')
695
+ if File.exist? regular_filename
696
+ if File.mtime(example_filename) > File.mtime(regular_filename)
697
+ example_basename = File.basename example_filename
698
+ regular_basename = File.basename regular_filename
699
+
700
+ ask_what_to_do = proc do
701
+ case answer = Question.new(
702
+ "\e[32m\e[1m#{example_basename}\e[22m\e[0m was modified after" \
703
+ " \e[31m\e[1m#{regular_basename}\e[22m\e[0m." \
704
+ " Do you want to edit \e[31m\e[1m#{regular_basename}\e[22m\e[0m ?",
705
+ %w[yes no show quit]
706
+ ).answer
707
+ when 'yes'
708
+ edit_file regular_filename
709
+ when 'show'
710
+ show_diff regular_filename, example_filename
711
+ answer = ask_what_to_do.call
712
+ end
713
+
714
+ answer
715
+ end
716
+
717
+ break if ask_what_to_do.call == 'quit'
718
+ end
719
+ else
720
+ FileUtils.cp example_filename, regular_filename
721
+ edit_file regular_filename
722
+ end
723
+ end
724
+ end
725
+ end
726
+
727
+ desc 'Start interactive console'
728
+ task :console, :environment do |_t, args|
729
+ require 'rack/console'
730
+
731
+ args = args.to_hash
732
+ args[:environment] ||= 'development'
733
+ ARGV.clear
734
+ Rack::Console.new(args).start
735
+ end
736
+
737
+ desc 'Start psql'
738
+ task :psql do
739
+ update_pgpass
740
+ sh "psql #{DB_ACCESS} #{DB_CONFIG[:database]}"
741
+ end
742
+
743
+ ## Command for update server
744
+ desc 'Update from git'
745
+ task :update, :branch, :without_restart do |_t, args|
746
+ args = args.to_hash
747
+ args[:branch] ||= :master
748
+ server = './server'
749
+ sh "git checkout #{args[:branch]}"
750
+ sh "git pull origin #{args[:branch]}"
751
+ next if args[:without_restart]
752
+ sh 'bundle check || bundle install'
753
+ sh "#{server} stop"
754
+ sh 'rake config:check'
755
+ sh 'rake db:migrate'
756
+ sh "#{server} start"
757
+ end
758
+
759
+ ## Command before creating new branch
760
+ desc 'Fetch origin and rebase branch from master'
761
+ task :rebase do
762
+ sh 'git fetch origin'
763
+ sh 'git rebase origin/master'
764
+ end
765
+
766
+ ## Command for deploy code from git to server
767
+ ## @example rake deploy
768
+ ## Update from git with migrations and restart (for .rb and .erb files update)
769
+ ## @example rake deploy[true]
770
+ ## Update from git without migrations and restart (for static files update)
771
+ desc 'Deploy to production server'
772
+ task :deploy, :without_restart do |_t, args|
773
+ servers = YAML.load_file File.join(__dir__, 'config', 'deploy.yml')
774
+ rake_command = "rake update[master#{',true' if args.without_restart}]"
775
+ servers.each do |server|
776
+ update_command = "cd #{server[:path]} && #{rake_command}"
777
+ sh "ssh -t #{server[:ssh]} 'bash --login -c \"#{update_command}\"'"
778
+ end
779
+ end
780
+
781
+ namespace :assets do
782
+ assets_dir = File.join __dir__, 'assets'
783
+ public_dir = File.join __dir__, 'public'
784
+
785
+ styles_input_dir = File.join assets_dir, 'styles'
786
+ styles_input_file = File.join styles_input_dir, 'main.scss'
787
+ styles_output_dir = File.join public_dir, 'styles'
788
+ styles_output_file = File.join styles_output_dir, 'main.css'
789
+
790
+ scripts_input_dir = File.join assets_dir, 'scripts'
791
+ scripts_input_file = File.join scripts_input_dir, 'app.js'
792
+ scripts_output_dir = File.join public_dir, 'scripts', 'app', 'compiled'
793
+ scripts_output_file = 'app.js'
794
+
795
+ namespace :build do
796
+ desc 'Build all assets'
797
+ task all: %w[assets:build:styles assets:build:scripts]
798
+
799
+ desc 'Build styles assets'
800
+ task :styles do
801
+ next unless File.exist? styles_input_file
802
+ FileUtils.mkdir_p styles_output_dir
803
+ sh "sass #{styles_input_file} #{styles_output_file} -t compact"
804
+ end
805
+
806
+ desc 'Build scripts assets'
807
+ task :scripts do
808
+ next unless File.exist? scripts_input_file
809
+ sh 'yarn run webpack' \
810
+ " --entry #{scripts_input_file}" \
811
+ " --output-path #{scripts_output_dir}" \
812
+ " --output-filename #{scripts_output_file}"
813
+ end
814
+ end
815
+
816
+ alias_task :build, 'build:all'
817
+
818
+ namespace :watch do
819
+ desc 'Watch for styles assets'
820
+ task :styles do
821
+ sh "sass --watch #{styles_input_dir}:#{styles_output_dir} -t compact"
822
+ end
823
+ end
824
+ end