annotated_models 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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)