Module: AnnotateRoutes

Defined in:
lib/annotate/annotate_routes.rb

Overview

Annotate Routes

Based on:

Prepends the output of “rake routes” to the top of your routes.rb file. Yes, it’s simple but I’m thick and often need a reminder of what my routes mean.

Running this task will replace any exising route comment generated by the task. Best to back up your routes file before running:

Author:

Gavin Montague
gavin@leftbrained.co.uk

Released under the same license as Ruby. No Support. No Warranty.

Constant Summary collapse

PREFIX =
'== Route Map'.freeze
PREFIX_MD =
'## Route Map'.freeze
HEADER_ROW =
['Prefix', 'Verb', 'URI Pattern', 'Controller#Action']

Class Method Summary collapse

Class Method Details

.annotate_routes(header, content, where_header_found, options = {}) ⇒ Object



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/annotate/annotate_routes.rb', line 175

def self.annotate_routes(header, content, where_header_found, options = {})
  magic_comments_map, content = extract_magic_comments_from_array(content)
  if %w(before top).include?(options[:position_in_routes])
    header = header << '' if content.first != ''
    magic_comments_map << '' if magic_comments_map.any?
    new_content = magic_comments_map + header + content
  else
    # Ensure we have adequate trailing newlines at the end of the file to
    # ensure a blank line separating the content from the annotation.
    content << '' unless content.last == ''

    # We're moving something from the top of the file to the bottom, so ditch
    # the spacer we put in the first time around.
    content.shift if where_header_found == :before && content.first == ''

    new_content = magic_comments_map + content + header
  end

  new_content
end

.app_routes_map(options) ⇒ Object



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/annotate/annotate_routes.rb', line 115

def self.app_routes_map(options)
  routes_map = `rake routes`.chomp("\n").split(/\n/, -1)

  # In old versions of Rake, the first line of output was the cwd.  Not so
  # much in newer ones.  We ditch that line if it exists, and if not, we
  # keep the line around.
  routes_map.shift if routes_map.first =~ /^\(in \//

  # Skip routes which match given regex
  # Note: it matches the complete line (route_name, path, controller/action)
  if options[:ignore_routes]
    routes_map.reject! { |line| line =~ /#{options[:ignore_routes]}/ }
  end

  routes_map
end

.content(line, maxs, options = {}) ⇒ Object



28
29
30
31
32
33
34
35
36
# File 'lib/annotate/annotate_routes.rb', line 28

def content(line, maxs, options = {})
  return line.rstrip unless options[:format_markdown]

  line.each_with_index.map do |elem, index|
    min_length = maxs.map { |arr| arr[index] }.max || 0

    sprintf("%-#{min_length}.#{min_length}s", elem.tr('|', '-'))
  end.join(' | ')
end

.do_annotations(options = {}) ⇒ Object



73
74
75
76
77
78
79
80
# File 'lib/annotate/annotate_routes.rb', line 73

def do_annotations(options = {})
  return unless routes_exists?
  existing_text = File.read(routes_file)

  if rewrite_contents_with_header(existing_text, header(options), options)
    puts "#{routes_file} annotated."
  end
end

.extract_magic_comments_from_array(content_array) ⇒ Array<String>

Parameters:

  • content (Array<String>)

Returns:

  • (Array<String>)

    all found magic comments

  • (Array<String>)

    content without magic comments



100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/annotate/annotate_routes.rb', line 100

def self.extract_magic_comments_from_array(content_array)
  magic_comments = []
  new_content = []

  content_array.map do |row|
    if row =~ magic_comment_matcher
      magic_comments << row.strip
    else
      new_content << row
    end
  end

  [magic_comments, new_content]
end

.header(options = {}) ⇒ Object



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/annotate/annotate_routes.rb', line 38

def header(options = {})
  routes_map = app_routes_map(options)

  magic_comments_map, routes_map = extract_magic_comments_from_array(routes_map)

  out = []

  magic_comments_map.each do |magic_comment|
    out << magic_comment
  end
  out << '' if magic_comments_map.any?

  out += ["# #{options[:wrapper_open]}"] if options[:wrapper_open]

  out += ["# #{options[:format_markdown] ? PREFIX_MD : PREFIX}" + (options[:timestamp] ? " (Updated #{Time.now.strftime('%Y-%m-%d %H:%M')})" : '')]
  out += ['#']
  return out if routes_map.size.zero?

  maxs = [HEADER_ROW.map(&:size)] + routes_map[1..-1].map { |line| line.split.map(&:size) }

  if options[:format_markdown]
    max = maxs.map(&:max).compact.max

    out += ["# #{content(HEADER_ROW, maxs, options)}"]
    out += ["# #{content(['-' * max, '-' * max, '-' * max, '-' * max], maxs, options)}"]
  else
    out += ["# #{content(routes_map[0], maxs, options)}"]
  end

  out += routes_map[1..-1].map { |line| "# #{content(options[:format_markdown] ? line.split(' ') : line, maxs, options)}" }
  out += ["# #{options[:wrapper_close]}"] if options[:wrapper_close]

  out
end

.magic_comment_matcherObject



93
94
95
# File 'lib/annotate/annotate_routes.rb', line 93

def self.magic_comment_matcher
  Regexp.new(/(^#\s*encoding:.*)|(^# coding:.*)|(^# -\*- coding:.*)|(^# -\*- encoding\s?:.*)|(^#\s*frozen_string_literal:.+)|(^# -\*- frozen_string_literal\s*:.+-\*-)/)
end

.remove_annotations(_options = {}) ⇒ Object



82
83
84
85
86
87
88
89
90
# File 'lib/annotate/annotate_routes.rb', line 82

def remove_annotations(_options={})
  return unless routes_exists?
  existing_text = File.read(routes_file)
  content, where_header_found = strip_annotations(existing_text)
  new_content = strip_on_removal(content, where_header_found)
  if rewrite_contents(existing_text, new_content)
    puts "Removed annotations from #{routes_file}."
  end
end

.rewrite_contents(existing_text, new_content) ⇒ Object

Parameters:

  • (String, Array<String>)


144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/annotate/annotate_routes.rb', line 144

def self.rewrite_contents(existing_text, new_content)
  # Make sure we end on a trailing newline.
  new_content << '' unless new_content.last == ''
  new_text = new_content.join("\n")

  if existing_text == new_text
    puts "#{routes_file} unchanged."
    false
  else
    File.open(routes_file, 'wb') { |f| f.puts(new_text) }
    true
  end
end

.rewrite_contents_with_header(existing_text, header, options = {}) ⇒ Object



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/annotate/annotate_routes.rb', line 158

def self.rewrite_contents_with_header(existing_text, header, options = {})
  content, where_header_found = strip_annotations(existing_text)
  new_content = annotate_routes(header, content, where_header_found, options)

  # Make sure we end on a trailing newline.
  new_content << '' unless new_content.last == ''
  new_text = new_content.join("\n")

  if existing_text == new_text
    puts "#{routes_file} unchanged."
    false
  else
    File.open(routes_file, 'wb') { |f| f.puts(new_text) }
    true
  end
end

.routes_exists?Boolean

Returns:

  • (Boolean)


136
137
138
139
140
141
# File 'lib/annotate/annotate_routes.rb', line 136

def self.routes_exists?
  routes_exists = File.exists?(routes_file)
  puts "Can't find routes.rb" unless routes_exists

  routes_exists
end

.routes_fileObject



132
133
134
# File 'lib/annotate/annotate_routes.rb', line 132

def self.routes_file
  @routes_rb ||= File.join('config', 'routes.rb')
end

.strip_annotations(content) ⇒ Object

TODO: write the method doc using ruby rdoc formats where_header_found => This will either be :before, :after, or a number. If the number is > 0, the annotation was found somewhere in the middle of the file. If the number is zero, no annotation was found.



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/annotate/annotate_routes.rb', line 202

def self.strip_annotations(content)
  real_content = []
  mode = :content
  header_found_at = 0

  content.split(/\n/, -1).each_with_index do |line, line_number|
    if mode == :header && line !~ /\s*#/
      mode = :content
      real_content << line unless line.blank?
    elsif mode == :content
      if line =~ /^\s*#\s*== Route.*$/
        header_found_at = line_number + 1 # index start's at 0
        mode = :header
      else
        real_content << line
      end
    end
  end

  where_header_found(real_content, header_found_at)
end

.strip_on_removal(content, where_header_found) ⇒ Object



237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/annotate/annotate_routes.rb', line 237

def self.strip_on_removal(content, where_header_found)
  if where_header_found == :before
    content.shift while content.first == ''
  elsif where_header_found == :after
    content.pop while content.last == ''
  end

  # TODO: If the user buried it in the middle, we should probably see about
  # TODO: preserving a single line of space between the content above and
  # TODO: below...
  content
end

.where_header_found(real_content, header_found_at) ⇒ Object



224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/annotate/annotate_routes.rb', line 224

def self.where_header_found(real_content, header_found_at)
  # By default assume the annotation was found in the middle of the file

  # ... unless we have evidence it was at the beginning ...
  return real_content, :before if header_found_at == 1

  # ... or that it was at the end.
  return real_content, :after if header_found_at >= real_content.count

  # and the default
  return real_content, header_found_at
end