lagoon 0.1.0 → 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 +4 -4
- data/Appraisals +39 -0
- data/CHANGELOG.md +14 -2
- data/README.md +111 -146
- data/Rakefile +17 -3
- data/exe/lagoon +3 -3
- data/gemfiles/adapters.gemfile +15 -0
- data/gemfiles/rails_6.1.gemfile +23 -0
- data/gemfiles/rails_7.0.gemfile +23 -0
- data/gemfiles/rails_7.1.gemfile +23 -0
- data/gemfiles/rails_7.2.gemfile +23 -0
- data/gemfiles/rails_8.0.gemfile +23 -0
- data/gemfiles/rails_8.1.gemfile +23 -0
- data/lib/lagoon/analyzer/action_controller_analyzer.rb +76 -0
- data/lib/lagoon/analyzer/active_record_analyzer.rb +202 -0
- data/lib/lagoon/analyzer/ast/controller_scope_collector.rb +104 -0
- data/lib/lagoon/analyzer/ast/method_reference_visitor.rb +170 -0
- data/lib/lagoon/analyzer/ast_model_reference_analyzer.rb +44 -0
- data/lib/lagoon/analyzer/database_schema_analyzer.rb +83 -0
- data/lib/lagoon/cli.rb +109 -80
- data/lib/lagoon/configuration.rb +42 -6
- data/lib/lagoon/diagram/base.rb +48 -8
- data/lib/lagoon/diagram/controller_diagram.rb +10 -16
- data/lib/lagoon/diagram/controller_model_diagram.rb +28 -0
- data/lib/lagoon/diagram/er_diagram.rb +10 -16
- data/lib/lagoon/diagram/model_diagram.rb +13 -16
- data/lib/lagoon/errors.rb +9 -0
- data/lib/lagoon/options.rb +182 -0
- data/lib/lagoon/parser/application_class_filter.rb +47 -0
- data/lib/lagoon/parser/controller_model_parser.rb +196 -0
- data/lib/lagoon/parser/controller_parser.rb +37 -54
- data/lib/lagoon/parser/model_parser.rb +57 -112
- data/lib/lagoon/parser/schema_parser.rb +120 -67
- data/lib/lagoon/railtie.rb +1 -1
- data/lib/lagoon/renderer/base_renderer.rb +49 -19
- data/lib/lagoon/renderer/class_diagram_renderer.rb +37 -31
- data/lib/lagoon/renderer/controller_model_er_renderer.rb +47 -0
- data/lib/lagoon/renderer/er_diagram_renderer.rb +43 -42
- data/lib/lagoon/result.rb +22 -0
- data/lib/lagoon/version.rb +1 -1
- data/lib/lagoon.rb +70 -21
- data/lib/tasks/lagoon.rake +15 -7
- data/sig/lagoon.rbs +107 -0
- metadata +57 -7
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require 'logger'
|
|
4
|
+
require 'active_support'
|
|
5
|
+
require 'active_support/core_ext/string/inflections'
|
|
4
6
|
|
|
5
7
|
module Lagoon
|
|
6
8
|
module Parser
|
|
@@ -8,8 +10,9 @@ module Lagoon
|
|
|
8
10
|
attr_reader :options, :config
|
|
9
11
|
|
|
10
12
|
def initialize(options = {})
|
|
11
|
-
@options = options
|
|
12
|
-
@
|
|
13
|
+
@options = options.is_a?(Options) ? options : Options.for(:model, options)
|
|
14
|
+
@analyzer = Lagoon::Analyzer::ActiveRecordAnalyzer.new
|
|
15
|
+
@filter = ApplicationClassFilter.new(directory: 'models', include_all: @options[:all_models])
|
|
13
16
|
end
|
|
14
17
|
|
|
15
18
|
def parse
|
|
@@ -17,144 +20,86 @@ module Lagoon
|
|
|
17
20
|
classes = []
|
|
18
21
|
relationships = []
|
|
19
22
|
|
|
20
|
-
|
|
23
|
+
warnings = []
|
|
24
|
+
|
|
25
|
+
models.sort_by { |model| model.name.to_s }.each do |model|
|
|
21
26
|
next if excluded?(model)
|
|
22
27
|
|
|
23
|
-
classes
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
analyze_model(model, classes, relationships)
|
|
29
|
+
rescue StandardError => e
|
|
30
|
+
raise if options[:strict]
|
|
31
|
+
|
|
32
|
+
warnings << "Failed to analyze model #{model.name || '(anonymous)'}: #{e.message}"
|
|
26
33
|
end
|
|
27
34
|
|
|
35
|
+
normalized_relationships = deduplicate_relationships(relationships)
|
|
28
36
|
{
|
|
29
|
-
classes: classes,
|
|
30
|
-
relationships:
|
|
37
|
+
classes: classes.sort_by { |model| model[:name] },
|
|
38
|
+
relationships: normalized_relationships,
|
|
39
|
+
warnings: warnings,
|
|
40
|
+
counts: { classes: classes.size, relationships: normalized_relationships.size, skipped: warnings.size }
|
|
31
41
|
}
|
|
32
42
|
end
|
|
33
43
|
|
|
34
44
|
private
|
|
35
45
|
|
|
36
46
|
def load_models
|
|
37
|
-
|
|
38
|
-
return [] unless defined?(Rails)
|
|
47
|
+
return [] unless defined?(ActiveRecord::Base)
|
|
39
48
|
|
|
40
|
-
Rails.application.eager_load!
|
|
41
|
-
ActiveRecord::Base.descendants
|
|
49
|
+
Rails.application.eager_load! if options[:eager_load] && defined?(Rails)
|
|
50
|
+
ActiveRecord::Base.descendants
|
|
42
51
|
end
|
|
43
52
|
|
|
44
53
|
def excluded?(model)
|
|
45
54
|
model_name = model.name
|
|
46
|
-
|
|
55
|
+
return true unless model_name
|
|
56
|
+
return true unless @filter.include?(model)
|
|
57
|
+
return true if options[:exclude].include?(model_name)
|
|
58
|
+
return !options[:specify].include?(model_name) if options[:specify].any?
|
|
59
|
+
|
|
60
|
+
false
|
|
47
61
|
end
|
|
48
62
|
|
|
49
|
-
def
|
|
63
|
+
def analysis_options
|
|
50
64
|
{
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
65
|
+
show_attributes: options[:show_attributes],
|
|
66
|
+
show_methods: options[:show_methods],
|
|
67
|
+
all_columns: options[:all_columns],
|
|
68
|
+
hide_magic: options[:hide_magic],
|
|
69
|
+
hide_through: options[:hide_through],
|
|
70
|
+
show_belongs_to: options[:show_belongs_to],
|
|
71
|
+
duplicate_sti_attributes: options[:duplicate_sti_attributes]
|
|
55
72
|
}
|
|
56
73
|
end
|
|
57
74
|
|
|
58
|
-
def
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
name: column.name,
|
|
70
|
-
type: column.type,
|
|
71
|
-
visibility: "+"
|
|
72
|
-
}
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def magic_field?(field_name)
|
|
77
|
-
# マジックフィールド(created_at, updated_at等)を判定
|
|
78
|
-
return false if options[:all_columns]
|
|
79
|
-
|
|
80
|
-
%w[id created_at updated_at].include?(field_name)
|
|
75
|
+
def analyze_model(model, classes, relationships)
|
|
76
|
+
classes << @analyzer.analyze_model(model, analysis_options)
|
|
77
|
+
relationships.concat(@analyzer.extract_associations(model, analysis_options))
|
|
78
|
+
return unless options[:include_inheritance]
|
|
79
|
+
|
|
80
|
+
relationships.concat(
|
|
81
|
+
@analyzer.extract_inheritance_with_options(
|
|
82
|
+
model,
|
|
83
|
+
include_framework_base: options[:include_framework_bases]
|
|
84
|
+
)
|
|
85
|
+
)
|
|
81
86
|
end
|
|
82
87
|
|
|
83
|
-
def
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def extract_associations(model)
|
|
89
|
-
associations = []
|
|
90
|
-
|
|
91
|
-
model.reflect_on_all_associations.each do |assoc|
|
|
92
|
-
next if assoc.options[:through] && options[:hide_through]
|
|
93
|
-
|
|
94
|
-
associations << build_association(model, assoc)
|
|
88
|
+
def deduplicate_relationships(relationships)
|
|
89
|
+
grouped = relationships.group_by { |relationship| relationship_key(relationship) }
|
|
90
|
+
deduplicated = grouped.values.map do |duplicates|
|
|
91
|
+
duplicates.min_by { |relationship| relationship[:macro] == :belongs_to ? 1 : 0 }
|
|
95
92
|
end
|
|
96
93
|
|
|
97
|
-
|
|
94
|
+
deduplicated.sort_by { |relationship| relationship_key(relationship) }
|
|
98
95
|
end
|
|
99
96
|
|
|
100
|
-
def
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
{
|
|
106
|
-
source: model.name,
|
|
107
|
-
target: assoc.class_name,
|
|
108
|
-
type: :association,
|
|
109
|
-
label: "belongs_to #{assoc.name}",
|
|
110
|
-
source_cardinality: "1",
|
|
111
|
-
target_cardinality: "0..1"
|
|
112
|
-
}
|
|
113
|
-
when :has_one
|
|
114
|
-
{
|
|
115
|
-
source: model.name,
|
|
116
|
-
target: assoc.class_name,
|
|
117
|
-
type: :association,
|
|
118
|
-
label: "has_one #{assoc.name}",
|
|
119
|
-
source_cardinality: "1",
|
|
120
|
-
target_cardinality: "0..1"
|
|
121
|
-
}
|
|
122
|
-
when :has_many
|
|
123
|
-
{
|
|
124
|
-
source: model.name,
|
|
125
|
-
target: assoc.class_name,
|
|
126
|
-
type: :association,
|
|
127
|
-
label: "has_many #{assoc.name}",
|
|
128
|
-
source_cardinality: "1",
|
|
129
|
-
target_cardinality: "*"
|
|
130
|
-
}
|
|
131
|
-
when :has_and_belongs_to_many
|
|
132
|
-
{
|
|
133
|
-
source: model.name,
|
|
134
|
-
target: assoc.class_name,
|
|
135
|
-
type: :association,
|
|
136
|
-
label: "has_and_belongs_to_many #{assoc.name}",
|
|
137
|
-
source_cardinality: "*",
|
|
138
|
-
target_cardinality: "*"
|
|
139
|
-
}
|
|
97
|
+
def relationship_key(relationship)
|
|
98
|
+
if relationship[:type] == :association && !relationship[:polymorphic]
|
|
99
|
+
[relationship[:type].to_s, *[relationship[:source], relationship[:target]].sort]
|
|
100
|
+
else
|
|
101
|
+
[relationship[:type].to_s, relationship[:source].to_s, relationship[:target].to_s]
|
|
140
102
|
end
|
|
141
|
-
rescue NameError
|
|
142
|
-
# アソシエーション先のクラスが見つからない場合はスキップ
|
|
143
|
-
nil
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def extract_inheritance(model)
|
|
147
|
-
return [] if model.superclass == ActiveRecord::Base
|
|
148
|
-
return [] if model.superclass.abstract_class?
|
|
149
|
-
|
|
150
|
-
[{
|
|
151
|
-
source: model.superclass.name,
|
|
152
|
-
target: model.name,
|
|
153
|
-
type: :inheritance,
|
|
154
|
-
label: nil
|
|
155
|
-
}]
|
|
156
|
-
rescue StandardError
|
|
157
|
-
[]
|
|
158
103
|
end
|
|
159
104
|
end
|
|
160
105
|
end
|
|
@@ -1,113 +1,166 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "active_support/core_ext/string"
|
|
4
|
-
|
|
5
3
|
module Lagoon
|
|
6
4
|
module Parser
|
|
7
5
|
class SchemaParser
|
|
8
|
-
|
|
6
|
+
ConnectionEntry = Data.define(:name, :connection)
|
|
7
|
+
TableMetadata = Data.define(:name, :raw_name, :prefix, :columns, :primary_keys, :foreign_keys, :indexes)
|
|
8
|
+
|
|
9
|
+
attr_reader :options
|
|
9
10
|
|
|
10
11
|
def initialize(options = {})
|
|
11
|
-
@options = options
|
|
12
|
-
@
|
|
12
|
+
@options = options.is_a?(Options) ? options : Options.for(:er, options)
|
|
13
|
+
@analyzer = Lagoon::Analyzer::DatabaseSchemaAnalyzer.new
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
def parse
|
|
16
|
-
tables = load_schema
|
|
17
17
|
entities = []
|
|
18
18
|
relationships = []
|
|
19
|
+
warnings = []
|
|
20
|
+
|
|
21
|
+
load_schema(warnings).each do |table|
|
|
22
|
+
next if excluded?(table)
|
|
19
23
|
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
analyze_table(table, entities, relationships)
|
|
25
|
+
rescue StandardError => e
|
|
26
|
+
raise if options[:strict]
|
|
22
27
|
|
|
23
|
-
|
|
24
|
-
entities << entity
|
|
25
|
-
relationships.concat(extract_foreign_keys(table_name, columns))
|
|
28
|
+
warnings << "Failed to analyze table #{table.name}: #{e.message}"
|
|
26
29
|
end
|
|
27
30
|
|
|
28
31
|
{
|
|
29
|
-
entities: entities,
|
|
30
|
-
relationships: relationships
|
|
32
|
+
entities: entities.sort_by { |entity| entity[:name] },
|
|
33
|
+
relationships: relationships.sort_by { |relationship| [relationship[:source], relationship[:target]] },
|
|
34
|
+
warnings: warnings,
|
|
35
|
+
counts: { entities: entities.size, relationships: relationships.size, skipped: warnings.size }
|
|
31
36
|
}
|
|
32
37
|
end
|
|
33
38
|
|
|
34
39
|
private
|
|
35
40
|
|
|
36
|
-
def load_schema
|
|
37
|
-
|
|
41
|
+
def load_schema(warnings)
|
|
42
|
+
entries = connection_entries
|
|
43
|
+
multiple_connections = entries.size > 1
|
|
38
44
|
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
entries.flat_map do |entry|
|
|
46
|
+
load_connection_schema(entry, multiple_connections, warnings)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
41
49
|
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
def connection_entries
|
|
51
|
+
configured = options[:connections]
|
|
52
|
+
return normalize_configured_connections(configured) if configured
|
|
53
|
+
return [] unless defined?(ActiveRecord::Base)
|
|
44
54
|
|
|
45
|
-
|
|
46
|
-
|
|
55
|
+
pools = all_connection_pools
|
|
56
|
+
connections = pools.filter_map do |pool|
|
|
57
|
+
ConnectionEntry.new(name: connection_name(pool), connection: pool.connection)
|
|
58
|
+
rescue ActiveRecord::ConnectionNotEstablished
|
|
59
|
+
nil
|
|
47
60
|
end
|
|
61
|
+
base = ConnectionEntry.new(name: 'primary', connection: ActiveRecord::Base.connection)
|
|
62
|
+
connections.unshift(base)
|
|
63
|
+
connections.uniq { |entry| entry.connection.object_id }
|
|
64
|
+
rescue NoMethodError
|
|
65
|
+
[ConnectionEntry.new(name: 'primary', connection: ActiveRecord::Base.connection)]
|
|
66
|
+
end
|
|
48
67
|
|
|
49
|
-
|
|
68
|
+
def all_connection_pools
|
|
69
|
+
handler = ActiveRecord::Base.connection_handler
|
|
70
|
+
return handler.all_connection_pools if handler.respond_to?(:all_connection_pools)
|
|
71
|
+
|
|
72
|
+
handler.connection_pool_list
|
|
50
73
|
end
|
|
51
74
|
|
|
52
|
-
def
|
|
53
|
-
|
|
54
|
-
|
|
75
|
+
def normalize_configured_connections(configured)
|
|
76
|
+
pairs = configured.respond_to?(:to_h) ? configured.to_h : Array(configured).to_h
|
|
77
|
+
pairs.map { |name, connection| ConnectionEntry.new(name: name.to_s, connection: connection) }
|
|
78
|
+
rescue TypeError
|
|
79
|
+
Array(configured).each_with_index.map do |connection, index|
|
|
80
|
+
ConnectionEntry.new(name: "database_#{index + 1}", connection: connection)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def load_connection_schema(entry, multiple_connections, warnings)
|
|
85
|
+
connection = entry.connection
|
|
86
|
+
prefix = multiple_connections ? entry.name : nil
|
|
87
|
+
|
|
88
|
+
connection.tables.sort.filter_map do |table_name|
|
|
89
|
+
next if @analyzer.internal_table?(table_name, internal_tables: options[:internal_tables])
|
|
90
|
+
|
|
91
|
+
load_table_metadata(connection, table_name, prefix)
|
|
92
|
+
rescue StandardError => e
|
|
93
|
+
raise if options[:strict]
|
|
94
|
+
|
|
95
|
+
warnings << "Failed to inspect table #{qualify(table_name, prefix)}: #{e.message}"
|
|
96
|
+
nil
|
|
97
|
+
end
|
|
55
98
|
end
|
|
56
99
|
|
|
57
|
-
def
|
|
58
|
-
|
|
100
|
+
def load_table_metadata(connection, table_name, prefix)
|
|
101
|
+
TableMetadata.new(
|
|
102
|
+
name: qualify(table_name, prefix),
|
|
103
|
+
raw_name: table_name,
|
|
104
|
+
prefix: prefix,
|
|
105
|
+
columns: cached_columns(connection, table_name),
|
|
106
|
+
primary_keys: primary_keys(connection, table_name),
|
|
107
|
+
foreign_keys: connection.foreign_keys(table_name),
|
|
108
|
+
indexes: connection.indexes(table_name)
|
|
109
|
+
)
|
|
59
110
|
end
|
|
60
111
|
|
|
61
|
-
def
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
112
|
+
def cached_columns(connection, table_name)
|
|
113
|
+
cache = connection.schema_cache
|
|
114
|
+
cache.columns(table_name)
|
|
115
|
+
rescue ArgumentError
|
|
116
|
+
cache.columns(connection.pool, table_name)
|
|
117
|
+
rescue NoMethodError
|
|
118
|
+
connection.columns(table_name)
|
|
66
119
|
end
|
|
67
120
|
|
|
68
|
-
def
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
primary_key: column.name == "id",
|
|
73
|
-
foreign_key: foreign_key?(column.name),
|
|
74
|
-
unique: false # 必要に応じて実装
|
|
75
|
-
}
|
|
121
|
+
def primary_keys(connection, table_name)
|
|
122
|
+
return Array(connection.primary_keys(table_name)) if connection.respond_to?(:primary_keys)
|
|
123
|
+
|
|
124
|
+
Array(connection.primary_key(table_name))
|
|
76
125
|
end
|
|
77
126
|
|
|
78
|
-
def
|
|
79
|
-
|
|
127
|
+
def analyze_table(table, entities, relationships)
|
|
128
|
+
entities << @analyzer.analyze_table(
|
|
129
|
+
table.name,
|
|
130
|
+
table.columns,
|
|
131
|
+
primary_keys: table.primary_keys,
|
|
132
|
+
foreign_keys: table.foreign_keys,
|
|
133
|
+
indexes: table.indexes
|
|
134
|
+
)
|
|
135
|
+
relationships.concat(
|
|
136
|
+
@analyzer.extract_foreign_keys(
|
|
137
|
+
table.name,
|
|
138
|
+
table.columns,
|
|
139
|
+
foreign_keys: table.foreign_keys,
|
|
140
|
+
indexes: table.indexes,
|
|
141
|
+
primary_keys: table.primary_keys,
|
|
142
|
+
table_prefix: table.prefix
|
|
143
|
+
)
|
|
144
|
+
)
|
|
80
145
|
end
|
|
81
146
|
|
|
82
|
-
def
|
|
83
|
-
|
|
147
|
+
def excluded?(table)
|
|
148
|
+
return true if options[:exclude].include?(table.name) || options[:exclude].include?(table.raw_name)
|
|
149
|
+
return false if options[:specify].empty?
|
|
84
150
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
# column_nameから参照先テーブルを推測 (例: user_id -> users)
|
|
89
|
-
target_table = infer_target_table(column.name)
|
|
90
|
-
next unless target_table
|
|
91
|
-
|
|
92
|
-
relationships << {
|
|
93
|
-
source: table_name,
|
|
94
|
-
target: target_table,
|
|
95
|
-
label: "has many",
|
|
96
|
-
source_cardinality: "||",
|
|
97
|
-
target_cardinality: "}o",
|
|
98
|
-
identifying: true
|
|
99
|
-
}
|
|
100
|
-
end
|
|
151
|
+
!options[:specify].include?(table.name) && !options[:specify].include?(table.raw_name)
|
|
152
|
+
end
|
|
101
153
|
|
|
102
|
-
|
|
154
|
+
def qualify(table_name, prefix)
|
|
155
|
+
prefix ? "#{prefix}.#{table_name}" : table_name.to_s
|
|
103
156
|
end
|
|
104
157
|
|
|
105
|
-
def
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
158
|
+
def connection_name(pool)
|
|
159
|
+
config = pool.respond_to?(:db_config) ? pool.db_config : nil
|
|
160
|
+
parts = [config&.name]
|
|
161
|
+
parts << pool.role if pool.respond_to?(:role)
|
|
162
|
+
parts << pool.shard if pool.respond_to?(:shard)
|
|
163
|
+
parts.compact.map(&:to_s).reject(&:empty?).join('_').then { |name| name.empty? ? 'database' : name }
|
|
111
164
|
end
|
|
112
165
|
end
|
|
113
166
|
end
|
data/lib/lagoon/railtie.rb
CHANGED
|
@@ -1,42 +1,72 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
3
6
|
module Lagoon
|
|
4
7
|
module Renderer
|
|
5
8
|
class BaseRenderer
|
|
6
9
|
attr_reader :direction
|
|
7
10
|
|
|
8
|
-
def initialize(direction:
|
|
9
|
-
@direction = direction
|
|
11
|
+
def initialize(direction: 'TB', **_options)
|
|
12
|
+
@direction = Configuration.validate_direction!(direction)
|
|
10
13
|
end
|
|
11
14
|
|
|
12
15
|
def render(parsed_data)
|
|
13
|
-
raise NotImplementedError,
|
|
16
|
+
raise NotImplementedError, 'Subclasses must implement #render'
|
|
14
17
|
end
|
|
15
18
|
|
|
16
19
|
protected
|
|
17
20
|
|
|
18
21
|
def escape_class_name(name)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
safe_identifier(name)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def safe_identifier(value, uppercase: false)
|
|
26
|
+
original = value.to_s
|
|
27
|
+
identifier = original.gsub(/[^a-zA-Z0-9_]/, '_').gsub(/_+/, '_')
|
|
28
|
+
identifier = "_#{identifier}" unless identifier.match?(/\A[a-zA-Z_]/)
|
|
29
|
+
identifier = "entity_#{Digest::SHA256.hexdigest(original)[0, 12]}" if identifier.empty? || identifier == '_'
|
|
30
|
+
uppercase ? identifier.upcase : identifier
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def aliased_identifier(value, uppercase: false)
|
|
34
|
+
identifier = safe_identifier(value, uppercase: uppercase)
|
|
35
|
+
display = uppercase ? value.to_s.upcase : value.to_s
|
|
36
|
+
return identifier if identifier == display
|
|
37
|
+
|
|
38
|
+
"#{identifier}[#{mermaid_string(display)}]"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def mermaid_string(value)
|
|
42
|
+
JSON.generate(value.to_s.gsub(/[\r\n]+/, ' '))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def escape_member(value)
|
|
46
|
+
value.to_s.gsub(/[\r\n]+/, ' ')
|
|
47
|
+
.gsub('&', '&')
|
|
48
|
+
.gsub('"', '"')
|
|
49
|
+
.gsub('{', '{')
|
|
50
|
+
.gsub('}', '}')
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def escape_label(value)
|
|
54
|
+
escape_member(value).gsub(':', ':')
|
|
25
55
|
end
|
|
26
56
|
|
|
27
57
|
def type_to_mermaid(type)
|
|
28
58
|
# Ruby/Rails型をMermaid表記に変換
|
|
29
59
|
case type.to_s
|
|
30
|
-
when /integer/i then
|
|
31
|
-
when /string/i then
|
|
32
|
-
when /text/i then
|
|
33
|
-
when /boolean/i then
|
|
34
|
-
when /datetime/i then
|
|
35
|
-
when /date/i then
|
|
36
|
-
when /time/i then
|
|
37
|
-
when /decimal/i then
|
|
38
|
-
when /float/i then
|
|
39
|
-
when /json/i then
|
|
60
|
+
when /integer/i then 'Integer'
|
|
61
|
+
when /string/i then 'String'
|
|
62
|
+
when /text/i then 'Text'
|
|
63
|
+
when /boolean/i then 'Boolean'
|
|
64
|
+
when /datetime/i then 'DateTime'
|
|
65
|
+
when /date/i then 'Date'
|
|
66
|
+
when /time/i then 'Time'
|
|
67
|
+
when /decimal/i then 'Decimal'
|
|
68
|
+
when /float/i then 'Float'
|
|
69
|
+
when /json/i then 'JSON'
|
|
40
70
|
else type.to_s.capitalize
|
|
41
71
|
end
|
|
42
72
|
end
|