schemard 0.2.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.
- 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
|