schemard 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.ja.md +93 -0
- data/README.md +56 -0
- data/Rakefile +2 -0
- data/bin/schemard +18 -0
- data/lib/contents/favicon.ico +0 -0
- data/lib/contents/jquery2.min.js +4 -0
- data/lib/contents/normalize.css +427 -0
- data/lib/contents/tableViewer.js +867 -0
- data/lib/locales/messages.yml +37 -0
- data/lib/schemard/controller.rb +160 -0
- data/lib/schemard/metadata.rb +134 -0
- data/lib/schemard/rdoc_parser.rb +66 -0
- data/lib/schemard/relation_generator.rb +54 -0
- data/lib/schemard/schema.rb +79 -0
- data/lib/schemard/schema_parser.rb +92 -0
- data/lib/schemard/utils/localizer.rb +43 -0
- data/lib/schemard/utils/singularizer.rb +56 -0
- data/lib/schemard/utils/struct_assigner.rb +12 -0
- data/lib/schemard/web_server.rb +56 -0
- data/lib/schemard.rb +6 -0
- data/lib/templates/index.html.erb +81 -0
- data/lib/templates/show.html.erb +156 -0
- data/schemard.gemspec +28 -0
- metadata +101 -0
@@ -0,0 +1,37 @@
|
|
1
|
+
en:
|
2
|
+
views:
|
3
|
+
edit_checkbox_label: "Edit table position"
|
4
|
+
table_summary_title: "Table Information"
|
5
|
+
table_name: "Table Name"
|
6
|
+
parent_tables: "Parent Tables"
|
7
|
+
child_tables: "Child Tables"
|
8
|
+
description: "Description"
|
9
|
+
column_definition: "Column Information"
|
10
|
+
column_name: "Column Name"
|
11
|
+
column_type: "Type"
|
12
|
+
column_precision: "precision"
|
13
|
+
column_default: "Default"
|
14
|
+
index_definition: "Index Information"
|
15
|
+
index_unique: "Unique"
|
16
|
+
index_target_columns: "Columns"
|
17
|
+
index_name: "Index Name"
|
18
|
+
|
19
|
+
ja:
|
20
|
+
views:
|
21
|
+
edit_checkbox_label: "テーブルの位置を編集"
|
22
|
+
table_summary_title: "テーブル基本情報"
|
23
|
+
table_name: "テーブル名"
|
24
|
+
table_name_en: "テーブル英名"
|
25
|
+
parent_tables: "親テーブル"
|
26
|
+
child_tables: "子テーブル"
|
27
|
+
description: "説明"
|
28
|
+
column_definition: "カラム定義"
|
29
|
+
column_name: "列名"
|
30
|
+
column_name_en: "列名(英)"
|
31
|
+
column_type: "型"
|
32
|
+
column_precision: "精度"
|
33
|
+
column_default: "デフォルト"
|
34
|
+
index_definition: "インデックス定義"
|
35
|
+
index_unique: "ユニーク"
|
36
|
+
index_target_columns: "対象カラム"
|
37
|
+
index_name: "インデックス名"
|
@@ -0,0 +1,160 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'yaml'
|
3
|
+
require_relative "schema"
|
4
|
+
require_relative "schema_parser"
|
5
|
+
require_relative "metadata"
|
6
|
+
require_relative "utils/localizer"
|
7
|
+
require_relative "utils/struct_assigner"
|
8
|
+
|
9
|
+
module SchemaRD
|
10
|
+
class Controller
|
11
|
+
attr_reader :config
|
12
|
+
|
13
|
+
def initialize(config)
|
14
|
+
@config = config;
|
15
|
+
end
|
16
|
+
|
17
|
+
def index(req, res)
|
18
|
+
locale = localizer(req)
|
19
|
+
schema = SchemaRD::SchemaParser.new(config.input_file).parse(with_comment: config.parse_db_comment?)
|
20
|
+
SchemaRD::Metadata.load(config: self.config, lang: locale.lang, schema: schema)
|
21
|
+
send(req, res, render("index.html.erb", binding))
|
22
|
+
end
|
23
|
+
|
24
|
+
def show(req, res)
|
25
|
+
locale = localizer(req)
|
26
|
+
match = req.path.match(/\/tables\/(\w+)/)
|
27
|
+
unless match
|
28
|
+
res.status = 404
|
29
|
+
else
|
30
|
+
schema = SchemaRD::SchemaParser.new(config.input_file).parse(with_comment: config.parse_db_comment?)
|
31
|
+
SchemaRD::Metadata.load(config: self.config, lang: locale.lang, schema: schema)
|
32
|
+
table_name = match[1]
|
33
|
+
send(req, res, render("show.html.erb", binding))
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def update(req, res)
|
38
|
+
match = req.path.match(/\/tables\/(\w+)/)
|
39
|
+
unless match
|
40
|
+
res.status = 404
|
41
|
+
else
|
42
|
+
if req.query['layout']
|
43
|
+
pos = req.query['layout'].split(",")
|
44
|
+
SchemaRD::Metadata::Writer.new(config.output_file).save(match[1], { "left" => pos[0], "top" => pos[1] })
|
45
|
+
end
|
46
|
+
send(req, res, "OK")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def static_file(req, res)
|
51
|
+
send(req, res, File.new(CONTENTS_DIR + req.path).read)
|
52
|
+
end
|
53
|
+
|
54
|
+
TEMPLATES_DIR = "#{File.dirname(File.expand_path(__FILE__))}/../templates/"
|
55
|
+
CONTENTS_DIR = "#{File.dirname(File.expand_path(__FILE__))}/../contents/"
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def localizer(req)
|
60
|
+
SchemaRD::Utils::MessageLocalizer.new(req.accept_language[0] || "en")
|
61
|
+
end
|
62
|
+
|
63
|
+
def send(req, res, body = nil)
|
64
|
+
res.status = 200
|
65
|
+
res.content_type = case req.path
|
66
|
+
when /.*\.js\Z/
|
67
|
+
"text/javascript"
|
68
|
+
when /.*\.css\Z/
|
69
|
+
"text/css"
|
70
|
+
when /.*\.ico\Z/
|
71
|
+
"image/x-icon"
|
72
|
+
else
|
73
|
+
"text/html"
|
74
|
+
end
|
75
|
+
res.body = body
|
76
|
+
end
|
77
|
+
|
78
|
+
def render(filename, current_binding)
|
79
|
+
ERB.new(File.new(TEMPLATES_DIR + filename).read, nil, '-').result(current_binding)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
CONFIG_FILE = ".schamard.config"
|
84
|
+
DEFAULT_CONFIG = {
|
85
|
+
input_file: "db/schema.rb",
|
86
|
+
output_file: "schema.metadata",
|
87
|
+
metadata_files: [],
|
88
|
+
rdoc_enabled: false,
|
89
|
+
parse_db_comment_as: "ignore",
|
90
|
+
log_output: STDOUT,
|
91
|
+
webserver_host: "127.0.0.1",
|
92
|
+
webserver_port: "10080"
|
93
|
+
}
|
94
|
+
|
95
|
+
class Configuration < Struct.new(*DEFAULT_CONFIG.keys)
|
96
|
+
include SchemaRD::Utils::StructAssigner
|
97
|
+
attr_reader :errors
|
98
|
+
def initialize(argv = nil)
|
99
|
+
hash = {}.merge(DEFAULT_CONFIG)
|
100
|
+
hash.merge(YAML.load_file(CONFIG_FILE)) if File.readable?(CONFIG_FILE)
|
101
|
+
|
102
|
+
unless argv.nil?
|
103
|
+
opt = OptionParser.new
|
104
|
+
opt.on('-i VAL', '--input-file=VAL') {|v| hash[:input_file] = v }
|
105
|
+
opt.on('-o VAL', '--output-file=VAL') {|v| hash[:output_file] = v }
|
106
|
+
opt.on('-f VAL', '-m VAL', '--metadata-file=VAL') {|v| hash[:metadata_files] << v }
|
107
|
+
opt.on('--rdoc', '--rdoc-enabled') { hash[:rdoc_enabled] = true }
|
108
|
+
opt.on('--parse-db-comment-as=VAL') {|v| hash[:parse_db_comment_as] = v }
|
109
|
+
opt.on('-s', '--silent', '--no-log-output') {|v| hash[:log_output] = File.open(File::NULL, 'w') }
|
110
|
+
opt.on('-h VAL', '--host=VAL') {|v| hash[:webserver_host] = v }
|
111
|
+
opt.on('-p VAL', '--port=VAL') {|v| hash[:webserver_port] = v }
|
112
|
+
opt.on('-l VAL', '--log-output=VAL') {|v| hash[:log_output] = self.class.str_to_io(v) }
|
113
|
+
opt.parse(argv)
|
114
|
+
end
|
115
|
+
self.assign(hash)
|
116
|
+
end
|
117
|
+
|
118
|
+
def parse_db_comment?
|
119
|
+
self.parse_db_comment_as != "ignore"
|
120
|
+
end
|
121
|
+
|
122
|
+
def valid?
|
123
|
+
@errors = []
|
124
|
+
unless File.readable?(self.input_file)
|
125
|
+
self.errors << "InputFile: \"#{self.input_file}\" is not readable!"
|
126
|
+
end
|
127
|
+
unless (File.writable?(self.output_file) || File.writable?(File.dirname(self.output_file)))
|
128
|
+
self.errors << "OutputFile: \"#{self.output_file}\" is not writable!"
|
129
|
+
end
|
130
|
+
self.metadata_files.each do |metadata_file|
|
131
|
+
unless File.readable?(metadata_file)
|
132
|
+
self.errors << "MetadataFile: \"#{metadata_file}\" is not readable!"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
unless %w(ignore name localized_name description custom).include?(self.parse_db_comment_as)
|
136
|
+
self.errors << "ParseDBCommentAs: \"#{self.parse_db_comment_as}\" is not allowed!"
|
137
|
+
end
|
138
|
+
if self.log_output.is_a?(String)
|
139
|
+
self.errors << "LogFile: \"#{self.log_output}\" is not writable!"
|
140
|
+
end
|
141
|
+
unless self.webserver_port =~ /^[0-9]+$/
|
142
|
+
self.errors << "WebServerPort: \"#{self.webserver_port}\" is invalid!"
|
143
|
+
end
|
144
|
+
self.errors.empty?
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
|
149
|
+
def self.str_to_io(str)
|
150
|
+
case str
|
151
|
+
when "stdout", "STDOUT"
|
152
|
+
STDOUT
|
153
|
+
when "stderr", "STDERR"
|
154
|
+
STDERR
|
155
|
+
else
|
156
|
+
File.open(str, 'w') rescue str
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require_relative 'utils/localizer'
|
3
|
+
require_relative 'rdoc_parser'
|
4
|
+
|
5
|
+
module SchemaRD
|
6
|
+
module Metadata
|
7
|
+
def self.load(config:, lang:, schema:)
|
8
|
+
metadata = Parser.new(config.output_file, *config.metadata_files).parse()
|
9
|
+
localizer = SchemaRD::Utils::SchemaLocalizer.new(lang, metadata)
|
10
|
+
# localized_name を設定
|
11
|
+
schema.tables.each do |table|
|
12
|
+
table.localized_name = localizer.table_name(table.name)
|
13
|
+
table.columns.each do |column|
|
14
|
+
column.localized_name = localizer.column_name(table.name, column.name)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
# set position, and relations
|
18
|
+
(metadata["tables"] || {}).each do |table_name, hash|
|
19
|
+
# skip when table name exists in metadata only.
|
20
|
+
next unless schema.table(table_name)
|
21
|
+
if hash["position_left"] && hash["position_top"]
|
22
|
+
schema.table(table_name).position =
|
23
|
+
{ "left" => hash["position_left"], "top" => hash["position_top"] }
|
24
|
+
end
|
25
|
+
self.add_relations(table_name, "belongs_to", hash["belongs_to"], schema)
|
26
|
+
self.add_relations(table_name, "has_many", hash["has_many"], schema)
|
27
|
+
self.add_relations(table_name, "has_one", hash["has_one"], schema)
|
28
|
+
end
|
29
|
+
# db_comment にメタ情報が含まれる場合に設定
|
30
|
+
if config.parse_db_comment?
|
31
|
+
schema.tables.each do |table|
|
32
|
+
if table.parsed_db_comment && table.parsed_db_comment.strip != ""
|
33
|
+
case config.parse_db_comment_as
|
34
|
+
when 'name', 'localized_name'
|
35
|
+
table.localized_name = table.parsed_db_comment.strip
|
36
|
+
when 'description'
|
37
|
+
table.description = table.parsed_db_comment.strip
|
38
|
+
when 'custom'
|
39
|
+
config.db_comment_parser.call(table: table)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
table.columns
|
43
|
+
.select{|c| c.parsed_db_comment && c.parsed_db_comment.strip != ""}.each do |column|
|
44
|
+
case config.parse_db_comment_as
|
45
|
+
when 'name', 'localized_name'
|
46
|
+
column.localized_name = column.parsed_db_comment.strip
|
47
|
+
when 'description'
|
48
|
+
column.description = column.parsed_db_comment.strip
|
49
|
+
when 'custom'
|
50
|
+
config.db_comment_parser.call(column: column)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
# RDocコメントとしてメタ情報が含まれる場合に設定
|
56
|
+
if config.rdoc_enabled
|
57
|
+
rdoc = SchemaRD::RDocParser.new(config.input_file)
|
58
|
+
schema.tables.select{|t| rdoc.table_comment(t.name) }.each do |table|
|
59
|
+
parser = DefaultTableCommentParser.new(rdoc.table_comment(table.name))
|
60
|
+
table.localized_name = parser.localized_name if parser.has_localized_name?
|
61
|
+
table.description = parser.description if parser.has_description?
|
62
|
+
|
63
|
+
%i(belongs_to has_many has_one).each do |rel_type|
|
64
|
+
if parser.has_relation_of?(rel_type)
|
65
|
+
self.add_relations(table.name, rel_type.to_s, parser.relation_of(rel_type), schema)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
# output_file がなければ作成
|
71
|
+
Writer.new(config.output_file).save_all(schema.tables) unless File.exist?(config.output_file)
|
72
|
+
schema
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.add_relations(table_name, type, relation_table_names, schema)
|
76
|
+
return unless relation_table_names
|
77
|
+
relation_table_names = relation_table_names.split(",") if relation_table_names.is_a?(String)
|
78
|
+
|
79
|
+
relation_table_names.map{|rel_table_name| schema.table(rel_table_name) }.compact.each do |rel_table|
|
80
|
+
parent_table = type == "belongs_to" ? rel_table : schema.table(table_name)
|
81
|
+
child_table = type == "belongs_to" ? schema.table(table_name) : rel_table
|
82
|
+
|
83
|
+
if parent_table.relation_to(child_table.name).nil?
|
84
|
+
schema.add_relation(TableRelation.new(parent_table: parent_table, child_table: child_table))
|
85
|
+
end
|
86
|
+
parent_table.relation_to(child_table.name).child_cardinality = "1" if type == "has_one"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Writer for metadata yaml
|
91
|
+
class Writer
|
92
|
+
def initialize(output_file)
|
93
|
+
@output_file = output_file
|
94
|
+
end
|
95
|
+
def save_all(tables)
|
96
|
+
hash = tables.each_with_object({}) do |t, hash|
|
97
|
+
hash[t.name] = { "position_top" => t.position["top"], "position_left" => t.position["left"] }
|
98
|
+
end
|
99
|
+
File.write(@output_file, YAML.dump({ "tables" => hash }))
|
100
|
+
end
|
101
|
+
def save(table_name, position)
|
102
|
+
hash = YAML.load_file(@output_file) || {}
|
103
|
+
hash["tables"] = {} unless hash.has_key?("tables")
|
104
|
+
hash["tables"] = {} unless hash["tables"].is_a?(Hash)
|
105
|
+
hash["tables"][table_name] = {} unless hash["tables"][table_name]
|
106
|
+
hash["tables"][table_name] = {} unless hash["tables"][table_name].is_a?(Hash)
|
107
|
+
hash["tables"][table_name]["position_top"] = position["top"].to_s
|
108
|
+
hash["tables"][table_name]["position_left"] = position["left"].to_s
|
109
|
+
File.write(@output_file, YAML.dump(hash))
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# parser for metadata yaml
|
114
|
+
class Parser
|
115
|
+
def initialize(output_file, *metadata_files)
|
116
|
+
@parsed = {}
|
117
|
+
metadata_files.select{|metadata_file| File.exist?(metadata_file) }.each do |metadata_file|
|
118
|
+
self.class.deep_merge(@parsed, YAML.load_file(metadata_file))
|
119
|
+
end
|
120
|
+
self.class.deep_merge(@parsed, YAML.load_file(output_file)) if File.exist?(output_file)
|
121
|
+
end
|
122
|
+
# get hash of metadata
|
123
|
+
def parse
|
124
|
+
@parsed
|
125
|
+
end
|
126
|
+
def self.deep_merge(source, other)
|
127
|
+
other.each do |k,v|
|
128
|
+
next self.deep_merge(source[k], other[k]) if other[k].is_a?(Hash) && source[k].is_a?(Hash)
|
129
|
+
source[k] = other[k]
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end # end of Parser
|
133
|
+
end # end of Metadata
|
134
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'rdoc'
|
2
|
+
module SchemaRD
|
3
|
+
class RDocParser
|
4
|
+
def initialize(filename)
|
5
|
+
parse(filename)
|
6
|
+
end
|
7
|
+
|
8
|
+
def table_comment(name)
|
9
|
+
method_obj = @clazz.find_method_named(name)
|
10
|
+
method_obj ? method_obj.comment.text : ""
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def parse(filename)
|
16
|
+
file_content = File.read(filename)
|
17
|
+
content = "module Schemafile\n#{file_content}\nend"
|
18
|
+
|
19
|
+
rdoc = RDoc::RDoc.new
|
20
|
+
store = RDoc::Store.new
|
21
|
+
options = rdoc.load_options
|
22
|
+
stats = RDoc::Stats.new(store, 1, options.verbosity)
|
23
|
+
top_level = store.add_file(filename)
|
24
|
+
RDoc::Parser::Ruby.new(top_level, filename, content, options, stats).scan
|
25
|
+
@clazz = top_level.find_module_named("Schemafile")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class DefaultTableCommentParser
|
30
|
+
def initialize(comment_text)
|
31
|
+
@hash = { relations: {} }
|
32
|
+
@hash[:description] = comment_text.split("\n").map(&:strip).map{|line|
|
33
|
+
if line =~ /^name\:\:/ || line =~ /^localized_name\:\:/
|
34
|
+
@hash[:localized_name] = line.match(/^[^:]*name\:\:(.+)$/)[1].strip
|
35
|
+
next
|
36
|
+
end
|
37
|
+
%w(belongs_to has_many has_one).each do |rel_type|
|
38
|
+
if line =~ /^#{rel_type}\:\:/
|
39
|
+
tables = line.match(/^#{rel_type}\:\:(.+)$/)[1].split(",").map(&:strip).select{|s| s != "" }
|
40
|
+
@hash[:relations][rel_type.to_sym] = tables unless tables.empty?
|
41
|
+
line = nil # skip this line (by compact)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
line
|
45
|
+
}.compact.join("\n")
|
46
|
+
end
|
47
|
+
def has_localized_name?
|
48
|
+
@hash[:localized_name] && @hash[:localized_name] != ""
|
49
|
+
end
|
50
|
+
def localized_name
|
51
|
+
@hash[:localized_name]
|
52
|
+
end
|
53
|
+
def has_description?
|
54
|
+
!!@hash[:description]
|
55
|
+
end
|
56
|
+
def description
|
57
|
+
@hash[:description]
|
58
|
+
end
|
59
|
+
def has_relation_of?(rel_type)
|
60
|
+
!!@hash[:relations][rel_type]
|
61
|
+
end
|
62
|
+
def relation_of(rel_type)
|
63
|
+
@hash[:relations][rel_type]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'pathname'
|
3
|
+
require 'optparse'
|
4
|
+
|
5
|
+
module SchemaRD
|
6
|
+
class RelationGenerator
|
7
|
+
def initialize(argv)
|
8
|
+
rails_root = Pathname.pwd
|
9
|
+
opt = OptionParser.new
|
10
|
+
opt.on('-d VAL', 'Rails.root.directory') {|v| rails_root = Pathname.new(v).expand_path }
|
11
|
+
opt.parse(argv)
|
12
|
+
|
13
|
+
require_path = rails_root + "config/environment.rb"
|
14
|
+
unless require_path.exist?
|
15
|
+
puts "<#{rails_root}> is not Rails.root Directory, Abort!"
|
16
|
+
puts "Usage: schemard -d <Rails.root.dir>"
|
17
|
+
else
|
18
|
+
Dir.chdir(rails_root) do
|
19
|
+
require require_path.to_s
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
def ready?
|
24
|
+
defined?(Rails)
|
25
|
+
end
|
26
|
+
def run
|
27
|
+
Dir.glob(Rails.root + "app/models/**/*")
|
28
|
+
.reject{|path| Dir.exist?(path) }.each{|filepath| require filepath }
|
29
|
+
|
30
|
+
hash = ObjectSpace.each_object(Class)
|
31
|
+
.select{|o| o.ancestors.include?(ActiveRecord::Base) && o != ActiveRecord::Base }
|
32
|
+
.select{|o| o.table_name }
|
33
|
+
.each_with_object({}) do |model, hash|
|
34
|
+
hash[model.table_name] = {}
|
35
|
+
|
36
|
+
relation_selector = ->(klass){ model._reflections.values.select{|r| r.is_a?(klass) } }
|
37
|
+
has_one_rels = relation_selector.call(ActiveRecord::Reflection::HasOneReflection)
|
38
|
+
has_many_rels = relation_selector.call(ActiveRecord::Reflection::HasManyReflection)
|
39
|
+
belongs_to_rels = relation_selector.call(ActiveRecord::Reflection::BelongsToReflection)
|
40
|
+
|
41
|
+
if has_one_rels.present?
|
42
|
+
hash[model.table_name]["has_one"] = has_one_rels.map{|r| r.klass.table_name }
|
43
|
+
end
|
44
|
+
if has_many_rels.present?
|
45
|
+
hash[model.table_name]["has_many"] = has_many_rels.map{|r| r.klass.table_name }
|
46
|
+
end
|
47
|
+
if belongs_to_rels.present?
|
48
|
+
hash[model.table_name]["belongs_to"] = belongs_to_rels.map{|r| r.klass.table_name }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
puts YAML.dump({ "tables" => hash })
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require_relative 'utils/struct_assigner'
|
2
|
+
|
3
|
+
module SchemaRD
|
4
|
+
class Schema
|
5
|
+
attr_reader :relations
|
6
|
+
def initialize
|
7
|
+
@tables = {}
|
8
|
+
@relations = []
|
9
|
+
end
|
10
|
+
def tables
|
11
|
+
@tables.values
|
12
|
+
end
|
13
|
+
def table(name)
|
14
|
+
@tables[name.to_s]
|
15
|
+
end
|
16
|
+
def add_table(name, table_object)
|
17
|
+
@tables[name.to_s] = table_object
|
18
|
+
table_object.set_schema(self)
|
19
|
+
end
|
20
|
+
def add_relation(relation)
|
21
|
+
@relations << relation
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class Table < Struct.new(*%i(columns indexes name localized_name description position parsed_db_comment))
|
26
|
+
include SchemaRD::Utils::StructAssigner
|
27
|
+
def initialize(hash = nil)
|
28
|
+
self.columns = []
|
29
|
+
self.indexes = []
|
30
|
+
self.position = { "left" => 0, "top" => 0 }
|
31
|
+
self.assign(hash)
|
32
|
+
end
|
33
|
+
def set_schema(schema)
|
34
|
+
@schema = schema
|
35
|
+
end
|
36
|
+
def relations_as_parent
|
37
|
+
@schema.relations.select{|r| r.parent_table == self }
|
38
|
+
end
|
39
|
+
def relations_as_child
|
40
|
+
@schema.relations.select{|r| r.child_table == self }
|
41
|
+
end
|
42
|
+
def relation_to(table_name)
|
43
|
+
self.relations_as_parent.find{|r| r.child_table.name == table_name }
|
44
|
+
end
|
45
|
+
def display_name
|
46
|
+
self.localized_name || self.name
|
47
|
+
end
|
48
|
+
def default_position?
|
49
|
+
self.position["left"] == 0 && self.position["top"] == 0
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class TableRelation < Struct.new(*%i(parent_table child_table parent_cardinality child_cardinality))
|
54
|
+
include SchemaRD::Utils::StructAssigner
|
55
|
+
def initialize(hash = nil)
|
56
|
+
self.parent_cardinality = "1"
|
57
|
+
self.child_cardinality = "N"
|
58
|
+
self.assign(hash)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class TableColumn < Struct.new(
|
63
|
+
*%i(name localized_name type null default limit precision scale description parsed_db_comment))
|
64
|
+
include SchemaRD::Utils::StructAssigner
|
65
|
+
def initialize(hash = nil)
|
66
|
+
self.assign(hash)
|
67
|
+
end
|
68
|
+
def display_name
|
69
|
+
self.localized_name || self.name
|
70
|
+
end
|
71
|
+
end
|
72
|
+
class TableIndex < Struct.new(*%i(name columns unique))
|
73
|
+
include SchemaRD::Utils::StructAssigner
|
74
|
+
def initialize(hash = nil)
|
75
|
+
self.columns = []
|
76
|
+
self.assign(hash)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require_relative "schema"
|
2
|
+
|
3
|
+
module SchemaRD
|
4
|
+
module MigrationContext
|
5
|
+
class Loader
|
6
|
+
class TableDefinition
|
7
|
+
[
|
8
|
+
:bigint,
|
9
|
+
:binary,
|
10
|
+
:boolean,
|
11
|
+
:date,
|
12
|
+
:datetime,
|
13
|
+
:decimal,
|
14
|
+
:float,
|
15
|
+
:integer,
|
16
|
+
:string,
|
17
|
+
:text,
|
18
|
+
:time,
|
19
|
+
:timestamp,
|
20
|
+
:virtual,
|
21
|
+
].each do |column_type|
|
22
|
+
module_eval <<-CODE, __FILE__, __LINE__ + 1
|
23
|
+
def #{column_type}(*args, **options)
|
24
|
+
args.each { |name| column(name, :#{column_type}, options) }
|
25
|
+
end
|
26
|
+
CODE
|
27
|
+
end
|
28
|
+
alias_method :numeric, :decimal
|
29
|
+
def initialize(table, with_comment:)
|
30
|
+
@table = table
|
31
|
+
@parse_db_comment = with_comment
|
32
|
+
end
|
33
|
+
def method_missing(name, *args)
|
34
|
+
self.column(args[0], "unknown", args[1])
|
35
|
+
end
|
36
|
+
def column(name, type, options = {})
|
37
|
+
if options[:comment] && @parse_db_comment
|
38
|
+
options[:parsed_db_comment] = options.delete(:comment)
|
39
|
+
end
|
40
|
+
@table.columns << SchemaRD::TableColumn.new(options.merge({ name: name, type: type }))
|
41
|
+
end
|
42
|
+
def timestamps
|
43
|
+
column("created_at", :timestamp, null: false)
|
44
|
+
column("updated_at", :timestamp, null: false)
|
45
|
+
end
|
46
|
+
def index(column_name, options = {})
|
47
|
+
column_name = [ column_name ] unless column_name.is_a?(Array)
|
48
|
+
@table.indexes << SchemaRD::TableIndex.new(options.merge({ columns: column_name }))
|
49
|
+
end
|
50
|
+
end
|
51
|
+
def initialize(schema, with_comment:)
|
52
|
+
@schema = schema
|
53
|
+
@parse_db_comment = with_comment
|
54
|
+
end
|
55
|
+
def create_table(table_name, options = {})
|
56
|
+
if options[:comment] && @parse_db_comment
|
57
|
+
options[:parsed_db_comment] = options.delete(:comment)
|
58
|
+
end
|
59
|
+
table = SchemaRD::Table.new(options.merge(name: table_name))
|
60
|
+
@schema.add_table(table_name, table)
|
61
|
+
yield TableDefinition.new(table, with_comment: @parse_db_comment)
|
62
|
+
end
|
63
|
+
def add_index(table_name, column_name, options = {})
|
64
|
+
column_name = [ column_name ] unless column_name.is_a?(Array)
|
65
|
+
index = SchemaRD::TableIndex.new(options.merge({ columns: column_name }))
|
66
|
+
@schema.table(table_name).indexes << index
|
67
|
+
end
|
68
|
+
def enable_extension(*args); end
|
69
|
+
|
70
|
+
module ActiveRecord
|
71
|
+
class Schema
|
72
|
+
def self.define(*args)
|
73
|
+
yield
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class SchemaParser
|
81
|
+
def initialize(filename)
|
82
|
+
@filename = filename
|
83
|
+
end
|
84
|
+
def parse(with_comment: false)
|
85
|
+
Schema.new.tap do |schema|
|
86
|
+
File.open(@filename) do |file|
|
87
|
+
MigrationContext::Loader.new(schema, with_comment: with_comment).instance_eval(file.read, @filename)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require_relative 'singularizer'
|
3
|
+
|
4
|
+
module SchemaRD::Utils
|
5
|
+
class Localizer
|
6
|
+
attr_reader :lang
|
7
|
+
def initialize(lang)
|
8
|
+
@lang = self.dictionary && self.dictionary.has_key?(lang) ? lang : "en"
|
9
|
+
end
|
10
|
+
def translate(key)
|
11
|
+
key.split(".").inject(self.dictionary[lang]) do |dict, k|
|
12
|
+
break if dict.nil? || !dict.is_a?(Hash)
|
13
|
+
dict[k]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
alias_method :t, :translate
|
17
|
+
end
|
18
|
+
|
19
|
+
class MessageLocalizer < Localizer
|
20
|
+
MESSAGES_FILE = "#{File.dirname(File.expand_path(__FILE__))}/../../locales/messages.yml"
|
21
|
+
|
22
|
+
def initialize(lang)
|
23
|
+
super(lang)
|
24
|
+
end
|
25
|
+
def dictionary
|
26
|
+
YAML.load_file(MESSAGES_FILE)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class SchemaLocalizer < Localizer
|
31
|
+
attr_reader :dictionary
|
32
|
+
def initialize(lang, hash)
|
33
|
+
super(lang)
|
34
|
+
@dictionary = hash
|
35
|
+
end
|
36
|
+
def table_name(name)
|
37
|
+
self.t("activerecord.models.#{name.singularize}")
|
38
|
+
end
|
39
|
+
def column_name(table_name, column_name)
|
40
|
+
self.t("activerecord.attributes.#{table_name.singularize}.#{column_name}")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|