annotaterb 4.0.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +0 -0
  3. data/LICENSE.txt +55 -0
  4. data/README.md +91 -0
  5. data/VERSION +1 -0
  6. data/exe/annotaterb +21 -0
  7. data/lib/annotate_rb/active_record_patch.rb +9 -0
  8. data/lib/annotate_rb/commands/annotate_models.rb +22 -0
  9. data/lib/annotate_rb/commands/annotate_routes.rb +19 -0
  10. data/lib/annotate_rb/commands/print_help.rb +16 -0
  11. data/lib/annotate_rb/commands/print_version.rb +12 -0
  12. data/lib/annotate_rb/commands.rb +10 -0
  13. data/lib/annotate_rb/config_finder.rb +21 -0
  14. data/lib/annotate_rb/config_loader.rb +63 -0
  15. data/lib/annotate_rb/core.rb +23 -0
  16. data/lib/annotate_rb/eager_loader.rb +23 -0
  17. data/lib/annotate_rb/env.rb +30 -0
  18. data/lib/annotate_rb/model_annotator/annotation_pattern_generator.rb +19 -0
  19. data/lib/annotate_rb/model_annotator/annotator.rb +74 -0
  20. data/lib/annotate_rb/model_annotator/bad_model_file_error.rb +11 -0
  21. data/lib/annotate_rb/model_annotator/constants.rb +22 -0
  22. data/lib/annotate_rb/model_annotator/file_annotation_remover.rb +25 -0
  23. data/lib/annotate_rb/model_annotator/file_annotator.rb +79 -0
  24. data/lib/annotate_rb/model_annotator/file_name_resolver.rb +16 -0
  25. data/lib/annotate_rb/model_annotator/file_patterns.rb +129 -0
  26. data/lib/annotate_rb/model_annotator/helper.rb +54 -0
  27. data/lib/annotate_rb/model_annotator/model_class_getter.rb +63 -0
  28. data/lib/annotate_rb/model_annotator/model_file_annotator.rb +118 -0
  29. data/lib/annotate_rb/model_annotator/model_files_getter.rb +62 -0
  30. data/lib/annotate_rb/model_annotator/pattern_getter.rb +27 -0
  31. data/lib/annotate_rb/model_annotator/schema_info.rb +480 -0
  32. data/lib/annotate_rb/model_annotator.rb +20 -0
  33. data/lib/annotate_rb/options.rb +204 -0
  34. data/lib/annotate_rb/parser.rb +385 -0
  35. data/lib/annotate_rb/rake_bootstrapper.rb +34 -0
  36. data/lib/annotate_rb/route_annotator/annotation_processor.rb +56 -0
  37. data/lib/annotate_rb/route_annotator/annotator.rb +40 -0
  38. data/lib/annotate_rb/route_annotator/base_processor.rb +104 -0
  39. data/lib/annotate_rb/route_annotator/header_generator.rb +113 -0
  40. data/lib/annotate_rb/route_annotator/helper.rb +104 -0
  41. data/lib/annotate_rb/route_annotator/removal_processor.rb +40 -0
  42. data/lib/annotate_rb/route_annotator.rb +12 -0
  43. data/lib/annotate_rb/runner.rb +34 -0
  44. data/lib/annotate_rb/tasks/annotate_models_migrate.rake +30 -0
  45. data/lib/annotate_rb.rb +30 -0
  46. data/lib/generators/annotate_rb/USAGE +4 -0
  47. data/lib/generators/annotate_rb/install_generator.rb +15 -0
  48. data/lib/generators/annotate_rb/templates/auto_annotate_models.rake +7 -0
  49. metadata +96 -0
@@ -0,0 +1,385 @@
1
+ require 'optparse'
2
+
3
+ module AnnotateRb
4
+ # Class for handling command line arguments
5
+ class Parser # rubocop:disable Metrics/ClassLength
6
+ def self.parse(args, existing_options = {})
7
+ new(args, existing_options).parse
8
+ end
9
+
10
+ BANNER_STRING = <<~BANNER.freeze
11
+ Usage: annotaterb [command] [options]
12
+
13
+ Commands:
14
+ models [options]
15
+ routes [options]
16
+ help
17
+ version
18
+ BANNER
19
+
20
+ DEFAULT_OPTIONS = {
21
+ target_action: :do_annotations,
22
+ exit: false
23
+ }.freeze
24
+
25
+ ANNOTATION_POSITIONS = %w[before top after bottom].freeze
26
+ FILE_TYPE_POSITIONS = %w[position_in_class position_in_factory position_in_fixture position_in_test position_in_routes position_in_serializer].freeze
27
+ EXCLUSION_LIST = %w[tests fixtures factories serializers].freeze
28
+ FORMAT_TYPES = %w[bare rdoc yard markdown].freeze
29
+
30
+ COMMAND_MAP = {
31
+ 'models' => :models,
32
+ 'routes' => :routes,
33
+ 'version' => :version,
34
+ 'help' => :help
35
+ }.freeze
36
+
37
+ def initialize(args, existing_options)
38
+ @args = args
39
+ base_options = DEFAULT_OPTIONS.dup
40
+ @options = base_options.merge(existing_options)
41
+ @commands = []
42
+ @options[:original_args] = args.dup
43
+ end
44
+
45
+ def parse
46
+ parse_command(@args)
47
+
48
+ parser.parse!(@args)
49
+
50
+ act_on_command
51
+
52
+ @options
53
+ end
54
+
55
+ private
56
+
57
+ def parse_command(args)
58
+ command_arg = args.first
59
+ command = COMMAND_MAP[command_arg]
60
+
61
+ if command
62
+ args.shift
63
+ @commands << command
64
+ end
65
+ end
66
+
67
+ def act_on_command
68
+ map = {
69
+ models: Commands::AnnotateModels.new,
70
+ routes: Commands::AnnotateRoutes.new,
71
+ help: Commands::PrintHelp.new(@parser),
72
+ version: Commands::PrintVersion.new
73
+ }
74
+
75
+ if @commands.any?
76
+ @options[:command] = map[@commands.first]
77
+ elsif @commands.size > 1
78
+ # TODO: Should raise or alert user that multiple commands were selected but only 1 command will be ran
79
+ @options[:command] = map[@commands.first]
80
+ else # None
81
+ @options[:command] = nil
82
+ end
83
+ end
84
+
85
+ def parser
86
+ @parser ||= OptionParser.new do |option_parser|
87
+ option_parser.banner = BANNER_STRING
88
+
89
+ # ------------------------------------------------------------------------------------------------------------=
90
+ option_parser.separator('')
91
+ option_parser.separator('Options:')
92
+
93
+ option_parser.on('-v', '--version', "Display the version..") do
94
+ @commands << :version
95
+ end
96
+
97
+ option_parser.on('-h', '--help', "You're looking at it.") do
98
+ @commands << :help
99
+ end
100
+
101
+ add_model_options_to_parser(option_parser)
102
+ add_route_options_to_parser(option_parser)
103
+ add_wrapper_options_to_parser(option_parser)
104
+ add_options_to_parser(option_parser)
105
+ add_position_options_to_parser(option_parser)
106
+ add_utils_to_parser(option_parser)
107
+ end
108
+ end
109
+
110
+ def add_wrapper_options_to_parser(option_parser)
111
+ option_parser.on('--w',
112
+ '--wrapper STR',
113
+ 'Wrap annotation with the text passed as parameter.',
114
+ 'If --w option is used, the same text will be used as opening and closing') do |wrapper|
115
+ @options[:wrapper] = wrapper
116
+ end
117
+
118
+ option_parser.on('--wo',
119
+ '--wrapper-open STR',
120
+ 'Annotation wrapper opening.') do |wrapper_open|
121
+ @options[:wrapper_open] = wrapper_open
122
+ end
123
+
124
+ option_parser.on('--wc',
125
+ '--wrapper-close STR',
126
+ 'Annotation wrapper closing') do |wrapper_close|
127
+ @options[:wrapper_close] = wrapper_close
128
+ end
129
+ end
130
+
131
+ def add_utils_to_parser(option_parser)
132
+ option_parser.on('--force',
133
+ 'Force new annotations even if there are no changes.') do
134
+ @options[:force] = true
135
+ end
136
+
137
+ option_parser.on('--debug',
138
+ 'Prints the options and outputs messages to make it easier to debug.') do
139
+ @options[:debug] = true
140
+ end
141
+
142
+ option_parser.on('--frozen',
143
+ 'Do not allow to change annotations. Exits non-zero if there are going to be changes to files.') do
144
+ @options[:frozen] = true
145
+ end
146
+
147
+ option_parser.on('--trace',
148
+ 'If unable to annotate a file, print the full stack trace, not just the exception message.') do
149
+ @options[:trace] = true
150
+ end
151
+ end
152
+
153
+ def add_model_options_to_parser(option_parser)
154
+ option_parser.separator('')
155
+ option_parser.separator('Annotate model options:')
156
+ option_parser.separator(' ' * 4 + 'Usage: annotaterb models [options]')
157
+ option_parser.separator('')
158
+
159
+ # option_parser.on('-m',
160
+ # '--models',
161
+ # "Annotate ActiveRecord models") do
162
+ # @options[:models] = true
163
+ # @options[:command] = Commands::AnnotateModels.new
164
+ # end
165
+
166
+ option_parser.on('-a',
167
+ '--active-admin',
168
+ 'Annotate active_admin models') do
169
+ @options[:active_admin] = true
170
+ end
171
+
172
+ option_parser.on('--show-migration',
173
+ 'Include the migration version number in the annotation') do
174
+ @options[:include_version] = true
175
+ end
176
+
177
+ option_parser.on('-k',
178
+ '--show-foreign-keys',
179
+ "List the table's foreign key constraints in the annotation") do
180
+ @options[:show_foreign_keys] = true
181
+ end
182
+
183
+ option_parser.on('--ck',
184
+ '--complete-foreign-keys',
185
+ 'Complete foreign key names in the annotation') do
186
+ @options[:show_foreign_keys] = true
187
+ @options[:show_complete_foreign_keys] = true
188
+ end
189
+
190
+ option_parser.on('-i',
191
+ '--show-indexes',
192
+ "List the table's database indexes in the annotation") do
193
+ @options[:show_indexes] = true
194
+ end
195
+
196
+ option_parser.on('-s',
197
+ '--simple-indexes',
198
+ "Concat the column's related indexes in the annotation") do
199
+ @options[:simple_indexes] = true
200
+ end
201
+
202
+ option_parser.on('--hide-limit-column-types VALUES',
203
+ "don't show limit for given column types, separated by commas (i.e., `integer,boolean,text`)") do |values|
204
+ @options[:hide_limit_column_types] = values.to_s
205
+ end
206
+
207
+ option_parser.on('--hide-default-column-types VALUES',
208
+ "don't show default for given column types, separated by commas (i.e., `json,jsonb,hstore`)") do |values|
209
+ @options[:hide_default_column_types] = values.to_s
210
+ end
211
+
212
+ option_parser.on('--ignore-unknown-models',
213
+ "don't display warnings for bad model files") do
214
+ @options[:ignore_unknown_models] = true
215
+ end
216
+
217
+ option_parser.on('-I',
218
+ '--ignore-columns REGEX',
219
+ "don't annotate columns that match a given REGEX (i.e., `annotate -I '^(id|updated_at|created_at)'`") do |regex|
220
+ @options[:ignore_columns] = regex
221
+ end
222
+
223
+ option_parser.on('--with-comment',
224
+ "include database comments in model annotations") do
225
+ @options[:with_comment] = true
226
+ end
227
+ end
228
+
229
+ def add_route_options_to_parser(option_parser)
230
+ option_parser.separator('')
231
+ option_parser.separator('Annotate routes options:')
232
+ option_parser.separator(' ' * 4 + 'Usage: annotaterb routes [options]')
233
+ option_parser.separator('')
234
+
235
+ # option_parser.on('-r',
236
+ # '--routes',
237
+ # "Annotate routes.rb with the output of 'rails routes'") do
238
+ # @options[:routes] = true
239
+ # @options[:command] = Commands::AnnotateRoutes.new
240
+ # end
241
+
242
+ option_parser.on('--ignore-routes REGEX',
243
+ "don't annotate routes that match a given REGEX (i.e., `annotate -I '(mobile|resque|pghero)'`") do |regex|
244
+ @options[:ignore_routes] = regex
245
+ end
246
+
247
+ option_parser.on('--timestamp',
248
+ 'Include timestamp in (routes) annotation') do
249
+ @options[:timestamp] = true
250
+ end
251
+ end
252
+
253
+ def add_position_options_to_parser(option_parser)
254
+ has_set_position = {}
255
+
256
+ option_parser.on('-p',
257
+ '--position [before|top|after|bottom]',
258
+ ANNOTATION_POSITIONS,
259
+ 'Place the annotations at the top (before) or the bottom (after) of the model/test/fixture/factory/route/serializer file(s)') do |position|
260
+ @options[:position] = position
261
+
262
+ FILE_TYPE_POSITIONS.each do |key|
263
+ @options[key.to_sym] = position unless has_set_position[key]
264
+ end
265
+ end
266
+
267
+ option_parser.on('--pc',
268
+ '--position-in-class [before|top|after|bottom]',
269
+ ANNOTATION_POSITIONS,
270
+ 'Place the annotations at the top (before) or the bottom (after) of the model file') do |position_in_class|
271
+ @options[:position_in_class] = position_in_class
272
+ has_set_position['position_in_class'] = true
273
+ end
274
+
275
+ option_parser.on('--pf',
276
+ '--position-in-factory [before|top|after|bottom]',
277
+ ANNOTATION_POSITIONS,
278
+ 'Place the annotations at the top (before) or the bottom (after) of any factory files') do |position_in_factory|
279
+ @options[:position_in_factory] = position_in_factory
280
+ has_set_position['position_in_factory'] = true
281
+ end
282
+
283
+ option_parser.on('--px',
284
+ '--position-in-fixture [before|top|after|bottom]',
285
+ ANNOTATION_POSITIONS,
286
+ 'Place the annotations at the top (before) or the bottom (after) of any fixture files') do |position_in_fixture|
287
+ @options[:position_in_fixture] = position_in_fixture
288
+ has_set_position['position_in_fixture'] = true
289
+ end
290
+
291
+ option_parser.on('--pt',
292
+ '--position-in-test [before|top|after|bottom]',
293
+ ANNOTATION_POSITIONS,
294
+ 'Place the annotations at the top (before) or the bottom (after) of any test files') do |position_in_test|
295
+ @options[:position_in_test] = position_in_test
296
+ has_set_position['position_in_test'] = true
297
+ end
298
+
299
+ option_parser.on('--pr',
300
+ '--position-in-routes [before|top|after|bottom]',
301
+ ANNOTATION_POSITIONS,
302
+ 'Place the annotations at the top (before) or the bottom (after) of the routes.rb file') do |position_in_routes|
303
+ @options[:position_in_routes] = position_in_routes
304
+ has_set_position['position_in_routes'] = true
305
+ end
306
+
307
+ option_parser.on('--ps',
308
+ '--position-in-serializer [before|top|after|bottom]',
309
+ ANNOTATION_POSITIONS,
310
+ 'Place the annotations at the top (before) or the bottom (after) of the serializer files') do |position_in_serializer|
311
+ @options[:position_in_serializer] = position_in_serializer
312
+ has_set_position['position_in_serializer'] = true
313
+ end
314
+ end
315
+
316
+ def add_options_to_parser(option_parser) # rubocop:disable Metrics/MethodLength
317
+ option_parser.separator('')
318
+ option_parser.separator('Command options:')
319
+ option_parser.separator('Additional options that work for annotating models and routes')
320
+ option_parser.separator('')
321
+
322
+ option_parser.on('--additional-file-patterns path1,path2,path3',
323
+ Array,
324
+ "Additional file paths or globs to annotate, separated by commas (e.g. `/foo/bar/%model_name%/*.rb,/baz/%model_name%.rb`)") do |additional_file_patterns|
325
+ @options[:additional_file_patterns] = additional_file_patterns
326
+ end
327
+
328
+ option_parser.on('-d',
329
+ '--delete',
330
+ 'Remove annotations from all model files or the routes.rb file') do
331
+ @options[:target_action] = :remove_annotations
332
+ end
333
+
334
+ option_parser.on('--model-dir dir',
335
+ "Annotate model files stored in dir rather than app/models, separate multiple dirs with commas") do |dir|
336
+ @options[:model_dir] = dir
337
+ end
338
+
339
+ option_parser.on('--root-dir dir',
340
+ "Annotate files stored within root dir projects, separate multiple dirs with commas") do |dir|
341
+ @options[:root_dir] = dir
342
+ end
343
+
344
+ option_parser.on('--ignore-model-subdirects',
345
+ "Ignore subdirectories of the models directory") do
346
+ @options[:ignore_model_sub_dir] = true
347
+ end
348
+
349
+ option_parser.on('--sort',
350
+ "Sort columns alphabetically, rather than in creation order") do
351
+ @options[:sort] = true
352
+ end
353
+
354
+ option_parser.on('--classified-sort',
355
+ "Sort columns alphabetically, but first goes id, then the rest columns, then the timestamp columns and then the association columns") do
356
+ @options[:classified_sort] = true
357
+ end
358
+
359
+ option_parser.on('-R',
360
+ '--require path',
361
+ "Additional file to require before loading models, may be used multiple times") do |path|
362
+ @options[:require] = if @options[:require].present?
363
+ [@options[:require], path].join(',')
364
+ else
365
+ path
366
+ end
367
+ end
368
+
369
+ option_parser.on('-e',
370
+ '--exclude [tests,fixtures,factories,serializers]',
371
+ Array,
372
+ "Do not annotate fixtures, test files, factories, and/or serializers") do |exclusions|
373
+ exclusions ||= EXCLUSION_LIST
374
+ exclusions.each { |exclusion| @options["exclude_#{exclusion}".to_sym] = true }
375
+ end
376
+
377
+ option_parser.on('-f',
378
+ '--format [bare|rdoc|yard|markdown]',
379
+ FORMAT_TYPES,
380
+ 'Render Schema Information as plain/RDoc/Yard/Markdown') do |format_type|
381
+ @options["format_#{format_type}".to_sym] = true
382
+ end
383
+ end
384
+ end
385
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ class RakeBootstrapper
5
+ class << self
6
+ def call(options)
7
+ begin
8
+ require 'rake/dsl_definition'
9
+ rescue StandardError => e
10
+ # We might just be on an old version of Rake...
11
+ $stderr.puts e.message
12
+ exit e.status_code
13
+ end
14
+
15
+ require 'rake'
16
+ load './Rakefile' if File.exist?('./Rakefile')
17
+
18
+ begin
19
+ Rake::Task[:environment].invoke
20
+ rescue
21
+ nil
22
+ end
23
+
24
+ unless defined?(Rails)
25
+ # Not in a Rails project, so time to load up the parts of
26
+ # ActiveSupport we need.
27
+ require 'active_support'
28
+ require 'active_support/core_ext/class/subclasses'
29
+ require 'active_support/core_ext/string/inflections'
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This module provides methods for annotating config/routes.rb.
4
+ module AnnotateRb
5
+ module RouteAnnotator
6
+ # This class is abstract class of classes adding and removing annotation to config/routes.rb.
7
+ class AnnotationProcessor < BaseProcessor
8
+
9
+ # @return [String]
10
+ def execute
11
+ if routes_file_exist?
12
+ if update
13
+ "#{routes_file} was annotated."
14
+ else
15
+ "#{routes_file} was not changed."
16
+ end
17
+ else
18
+ "#{routes_file} could not be found."
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def header
25
+ @header ||= HeaderGenerator.generate(options)
26
+ end
27
+
28
+ def generate_new_content_array(content, header_position)
29
+ magic_comments_map, content = Helper.extract_magic_comments_from_array(content)
30
+ if %w(before top).include?(options[:position_in_routes])
31
+ new_content_array = []
32
+ new_content_array += magic_comments_map
33
+ new_content_array << '' if magic_comments_map.any?
34
+ new_content_array += header
35
+ new_content_array << '' if content.first != ''
36
+ new_content_array += content
37
+ else
38
+ # Ensure we have adequate trailing newlines at the end of the file to
39
+ # ensure a blank line separating the content from the annotation.
40
+ content << '' unless content.last == ''
41
+
42
+ # We're moving something from the top of the file to the bottom, so ditch
43
+ # the spacer we put in the first time around.
44
+ content.shift if header_position == :before && content.first == ''
45
+
46
+ new_content_array = magic_comments_map + content + header
47
+ end
48
+
49
+ # Make sure we end on a trailing newline.
50
+ new_content_array << '' unless new_content_array.last == ''
51
+
52
+ new_content_array
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module RouteAnnotator
5
+ class Annotator
6
+ class << self
7
+ # TODO: Deprecate
8
+ def do_annotations(options = {})
9
+ add_annotations(options)
10
+ end
11
+
12
+ def add_annotations(options = {})
13
+ new(options).add_annotations
14
+ end
15
+
16
+ def remove_annotations(options = {})
17
+ new(options).remove_annotations
18
+ end
19
+ end
20
+
21
+ def initialize(options = {})
22
+ @options = options
23
+ end
24
+
25
+ def add_annotations
26
+ routes_file = File.join('config', 'routes.rb')
27
+ AnnotationProcessor.execute(@options, routes_file).tap do |result|
28
+ puts result
29
+ end
30
+ end
31
+
32
+ def remove_annotations
33
+ routes_file = File.join('config', 'routes.rb')
34
+ RemovalProcessor.execute(@options, routes_file).tap do |result|
35
+ puts result
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This module provides methods for annotating config/routes.rb.
4
+ module AnnotateRb
5
+ module RouteAnnotator
6
+ # This class is abstract class of classes adding and removing annotation to config/routes.rb.
7
+ class BaseProcessor
8
+ class << self
9
+ # @param options [Hash]
10
+ # @param routes_file [String]
11
+ # @return [String]
12
+ def execute(options, routes_file)
13
+ new(options, routes_file).execute
14
+ end
15
+
16
+ private :new
17
+ end
18
+
19
+ def initialize(options, routes_file)
20
+ @options = options
21
+ @routes_file = routes_file
22
+ end
23
+
24
+ # @return [Boolean]
25
+ def update
26
+ if existing_text == new_text
27
+ false
28
+ else
29
+ write(new_text)
30
+ true
31
+ end
32
+ end
33
+
34
+ def routes_file_exist?
35
+ File.exist?(routes_file)
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :options, :routes_file
41
+
42
+ def generate_new_content_array(_content, _header_position)
43
+ raise NoMethodError
44
+ end
45
+
46
+ def existing_text
47
+ @existing_text ||= File.read(routes_file)
48
+ end
49
+
50
+ # @return [String]
51
+ def new_text
52
+ content, header_position = strip_annotations(existing_text)
53
+ new_content = generate_new_content_array(content, header_position)
54
+ new_content.join("\n")
55
+ end
56
+
57
+ def write(text)
58
+ File.open(routes_file, 'wb') { |f| f.puts(text) }
59
+ end
60
+
61
+ # TODO: write the method doc using ruby rdoc formats
62
+ # This method returns an array of 'real_content' and 'header_position'.
63
+ # 'header_position' will either be :before, :after, or
64
+ # a number. If the number is > 0, the
65
+ # annotation was found somewhere in the
66
+ # middle of the file. If the number is
67
+ # zero, no annotation was found.
68
+ def strip_annotations(content)
69
+ real_content = []
70
+ mode = :content
71
+ header_position = 0
72
+
73
+ content.split(/\n/, -1).each_with_index do |line, line_number|
74
+ if mode == :header && line !~ /\s*#/
75
+ mode = :content
76
+ real_content << line unless line.blank?
77
+ elsif mode == :content
78
+ if line =~ /^\s*#\s*== Route.*$/
79
+ header_position = line_number + 1 # index start's at 0
80
+ mode = :header
81
+ else
82
+ real_content << line
83
+ end
84
+ end
85
+ end
86
+
87
+ real_content_and_header_position(real_content, header_position)
88
+ end
89
+
90
+ def real_content_and_header_position(real_content, header_position)
91
+ # By default assume the annotation was found in the middle of the file
92
+
93
+ # ... unless we have evidence it was at the beginning ...
94
+ return real_content, :before if header_position == 1
95
+
96
+ # ... or that it was at the end.
97
+ return real_content, :after if header_position >= real_content.count
98
+
99
+ # and the default
100
+ return real_content, header_position
101
+ end
102
+ end
103
+ end
104
+ end