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.
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +43 -0
- data/History.txt +51 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +145 -0
- data/Rakefile +55 -0
- data/VERSION.yml +5 -0
- data/annotated_models.gemspec +120 -0
- data/bin/annotate +74 -0
- data/lib/annotated_models.rb +357 -0
- data/lib/tasks/annotate_models.rake +24 -0
- data/lib/tasks/migrate.rake +50 -0
- data/spec/annotate_models_spec.rb +236 -0
- data/spec/spec_helper.rb +12 -0
- data/todo.txt +4 -0
- metadata +346 -0
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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