annotated_models 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,357 @@
1
+ module AnnotatedModels
2
+ # Annotate Models plugin use this header
3
+ COMPAT_PREFIX = "== Schema Info"
4
+ PREFIX = "== Schema Information"
5
+ END_MARK = "== Schema Information End"
6
+ PATTERN = /^\n?# #{COMPAT_PREFIX}.*?\n(#.*\n)*\n/
7
+
8
+ # File.join for windows reverse bar compat?
9
+ # I dont use windows, can`t test
10
+ UNIT_TEST_DIR = File.join("test", "unit" )
11
+ SPEC_MODEL_DIR = File.join("spec", "models")
12
+ FIXTURE_TEST_DIR = File.join("test", "fixtures")
13
+ FIXTURE_SPEC_DIR = File.join("spec", "fixtures")
14
+ # Object Daddy http://github.com/flogic/object_daddy/tree/master
15
+ EXEMPLARS_TEST_DIR = File.join("test", "exemplars")
16
+ EXEMPLARS_SPEC_DIR = File.join("spec", "exemplars")
17
+ # Machinist http://github.com/notahat/machinist
18
+ BLUEPRINTS_TEST_DIR = File.join("test", "blueprints")
19
+ BLUEPRINTS_SPEC_DIR = File.join("spec", "blueprints")
20
+ # Factory Girl http://github.com/thoughtbot/factory_girl
21
+ FACTORY_GIRL_TEST_DIR = File.join("test", "factories")
22
+ FACTORY_GIRL_SPEC_DIR = File.join("spec", "factories")
23
+
24
+ # Don't show limit (#) on these column types
25
+ # Example: show "integer" instead of "integer(4)"
26
+ NO_LIMIT_COL_TYPES = ["integer", "boolean"]
27
+
28
+ class << self
29
+ def model_dir
30
+ @model_dir || "app/models"
31
+ end
32
+
33
+ def model_dir=(dir)
34
+ @model_dir = dir
35
+ end
36
+
37
+ # Simple quoting for the default column value
38
+ def quote(value)
39
+ case value
40
+ when NilClass then "NULL"
41
+ when TrueClass then "TRUE"
42
+ when FalseClass then "FALSE"
43
+ when Float, Fixnum, Bignum then value.to_s
44
+ # BigDecimals need to be output in a non-normalized form and quoted.
45
+ when BigDecimal then value.to_s('F')
46
+ else
47
+ value.inspect
48
+ end
49
+ end
50
+
51
+ # Use the column information in an ActiveRecord class
52
+ # to create a comment block containing a line for
53
+ # each column. The line contains the column name,
54
+ # the type (and length), and any optional attributes
55
+ def get_schema_info(klass, header, options = {})
56
+ info = "# #{header}\n#\n"
57
+ info << "# Table name: #{klass.table_name}\n"
58
+ info << "# Human name: #{klass.model_name.human}\n" unless klass.model_name.human(:default => "").blank?
59
+ info << "#\n"
60
+
61
+ max_size = klass.column_names.map{|name| name.size}.max + (ENV['format_rdoc'] ? 5 : 1)
62
+ klass.columns.each do |col|
63
+ attrs = []
64
+ attrs << "'#{klass.human_attribute_name(col.name)}'" unless klass.human_attribute_name(col.name, :default => "").blank?
65
+ attrs << "default(#{quote(col.default)})" unless col.default.nil?
66
+ attrs << "not null" unless col.null
67
+ attrs << "primary key" if col.name.to_sym == klass.primary_key.to_sym
68
+
69
+ col_type = col.type.to_s
70
+ if col_type == "decimal"
71
+ col_type << "(#{col.precision}, #{col.scale})"
72
+ else
73
+ if (col.limit)
74
+ col_type << "(#{col.limit})" unless NO_LIMIT_COL_TYPES.include?(col_type)
75
+ end
76
+ end
77
+
78
+ # Check out if we got a geometric column
79
+ # and print the type and SRID
80
+ if col.respond_to?(:geometry_type)
81
+ attrs << "#{col.geometry_type}, #{col.srid}"
82
+ end
83
+
84
+ # Check if the column has indices and print "indexed" if true
85
+ # If the indice include another colum, print it too.
86
+ if options[:simple_indexes] # Check out if this column is indexed
87
+ indices = klass.connection.indexes(klass.table_name)
88
+ if indices = indices.select { |ind| ind.columns.include? col.name }
89
+ indices.each do |ind|
90
+ ind = ind.columns.reject! { |i| i == col.name }
91
+ attrs << (ind.length == 0 ? "indexed" : "indexed => [#{ind.join(", ")}]")
92
+ end
93
+ end
94
+ end
95
+
96
+ if ENV['format_rdoc']
97
+ info << sprintf("# %-#{max_size}.#{max_size}s<tt>%s</tt>", "*#{col.name}*::", attrs.unshift(col_type).join(", ")).rstrip + "\n"
98
+ else
99
+ info << sprintf("# %-#{max_size}.#{max_size}s:%-15.15s %s", col.name, col_type, attrs.join(", ")).rstrip + "\n"
100
+ end
101
+ end
102
+
103
+ if options[:show_indexes]
104
+ info << get_index_info(klass)
105
+ end
106
+
107
+ if ENV['format_rdoc']
108
+ info << "#--\n"
109
+ info << "# #{END_MARK}\n"
110
+ info << "#++\n\n"
111
+ else
112
+ info << "#\n\n"
113
+ end
114
+ end
115
+
116
+ def get_index_info(klass)
117
+ index_info = "#\n# Indexes\n#\n"
118
+
119
+ indexes = klass.connection.indexes(klass.table_name)
120
+ return "" if indexes.empty?
121
+
122
+ max_size = indexes.collect{|index| index.name.size}.max + 1
123
+ indexes.each do |index|
124
+ index_info << sprintf("# %-#{max_size}.#{max_size}s %s %s", index.name, "(#{index.columns.join(",")})", index.unique ? "UNIQUE" : "").rstrip + "\n"
125
+ end
126
+ return index_info
127
+ end
128
+
129
+ # Add a schema block to a file. If the file already contains
130
+ # a schema info block (a comment starting with "== Schema Information"), check if it
131
+ # matches the block that is already there. If so, leave it be. If not, remove the old
132
+ # info block and write a new one.
133
+ # Returns true or false depending on whether the file was modified.
134
+ #
135
+ # === Options (opts)
136
+ # :position<Symbol>:: where to place the annotated section in fixture or model file,
137
+ # "before" or "after". Default is "before".
138
+ # :position_in_class<Symbol>:: where to place the annotated section in model file
139
+ # :position_in_fixture<Symbol>:: where to place the annotated section in fixture file
140
+ # :position_in_others<Symbol>:: where to place the annotated section in the rest of
141
+ # supported files
142
+ #
143
+ def annotate_one_file(file_name, info_block, options={})
144
+ if File.exist?(file_name)
145
+ old_content = File.read(file_name)
146
+
147
+ # Ignore the Schema version line because it changes with each migration
148
+ header = Regexp.new(/(^# Table name:.*?\n(#.*\n)*\n)/)
149
+ old_header = old_content.match(header).to_s
150
+ new_header = info_block.match(header).to_s
151
+
152
+ if old_header == new_header
153
+ false
154
+ else
155
+ # Remove old schema info
156
+ old_content.sub!(PATTERN, '')
157
+
158
+ # Write it back
159
+ new_content = options[:position].to_s == 'after' ? (old_content + "\n" + info_block) : (info_block + old_content)
160
+
161
+ File.open(file_name, "wb") { |f| f.puts new_content }
162
+ true
163
+ end
164
+ end
165
+ end
166
+
167
+ def remove_annotation_of_file(file_name)
168
+ if File.exist?(file_name)
169
+ content = File.read(file_name)
170
+
171
+ content.sub!(PATTERN, '')
172
+
173
+ File.open(file_name, "wb") { |f| f.puts content }
174
+ end
175
+ end
176
+
177
+ # Given the name of an ActiveRecord class, create a schema
178
+ # info block (basically a comment containing information
179
+ # on the columns and their types) and put it at the front
180
+ # of the model and fixture source files.
181
+ # Returns true or false depending on whether the source
182
+ # files were modified.
183
+ def annotate(klass, file, header,options={})
184
+ info = get_schema_info(klass, header, options)
185
+ annotated = false
186
+ model_name = klass.name.underscore
187
+ model_file_name = File.join(model_dir, file)
188
+
189
+ if annotate_one_file(model_file_name, info, options_with_position(options, :position_in_class))
190
+ annotated = true
191
+ end
192
+
193
+ unless options[:exclude_tests]
194
+ [
195
+ File.join(UNIT_TEST_DIR, "#{model_name}_test.rb"), # test
196
+ File.join(SPEC_MODEL_DIR, "#{model_name}_spec.rb"), # spec
197
+ ].each do |file|
198
+ # todo: add an option "position_in_test" -- or maybe just ask if anyone ever wants different positions for model vs. test vs. fixture
199
+ annotate_one_file(file, info, options_with_position(options, :position_in_fixture)) if File.exist?(file)
200
+ end
201
+ end
202
+
203
+ unless options[:exclude_fixtures]
204
+ [
205
+ File.join(FIXTURE_TEST_DIR, "#{klass.table_name}.yml"), # fixture
206
+ File.join(FIXTURE_SPEC_DIR, "#{klass.table_name}.yml"), # fixture
207
+ File.join(EXEMPLARS_TEST_DIR, "#{model_name}_exemplar.rb"), # Object Daddy
208
+ File.join(EXEMPLARS_SPEC_DIR, "#{model_name}_exemplar.rb"), # Object Daddy
209
+ File.join(BLUEPRINTS_TEST_DIR, "#{model_name}_blueprint.rb"), # Machinist Blueprints
210
+ File.join(BLUEPRINTS_SPEC_DIR, "#{model_name}_blueprint.rb"), # Machinist Blueprints
211
+ File.join(FACTORY_GIRL_TEST_DIR, "#{model_name}_factory.rb"), # Factory Girl Factories
212
+ File.join(FACTORY_GIRL_SPEC_DIR, "#{model_name}_factory.rb"), # Factory Girl Factories
213
+ ].each do |file|
214
+ annotate_one_file(file, info, options_with_position(options, :position_in_fixture)) if File.exist?(file)
215
+ end
216
+ end
217
+
218
+ annotated
219
+ end
220
+
221
+ # position = :position_in_fixture or :position_in_class
222
+ def options_with_position(options, position_in)
223
+ options.merge(:position=>(options[position_in] || options[:position]))
224
+ end
225
+
226
+ # Return a list of the model files to annotate. If we have
227
+ # command line arguments, they're assumed to be either
228
+ # the underscore or CamelCase versions of model names.
229
+ # Otherwise we take all the model files in the
230
+ # model_dir directory.
231
+ def get_model_files
232
+ models = ARGV.dup
233
+ models.shift
234
+ models.reject!{|m| m.match(/^(.*)=/)}
235
+ if models.empty?
236
+ begin
237
+ Dir.chdir(model_dir) do
238
+ models = Dir["**/*.rb"]
239
+ end
240
+ rescue SystemCallError
241
+ puts "No models found in directory '#{model_dir}'."
242
+ puts "Either specify models on the command line, or use the --model-dir option."
243
+ puts "Call 'annotate --help' for more info."
244
+ exit 1;
245
+ end
246
+ end
247
+ models
248
+ end
249
+
250
+ # Retrieve the classes belonging to the model names we're asked to process
251
+ # Check for namespaced models in subdirectories as well as models
252
+ # in subdirectories without namespacing.
253
+ def get_model_class(file)
254
+ require File.expand_path("#{model_dir}/#{file}") # this is for non-rails projects, which don't get Rails auto-require magic
255
+ model = file.gsub(/\.rb$/, '').camelize
256
+ parts = model.split('::')
257
+ begin
258
+ parts.inject(Object) {|klass, part| klass.const_get(part) }
259
+ rescue LoadError, NameError
260
+ Object.const_get(parts.last)
261
+ end
262
+ end
263
+
264
+ # We're passed a name of things that might be
265
+ # ActiveRecord models. If we can find the class, and
266
+ # if its a subclass of ActiveRecord::Base,
267
+ # then pass it to the associated block
268
+ def do_annotations(options={})
269
+ if options[:require]
270
+ options[:require].each do |path|
271
+ require path
272
+ end
273
+ end
274
+
275
+ header = PREFIX.dup
276
+
277
+ if options[:include_version]
278
+ version = ActiveRecord::Migrator.current_version rescue 0
279
+ if version > 0
280
+ header << "\n# Schema version: #{version}"
281
+ end
282
+ end
283
+
284
+ if options[:model_dir]
285
+ self.model_dir = options[:model_dir]
286
+ end
287
+
288
+ annotated = []
289
+ get_model_files.each do |file|
290
+ begin
291
+ klass = get_model_class(file)
292
+ if klass < ActiveRecord::Base && !klass.abstract_class?
293
+ if annotate(klass, file, header, options)
294
+ annotated << klass
295
+ end
296
+ end
297
+ rescue Exception => e
298
+ puts "Unable to annotate #{file}: #{e.message} (#{e.backtrace.first})"
299
+ end
300
+ end
301
+ if annotated.empty?
302
+ puts "Nothing annotated."
303
+ else
304
+ puts "Annotated (#{annotated.length}): #{annotated.join(', ')}"
305
+ end
306
+ end
307
+
308
+ def remove_annotations(options={})
309
+ if options[:model_dir]
310
+ puts "removing"
311
+ self.model_dir = options[:model_dir]
312
+ end
313
+ deannotated = []
314
+ get_model_files.each do |file|
315
+ begin
316
+ klass = get_model_class(file)
317
+ if klass < ActiveRecord::Base && !klass.abstract_class?
318
+ deannotated << klass
319
+
320
+ model_name = klass.name.underscore
321
+ model_file_name = File.join(model_dir, file)
322
+ remove_annotation_of_file(model_file_name)
323
+
324
+ [
325
+ File.join(UNIT_TEST_DIR, "#{model_name}_test.rb"),
326
+ File.join(SPEC_MODEL_DIR, "#{model_name}_spec.rb"),
327
+ File.join(FIXTURE_TEST_DIR, "#{klass.table_name}.yml"), # fixture
328
+ File.join(FIXTURE_SPEC_DIR, "#{klass.table_name}.yml"), # fixture
329
+ File.join(EXEMPLARS_TEST_DIR, "#{model_name}_exemplar.rb"), # Object Daddy
330
+ File.join(EXEMPLARS_SPEC_DIR, "#{model_name}_exemplar.rb"), # Object Daddy
331
+ File.join(BLUEPRINTS_TEST_DIR, "#{model_name}_blueprint.rb"), # Machinist Blueprints
332
+ File.join(BLUEPRINTS_SPEC_DIR, "#{model_name}_blueprint.rb"), # Machinist Blueprints
333
+ File.join(FACTORY_GIRL_TEST_DIR, "#{model_name}_factory.rb"), # Factory Girl Factories
334
+ File.join(FACTORY_GIRL_SPEC_DIR, "#{model_name}_factory.rb"), # Factory Girl Factories
335
+ ].each do |file|
336
+ remove_annotation_of_file(file) if File.exist?(file)
337
+ end
338
+
339
+ end
340
+ rescue Exception => e
341
+ puts "Unable to annotate #{file}: #{e.message}"
342
+ end
343
+ end
344
+ puts "Removed annotation from: #{deannotated.join(', ')}"
345
+ end
346
+ end
347
+ end
348
+
349
+ # monkey patches
350
+
351
+ module ::ActiveRecord
352
+ class Base
353
+ def self.method_missing(name, *args)
354
+ # ignore this, so unknown/unloaded macros won't cause parsing to fail
355
+ end
356
+ end
357
+ end
@@ -0,0 +1,24 @@
1
+ desc "Add schema information (as comments) to model and fixture files"
2
+ task :annotate_models => :environment do
3
+ require File.join(File.dirname(__FILE__), '..', 'annotate/annotate_models')
4
+ options={}
5
+ options[:position_in_class] = ENV['position_in_class'] || ENV['position'] || :before
6
+ options[:position_in_factory] = ENV['position_in_factory'] || ENV['position'] || :before
7
+ options[:position_in_fixture] = ENV['position_in_fixture'] || ENV['position'] || :before
8
+ options[:show_indexes] = ENV['show_indexes']
9
+ options[:simple_indexes] = ENV['simple_indexes']
10
+ options[:model_dir] = ENV['model_dir']
11
+ options[:include_version] = ENV['include_version']
12
+ options[:require] = ENV['require'] ? ENV['require'].split(', ') : []
13
+ options[:exclude_tests] = ENV['exclude_tests']
14
+ options[:exclude_fixtures] = ENV['exclude_fixtures']
15
+ AnnotateModels.do_annotations(options)
16
+ end
17
+
18
+ desc "Remove schema information from model and fixture files"
19
+ task :remove_annotation => :environment do
20
+ require File.join(File.dirname(__FILE__), '..', 'annotate/annotate_models')
21
+ options={}
22
+ options[:model_dir] = ENV['model_dir']
23
+ AnnotateModels.remove_annotations(options)
24
+ end
@@ -0,0 +1,50 @@
1
+ # These tasks are added to the project if you install annotate as a Rails plugin.
2
+ # (They are not used to build annotate itself.)
3
+
4
+ # Append annotations to Rake tasks for ActiveRecord, so annotate automatically gets
5
+ # run after doing db:migrate.
6
+ # Unfortunately it relies on ENV for options; it'd be nice to be able to set options
7
+ # in a per-project config file so this task can read them.
8
+
9
+ def run_annotate_models?
10
+ update_on_migrate = true
11
+ (defined? ANNOTATE_MODELS_PREFS::UPDATE_ON_MIGRATE) &&
12
+ (!ANNOTATE_MODELS_PREFS::UPDATE_ON_MIGRATE.nil?) ?
13
+ ANNOTATE_MODELS_PREFS::UPDATE_ON_MIGRATE : true
14
+ end
15
+
16
+
17
+ namespace :db do
18
+ task :migrate do
19
+ if run_annotate_models?
20
+ Annotate::Migration.update_annotations
21
+ end
22
+ end
23
+
24
+ task :update => [:migrate] do
25
+ Annotate::Migration.update_annotations
26
+ end
27
+
28
+ namespace :migrate do
29
+ [:up, :down, :reset, :redo].each do |t|
30
+ task t do
31
+ if run_annotate_models?
32
+ Annotate::Migration.update_annotations
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ module Annotate
40
+ class Migration
41
+ @@working = false
42
+
43
+ def self.update_annotations
44
+ unless @@working
45
+ @@working = true
46
+ Rake::Task['annotate_models'].invoke
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,236 @@
1
+ #encoding: utf-8
2
+
3
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
4
+ require 'annotated_models'
5
+ require 'active_support/core_ext'
6
+ require 'active_support/inflector'
7
+ require 'fakefs/spec_helpers'
8
+ require 'tmpdir'
9
+
10
+ describe AnnotatedModels do
11
+ include FakeFS::SpecHelpers
12
+
13
+ def mock_class(table_name, primary_key, columns)
14
+ options = {
15
+ :connection => mock("Conn", :indexes => []),
16
+ :table_name => table_name,
17
+ :model_name => mock("ModelName", :human => ""),
18
+ :primary_key => primary_key.to_s,
19
+ :column_names => columns.map { |col| col.name.to_s },
20
+ :columns => columns
21
+ }
22
+ klass = mock("An ActiveRecord class", options)
23
+ columns.reverse.each do | column |
24
+ klass.stub!(:human_attribute_name).with(column.name, {:default=>""}).and_return("")
25
+ end
26
+ klass
27
+ end
28
+
29
+ def mock_column(name, type, options={})
30
+ default_options = {
31
+ :limit => nil,
32
+ :null => false,
33
+ :default => nil
34
+ }
35
+
36
+ stubs = default_options.dup
37
+ stubs.merge!(options)
38
+ stubs.merge!(:name => name, :type => type, :humanize => name.to_s.humanize)
39
+
40
+ mock("Column", stubs)
41
+ end
42
+
43
+ it { AnnotatedModels.quote(nil).should eql("NULL") }
44
+ it { AnnotatedModels.quote(true).should eql("TRUE") }
45
+ it { AnnotatedModels.quote(false).should eql("FALSE") }
46
+ it { AnnotatedModels.quote(25).should eql("25") }
47
+ it { AnnotatedModels.quote(25.6).should eql("25.6") }
48
+ it { AnnotatedModels.quote(1e-20).should eql("1.0e-20") }
49
+
50
+ it "should get schema info" do
51
+ klass = mock_class(:users, :id, [
52
+ mock_column(:id, :integer),
53
+ mock_column(:name, :string, :limit => 50)
54
+ ])
55
+
56
+ AnnotatedModels.get_schema_info(klass, "Schema Info").should eql(<<-EOS)
57
+ # Schema Info
58
+ #
59
+ # Table name: users
60
+ #
61
+ # id :integer not null, primary key
62
+ # name :string(50) not null
63
+ #
64
+
65
+ EOS
66
+ end
67
+
68
+ it "should get schema info as RDoc" do
69
+ klass = mock_class(:users, :id, [
70
+ mock_column(:id, :integer),
71
+ mock_column(:name, :string, :limit => 50)
72
+ ])
73
+ ENV.stub!(:[]).with('format_rdoc').and_return(true)
74
+ AnnotatedModels.get_schema_info(klass, AnnotatedModels::PREFIX).should eql(<<-EOS)
75
+ # #{AnnotatedModels::PREFIX}
76
+ #
77
+ # Table name: users
78
+ #
79
+ # *id*:: <tt>integer, not null, primary key</tt>
80
+ # *name*:: <tt>string(50), not null</tt>
81
+ #--
82
+ # #{AnnotatedModels::END_MARK}
83
+ #++
84
+
85
+ EOS
86
+ end
87
+
88
+ describe "#get_model_class" do
89
+
90
+ def create(file, body="hi")
91
+ path = @dir + '/' + file
92
+ File.open(path, "w") do |f|
93
+ f.puts(body)
94
+ end
95
+ path
96
+ end
97
+
98
+ before :all do
99
+ @dir = File.join Dir.tmpdir, "annotate_models"
100
+ FileUtils.mkdir_p(@dir)
101
+ AnnotatedModels.model_dir = @dir
102
+
103
+ create('foo.rb', <<-EOS)
104
+ class Foo < ActiveRecord::Base
105
+ end
106
+ EOS
107
+ create('foo_with_macro.rb', <<-EOS)
108
+ class FooWithMacro < ActiveRecord::Base
109
+ acts_as_awesome :yah
110
+ end
111
+ EOS
112
+ create('foo_with_utf8.rb', <<-EOS)
113
+ #encoding: utf-8
114
+ class FooWithUtf8 < ActiveRecord::Base
115
+ UTF8STRINGS = %w[résumé façon âge]
116
+ end
117
+ EOS
118
+ end
119
+
120
+ it "should work" do
121
+ klass = AnnotatedModels.get_model_class("foo.rb")
122
+ klass.name.should == "Foo"
123
+ end
124
+
125
+ it "should not care about unknown macros" do
126
+ klass = AnnotatedModels.get_model_class("foo_with_macro.rb")
127
+ klass.name.should == "FooWithMacro"
128
+ end
129
+
130
+ it "should not complain of invalid multibyte char (USASCII)" do
131
+ klass = AnnotatedModels.get_model_class("foo_with_utf8.rb")
132
+ klass.name.should == "FooWithUtf8"
133
+ end
134
+ end
135
+
136
+ describe "#remove_annotation_of_file" do
137
+ def create(file, body="hi")
138
+ File.open(file, "w") do |f|
139
+ f.puts(body)
140
+ end
141
+ end
142
+
143
+ def content(file)
144
+ File.read(file)
145
+ end
146
+
147
+ it "should remove before annotate" do
148
+ create("before.rb", <<-EOS)
149
+ # == Schema Information
150
+ #
151
+ # Table name: foo
152
+ #
153
+ # id :integer not null, primary key
154
+ # created_at :datetime
155
+ # updated_at :datetime
156
+ #
157
+
158
+ class Foo < ActiveRecord::Base
159
+ end
160
+ EOS
161
+
162
+ AnnotatedModels.remove_annotation_of_file("before.rb")
163
+
164
+ content("before.rb").should == <<-EOS
165
+ class Foo < ActiveRecord::Base
166
+ end
167
+ EOS
168
+ end
169
+
170
+ it "should remove after annotate" do
171
+ create("after.rb", <<-EOS)
172
+ class Foo < ActiveRecord::Base
173
+ end
174
+
175
+ # == Schema Information
176
+ #
177
+ # Table name: foo
178
+ #
179
+ # id :integer not null, primary key
180
+ # created_at :datetime
181
+ # updated_at :datetime
182
+ #
183
+
184
+ EOS
185
+
186
+ AnnotatedModels.remove_annotation_of_file("after.rb")
187
+
188
+ content("after.rb").should == <<-EOS
189
+ class Foo < ActiveRecord::Base
190
+ end
191
+ EOS
192
+ end
193
+ end
194
+
195
+ describe "annotating a file" do
196
+ before do
197
+ @file_name = "user.rb"
198
+ @file_content = <<-EOS
199
+ class User < ActiveRecord::Base
200
+ end
201
+ EOS
202
+ File.open(@file_name, "wb") { |f| f.write @file_content }
203
+ @klass = mock_class(:users, :id, [
204
+ mock_column(:id, :integer),
205
+ mock_column(:name, :string, :limit => 50)
206
+ ])
207
+ @schema_info = AnnotatedModels.get_schema_info(@klass, "== Schema Info")
208
+ end
209
+
210
+ it "should annotate the file before the model if position == 'before'" do
211
+ AnnotatedModels.annotate_one_file(@file_name, @schema_info, :position => "before")
212
+ File.read(@file_name).should == "#{@schema_info}#{@file_content}"
213
+ end
214
+
215
+ it "should annotate before if given :position => :before" do
216
+ AnnotatedModels.annotate_one_file(@file_name, @schema_info, :position => :before)
217
+ File.read(@file_name).should == "#{@schema_info}#{@file_content}"
218
+ end
219
+
220
+ it "should annotate before if given :position => :after" do
221
+ AnnotatedModels.annotate_one_file(@file_name, @schema_info, :position => :after)
222
+ File.read(@file_name).should == "#{@file_content}\n#{@schema_info}"
223
+ end
224
+
225
+ it "should update annotate position" do
226
+ AnnotatedModels.annotate_one_file(@file_name, @schema_info, :position => :before)
227
+
228
+ another_schema_info = AnnotatedModels.get_schema_info(mock_class(:users, :id, [mock_column(:id, :integer),]),
229
+ "== Schema Info")
230
+
231
+ AnnotatedModels.annotate_one_file(@file_name, another_schema_info, :position => :after)
232
+
233
+ File.read(@file_name).should == "#{@file_content}\n#{another_schema_info}"
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,12 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rspec'
4
+ require 'annotated_models'
5
+
6
+ # Requires supporting files with custom matchers and macros, etc,
7
+ # in ./support/ and its subdirectories.
8
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
9
+
10
+ RSpec.configure do |config|
11
+
12
+ end
data/todo.txt ADDED
@@ -0,0 +1,4 @@
1
+ TODO
2
+ -----
3
+ add "top" and "bottom" as synonyms for "before" and "after"
4
+ change 'exclude' to 'only' (double negatives are not unconfusing)