module Chusaku

Handles core functionality of annotating projects.

Constants

VERSION

Public Class Methods

call(flags = {}) click to toggle source

The main method to run Chusaku. Annotate all actions in a Rails project as follows:

# @route GET /waterlilies/:id (waterlilies)
def show
  # ...
end

@param flags [Hash] CLI flags @return [Integer] 0 on success, 1 on error

# File lib/chusaku.rb, line 20
def call(flags = {})
  @flags = flags
  @routes = Chusaku::Routes.call
  @annotated_paths = []
  controllers_pattern = 'app/controllers/**/*_controller.rb'

  Dir.glob(Rails.root.join(controllers_pattern)).each do |path|
    controller = %r{controllers\/(.*)_controller\.rb}.match(path)[1]
    actions = @routes[controller]
    next if actions.nil?

    annotate_file(path: path, controller: controller, actions: actions.keys)
  end

  output_results
end

Private Class Methods

annotate_file(path:, controller:, actions:) click to toggle source

Adds annotations to the given file.

@param path [String] Path to file @param controller [String] Controller name @param actions [Array<String>] List of valid actions for the controller @return [void]

# File lib/chusaku.rb, line 45
def annotate_file(path:, controller:, actions:)
  parsed_file = Chusaku::Parser.call(path: path, actions: actions)
  parsed_file[:groups].each_cons(2) do |prev, curr|
    clean_group(prev)
    next unless curr[:type] == :action

    route_data = @routes[controller][curr[:action]]
    next unless route_data.any?

    annotate_group(group: curr, route_data: route_data)
  end

  write_to_file(path: path, parsed_file: parsed_file)
end
annotate_group(group:, route_data:) click to toggle source

Add an annotation to the given group given by Chusaku::Parser that looks like:

@route GET /waterlilies/:id (waterlilies)

@param group [Hash] Parsed content given by Chusaku::Parser @param route_data [Hash] Individual route data given by Chusaku::Routes @return [void]

# File lib/chusaku.rb, line 80
def annotate_group(group:, route_data:)
  whitespace = /^(\s*).*$/.match(group[:body])[1]
  route_data.reverse_each do |datum|
    comment = "#{whitespace}# #{annotate_route(**datum)}\n"
    group[:body] = comment + group[:body]
  end
end
annotate_route(verb:, path:, name:, defaults:) click to toggle source

Generate route annotation.

@param verb [String] HTTP verb for route @param path [String] Rails path for route @param name [String] Name used in route helpers @param defaults [Hash] Default parameters for route @return [String] “@route <verb> <path> {<defaults>} (<name>)”

# File lib/chusaku.rb, line 95
def annotate_route(verb:, path:, name:, defaults:)
  annotation = "@route #{verb} #{path}"
  if defaults&.any?
    defaults_str =
      defaults
      .map { |key, value| "#{key}: #{value.inspect}" }
      .join(', ')
    annotation += " {#{defaults_str}}"
  end
  annotation += " (#{name})" unless name.nil?
  annotation
end
clean_group(group) click to toggle source

Given a parsed group, clean out its contents.

@param group [Hash] { type => Symbol, body => String } @return {void}

# File lib/chusaku.rb, line 64
def clean_group(group)
  return unless group[:type] == :comment

  group[:body] = group[:body].gsub(/^\s*#\s*@route.*$\n/, '')
  group[:body] =
    group[:body].gsub(%r{^\s*# (GET|POST|PATCH\/PUT|DELETE) \/\S+$\n}, '')
end
file_mode() click to toggle source

When running the test suite, we want to make sure we're not overwriting any files. `r` mode ensures that, and `w` is used for actual usage.

@return [String] 'r' or 'w'

# File lib/chusaku.rb, line 149
def file_mode
  File.instance_methods.include?(:test_write) ? 'r' : 'w'
end
new_content_for(parsed_file) click to toggle source

Extracts the new file content for the given parsed file.

@param parsed_file [Hash] { groups => Array<Hash> } @return [String] New file content

# File lib/chusaku.rb, line 125
def new_content_for(parsed_file)
  parsed_file[:groups].map { |pf| pf[:body] }.join
end
output_copy() click to toggle source

Determines the copy to be used in the program output.

@return [String] Copy to be outputted to user

# File lib/chusaku.rb, line 166
    def output_copy
      return 'Nothing to annotate.' if @annotated_paths.empty?

      annotated_paths = @annotated_paths.join(', ')
      dry_run = @flags.include?(:dry)
      error_on_annotation = @flags.include?(:error_on_annotation)

      if dry_run && error_on_annotation
        <<~COPY
          Annotations missing in the following files: #{annotated_paths}

          Run `chusaku` to annotate them. Exiting with status code 1.
        COPY
      elsif dry_run
        "The following files would be annotated without `--dry-run`: #{annotated_paths}"
      elsif error_on_annotation
        "Annotated #{annotated_paths}.\n\nExiting with status code 1."
      else
        "Annotated #{annotated_paths}."
      end
    end
output_results() click to toggle source

Output results to user.

@return [Integer] 0 for success, 1 for error

# File lib/chusaku.rb, line 156
def output_results
  puts(output_copy)
  exit_code = 0
  exit_code = 1 if @annotated_paths.any? && @flags.include?(:error_on_annotation)
  exit_code
end
perform_write(path:, content:) click to toggle source

Wraps the write operation. Needed to clearly distinguish whether it's a write in the test suite or a write in actual use.

@param path [String] File path @param content [String] File content @return [void]

# File lib/chusaku.rb, line 135
def perform_write(path:, content:)
  File.open(path, file_mode) do |file|
    if file.respond_to?(:test_write)
      file.test_write(content, path)
    else
      file.write(content)
    end
  end
end
write_to_file(path:, parsed_file:) click to toggle source

Write annotated content to a file if it differs from the original.

@param path [String] File path to write to @param parsed_file [Hash] Hash mutated by {#annotate_group} @return [void]

# File lib/chusaku.rb, line 113
def write_to_file(path:, parsed_file:)
  new_content = new_content_for(parsed_file)
  return unless parsed_file[:content] != new_content

  !@flags.include?(:dry) && perform_write(path: path, content: new_content)
  @annotated_paths.push(path)
end