aam 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,15 @@
1
+ require "active_record"
2
+ require "active_support/core_ext/string/filters"
3
+ require "org_tp"
4
+
5
+ module Aam
6
+ SCHEMA_HEADER = "# == Schema Information =="
7
+
8
+ mattr_accessor :logger
9
+ self.logger = ActiveSupport::Logger.new(STDOUT)
10
+ end
11
+
12
+ require "aam/version"
13
+ require "aam/schema_info_generator"
14
+ require "aam/annotation"
15
+ require "aam/railtie" if defined? Rails::Railtie
@@ -0,0 +1,229 @@
1
+ module Aam
2
+ class Annotation
3
+ MAGIC_COMMENT_LINE = "# -*- coding: utf-8 -*-\n"
4
+
5
+ attr_accessor :counts, :options
6
+
7
+ def self.run(options = {})
8
+ new(options).run
9
+ end
10
+
11
+ def initialize(options = {})
12
+ @options = {
13
+ :root_dir => Rails.root,
14
+ :dry_run => false,
15
+ :skip_columns => [], # %w(id created_at updated_at),
16
+ :models => ENV["MODEL"].presence || ENV["MODELS"].presence,
17
+ }.merge(options)
18
+ @counts = Hash.new(0)
19
+ STDOUT.sync = true
20
+ end
21
+
22
+ def run
23
+ schema_info_text_write
24
+ puts
25
+ model_file_write_all
26
+ end
27
+
28
+ def model_file_write_all
29
+ target_ar_klasses_from_model_filenames.each do |klass|
30
+ begin
31
+ model = Model.new(self, klass)
32
+ model.write_to_relation_files
33
+ rescue ActiveRecord::ActiveRecordError => error
34
+ if @options[:debug]
35
+ puts "--------------------------------------------------------------------------------"
36
+ p error
37
+ puts "--------------------------------------------------------------------------------"
38
+ end
39
+ @counts[:error] += 1
40
+ end
41
+ end
42
+ puts "#{@counts[:success]} success, #{@counts[:skip]} skip, #{@counts[:error]} errors"
43
+ end
44
+
45
+ def schema_info_text_write
46
+ @all = []
47
+ target_ar_klasses_from_model_require_and_ar_subclasses.each do |klass|
48
+ begin
49
+ model = Model.new(self, klass)
50
+ @all << model.schema_info
51
+ rescue ActiveRecord::ActiveRecordError => error
52
+ end
53
+ end
54
+ file = root_dir.join("db", "schema_info.txt")
55
+ magic_comment = "-*- truncate-lines: t -*-"
56
+ file.write("#{magic_comment}\n\n#{@all.join}")
57
+ puts "output: #{file} (#{@all.size} counts)"
58
+ end
59
+
60
+ def root_dir
61
+ @root_dir ||= Pathname(@options[:root_dir].to_s).expand_path
62
+ end
63
+
64
+ private
65
+
66
+ class Model
67
+ def initialize(base, klass)
68
+ @base = base
69
+ @klass = klass
70
+ end
71
+
72
+ def schema_info
73
+ @schema_info ||= SchemaInfoGenerator.new(@klass, @base.options).generate + "\n"
74
+ end
75
+
76
+ def write_to_relation_files
77
+ puts "--------------------------------------------------------------------------------"
78
+ puts "--> #{@klass}"
79
+ target_files = search_paths.collect {|search_path|
80
+ v = Pathname.glob((@base.root_dir + search_path).expand_path)
81
+ v.reject{|e|e.to_s.include?("node_modules")}
82
+ }.flatten.uniq
83
+ target_files.each {|e| annotate_write(e) }
84
+ end
85
+
86
+ private
87
+
88
+ # TODO: アプリの構成に依存しすぎ?
89
+ def search_paths
90
+ paths = []
91
+ paths << "app/models/**/#{@klass.name.underscore}.rb"
92
+ # paths << "app/models/**/#{@klass.name.underscore}_{search,observer,callback,sweeper}.rb"
93
+ paths << "test/unit/**/#{@klass.name.underscore}_test.rb"
94
+ paths << "test/fixtures/**/#{@klass.name.underscore.pluralize}.yml"
95
+ paths << "test/unit/helpers/**/#{@klass.name.underscore}_helper_test.rb"
96
+ paths << "spec/models/**/#{@klass.name.underscore}_spec.rb"
97
+ paths << "{test,spec}/**/#{@klass.name.underscore}_factory.rb"
98
+ [:pluralize, :singularize].each{|method|
99
+ prefix = @klass.name.underscore.send(method)
100
+ [
101
+ "app/controllers/**/#{prefix}_controller.rb",
102
+ "app/helpers/**/#{prefix}_helper.rb",
103
+ "test/functional/**/#{prefix}_controller_test.rb",
104
+ "test/factories/**/#{prefix}_factory.rb",
105
+ "test/factories/**/#{prefix}.rb",
106
+ "db/seeds/**/{[0-9]*_,}#{prefix}_setup.rb",
107
+ "db/seeds/**/{[0-9]*_,}#{prefix}_seed.rb",
108
+ "db/seeds/**/{[0-9]*_,}#{prefix}.rb",
109
+ "db/migrate/*_{create,to,from}_#{prefix}.rb",
110
+ "spec/**/#{prefix}_{controller,helper}_spec.rb",
111
+ ].each{|path|
112
+ paths << path
113
+ }
114
+ }
115
+ paths
116
+ end
117
+
118
+ def annotate_write(file_name)
119
+ body = file_name.read
120
+ regexp = /^#{SCHEMA_HEADER}\n(#.*\n)*\n+/
121
+ if body.match(regexp)
122
+ body = body.sub(regexp, schema_info)
123
+ elsif body.include?(MAGIC_COMMENT_LINE)
124
+ body = body.sub(/#{Regexp.escape(MAGIC_COMMENT_LINE)}\s*/) {MAGIC_COMMENT_LINE + schema_info}
125
+ else
126
+ body = body.sub(/^\s*/, schema_info)
127
+ end
128
+ body = insert_magick_comment(body)
129
+ unless @base.options[:dry_run]
130
+ file_name.write(body)
131
+ end
132
+ puts "write: #{file_name}"
133
+ @base.counts[:success] += 1
134
+ end
135
+
136
+ def insert_magick_comment(body, force = false)
137
+ if force
138
+ body = body.sub(/#{Regexp.escape(MAGIC_COMMENT_LINE)}\s*/, "")
139
+ end
140
+ unless body.include?(MAGIC_COMMENT_LINE)
141
+ body = body.sub(/^\s*/, MAGIC_COMMENT_LINE)
142
+ end
143
+ body
144
+ end
145
+ end
146
+
147
+ #
148
+ # テーブルを持っているクラスたち
149
+ #
150
+ def target_ar_klasses
151
+ target_ar_klasses_from_model_require_and_ar_subclasses
152
+ # ActiveRecord::Base.subclasses
153
+ end
154
+
155
+ # すべての app/models/**/*.rb を require したあと ActiveRecord::Base.subclasses を参照
156
+ def target_ar_klasses_from_model_require_and_ar_subclasses
157
+ target_model_files.each do |file|
158
+ begin
159
+ silence_warnings do
160
+ require file
161
+ end
162
+ puts "require: #{file}"
163
+ rescue Exception
164
+ end
165
+ end
166
+ if defined?(ApplicationRecord)
167
+ ApplicationRecord.subclasses
168
+ else
169
+ ActiveRecord::Base.subclasses
170
+ end
171
+ end
172
+
173
+ # app/models/* のファイル名を constantize してみることでクラスを収集する
174
+ def target_ar_klasses_from_model_filenames
175
+ models = []
176
+ target_model_files.each do |file|
177
+ file = file.expand_path
178
+ klass = nil
179
+
180
+ md = file.to_s.match(/\A.*\/app\/models\/(.*)\.rb\z/)
181
+ underscore_class_name = md.captures.first
182
+ class_name = underscore_class_name.camelize # classify だと boss が bos になってしまう
183
+ begin
184
+ klass = class_name.constantize
185
+ rescue LoadError => error # LoadError は rescue nil では捕捉できないため
186
+ puts "#{class_name} に対応するファイルは見つかりませんでした : #{error}"
187
+ rescue
188
+ end
189
+
190
+ # klass.class == Class を入れないと [] < ActiveRecord::Base のときにエラーになる
191
+ if klass && klass.class == Class && klass < ActiveRecord::Base && !klass.abstract_class?
192
+ # puts "#{file} は ActiveRecord::Base のサブクラスなので対象とします。"
193
+ puts "model: #{file}"
194
+ models << klass
195
+ else
196
+ # puts "#{file} (クラス名:#{class_name}) は ActiveRecord::Base のサブクラスではありませんでした。"
197
+ end
198
+ end
199
+ models
200
+ end
201
+
202
+ #
203
+ # 対象のモデルファイル
204
+ #
205
+ def target_model_files
206
+ files = []
207
+ files += Pathname.glob("#{root_dir}/app/models/**/*.rb")
208
+ files += Pathname.glob("#{root_dir}/vendor/plugins/*/app/models/**/*.rb")
209
+ if @options[:models]
210
+ @options[:models].split(",").collect { |m|
211
+ files.find_all { |e|
212
+ e.basename(".*").to_s.match(/#{m.camelize}|#{m.underscore}/i)
213
+ }
214
+ }.flatten.uniq
215
+ else
216
+ files
217
+ end
218
+ end
219
+ end
220
+ end
221
+
222
+ if $0 == __FILE__
223
+ require "active_record"
224
+ require "rails"
225
+ require "org_tp"
226
+ obj = Aam::Annotation.new(root_dir: "~/src/shogi_web")
227
+ tp obj.send(:target_model_files)
228
+ tp obj.send(:target_ar_klasses_from_model_filenames)
229
+ end
@@ -0,0 +1,7 @@
1
+ module Aam
2
+ class Railtie < Rails::Railtie
3
+ rake_tasks do
4
+ load "aam/tasks/aam.rake"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,313 @@
1
+ require "active_record"
2
+ require "active_support/core_ext/string/filters"
3
+ require "org_tp"
4
+
5
+ module Aam
6
+ class SchemaInfoGenerator
7
+ def initialize(klass, options = {})
8
+ @klass = klass
9
+ @options = {
10
+ :skip_columns => [],
11
+ :debug => false,
12
+ }.merge(options)
13
+ @memos = []
14
+ end
15
+
16
+ def generate
17
+ columns = @klass.columns.reject { |e|
18
+ @options[:skip_columns].include?(e.name)
19
+ }
20
+ rows = columns.collect {|e|
21
+ {
22
+ "カラム名" => e.name,
23
+ "意味" => column_to_human_name(e.name),
24
+ "タイプ" => column_type_inspect_of(e),
25
+ "属性" => column_attribute_inspect_of(e),
26
+ "参照" => reflections_inspect_of(e),
27
+ "INDEX" => index_info(e),
28
+ }
29
+ }
30
+ out = []
31
+ out << "#{SCHEMA_HEADER}\n#\n"
32
+ out << "# #{@klass.model_name.human}テーブル (#{@klass.table_name} as #{@klass.name})\n"
33
+ out << "#\n"
34
+ out << rows.to_t.lines.collect { |e| "# #{e}" }.join
35
+ if @memos.present?
36
+ out << "#\n"
37
+ out << "#- 備考 -------------------------------------------------------------------------\n"
38
+ out << @memos.collect{|row|"# ・#{row}\n"}.join
39
+ out << "#--------------------------------------------------------------------------------\n"
40
+ end
41
+ out.join
42
+ end
43
+
44
+ private
45
+
46
+ def column_type_inspect_of(column)
47
+ size = nil
48
+ if column.type.to_s == "decimal"
49
+ size = "(#{column.precision}, #{column.scale})"
50
+ else
51
+ if column.limit
52
+ size = "(#{column.limit})"
53
+ end
54
+ end
55
+
56
+ # シリアライズされているかチェック
57
+ serialized_klass = nil
58
+ if @klass.respond_to?(:serialized_attributes) # Rails5 から無くなったため存在チェック
59
+ if serialized_klass = @klass.serialized_attributes[column.name]
60
+ if serialized_klass.kind_of? ActiveRecord::Coders::YAMLColumn
61
+ serialized_klass = "=> #{serialized_klass.object_class}"
62
+ else
63
+ serialized_klass = "=> #{serialized_klass}"
64
+ end
65
+ end
66
+ end
67
+
68
+ "#{column.type}#{size} #{serialized_klass}".squish
69
+ end
70
+
71
+ def column_attribute_inspect_of(column)
72
+ attrs = []
73
+ unless column.default.nil?
74
+ default = column.default
75
+ if default.kind_of? BigDecimal
76
+ default = default.to_f
77
+ if default.zero?
78
+ default = 0
79
+ end
80
+ end
81
+ attrs << "DEFAULT(#{default})"
82
+ end
83
+ unless column.null
84
+ attrs << "NOT NULL"
85
+ end
86
+ if column.name == @klass.primary_key
87
+ attrs << "PK"
88
+ end
89
+ attrs * " "
90
+ end
91
+
92
+ def reflections_inspect_of(column)
93
+ [reflections_inspect_ary_of(column)].flatten.compact.sort.join(" と ")
94
+ end
95
+
96
+ def reflections_inspect_ary_of(column)
97
+ if column.name == @klass.inheritance_column # カラムが "type" のとき
98
+ return "モデル名(STI)"
99
+ end
100
+
101
+ index_check(column)
102
+
103
+ my_refrections = @klass.reflections.find_all do |key, reflection|
104
+ if !reflection.is_a?(ActiveRecord::Reflection::ThroughReflection) && reflection.respond_to?(:foreign_key)
105
+ reflection.foreign_key.to_s == column.name
106
+ end
107
+ end
108
+
109
+ if my_refrections.empty?
110
+ # "xxx_id" は belongs_to されていることを確認
111
+ if md = column.name.match(/(\w+)_id\z/)
112
+ name = md.captures.first
113
+ if @klass.column_names.include?("#{name}_type")
114
+ syntax = "belongs_to :#{name}, :polymorphic => true"
115
+ else
116
+ syntax = "belongs_to :#{name}"
117
+ end
118
+ memo_puts "【警告】#{@klass} モデルに #{syntax} を追加してください"
119
+ else
120
+ # "xxx_type" は polymorphic 指定されていることを確認
121
+ key, reflection = @klass.reflections.find do |key, reflection|
122
+ _options = reflection.options
123
+ if true
124
+ # >= 3.1.3
125
+ _options[:polymorphic] && column.name == "#{key}_type"
126
+ else
127
+ # < 3.1.3
128
+ _options[:polymorphic] && _options[:foreign_type] == column.name
129
+ end
130
+ end
131
+ if reflection
132
+ "モデル名(polymorphic)"
133
+ end
134
+ end
135
+ else
136
+ # 一つのカラムを複数の方法で利用している場合に対応するため回している。
137
+ my_refrections.collect do |key, reflection|
138
+ begin
139
+ reflection_inspect_of(column, reflection)
140
+ rescue NameError => error
141
+ if @options[:debug]
142
+ puts "--------------------------------------------------------------------------------"
143
+ puts "【警告】以下のクラスがないため NameError になっちゃってます"
144
+ p error
145
+ puts "--------------------------------------------------------------------------------"
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
151
+
152
+ def reflection_inspect_of(column, reflection)
153
+ return unless reflection.macro == :belongs_to
154
+ desc = nil
155
+ if reflection.options[:polymorphic]
156
+ if true
157
+ # >= 3.1.3
158
+ target = "(#{reflection.name}_type)##{reflection.active_record.primary_key}"
159
+ else
160
+ # < 3.1.3
161
+ target = "(#{reflection.options[:foreign_type]})##{reflection.active_record.primary_key}"
162
+ end
163
+ else
164
+ target = "#{reflection.class_name}##{reflection.active_record.primary_key}"
165
+ desc = belongs_to_model_has_many_syntax(column, reflection)
166
+ end
167
+ assoc_name = ""
168
+ unless "#{reflection.name}_id" == column.name
169
+ assoc_name = ":#{reflection.name}"
170
+ end
171
+ "#{assoc_name} => #{target} #{desc}".squish
172
+ end
173
+
174
+ # belongs_to :user している場合 User モデルから has_many :articles されていることを確認。
175
+ #
176
+ # 1. assoc_reflection.foreign_key.to_s == column.name という比較では foreign_key 指定されると不一致になるので注意すること。
177
+ # と書いたけど不一致になってもよかった。これでリレーション正しく貼られてないと判断してよい。
178
+ # 理由は belongs_to に foreign_key が指定されたら has_many 側も has_many :foos, :foreign_key => "bar_id" とならないといけないため。
179
+ #
180
+ def belongs_to_model_has_many_syntax(column, reflection)
181
+ assoc_key, assoc_reflection = reflection.class_name.constantize.reflections.find do |assoc_key, assoc_reflection|
182
+ if false
183
+ r = reflection.class_name.constantize == assoc_reflection.active_record && [:has_many, :has_one].include?(assoc_reflection.macro)
184
+ else
185
+ r = assoc_reflection.respond_to?(:foreign_key) && assoc_reflection.foreign_key.to_s == column.name
186
+ end
187
+ if r
188
+ syntax = ["#{assoc_reflection.macro} :#{assoc_reflection.name}"]
189
+ if assoc_reflection.options[:foreign_key]
190
+ syntax << ":foreign_key => :#{assoc_reflection.options[:foreign_key]}"
191
+ end
192
+ memo_puts "#{@klass.name} モデルは #{assoc_reflection.active_record} モデルから #{syntax.join(', ')} されています。"
193
+ r
194
+ end
195
+ end
196
+ unless assoc_reflection
197
+ syntax = ["has_many :#{@klass.name.underscore.pluralize}"]
198
+ if false
199
+ # has_many :sub_articles の場合デフォルトで SubArticle を見るため不要
200
+ syntax << ":class_name => \"#{@klass.name}\""
201
+ end
202
+ if reflection.options[:foreign_key]
203
+ syntax << ":foreign_key => :#{reflection.options[:foreign_key]}"
204
+ end
205
+ memo_puts "【警告:リレーション欠如】#{reflection.class_name}モデルで #{syntax.join(', ')} されていません"
206
+ end
207
+ end
208
+
209
+ # カラム翻訳
210
+ #
211
+ # ja.rb:
212
+ # :item => "アイテム"
213
+ #
214
+ # 実行結果:
215
+ # column_to_human_name("item") #=> "アイテム"
216
+ # column_to_human_name("item_id") #=> "アイテムID"
217
+ #
218
+ def column_to_human_name(name)
219
+ resp = nil
220
+ suffixes = {
221
+ :id => "ID",
222
+ :type => "タイプ",
223
+ }
224
+ suffixes.each do |key, value|
225
+ if md = name.match(/(?<name_without_suffix>\w+)_#{key}$/)
226
+ # サフィックス付きのまま明示的に翻訳されている場合はそれを使う
227
+ resp = @klass.human_attribute_name(name, :default => "").presence
228
+ # サフィックスなしが明示的に翻訳されていたらそれを使う
229
+ unless resp
230
+ if v = @klass.human_attribute_name(md[:name_without_suffix], :default => "").presence
231
+ resp = "#{v}#{value}"
232
+ end
233
+ end
234
+ end
235
+ if resp
236
+ break
237
+ end
238
+ end
239
+ # 翻訳が効いてないけどid付きのまま仕方なく変換する
240
+ resp ||= @klass.human_attribute_name(name)
241
+ end
242
+
243
+ #
244
+ # インデックス情報の取得
245
+ #
246
+ # add_index :articles, :name #=> "I"
247
+ # add_index :articles, :name, :unique => true #=> "UI"
248
+ #
249
+ def index_info(column)
250
+ indexes = @klass.connection.indexes(@klass.table_name)
251
+ # 関係するインデックスに絞る
252
+ indexes2 = indexes.find_all {|e| e.columns.include?(column.name) }
253
+ indexes2.collect {|e|
254
+ mark = ""
255
+ # そのインデックスは何番目にあるかを調べる
256
+ mark << ("A".."Z").to_a.at(indexes.index(e)).to_s
257
+ # ユニークなら「!」
258
+ if e.unique
259
+ mark << "!"
260
+ end
261
+ # mark << e.columns.size.to_s # 1なら単独、2ならペア、3ならトリプル指定みたいなのわかる
262
+ mark
263
+ }.join(" ")
264
+ end
265
+
266
+ #
267
+ # 指定のカラムは何かのインデックスに含まれているか?
268
+ #
269
+ def index_column?(column)
270
+ indexes = @klass.connection.indexes(@klass.table_name)
271
+ indexes.any?{|e|e.columns.include?(column.name)}
272
+ end
273
+
274
+ #
275
+ # belongs_to のカラムか?
276
+ #
277
+ def belongs_to_column?(column)
278
+ @klass.reflections.any? do |key, reflection|
279
+ if reflection.macro == :belongs_to
280
+ if reflection.respond_to?(:foreign_key)
281
+ reflection.foreign_key.to_s == column.name
282
+ end
283
+ end
284
+ end
285
+ end
286
+
287
+ #
288
+ # 指定のカラムがインデックスを貼るべきかどうかを表示する
289
+ #
290
+ def index_check(column)
291
+ if column.name.match(/(\w+)_id\z/) || belongs_to_column?(column)
292
+ # belongs_to :xxx, :polymorphic => true の場合は xxx_id と xxx_type のペアでインデックスを貼る
293
+ if (md = column.name.match(/(\w+)_id\z/)) && (type_column = @klass.columns_hash["#{md.captures.first}_type"])
294
+ unless index_column?(column) && index_column?(type_column)
295
+ memo_puts "【警告:インデックス欠如】create_#{@klass.table_name} マイグレーションに add_index :#{@klass.table_name}, [:#{column.name}, :#{type_column.name}] を追加してください"
296
+ end
297
+ else
298
+ unless index_column?(column)
299
+ memo_puts "【警告:インデックス欠如】create_#{@klass.table_name} マイグレーションに add_index :#{@klass.table_name}, :#{column.name} を追加してください"
300
+ end
301
+ end
302
+ end
303
+ end
304
+
305
+ def memo_puts(str)
306
+ if @options[:debug]
307
+ Aam.logger.debug str if Aam.logger
308
+ end
309
+ @memos << str
310
+ nil
311
+ end
312
+ end
313
+ end