diver_down 0.0.1.alpha13 → 0.0.1.alpha15
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/README.md +3 -3
- data/exe/diver_down_web +6 -7
- data/lib/diver_down/definition/dependency.rb +10 -0
- data/lib/diver_down/definition/method_id.rb +6 -0
- data/lib/diver_down/definition/source.rb +13 -0
- data/lib/diver_down/definition.rb +8 -0
- data/lib/diver_down/version.rb +1 -1
- data/lib/diver_down/web/action.rb +174 -102
- data/lib/diver_down/web/definition_filter.rb +112 -0
- data/lib/diver_down/web/definition_module_dependencies.rb +79 -0
- data/lib/diver_down/web/definition_store.rb +20 -2
- data/lib/diver_down/web/definition_to_dot.rb +80 -123
- data/lib/diver_down/web/metadata/source_alias.rb +128 -0
- data/lib/diver_down/web/metadata/source_metadata.rb +60 -0
- data/lib/diver_down/web/metadata.rb +71 -0
- data/lib/diver_down/web/source_alias_resolver.rb +46 -0
- data/lib/diver_down/web.rb +56 -22
- data/web/assets/DyDCovOQ.css +1 -0
- data/web/assets/bundle.js +184 -166
- data/web/index.html +1 -1
- metadata +9 -4
- data/lib/diver_down/web/module_store.rb +0 -92
- data/web/assets/CjLq7LhZ.css +0 -1
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DiverDown
|
4
|
+
class Web
|
5
|
+
class DefinitionModuleDependencies
|
6
|
+
class ModuleDependency
|
7
|
+
attr_accessor :module
|
8
|
+
attr_reader :module_dependencies, :module_reverse_dependencies, :source_map, :source_reverse_dependency_map
|
9
|
+
|
10
|
+
def initialize(modulee)
|
11
|
+
@module = modulee
|
12
|
+
@module_dependencies = Set.new
|
13
|
+
@module_reverse_dependencies = Set.new
|
14
|
+
@source_map = {}
|
15
|
+
@source_reverse_dependency_map = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
# @return [Array<Source>]
|
19
|
+
def sources
|
20
|
+
@source_map.keys.sort.map { @source_map.fetch(_1) }
|
21
|
+
end
|
22
|
+
|
23
|
+
# @return [Array<Source>]
|
24
|
+
def source_reverse_dependencies
|
25
|
+
@source_reverse_dependency_map.keys.sort.map { @source_reverse_dependency_map.fetch(_1) }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(metadata, definition)
|
30
|
+
@metadata = metadata
|
31
|
+
@definition = definition
|
32
|
+
end
|
33
|
+
|
34
|
+
# @return [Hash{ String => Hash{ String => Array<DiverDown::Definition::Source> } }]
|
35
|
+
def build_module_dependency_map
|
36
|
+
module_dependency_map = Hash.new { |h, k| h[k] = ModuleDependency.new(k) }
|
37
|
+
|
38
|
+
@definition.sources.each do |source|
|
39
|
+
source_module = @metadata.source(source.source_name).module
|
40
|
+
next if source_module.nil?
|
41
|
+
|
42
|
+
source_module_dependency = module_dependency_map[source_module]
|
43
|
+
source_module_dependency.source_map[source.source_name] ||= DiverDown::Definition::Source.new(source_name: source.source_name)
|
44
|
+
|
45
|
+
source.dependencies.each do |dependency|
|
46
|
+
dependency_module = @metadata.source(dependency.source_name).module
|
47
|
+
|
48
|
+
next if source_module == dependency_module
|
49
|
+
next if dependency_module.nil?
|
50
|
+
|
51
|
+
dependency_module_dependency = module_dependency_map[dependency_module]
|
52
|
+
|
53
|
+
# Add module dependencies
|
54
|
+
source_module_dependency.module_dependencies.add(dependency_module)
|
55
|
+
|
56
|
+
# Add module reverse dependencies
|
57
|
+
dependency_module_dependency.module_reverse_dependencies.add(source_module)
|
58
|
+
|
59
|
+
# Add source
|
60
|
+
definition_source = source_module_dependency.source_map[source.source_name]
|
61
|
+
definition_dependency = definition_source.find_or_build_dependency(dependency.source_name)
|
62
|
+
dependency.method_ids.each do |method_id|
|
63
|
+
definition_dependency.find_or_build_method_id(name: method_id.name, context: method_id.context).add_path(*method_id.paths)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Add source reverse dependencies
|
67
|
+
definition_source = dependency_module_dependency.source_reverse_dependency_map[dependency.source_name] ||= DiverDown::Definition::Source.new(source_name: dependency.source_name)
|
68
|
+
definition_dependency = definition_source.find_or_build_dependency(source.source_name)
|
69
|
+
dependency.method_ids.each do |method_id|
|
70
|
+
definition_dependency.find_or_build_method_id(name: method_id.name, context: method_id.context).add_path(*method_id.paths)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
module_dependency_map
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -5,12 +5,11 @@ module DiverDown
|
|
5
5
|
class DefinitionStore
|
6
6
|
include Enumerable
|
7
7
|
|
8
|
-
attr_reader :bit_id
|
9
|
-
|
10
8
|
def initialize
|
11
9
|
# Hash{ Integer(unique bit flag) => DiverDown::Definition }
|
12
10
|
@definitions = []
|
13
11
|
@definition_group_store = Hash.new { |h, k| h[k] = [] }
|
12
|
+
@combined_definition = nil
|
14
13
|
end
|
15
14
|
|
16
15
|
# @param id [Integer]
|
@@ -31,14 +30,33 @@ module DiverDown
|
|
31
30
|
raise(ArgumentError, 'definition already set') if _1.store_id
|
32
31
|
|
33
32
|
_1.store_id = @definitions.size + 1
|
33
|
+
_1.freeze
|
34
34
|
|
35
35
|
@definitions.push(_1)
|
36
36
|
@definition_group_store[_1.definition_group] << _1
|
37
37
|
|
38
|
+
# Reset combined_definition
|
39
|
+
@combined_definition = nil
|
40
|
+
|
38
41
|
_1.store_id
|
39
42
|
end
|
40
43
|
end
|
41
44
|
|
45
|
+
# @return [DiverDown::Definition]
|
46
|
+
def combined_definition
|
47
|
+
if @combined_definition.nil?
|
48
|
+
@combined_definition = DiverDown::Definition.combine(
|
49
|
+
definition_group: nil,
|
50
|
+
title: 'All Definitions',
|
51
|
+
definitions: @definitions
|
52
|
+
)
|
53
|
+
|
54
|
+
@combined_definition.freeze
|
55
|
+
end
|
56
|
+
|
57
|
+
@combined_definition
|
58
|
+
end
|
59
|
+
|
42
60
|
# @return [Array<String, nil>]
|
43
61
|
def definition_groups
|
44
62
|
keys = @definition_group_store.keys
|
@@ -7,13 +7,12 @@ module DiverDown
|
|
7
7
|
class Web
|
8
8
|
class DefinitionToDot
|
9
9
|
ATTRIBUTE_DELIMITER = ' '
|
10
|
-
MODULE_DELIMITER = '::'
|
11
10
|
|
12
11
|
# Between modules is prominently distanced
|
13
12
|
MODULE_MINLEN = 3
|
14
13
|
|
15
|
-
class
|
16
|
-
|
14
|
+
class DotMetadataStore
|
15
|
+
DotMetadata = Data.define(:id, :type, :data, :metadata) do
|
17
16
|
# @return [Hash]
|
18
17
|
def to_h
|
19
18
|
case type
|
@@ -31,18 +30,12 @@ module DiverDown
|
|
31
30
|
private
|
32
31
|
|
33
32
|
def source_to_h
|
34
|
-
modules = module_store.get_modules(data.source_name).map do
|
35
|
-
{
|
36
|
-
module_name: _1,
|
37
|
-
}
|
38
|
-
end
|
39
|
-
|
40
33
|
{
|
41
34
|
id:,
|
42
35
|
type: 'source',
|
43
36
|
source_name: data.source_name,
|
44
|
-
memo:
|
45
|
-
|
37
|
+
memo: metadata.source(data.source_name).memo,
|
38
|
+
module: metadata.source(data.source_name).module,
|
46
39
|
}
|
47
40
|
end
|
48
41
|
|
@@ -68,20 +61,16 @@ module DiverDown
|
|
68
61
|
{
|
69
62
|
id:,
|
70
63
|
type: 'module',
|
71
|
-
|
72
|
-
{
|
73
|
-
module_name: _1,
|
74
|
-
}
|
75
|
-
end,
|
64
|
+
module: data,
|
76
65
|
}
|
77
66
|
end
|
78
67
|
end
|
79
68
|
|
80
|
-
def initialize(
|
69
|
+
def initialize(metadata)
|
81
70
|
@prefix = 'graph_'
|
82
|
-
@
|
71
|
+
@metadata = metadata
|
83
72
|
|
84
|
-
# Hash{ id =>
|
73
|
+
# Hash{ id => DotMetadata }
|
85
74
|
@to_h = {}
|
86
75
|
end
|
87
76
|
|
@@ -89,34 +78,34 @@ module DiverDown
|
|
89
78
|
# @param record [DiverDown::Definition::Source]
|
90
79
|
# @return [String]
|
91
80
|
def issue_source_id(source)
|
92
|
-
|
81
|
+
build_dot_metadata_and_return_id(:source, source)
|
93
82
|
end
|
94
83
|
|
95
84
|
# @param dependency [DiverDown::Definition::Dependency]
|
96
85
|
# @return [String]
|
97
86
|
def issue_dependency_id(dependency)
|
98
|
-
|
87
|
+
build_dot_metadata_and_return_id(:dependency, [dependency])
|
99
88
|
end
|
100
89
|
|
101
90
|
# @param module_names [Array<String>]
|
102
91
|
# @return [String]
|
103
|
-
def
|
104
|
-
issued_metadata = @to_h.values.find { _1.type == :module && _1.data ==
|
92
|
+
def issue_module_id(modulee)
|
93
|
+
issued_metadata = @to_h.values.find { _1.type == :module && _1.data == modulee }
|
105
94
|
|
106
95
|
if issued_metadata
|
107
96
|
issued_metadata.id
|
108
97
|
else
|
109
|
-
|
98
|
+
build_dot_metadata_and_return_id(:module, modulee)
|
110
99
|
end
|
111
100
|
end
|
112
101
|
|
113
102
|
# @param id [String]
|
114
103
|
# @param dependency [DiverDown::Definition::Dependency]
|
115
104
|
def append_dependency(id, dependency)
|
116
|
-
|
117
|
-
dependencies =
|
105
|
+
dot_metadata = @to_h.fetch(id)
|
106
|
+
dependencies = dot_metadata.data
|
118
107
|
combined_dependencies = DiverDown::Definition::Dependency.combine(*dependencies, dependency)
|
119
|
-
|
108
|
+
dot_metadata.data.replace(combined_dependencies)
|
120
109
|
end
|
121
110
|
|
122
111
|
# @return [Array<Hash>]
|
@@ -126,10 +115,10 @@ module DiverDown
|
|
126
115
|
|
127
116
|
private
|
128
117
|
|
129
|
-
def
|
118
|
+
def build_dot_metadata_and_return_id(type, data)
|
130
119
|
id = "#{@prefix}#{length + 1}"
|
131
|
-
|
132
|
-
@to_h[id] =
|
120
|
+
dot_metadata = DotMetadata.new(id:, type:, data:, metadata: @metadata)
|
121
|
+
@to_h[id] = dot_metadata
|
133
122
|
|
134
123
|
id
|
135
124
|
end
|
@@ -140,24 +129,24 @@ module DiverDown
|
|
140
129
|
end
|
141
130
|
|
142
131
|
# @param definition [DiverDown::Definition]
|
143
|
-
# @param
|
132
|
+
# @param metadata [DiverDown::Web::Metadata]
|
144
133
|
# @param compound [Boolean]
|
145
134
|
# @param concentrate [Boolean] https://graphviz.org/docs/attrs/concentrate/
|
146
|
-
def initialize(definition,
|
135
|
+
def initialize(definition, metadata, compound: false, concentrate: false, only_module: false)
|
147
136
|
@definition = definition
|
148
|
-
@
|
137
|
+
@metadata = metadata
|
149
138
|
@io = DiverDown::Web::IndentedStringIo.new
|
150
139
|
@indent = 0
|
151
140
|
@compound = compound || only_module # When only-module is enabled, dependencies between modules are displayed as compound.
|
152
141
|
@compound_map = Hash.new { |h, k| h[k] = {} } # Hash{ ltail => Hash{ lhead => issued id } }
|
153
142
|
@concentrate = concentrate
|
154
143
|
@only_module = only_module
|
155
|
-
@
|
144
|
+
@dot_metadata_store = DotMetadataStore.new(metadata)
|
156
145
|
end
|
157
146
|
|
158
147
|
# @return [Array<Hash>]
|
159
|
-
def
|
160
|
-
@
|
148
|
+
def dot_metadata
|
149
|
+
@dot_metadata_store.to_a
|
161
150
|
end
|
162
151
|
|
163
152
|
# @return [String]
|
@@ -179,77 +168,62 @@ module DiverDown
|
|
179
168
|
|
180
169
|
private
|
181
170
|
|
182
|
-
attr_reader :definition, :
|
171
|
+
attr_reader :definition, :metadata, :io
|
183
172
|
|
184
173
|
def render_only_modules
|
185
174
|
# Hash{ from_module => { to_module => Array<DiverDown::Definition::Dependency> } }
|
186
175
|
dependency_map = Hash.new { |h, k| h[k] = Hash.new { |hi, ki| hi[ki] = [] } }
|
187
176
|
|
188
177
|
definition.sources.sort_by(&:source_name).each do |source|
|
189
|
-
|
190
|
-
next if
|
178
|
+
source_module = metadata.source(source.source_name).module
|
179
|
+
next if source_module.nil?
|
191
180
|
|
192
181
|
source.dependencies.each do |dependency|
|
193
|
-
|
194
|
-
next if
|
182
|
+
dependency_module = metadata.source(dependency.source_name).module
|
183
|
+
next if dependency_module.nil?
|
195
184
|
|
196
|
-
dependency_map[
|
185
|
+
dependency_map[source_module][dependency_module].push(dependency)
|
197
186
|
end
|
198
187
|
end
|
199
188
|
|
200
189
|
# Remove duplicated prefix modules
|
201
|
-
|
202
|
-
uniq_modules
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
indexes = (0..(specific_module_names.length - 1)).to_a
|
211
|
-
|
212
|
-
chain_yield(indexes) do |index, next_proc|
|
213
|
-
module_names = specific_module_names[0..index]
|
214
|
-
module_name = specific_module_names[index]
|
215
|
-
|
216
|
-
io.puts %(subgraph "#{escape_quote(module_label(module_names))}" {)
|
217
|
-
io.indented do
|
218
|
-
io.puts %(id="#{@metadata_store.issue_modules_id(module_names)}")
|
219
|
-
io.puts %(label="#{escape_quote(module_name)}")
|
220
|
-
io.puts %("#{escape_quote(module_name)}" #{build_attributes(label: module_name, id: @metadata_store.issue_modules_id(module_names))})
|
221
|
-
|
222
|
-
next_proc&.call
|
223
|
-
end
|
224
|
-
io.puts '}'
|
225
|
-
end
|
190
|
+
uniq_modules = [*dependency_map.keys, *dependency_map.values.map(&:keys).flatten].uniq.sort
|
191
|
+
uniq_modules.reject!(&:nil?)
|
192
|
+
|
193
|
+
uniq_modules.each do |modulee|
|
194
|
+
io.puts %(subgraph "#{escape_quote(module_label(modulee))}" {)
|
195
|
+
io.indented do
|
196
|
+
io.puts %(id="#{@dot_metadata_store.issue_module_id(modulee)}")
|
197
|
+
io.puts %(label="#{escape_quote(modulee)}")
|
198
|
+
io.puts %("#{escape_quote(modulee)}" #{build_attributes(label: modulee, id: @dot_metadata_store.issue_module_id(modulee))})
|
226
199
|
end
|
227
|
-
|
228
|
-
io.write buf.string
|
200
|
+
io.puts '}'
|
229
201
|
end
|
230
202
|
|
231
|
-
dependency_map.each do |
|
232
|
-
|
203
|
+
dependency_map.keys.sort_by(&:to_s).each do |from_module|
|
204
|
+
dependency_map.fetch(from_module).keys.sort_by(&:to_s).each do |to_module|
|
205
|
+
all_dependencies = dependency_map.fetch(from_module).fetch(to_module)
|
206
|
+
|
233
207
|
# Do not render standalone source
|
234
208
|
# Do not render self-dependency
|
235
|
-
next if
|
209
|
+
next if from_module.nil? || to_module.empty? || from_module == to_module
|
236
210
|
|
237
211
|
dependencies = DiverDown::Definition::Dependency.combine(*all_dependencies)
|
238
212
|
|
239
213
|
dependencies.each do
|
240
214
|
attributes = {}
|
241
|
-
ltail = module_label(
|
242
|
-
lhead = module_label(
|
215
|
+
ltail = module_label(from_module)
|
216
|
+
lhead = module_label(to_module)
|
243
217
|
|
244
218
|
# Already rendered dependencies between modules
|
245
219
|
# Add the dependency to the edge of the compound
|
246
220
|
if @compound_map[ltail].include?(lhead)
|
247
221
|
compound_id = @compound_map[ltail][lhead]
|
248
|
-
@
|
222
|
+
@dot_metadata_store.append_dependency(compound_id, _1)
|
249
223
|
next
|
250
224
|
end
|
251
225
|
|
252
|
-
compound_id = @
|
226
|
+
compound_id = @dot_metadata_store.issue_dependency_id(_1)
|
253
227
|
@compound_map[ltail][lhead] = compound_id
|
254
228
|
|
255
229
|
attributes.merge!(
|
@@ -259,7 +233,7 @@ module DiverDown
|
|
259
233
|
minlen: MODULE_MINLEN
|
260
234
|
)
|
261
235
|
|
262
|
-
io.write(%("#{escape_quote(
|
236
|
+
io.write(%("#{escape_quote(from_module)}" -> "#{escape_quote(to_module)}"))
|
263
237
|
io.write(%( #{build_attributes(**attributes)}), indent: false) unless attributes.empty?
|
264
238
|
io.write("\n")
|
265
239
|
end
|
@@ -268,67 +242,49 @@ module DiverDown
|
|
268
242
|
end
|
269
243
|
|
270
244
|
def render_sources
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
# Remove duplicated prefix modules
|
276
|
-
# from [["A"], ["A", "B"]] to [["A", "B"]]
|
277
|
-
uniq_modules = by_modules.keys.uniq
|
278
|
-
uniq_modules = uniq_modules.reject do |modules|
|
279
|
-
uniq_modules.any? { _1[0..modules.size - 1] == modules && _1.length > modules.size }
|
245
|
+
# Hash{ module => sources }
|
246
|
+
# Hash{ String => Array<DiverDown::Definition::Source> }
|
247
|
+
by_module = definition.sources.group_by do |source|
|
248
|
+
metadata.source(source.source_name).module
|
280
249
|
end
|
281
250
|
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
sources = by_modules[full_modules].sort_by(&:source_name)
|
251
|
+
# Render subgraph for each module and its sources second
|
252
|
+
by_module.keys.sort_by(&:to_s).each do |modulee|
|
253
|
+
sources = by_module.fetch(modulee).sort_by(&:source_name)
|
286
254
|
|
255
|
+
if modulee.nil?
|
287
256
|
sources.each do |source|
|
288
|
-
|
257
|
+
io.puts build_source_node(source)
|
289
258
|
end
|
290
259
|
else
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
io.puts %(subgraph "#{escape_quote(module_label(module_names))}" {)
|
299
|
-
io.indented do
|
300
|
-
io.puts %(id="#{@metadata_store.issue_modules_id(module_names)}")
|
301
|
-
io.puts %(label="#{escape_quote(module_name)}")
|
302
|
-
|
303
|
-
sources = (by_modules[module_names] || []).sort_by(&:source_name)
|
304
|
-
sources.each do |source|
|
305
|
-
insert_source(source)
|
306
|
-
end
|
307
|
-
|
308
|
-
next_proc&.call
|
309
|
-
end
|
310
|
-
io.puts '}'
|
260
|
+
io.puts %(subgraph "#{escape_quote(module_label(modulee))}" {)
|
261
|
+
io.indented do
|
262
|
+
io.puts %(id="#{@dot_metadata_store.issue_module_id(modulee)}")
|
263
|
+
io.puts %(label="#{escape_quote(modulee)}")
|
264
|
+
|
265
|
+
sources.each do |source|
|
266
|
+
io.puts build_source_node(source)
|
311
267
|
end
|
312
268
|
end
|
313
|
-
|
314
|
-
io.write buf.string
|
269
|
+
io.puts '}'
|
315
270
|
end
|
316
271
|
end
|
317
272
|
|
273
|
+
# Render dependencies last
|
318
274
|
definition.sources.sort_by(&:source_name).each do |source|
|
319
275
|
insert_dependencies(source)
|
320
276
|
end
|
321
277
|
end
|
322
278
|
|
323
|
-
def
|
324
|
-
|
279
|
+
def build_source_node(source)
|
280
|
+
%("#{escape_quote(source.source_name)}" #{build_attributes(label: source.source_name, id: @dot_metadata_store.issue_source_id(source))})
|
325
281
|
end
|
326
282
|
|
327
283
|
def insert_dependencies(source)
|
328
284
|
source.dependencies.each do
|
329
285
|
attributes = {}
|
330
|
-
ltail = module_label(
|
331
|
-
lhead = module_label(
|
286
|
+
ltail = module_label(metadata.source(source.source_name).module)
|
287
|
+
lhead = module_label(metadata.source(_1.source_name).module)
|
332
288
|
|
333
289
|
if @compound && (ltail || lhead)
|
334
290
|
# Rendering of dependencies between modules is done only once
|
@@ -338,11 +294,11 @@ module DiverDown
|
|
338
294
|
# Add the dependency to the edge of the compound
|
339
295
|
if between_modules && @compound_map[ltail].include?(lhead)
|
340
296
|
compound_id = @compound_map[ltail][lhead]
|
341
|
-
@
|
297
|
+
@dot_metadata_store.append_dependency(compound_id, _1)
|
342
298
|
next
|
343
299
|
end
|
344
300
|
|
345
|
-
compound_id = @
|
301
|
+
compound_id = @dot_metadata_store.issue_dependency_id(_1)
|
346
302
|
@compound_map[ltail][lhead] = compound_id
|
347
303
|
|
348
304
|
attributes.merge!(
|
@@ -353,7 +309,8 @@ module DiverDown
|
|
353
309
|
)
|
354
310
|
else
|
355
311
|
attributes.merge!(
|
356
|
-
id: @
|
312
|
+
id: @dot_metadata_store.issue_dependency_id(_1),
|
313
|
+
minlen: MODULE_MINLEN
|
357
314
|
)
|
358
315
|
end
|
359
316
|
|
@@ -411,10 +368,10 @@ module DiverDown
|
|
411
368
|
@io = old_io
|
412
369
|
end
|
413
370
|
|
414
|
-
def module_label(
|
415
|
-
return if
|
371
|
+
def module_label(modulee)
|
372
|
+
return if modulee.nil?
|
416
373
|
|
417
|
-
"cluster_#{
|
374
|
+
"cluster_#{modulee}"
|
418
375
|
end
|
419
376
|
|
420
377
|
def escape_quote(string)
|
@@ -0,0 +1,128 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DiverDown
|
4
|
+
class Web
|
5
|
+
class Metadata
|
6
|
+
class SourceAlias
|
7
|
+
class ConflictError < StandardError
|
8
|
+
attr_reader :source_name
|
9
|
+
|
10
|
+
def initialize(message, source_name:)
|
11
|
+
@source_name = source_name
|
12
|
+
|
13
|
+
super(message)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
BLANK_RE = /\A\s*\z/
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
# Hash{ alias_name => Set<source_name, ...> }
|
21
|
+
@alias_to_source_names = {}
|
22
|
+
@source_name_to_alias = {}
|
23
|
+
end
|
24
|
+
|
25
|
+
# @param alias_name [String]
|
26
|
+
# @param source_names [Array<String>]
|
27
|
+
# @return [void]
|
28
|
+
def update_alias(alias_name, source_names)
|
29
|
+
source_names = source_names.reject { BLANK_RE.match?(_1) }
|
30
|
+
|
31
|
+
if source_names.empty?
|
32
|
+
prev_source_names = @alias_to_source_names.delete(alias_name)
|
33
|
+
prev_source_names&.each do |prev_source_name|
|
34
|
+
@source_name_to_alias.delete(prev_source_name)
|
35
|
+
end
|
36
|
+
else
|
37
|
+
check_conflict(alias_name, source_names)
|
38
|
+
@alias_to_source_names[alias_name] = source_names.sort
|
39
|
+
|
40
|
+
source_names.each do |source_name|
|
41
|
+
@source_name_to_alias[source_name] = alias_name
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# @param alias_name [String]
|
47
|
+
# @return [String]
|
48
|
+
def resolve_alias(source_name)
|
49
|
+
@source_name_to_alias[source_name] if @source_name_to_alias.key?(source_name)
|
50
|
+
end
|
51
|
+
|
52
|
+
# @param alias_name [String]
|
53
|
+
# @return [Array<String>]
|
54
|
+
def aliased_source_names(alias_name)
|
55
|
+
@alias_to_source_names[alias_name] if @alias_to_source_names.key?(alias_name)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Rollback the changes made in the block if an conflict error occurs.
|
59
|
+
#
|
60
|
+
# @raise [ConflictError]
|
61
|
+
def transaction
|
62
|
+
alias_to_source_names = deep_dup(@alias_to_source_names)
|
63
|
+
source_name_to_alias = deep_dup(@source_name_to_alias)
|
64
|
+
|
65
|
+
yield
|
66
|
+
rescue ConflictError
|
67
|
+
@alias_to_source_names = alias_to_source_names
|
68
|
+
@source_name_to_alias = source_name_to_alias
|
69
|
+
|
70
|
+
raise
|
71
|
+
end
|
72
|
+
|
73
|
+
# @return [Hash]
|
74
|
+
def to_h
|
75
|
+
keys = @alias_to_source_names.keys.sort
|
76
|
+
keys.to_h { [_1, aliased_source_names(_1)] }
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def deep_dup(hash)
|
82
|
+
hash.transform_values do
|
83
|
+
if _1.is_a?(Hash)
|
84
|
+
deep_dup(_1)
|
85
|
+
else
|
86
|
+
_1.dup
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def added_source_names(excluding_alias_name)
|
92
|
+
source_names = Set.new
|
93
|
+
|
94
|
+
@alias_to_source_names.each_key do |name|
|
95
|
+
next if excluding_alias_name == name
|
96
|
+
|
97
|
+
source_names.add(name)
|
98
|
+
|
99
|
+
@alias_to_source_names[name].each do |source_name|
|
100
|
+
source_names.add(source_name)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
source_names
|
105
|
+
end
|
106
|
+
|
107
|
+
def check_conflict(alias_name, source_names)
|
108
|
+
if source_names.any? { _1 == alias_name }
|
109
|
+
raise(ConflictError.new("Cannot create an alias for '#{alias_name}' that refers to itself.", source_name: alias_name))
|
110
|
+
end
|
111
|
+
|
112
|
+
registered_source_names = added_source_names(alias_name)
|
113
|
+
|
114
|
+
if registered_source_names.include?(alias_name)
|
115
|
+
raise ConflictError.new("Alias '#{alias_name}' is already aliased.", source_name: alias_name)
|
116
|
+
end
|
117
|
+
|
118
|
+
conflicted_source_name = source_names.find { registered_source_names.include?(_1) }
|
119
|
+
|
120
|
+
if conflicted_source_name
|
121
|
+
resolved_source_name = resolve_alias(conflicted_source_name)
|
122
|
+
raise ConflictError.new("Source '#{conflicted_source_name}' is already aliased to '#{resolved_source_name}'.", source_name: conflicted_source_name)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|