class RSpecJumpstart::Generator

Constants

RAILS_RESOURCE_METHOD_AND_HTTP_METHOD

Attributes

delta_template[RW]
full_template[RW]
spec_dir[RW]

Public Class Methods

new(spec_dir = './spec', delta_template = nil, full_template = nil) click to toggle source
# File lib/rspec_jumpstart/generator.rb, line 18
def initialize(spec_dir = './spec', delta_template = nil, full_template = nil)
  @spec_dir       = spec_dir.gsub(/\/$/, '')
  @delta_template = delta_template
  @full_template  = full_template
end

Public Instance Methods

append_to_existing_spec(class_or_module, dry_run, rails_mode, file_path, spec_path) click to toggle source

Appends new tests to the existing spec.

rubocop:disable Metrics/AbcSize

# File lib/rspec_jumpstart/generator.rb, line 176
def append_to_existing_spec(class_or_module, dry_run, rails_mode, file_path, spec_path)
  existing_spec = File.read(spec_path)
  if skip?(existing_spec)
    return
  end
  lacking_methods = public_methods_found(class_or_module).
    reject { |m| existing_spec.match(signature(m)) }

  scope_methods_to_generate = scopes(class_or_module, file_path, spec_path)
  if lacking_methods.empty? && scope_methods_to_generate.empty?
    # puts yellow("#{spec_path} skipped.")
  else
    # These names are used in ERB template, don't delete.
    methods_to_generate = lacking_methods
    c                   = class_or_module
    # rubocop:enable Lint/UselessAssignment

    erb             = RSpecJumpstart::ERBFactory.new(@delta_template).get_instance_for_appending(rails_mode, spec_path)
    additional_spec = erb.result(binding).strip

    last_end_not_found = true
    code               = existing_spec.split("\n").reverse.reject do |line|
      before_modified    = last_end_not_found
      last_end_not_found = line.gsub(/#.+$/, '').strip != 'end' if before_modified
      before_modified
    end.reverse.join("\n")

    unless additional_spec.empty?
      code += "\n" + additional_spec + "\n"
    end

    code += "\nend\n"

    if dry_run
      puts "----- #{spec_path} -----"
      puts code
    else
      File.open(spec_path, 'w') { |f| f.write(code) }
    end
    puts green("#{spec_path} modified.")
  end

  code
end
create_new_spec(class_or_module, dry_run, rails_mode, file_path, spec_path) click to toggle source

Creates new spec.

rubocop:disable Metrics/AbcSize

# File lib/rspec_jumpstart/generator.rb, line 137
def create_new_spec(class_or_module, dry_run, rails_mode, file_path, spec_path)
  # These names are used in ERB template, don't delete.
  methods_to_generate       = public_methods_found(class_or_module)
  scope_methods_to_generate = scopes(class_or_module, file_path, spec_path)
  c                         = class_or_module
  self_path                 = to_string_value_to_require(file_path)
  # rubocop:enable Lint/UselessAssignment

  erb  = RSpecJumpstart::ERBFactory.
    new(@full_template).
    get_instance_for_new_spec(rails_mode, file_path)
  code = erb.result(binding)

  if dry_run
    puts "----- #{spec_path} -----"
    puts code
  elsif File.exist?(spec_path)
    # puts yellow("#{spec_path} already exists.")
  else
    FileUtils.mkdir_p(File.dirname(spec_path))
    File.open(spec_path, 'w') { |f| f.write(code) }
    puts green("#{spec_path} created.")
  end

  code
end
decorated_name(method) click to toggle source
# File lib/rspec_jumpstart/generator.rb, line 74
def decorated_name(method)
  (method.singleton ? '.' : '#') + method.name
end
get_block_code(method) click to toggle source

e.g. { |a, b| }

# File lib/rspec_jumpstart/generator.rb, line 284
def get_block_code(method)
  return '' if method.block_params.nil? || method.block_params.empty?

  " { |#{method.block_params}| }"
end
get_complete_class_name(class_or_module, name = class_or_module.name) click to toggle source

Gets the complete class name from RDoc::NormalClass/RDoc::NormalModule instance.

# File lib/rspec_jumpstart/generator.rb, line 52
def get_complete_class_name(class_or_module, name = class_or_module.name)
  if class_or_module.parent.name && class_or_module.parent.is_a?(RDoc::NormalModule)
    get_complete_class_name(class_or_module.parent, "#{class_or_module.parent.name}::#{name}")
  else
    name
  end
end
get_instantiation_code(c, method) click to toggle source

e.g.

a = double('a')
b = double('b')
bar_baz = BarBaz.new(a, b)
# File lib/rspec_jumpstart/generator.rb, line 241
def get_instantiation_code(c, method)
  return '' if method.singleton

  constructor = c.method_list.find { |m| m.name == 'new' }
  if constructor.nil?
    "      #{instance_name(c)} = described_class.new\n"
  else
    get_params_initialization_code(constructor) +
      "      #{instance_name(c)} = described_class.new#{to_params_part(constructor.params)}\n"
  end
end
get_method_invocation_code(c, method) click to toggle source

e.g. BarBaz.do_something(a, b) { |c| }

# File lib/rspec_jumpstart/generator.rb, line 269
def get_method_invocation_code(c, method)
  target = method.singleton ? 'described_class' : instance_name(c)
  "#{target}.#{method.name}#{to_params_part(method.params)}#{get_block_code(method)}"
end
get_params_initialization_code(method) click to toggle source

e.g.

a = double('a')
b = double('b')
# File lib/rspec_jumpstart/generator.rb, line 258
def get_params_initialization_code(method)
  code = to_param_names_array(method.params).map do |p|
    x = p.sub('*', '').sub('&', '')
    "      #{x} = double('#{x}')" unless x.empty?
  end.compact.join("\n")
  code.empty? ? '' : "#{code}\n"
end
get_rails_helper_method_invocation_code(method) click to toggle source

e.g. do_something(a, b) { |c| }

# File lib/rspec_jumpstart/generator.rb, line 277
def get_rails_helper_method_invocation_code(method)
  "#{method.name}#{to_params_part(method.params)}#{get_block_code(method)}"
end
get_rails_http_method(method_name) click to toggle source
# File lib/rspec_jumpstart/generator.rb, line 290
def get_rails_http_method(method_name)
  RAILS_RESOURCE_METHOD_AND_HTTP_METHOD[method_name] || 'get'
end
get_spec_path(file_path) click to toggle source

Returns spec file path. e.g. “lib/foo/bar_baz.rb” -> “spec/foo/bar_baz_spec.rb”

# File lib/rspec_jumpstart/generator.rb, line 82
def get_spec_path(file_path)
  spec_dir + '/' +
    file_path
      .gsub(/^\.\//, '')
      .gsub(%r{^(lib/)|(app/)}, '')
      .sub(/\.rb$/, '_spec.rb')
end
instance_name(c) click to toggle source

Returns snake_case name. e.g. FooBar -> “foo_bar”

# File lib/rspec_jumpstart/generator.rb, line 102
def instance_name(c)
  c.name.
    gsub(/::/, '/').
    gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').
    gsub(/([a-z\d])([A-Z])/, '\1_\2').
    tr('-', '_').
    downcase
end
public_methods_found(class_or_module) click to toggle source
# File lib/rspec_jumpstart/generator.rb, line 164
def public_methods_found(class_or_module)
  class_or_module.method_list.select do |m|
    m.visibility.equal?(:public) && m.name != 'new'
  end
end
skip?(text) click to toggle source
# File lib/rspec_jumpstart/generator.rb, line 221
def skip?(text)
  RSpecJumpstart.config.behaves_like_exclusions.each do |exclude_pattern|
    return true if text.match(exclude_pattern)
  end

  false
end
to_param_names_array(params) click to toggle source

Extracts parameter names as an Array. e.g. “()” -> [] e.g. “(a, b = 'foo')” -> [“a”, “b”]

# File lib/rspec_jumpstart/generator.rb, line 116
def to_param_names_array(params)
  params.
    split(',').
    map { |p| p.gsub(/[()\s]/, '').gsub(/=.+$/, '') }.
    reject { |p| p.nil? || p.empty? }
end
to_params_part(params) click to toggle source

Returns params part e.g. [“a”,“b”] -> “(a, b)” e.g. [] -> “”

# File lib/rspec_jumpstart/generator.rb, line 128
def to_params_part(params)
  param_csv = to_param_names_array(params).join(', ')
  param_csv.empty? ? '' : "(#{param_csv})"
end
to_string_namespaced_path(self_path) click to toggle source
# File lib/rspec_jumpstart/generator.rb, line 60
def to_string_namespaced_path(self_path)
  path = self_path.split('/').map { |x| camelize(x) }[1..-2].uniq.join('::')
  path.empty? ? '' : path + '::'
end
to_string_namespaced_path_whole(self_path) click to toggle source
# File lib/rspec_jumpstart/generator.rb, line 65
def to_string_namespaced_path_whole(self_path)
  self_path.
    sub('.rb', '').
    split('/').
    map { |x| camelize(x) }[2..-1].
    uniq.
    join('::')
end
to_string_value_to_require(file_path) click to toggle source

Returns string value to require. e.g. “lib/foo/bar_baz.rb” -> “foo/bar_baz”

# File lib/rspec_jumpstart/generator.rb, line 94
def to_string_value_to_require(file_path)
  file_path.gsub(%r{^(lib/)|(app/)}, '').gsub(/\.rb$/, '')
end
write_spec(file_path, force_write = false, dry_run = false, rails_mode = false) click to toggle source

Writes new spec or appends to the existing spec.

# File lib/rspec_jumpstart/generator.rb, line 27
def write_spec(file_path, force_write = false, dry_run = false, rails_mode = false)
  begin
    code            = ''
    class_or_module = RSpecJumpstart::RDocFactory.get_rdoc_class_or_module(file_path)
    if class_or_module
      spec_path = get_spec_path(file_path)

      code = if force_write && File.exist?(spec_path)
               append_to_existing_spec(class_or_module, dry_run, rails_mode, file_path, spec_path)
             else
               create_new_spec(class_or_module, dry_run, rails_mode, file_path, spec_path)
             end
    else
      puts red("#{file_path} skipped (Class/Module not found).")
    end
  rescue StandardError => e
    puts red("#{file_path} aborted - #{e.message}")
  end

  code
end

Private Instance Methods

blue(text) click to toggle source
# File lib/rspec_jumpstart/generator.rb, line 371
def blue(text)
  colorize(text, "\033[34m")
end
camelize(str) click to toggle source
# File lib/rspec_jumpstart/generator.rb, line 379
def camelize(str)
  str.split('_').map { |w| w.capitalize }.join
end
colorize(text, color_code) click to toggle source
# File lib/rspec_jumpstart/generator.rb, line 359
def colorize(text, color_code)
  "#{color_code}#{text}\033[0m"
end
green(text) click to toggle source
# File lib/rspec_jumpstart/generator.rb, line 367
def green(text)
  colorize(text, "\033[32m")
end
parse_sexp(sexp, scopes, methods, stack = []) click to toggle source
# File lib/rspec_jumpstart/generator.rb, line 296
def parse_sexp(sexp, scopes, methods, stack = [])
  case sexp[0]
    when :module
      parse_sexp(sexp[2], scopes, methods, stack + [sexp[0], sexp[1][1][1]])

    when :vcall
      name = sexp[1][1]
      return if name.eql?('private')

    when :command
      if sexp[1][0] == :@ident && sexp[1][1] == 'scope'
        name = sexp[2][1][0][1][1][1]
        scopes << name
      end

    when :class
      parse_sexp(sexp[3], scopes, methods, stack + [sexp[0], sexp[1][1][1]])

    when :def
      name = sexp[1][1]
      # line_number = sexp[1][2][0]

      parse_sexp(sexp[3], scopes, methods, stack + [sexp[0], sexp[1][1]])

      # puts "#{line_number}: Method: #{stack.last}##{name}\n"
      methods << name
    else
      if sexp.is_a?(Array)
        sexp.each { |s| parse_sexp(s, scopes, methods, stack) if s.is_a?(Array) }
      end
  end
end
red(text) click to toggle source
# File lib/rspec_jumpstart/generator.rb, line 363
def red(text)
  colorize(text, "\033[31m")
end
scopes(_klass, file_path, spec_path) click to toggle source
# File lib/rspec_jumpstart/generator.rb, line 331
def scopes(_klass, file_path, spec_path)
  content      = File.read(file_path)
  spec_content = (
  begin
    File.read(spec_path)
  rescue
    ''
  end)
  sexp         = Ripper.sexp(content)
  methods      = []
  scopes       = []

  parse_sexp(sexp, scopes, methods)

  scope_methods = []
  scopes.each do |method|
    unless spec_content.include?("'.#{method}'")
      scope_methods << method
    end
  end

  scope_methods
end
signature(method) click to toggle source
# File lib/rspec_jumpstart/generator.rb, line 355
def signature(method)
  "'#{decorated_name(method).sub('?', '\?').gsub('[', '\[').gsub(']', '\]')}'"
end
yellow(text) click to toggle source
# File lib/rspec_jumpstart/generator.rb, line 375
def yellow(text)
  colorize(text, "\033[33m")
end