rbytes 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,458 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Rbytes < Thor
4
+ desc "template", "Load and run generator from RailsBytes"
5
+ def template(url)
6
+ puts "Run template from: #{url}"
7
+
8
+ # require gems typically used in templates
9
+ require "bundler"
10
+
11
+ Base.new.apply(url)
12
+ end
13
+
14
+ # Rails core extensions can be used in templates.
15
+ # Let's try to load ActiveSupport first and fallback to our custom set of extensions otherwise.
16
+ begin
17
+ require "active_support"
18
+ require "active_support/core_ext"
19
+ rescue LoadError
20
+ # TODO: Add more extensions
21
+ class ::String
22
+ def parameterize
23
+ gsub("::", "/").gsub(/([a-z])([A-Z])/, '\1-\2').downcase
24
+ end
25
+
26
+ def underscore
27
+ gsub("::", "/").gsub(/([a-z])([A-Z])/, '\1_\2').downcase
28
+ end
29
+ end
30
+
31
+ class ::Object
32
+ alias empty? nil?
33
+ end
34
+ end
35
+
36
+
37
+ class Base < Thor::Group
38
+ # Stub `Rails.application` to have a correct application name
39
+ module Rails
40
+ class << self
41
+ def application
42
+ return unless File.exist?("config/application.rb")
43
+
44
+ File.read("config/application.rb").then do |contents|
45
+ contents.match(/^module (\S+)\s*$/)
46
+ end.then do |matches|
47
+ next unless matches
48
+
49
+ Module.new.then do |mod|
50
+ Object.const_set(matches[1], mod)
51
+ app_class = Class.new
52
+ mod.const_set(:Application, app_class)
53
+ app_class.new
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ # Copied from https://github.com/rails/rails/blob/38275763257221e381cd4e37958ce1413fd0433c/railties/lib/rails/generators/actions.rb
61
+ module Rails
62
+ module Actions
63
+ # Adds an entry into +Gemfile+ for the supplied gem.
64
+ #
65
+ # gem "rspec", group: :test
66
+ # gem "technoweenie-restful-authentication", lib: "restful-authentication", source: "http://gems.github.com/"
67
+ # gem "rails", "3.0", git: "https://github.com/rails/rails"
68
+ # gem "RedCloth", ">= 4.1.0", "< 4.2.0"
69
+ # gem "rspec", comment: "Put this comment above the gem declaration"
70
+ def gem(*args, **options)
71
+ name, *versions = args
72
+
73
+ # Set the message to be shown in logs. Uses the git repo if one is given,
74
+ # otherwise use name (version).
75
+ parts, message = [quote(name)], name.dup
76
+
77
+ # Output a comment above the gem declaration.
78
+ comment = options.delete(:comment)
79
+
80
+ if versions = versions.any? ? versions : options.delete(:version)
81
+ _versions = Array(versions)
82
+ _versions.each do |version|
83
+ parts << quote(version)
84
+ end
85
+ message << " (#{_versions.join(", ")})"
86
+ end
87
+ message = options[:git] if options[:git]
88
+
89
+ log :gemfile, message
90
+
91
+ parts << quote(options) unless options.empty?
92
+
93
+ in_root do
94
+ str = []
95
+ if comment
96
+ comment.each_line do |comment_line|
97
+ str << indentation
98
+ str << "# #{comment_line}"
99
+ end
100
+ str << "\n"
101
+ end
102
+ str << indentation
103
+ str << "gem #{parts.join(", ")}"
104
+ append_file_with_newline "Gemfile", str.join, verbose: false
105
+ end
106
+ end
107
+
108
+ # Wraps gem entries inside a group.
109
+ #
110
+ # gem_group :development, :test do
111
+ # gem "rspec-rails"
112
+ # end
113
+ def gem_group(*names, **options, &block)
114
+ str = names.map(&:inspect)
115
+ str << quote(options) unless options.empty?
116
+ str = str.join(", ")
117
+ log :gemfile, "group #{str}"
118
+
119
+ in_root do
120
+ append_file_with_newline "Gemfile", "\ngroup #{str} do", force: true
121
+ with_indentation(&block)
122
+ append_file_with_newline "Gemfile", "end", force: true
123
+ end
124
+ end
125
+
126
+ def github(repo, options = {}, &block)
127
+ str = [quote(repo)]
128
+ str << quote(options) unless options.empty?
129
+ str = str.join(", ")
130
+ log :github, "github #{str}"
131
+
132
+ in_root do
133
+ if @indentation.zero?
134
+ append_file_with_newline "Gemfile", "\ngithub #{str} do", force: true
135
+ else
136
+ append_file_with_newline "Gemfile", "#{indentation}github #{str} do", force: true
137
+ end
138
+ with_indentation(&block)
139
+ append_file_with_newline "Gemfile", "#{indentation}end", force: true
140
+ end
141
+ end
142
+
143
+ # Add the given source to +Gemfile+
144
+ #
145
+ # If block is given, gem entries in block are wrapped into the source group.
146
+ #
147
+ # add_source "http://gems.github.com/"
148
+ #
149
+ # add_source "http://gems.github.com/" do
150
+ # gem "rspec-rails"
151
+ # end
152
+ def add_source(source, options = {}, &block)
153
+ log :source, source
154
+
155
+ in_root do
156
+ if block
157
+ append_file_with_newline "Gemfile", "\nsource #{quote(source)} do", force: true
158
+ with_indentation(&block)
159
+ append_file_with_newline "Gemfile", "end", force: true
160
+ else
161
+ prepend_file "Gemfile", "source #{quote(source)}\n", verbose: false
162
+ end
163
+ end
164
+ end
165
+
166
+ # Adds a line inside the Application class for <tt>config/application.rb</tt>.
167
+ #
168
+ # If options <tt>:env</tt> is specified, the line is appended to the corresponding
169
+ # file in <tt>config/environments</tt>.
170
+ #
171
+ # environment do
172
+ # "config.asset_host = 'cdn.provider.com'"
173
+ # end
174
+ #
175
+ # environment(nil, env: "development") do
176
+ # "config.asset_host = 'localhost:3000'"
177
+ # end
178
+ def environment(data = nil, options = {})
179
+ sentinel = "class Application < Rails::Application\n"
180
+ env_file_sentinel = "Rails.application.configure do\n"
181
+ data ||= yield if block_given?
182
+
183
+ in_root do
184
+ if options[:env].nil?
185
+ inject_into_file "config/application.rb", optimize_indentation(data, 4), after: sentinel, verbose: false
186
+ else
187
+ Array(options[:env]).each do |env|
188
+ inject_into_file "config/environments/#{env}.rb", optimize_indentation(data, 2), after: env_file_sentinel, verbose: false
189
+ end
190
+ end
191
+ end
192
+ end
193
+ alias_method :application, :environment
194
+
195
+ # Run a command in git.
196
+ #
197
+ # git :init
198
+ # git add: "this.file that.rb"
199
+ # git add: "onefile.rb", rm: "badfile.cxx"
200
+ def git(commands = {})
201
+ if commands.is_a?(Symbol)
202
+ run "git #{commands}"
203
+ else
204
+ commands.each do |cmd, options|
205
+ run "git #{cmd} #{options}"
206
+ end
207
+ end
208
+ end
209
+
210
+ # Create a new file in the <tt>vendor/</tt> directory. Code can be specified
211
+ # in a block or a data string can be given.
212
+ #
213
+ # vendor("sekrit.rb") do
214
+ # sekrit_salt = "#{Time.now}--#{3.years.ago}--#{rand}--"
215
+ # "salt = '#{sekrit_salt}'"
216
+ # end
217
+ #
218
+ # vendor("foreign.rb", "# Foreign code is fun")
219
+ def vendor(filename, data = nil)
220
+ log :vendor, filename
221
+ data ||= yield if block_given?
222
+ create_file("vendor/#{filename}", optimize_indentation(data), verbose: false)
223
+ end
224
+
225
+ # Create a new file in the <tt>lib/</tt> directory. Code can be specified
226
+ # in a block or a data string can be given.
227
+ #
228
+ # lib("crypto.rb") do
229
+ # "crypted_special_value = '#{rand}--#{Time.now}--#{rand(1337)}--'"
230
+ # end
231
+ #
232
+ # lib("foreign.rb", "# Foreign code is fun")
233
+ def lib(filename, data = nil)
234
+ log :lib, filename
235
+ data ||= yield if block_given?
236
+ create_file("lib/#{filename}", optimize_indentation(data), verbose: false)
237
+ end
238
+
239
+ # Create a new +Rakefile+ with the provided code (either in a block or a string).
240
+ #
241
+ # rakefile("bootstrap.rake") do
242
+ # project = ask("What is the UNIX name of your project?")
243
+ #
244
+ # <<-TASK
245
+ # namespace :#{project} do
246
+ # task :bootstrap do
247
+ # puts "I like boots!"
248
+ # end
249
+ # end
250
+ # TASK
251
+ # end
252
+ #
253
+ # rakefile('seed.rake', 'puts "Planting seeds"')
254
+ def rakefile(filename, data = nil)
255
+ log :rakefile, filename
256
+ data ||= yield if block_given?
257
+ create_file("lib/tasks/#{filename}", optimize_indentation(data), verbose: false)
258
+ end
259
+
260
+ # Create a new initializer with the provided code (either in a block or a string).
261
+ #
262
+ # initializer("globals.rb") do
263
+ # data = ""
264
+ #
265
+ # ['MY_WORK', 'ADMINS', 'BEST_COMPANY_EVAR'].each do |const|
266
+ # data << "#{const} = :entp\n"
267
+ # end
268
+ #
269
+ # data
270
+ # end
271
+ #
272
+ # initializer("api.rb", "API_KEY = '123456'")
273
+ def initializer(filename, data = nil)
274
+ log :initializer, filename
275
+ data ||= yield if block_given?
276
+ create_file("config/initializers/#{filename}", optimize_indentation(data), verbose: false)
277
+ end
278
+
279
+ # Generate something using a generator from Rails or a plugin.
280
+ # The second parameter is the argument string that is passed to
281
+ # the generator or an Array that is joined.
282
+ #
283
+ # generate(:authenticated, "user session")
284
+ def generate(what, *args)
285
+ log :generate, what
286
+
287
+ options = args.extract_options!
288
+ options[:abort_on_failure] = !options[:inline]
289
+
290
+ rails_command "generate #{what} #{args.join(" ")}", options
291
+ end
292
+
293
+ # Runs the supplied rake task (invoked with 'rake ...')
294
+ #
295
+ # rake("db:migrate")
296
+ # rake("db:migrate", env: "production")
297
+ # rake("gems:install", sudo: true)
298
+ # rake("gems:install", capture: true)
299
+ def rake(command, options = {})
300
+ execute_command :rake, command, options
301
+ end
302
+
303
+ # Runs the supplied rake task (invoked with 'rails ...')
304
+ #
305
+ # rails_command("db:migrate")
306
+ # rails_command("db:migrate", env: "production")
307
+ # rails_command("gems:install", sudo: true)
308
+ # rails_command("gems:install", capture: true)
309
+ def rails_command(command, options = {})
310
+ if options[:inline]
311
+ log :rails, command
312
+ command, *args = Shellwords.split(command)
313
+ in_root do
314
+ silence_warnings do
315
+ ::Rails::Command.invoke(command, args, **options)
316
+ end
317
+ end
318
+ else
319
+ execute_command :rails, command, options
320
+ end
321
+ end
322
+
323
+ # Make an entry in Rails routing file <tt>config/routes.rb</tt>
324
+ #
325
+ # route "root 'welcome#index'"
326
+ # route "root 'admin#index'", namespace: :admin
327
+ def route(routing_code, namespace: nil)
328
+ namespace = Array(namespace)
329
+ namespace_pattern = route_namespace_pattern(namespace)
330
+ routing_code = namespace.reverse.reduce(routing_code) do |code, name|
331
+ "namespace :#{name} do\n#{rebase_indentation(code, 2)}end"
332
+ end
333
+
334
+ log :route, routing_code
335
+
336
+ in_root do
337
+ if namespace_match = match_file("config/routes.rb", namespace_pattern)
338
+ base_indent, *, existing_block_indent = namespace_match.captures.compact.map(&:length)
339
+ existing_line_pattern = /^ {,#{existing_block_indent}}\S.+\n?/
340
+ routing_code = rebase_indentation(routing_code, base_indent + 2).gsub(existing_line_pattern, "")
341
+ namespace_pattern = /#{Regexp.escape namespace_match.to_s}/
342
+ end
343
+
344
+ inject_into_file "config/routes.rb", routing_code, after: namespace_pattern, verbose: false, force: false
345
+
346
+ if behavior == :revoke && namespace.any? && namespace_match
347
+ empty_block_pattern = /(#{namespace_pattern})((?:\s*end\n){1,#{namespace.size}})/
348
+ gsub_file "config/routes.rb", empty_block_pattern, verbose: false, force: true do |matched|
349
+ beginning, ending = empty_block_pattern.match(matched).captures
350
+ ending.sub!(/\A\s*end\n/, "") while !ending.empty? && beginning.sub!(/^ *namespace .+ do\n\s*\z/, "")
351
+ beginning + ending
352
+ end
353
+ end
354
+ end
355
+ end
356
+
357
+ # Reads the given file at the source root and prints it in the console.
358
+ #
359
+ # readme "README"
360
+ def readme(path)
361
+ log File.read(find_in_source_paths(path))
362
+ end
363
+
364
+ private
365
+
366
+ # Define log for backwards compatibility. If just one argument is sent,
367
+ # invoke say, otherwise invoke say_status. Differently from say and
368
+ # similarly to say_status, this method respects the quiet? option given.
369
+ def log(*args) # :doc:
370
+ if args.size == 1
371
+ say args.first.to_s unless options.quiet?
372
+ else
373
+ args << ((behavior == :invoke) ? :green : :red)
374
+ say_status(*args)
375
+ end
376
+ end
377
+
378
+ # Runs the supplied command using either "rake ..." or "rails ..."
379
+ # based on the executor parameter provided.
380
+ def execute_command(executor, command, options = {}) # :doc:
381
+ log executor, command
382
+ sudo = (options[:sudo] && !Gem.win_platform?) ? "sudo " : ""
383
+ config = {
384
+ env: {"RAILS_ENV" => (options[:env] || ENV["RAILS_ENV"] || "development")},
385
+ verbose: false,
386
+ capture: options[:capture],
387
+ abort_on_failure: options[:abort_on_failure]
388
+ }
389
+
390
+ in_root { run("#{sudo}#{Shellwords.escape Gem.ruby} bin/#{executor} #{command}", config) }
391
+ end
392
+
393
+ # Always returns value in double quotes.
394
+ def quote(value) # :doc:
395
+ if value.respond_to? :each_pair
396
+ return value.map do |k, v|
397
+ "#{k}: #{quote(v)}"
398
+ end.join(", ")
399
+ end
400
+ return value.inspect unless value.is_a? String
401
+
402
+ "\"#{value.tr("'", '"')}\""
403
+ end
404
+
405
+ # Returns optimized string with indentation
406
+ def optimize_indentation(value, amount = 0) # :doc:
407
+ return "#{value}\n" unless value.is_a?(String)
408
+ "#{value.strip_heredoc.indent(amount).chomp}\n"
409
+ end
410
+ alias_method :rebase_indentation, :optimize_indentation
411
+
412
+ # Indent the +Gemfile+ to the depth of @indentation
413
+ def indentation # :doc:
414
+ " " * @indentation
415
+ end
416
+
417
+ # Manage +Gemfile+ indentation for a DSL action block
418
+ def with_indentation(&block) # :doc:
419
+ @indentation += 1
420
+ instance_eval(&block)
421
+ ensure
422
+ @indentation -= 1
423
+ end
424
+
425
+ # Append string to a file with a newline if necessary
426
+ def append_file_with_newline(path, str, options = {})
427
+ gsub_file path, /\n?\z/, options do |match|
428
+ match.end_with?("\n") ? "" : "\n#{str}\n"
429
+ end
430
+ end
431
+
432
+ def match_file(path, pattern)
433
+ File.read(path).match(pattern) if File.exist?(path)
434
+ end
435
+
436
+ def route_namespace_pattern(namespace)
437
+ namespace.each_with_index.reverse_each.reduce(nil) do |pattern, (name, i)|
438
+ cummulative_margin = "\\#{i + 1}[ ]{2}"
439
+ blank_or_indented_line = "^[ ]*\n|^#{cummulative_margin}.*\n"
440
+ "(?:(?:#{blank_or_indented_line})*?^(#{cummulative_margin})namespace :#{name} do\n#{pattern})?"
441
+ end.then do |pattern|
442
+ /^( *).+\.routes\.draw do *\n#{pattern}/
443
+ end
444
+ end
445
+ end
446
+ end
447
+
448
+
449
+ include Thor::Actions
450
+ include Rails::Actions
451
+
452
+ # Custom methods defined on AppGenerator
453
+ # https://github.com/rails/rails/blob/38275763257221e381cd4e37958ce1413fd0433c/railties/lib/rails/generators/rails/app/app_generator.rb#L558
454
+ def file(*args, &block)
455
+ create_file(*args, &block)
456
+ end
457
+ end
458
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyBytes # :nodoc:
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,21 @@
1
+ # Rails core extensions can be used in templates.
2
+ # Let's try to load ActiveSupport first and fallback to our custom set of extensions otherwise.
3
+ begin
4
+ require "active_support"
5
+ require "active_support/core_ext"
6
+ rescue LoadError
7
+ # TODO: Add more extensions
8
+ class ::String
9
+ def parameterize
10
+ gsub("::", "/").gsub(/([a-z])([A-Z])/, '\1-\2').downcase
11
+ end
12
+
13
+ def underscore
14
+ gsub("::", "/").gsub(/([a-z])([A-Z])/, '\1_\2').downcase
15
+ end
16
+ end
17
+
18
+ class ::Object
19
+ alias empty? nil?
20
+ end
21
+ end