diver_down 0.0.1.alpha1
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/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +132 -0
- data/exe/diver_down_web +55 -0
- data/lib/diver_down/definition/dependency.rb +107 -0
- data/lib/diver_down/definition/method_id.rb +83 -0
- data/lib/diver_down/definition/source.rb +90 -0
- data/lib/diver_down/definition.rb +112 -0
- data/lib/diver_down/helper.rb +81 -0
- data/lib/diver_down/trace/call_stack.rb +45 -0
- data/lib/diver_down/trace/ignored_method_ids.rb +136 -0
- data/lib/diver_down/trace/module_set/array_module_set.rb +31 -0
- data/lib/diver_down/trace/module_set/const_source_location_module_set.rb +28 -0
- data/lib/diver_down/trace/module_set.rb +78 -0
- data/lib/diver_down/trace/redefine_ruby_methods.rb +64 -0
- data/lib/diver_down/trace/tracer/session.rb +121 -0
- data/lib/diver_down/trace/tracer.rb +96 -0
- data/lib/diver_down/trace.rb +27 -0
- data/lib/diver_down/version.rb +5 -0
- data/lib/diver_down/web/action.rb +344 -0
- data/lib/diver_down/web/bit_id.rb +41 -0
- data/lib/diver_down/web/definition_enumerator.rb +54 -0
- data/lib/diver_down/web/definition_loader.rb +37 -0
- data/lib/diver_down/web/definition_store.rb +89 -0
- data/lib/diver_down/web/definition_to_dot.rb +399 -0
- data/lib/diver_down/web/dev_server_middleware.rb +72 -0
- data/lib/diver_down/web/indented_string_io.rb +59 -0
- data/lib/diver_down/web/module_store.rb +59 -0
- data/lib/diver_down/web.rb +101 -0
- data/lib/diver_down-trace.rb +4 -0
- data/lib/diver_down-web.rb +4 -0
- data/lib/diver_down.rb +14 -0
- data/web/assets/CjLq7LhZ.css +1 -0
- data/web/assets/bundle.js +978 -0
- data/web/index.html +13 -0
- metadata +122 -0
@@ -0,0 +1,399 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'cgi'
|
5
|
+
|
6
|
+
module DiverDown
|
7
|
+
class Web
|
8
|
+
class DefinitionToDot
|
9
|
+
ATTRIBUTE_DELIMITER = ' '
|
10
|
+
MODULE_DELIMITER = '::'
|
11
|
+
|
12
|
+
# Between modules is prominently distanced
|
13
|
+
MODULE_MINLEN = 3
|
14
|
+
|
15
|
+
class MetadataStore
|
16
|
+
Metadata = Data.define(:id, :type, :data, :module_store) do
|
17
|
+
# @return [Hash]
|
18
|
+
def to_h
|
19
|
+
case type
|
20
|
+
when :source
|
21
|
+
source_to_h
|
22
|
+
when :dependency
|
23
|
+
dependency_to_h
|
24
|
+
when :module
|
25
|
+
module_to_h
|
26
|
+
else
|
27
|
+
raise NotImplementedError, "not implemented yet #{type}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def source_to_h
|
34
|
+
modules = module_store.get(data.source_name).map do
|
35
|
+
{
|
36
|
+
module_name: _1,
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
{
|
41
|
+
id:,
|
42
|
+
type: 'source',
|
43
|
+
source_name: data.source_name,
|
44
|
+
modules:,
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
def dependency_to_h
|
49
|
+
{
|
50
|
+
id:,
|
51
|
+
type: 'dependency',
|
52
|
+
dependencies: data.map do |dependency|
|
53
|
+
{
|
54
|
+
source_name: dependency.source_name,
|
55
|
+
method_ids: dependency.method_ids.sort.map do
|
56
|
+
{
|
57
|
+
name: _1.name,
|
58
|
+
context: _1.context,
|
59
|
+
}
|
60
|
+
end,
|
61
|
+
}
|
62
|
+
end,
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
def module_to_h
|
67
|
+
{
|
68
|
+
id:,
|
69
|
+
type: 'module',
|
70
|
+
modules: data.map do
|
71
|
+
{
|
72
|
+
module_name: _1,
|
73
|
+
}
|
74
|
+
end,
|
75
|
+
}
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def initialize(module_store)
|
80
|
+
@prefix = 'graph_'
|
81
|
+
@module_store = module_store
|
82
|
+
|
83
|
+
# Hash{ id => Metadata }
|
84
|
+
@to_h = {}
|
85
|
+
end
|
86
|
+
|
87
|
+
# @param type [Symbol]
|
88
|
+
# @param record [DiverDown::Definition::Source]
|
89
|
+
# @return [String]
|
90
|
+
def issue_source_id(source)
|
91
|
+
build_metadata_and_return_id(:source, source)
|
92
|
+
end
|
93
|
+
|
94
|
+
# @param dependency [DiverDown::Definition::Dependency]
|
95
|
+
# @return [String]
|
96
|
+
def issue_dependency_id(dependency)
|
97
|
+
build_metadata_and_return_id(:dependency, [dependency])
|
98
|
+
end
|
99
|
+
|
100
|
+
# @param module_names [Array<String>]
|
101
|
+
# @return [String]
|
102
|
+
def issue_modules_id(module_names)
|
103
|
+
issued_metadata = @to_h.values.find { _1.type == :module && _1.data == module_names }
|
104
|
+
|
105
|
+
if issued_metadata
|
106
|
+
issued_metadata.id
|
107
|
+
else
|
108
|
+
build_metadata_and_return_id(:module, module_names)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# @param id [String]
|
113
|
+
# @param dependency [DiverDown::Definition::Dependency]
|
114
|
+
def append_dependency(id, dependency)
|
115
|
+
metadata = @to_h.fetch(id)
|
116
|
+
dependencies = metadata.data
|
117
|
+
combined_dependencies = DiverDown::Definition::Dependency.combine(*dependencies, dependency)
|
118
|
+
metadata.data.replace(combined_dependencies)
|
119
|
+
end
|
120
|
+
|
121
|
+
# @return [Array<Hash>]
|
122
|
+
def to_a
|
123
|
+
@to_h.values.map(&:to_h)
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
def build_metadata_and_return_id(type, data)
|
129
|
+
id = "#{@prefix}#{length + 1}"
|
130
|
+
metadata = Metadata.new(id:, type:, data:, module_store: @module_store)
|
131
|
+
@to_h[id] = metadata
|
132
|
+
|
133
|
+
id
|
134
|
+
end
|
135
|
+
|
136
|
+
def length
|
137
|
+
@to_h.length
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# @param definition [DiverDown::Definition]
|
142
|
+
# @param module_store [DiverDown::ModuleStore]
|
143
|
+
# @param compound [Boolean]
|
144
|
+
# @param concentrate [Boolean] https://graphviz.org/docs/attrs/concentrate/
|
145
|
+
def initialize(definition, module_store, compound: false, concentrate: false, only_module: false)
|
146
|
+
@definition = definition
|
147
|
+
@module_store = module_store
|
148
|
+
@io = DiverDown::Web::IndentedStringIo.new
|
149
|
+
@indent = 0
|
150
|
+
@compound = compound || only_module # When only-module is enabled, dependencies between modules are displayed as compound.
|
151
|
+
@compound_map = Hash.new { |h, k| h[k] = {} } # Hash{ ltail => Hash{ lhead => issued id } }
|
152
|
+
@concentrate = concentrate
|
153
|
+
@only_module = only_module
|
154
|
+
@metadata_store = MetadataStore.new(module_store)
|
155
|
+
end
|
156
|
+
|
157
|
+
# @return [Array<Hash>]
|
158
|
+
def metadata
|
159
|
+
@metadata_store.to_a
|
160
|
+
end
|
161
|
+
|
162
|
+
# @return [String]
|
163
|
+
def to_s
|
164
|
+
io.puts %(strict digraph "#{definition.title}" {)
|
165
|
+
io.indented do
|
166
|
+
io.puts('compound=true') if @compound
|
167
|
+
io.puts('concentrate=true') if @concentrate
|
168
|
+
|
169
|
+
if @only_module
|
170
|
+
render_only_modules
|
171
|
+
else
|
172
|
+
definition.sources.sort_by(&:source_name).each do
|
173
|
+
insert_source(_1)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
io.puts '}'
|
178
|
+
io.string
|
179
|
+
end
|
180
|
+
|
181
|
+
private
|
182
|
+
|
183
|
+
attr_reader :definition, :module_store, :io
|
184
|
+
|
185
|
+
def render_only_modules
|
186
|
+
# Hash{ from_module => { to_module => Array<DiverDown::Definition::Dependency> } }
|
187
|
+
dependency_map = Hash.new { |h, k| h[k] = Hash.new { |hi, ki| hi[ki] = [] } }
|
188
|
+
|
189
|
+
definition.sources.sort_by(&:source_name).each do |source|
|
190
|
+
source_modules = module_store.get(source.source_name)
|
191
|
+
next if source_modules.empty?
|
192
|
+
|
193
|
+
source.dependencies.each do |dependency|
|
194
|
+
dependency_modules = module_store.get(dependency.source_name)
|
195
|
+
next if dependency_modules.empty?
|
196
|
+
|
197
|
+
dependency_map[source_modules][dependency_modules].push(dependency)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# Remove duplicated prefix modules
|
202
|
+
# from [["A"], ["A", "B"]] to [["A", "B"]]
|
203
|
+
uniq_modules = [*dependency_map.keys, *dependency_map.values.map(&:keys).flatten(1)].uniq
|
204
|
+
uniq_modules.reject! do |modules|
|
205
|
+
modules.empty? ||
|
206
|
+
uniq_modules.any? { _1[0..modules.size - 1] == modules && _1.length > modules.size }
|
207
|
+
end
|
208
|
+
|
209
|
+
uniq_modules.each do |specific_module_names|
|
210
|
+
buf = swap_io do
|
211
|
+
indexes = (0..(specific_module_names.length - 1)).to_a
|
212
|
+
|
213
|
+
chain_yield(indexes) do |index, next_proc|
|
214
|
+
module_names = specific_module_names[0..index]
|
215
|
+
module_name = specific_module_names[index]
|
216
|
+
|
217
|
+
io.puts %(subgraph "#{module_label(module_names)}" {)
|
218
|
+
io.indented do
|
219
|
+
io.puts %(id="#{@metadata_store.issue_modules_id(module_names)}")
|
220
|
+
io.puts %(label="#{module_name}")
|
221
|
+
io.puts %("#{module_name}" #{build_attributes(label: module_name, id: @metadata_store.issue_modules_id(module_names))})
|
222
|
+
|
223
|
+
next_proc&.call
|
224
|
+
end
|
225
|
+
io.puts '}'
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
io.write buf.string
|
230
|
+
end
|
231
|
+
|
232
|
+
dependency_map.each do |from_modules, h|
|
233
|
+
h.each do |to_modules, all_dependencies|
|
234
|
+
# Do not render standalone source
|
235
|
+
# Do not render self-dependency
|
236
|
+
next if from_modules.empty? || to_modules.empty? || from_modules == to_modules
|
237
|
+
|
238
|
+
dependencies = DiverDown::Definition::Dependency.combine(*all_dependencies)
|
239
|
+
|
240
|
+
dependencies.each do
|
241
|
+
attributes = {}
|
242
|
+
ltail = module_label(*from_modules)
|
243
|
+
lhead = module_label(*to_modules)
|
244
|
+
|
245
|
+
# Already rendered dependencies between modules
|
246
|
+
# Add the dependency to the edge of the compound
|
247
|
+
if @compound_map[ltail].include?(lhead)
|
248
|
+
compound_id = @compound_map[ltail][lhead]
|
249
|
+
@metadata_store.append_dependency(compound_id, _1)
|
250
|
+
next
|
251
|
+
end
|
252
|
+
|
253
|
+
compound_id = @metadata_store.issue_dependency_id(_1)
|
254
|
+
@compound_map[ltail][lhead] = compound_id
|
255
|
+
|
256
|
+
attributes.merge!(
|
257
|
+
id: compound_id,
|
258
|
+
ltail:,
|
259
|
+
lhead:,
|
260
|
+
minlen: MODULE_MINLEN
|
261
|
+
)
|
262
|
+
|
263
|
+
io.write(%("#{from_modules[-1]}" -> "#{to_modules[-1]}"))
|
264
|
+
io.write(%( #{build_attributes(**attributes)}), indent: false) unless attributes.empty?
|
265
|
+
io.write("\n")
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
def insert_source(source)
|
272
|
+
if module_store.get(source.source_name).empty?
|
273
|
+
io.puts %("#{source.source_name}" #{build_attributes(label: source.source_name, id: @metadata_store.issue_source_id(source))})
|
274
|
+
else
|
275
|
+
insert_modules(source)
|
276
|
+
end
|
277
|
+
|
278
|
+
source.dependencies.each do
|
279
|
+
attributes = {}
|
280
|
+
ltail = module_label(*module_store.get(source.source_name))
|
281
|
+
lhead = module_label(*module_store.get(_1.source_name))
|
282
|
+
|
283
|
+
if @compound && (ltail || lhead)
|
284
|
+
# Rendering of dependencies between modules is done only once
|
285
|
+
between_modules = ltail != lhead
|
286
|
+
|
287
|
+
# Already rendered dependencies between modules
|
288
|
+
# Add the dependency to the edge of the compound
|
289
|
+
if between_modules && @compound_map[ltail].include?(lhead)
|
290
|
+
compound_id = @compound_map[ltail][lhead]
|
291
|
+
@metadata_store.append_dependency(compound_id, _1)
|
292
|
+
next
|
293
|
+
end
|
294
|
+
|
295
|
+
compound_id = @metadata_store.issue_dependency_id(_1)
|
296
|
+
@compound_map[ltail][lhead] = compound_id
|
297
|
+
|
298
|
+
attributes.merge!(
|
299
|
+
id: compound_id,
|
300
|
+
ltail:,
|
301
|
+
lhead:,
|
302
|
+
minlen: MODULE_MINLEN
|
303
|
+
)
|
304
|
+
else
|
305
|
+
attributes.merge!(
|
306
|
+
id: @metadata_store.issue_dependency_id(_1)
|
307
|
+
)
|
308
|
+
end
|
309
|
+
|
310
|
+
io.write(%("#{source.source_name}" -> "#{_1.source_name}"))
|
311
|
+
io.write(%( #{build_attributes(**attributes)}), indent: false) unless attributes.empty?
|
312
|
+
io.write("\n")
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
def insert_modules(source)
|
317
|
+
buf = swap_io do
|
318
|
+
all_module_names = module_store.get(source.source_name)
|
319
|
+
indexes = (0..(all_module_names.length - 1)).to_a
|
320
|
+
|
321
|
+
chain_yield(indexes) do |index, next_proc|
|
322
|
+
module_names = all_module_names[0..index]
|
323
|
+
module_name = module_names[-1]
|
324
|
+
|
325
|
+
io.puts %(subgraph "#{module_label(module_names)}" {)
|
326
|
+
io.indented do
|
327
|
+
io.puts %(id="#{@metadata_store.issue_modules_id(module_names)}")
|
328
|
+
io.puts %(label="#{module_name}")
|
329
|
+
|
330
|
+
if next_proc
|
331
|
+
next_proc.call
|
332
|
+
else
|
333
|
+
# last. equals indexes[-1] == index
|
334
|
+
io.puts %("#{source.source_name}" #{build_attributes(label: source.source_name, id: @metadata_store.issue_source_id(source))})
|
335
|
+
end
|
336
|
+
end
|
337
|
+
io.puts '}'
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
io.write buf.string
|
342
|
+
end
|
343
|
+
|
344
|
+
def chain_yield(values, &block)
|
345
|
+
*head, tail = values
|
346
|
+
|
347
|
+
last_proc = proc do
|
348
|
+
block.call(tail, nil)
|
349
|
+
end
|
350
|
+
|
351
|
+
chain_proc = head.inject(last_proc) do |next_proc, value|
|
352
|
+
proc do
|
353
|
+
block.call(value, next_proc)
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
chain_proc.call
|
358
|
+
end
|
359
|
+
|
360
|
+
# rubocop:disable Lint/UnderscorePrefixedVariableName
|
361
|
+
# attrsの参考 https://qiita.com/rubytomato@github/items/51779135bc4b77c8c20d
|
362
|
+
def build_attributes(_wrap: '[]', **attrs)
|
363
|
+
attrs = attrs.reject { _2.nil? || _2 == '' }
|
364
|
+
return if attrs.empty?
|
365
|
+
|
366
|
+
attrs_str = attrs.map { %(#{_1}="#{_2}") }.join(ATTRIBUTE_DELIMITER)
|
367
|
+
|
368
|
+
if _wrap
|
369
|
+
"#{_wrap[0]}#{attrs_str}#{_wrap[1]}"
|
370
|
+
else
|
371
|
+
attrs_str
|
372
|
+
end
|
373
|
+
end
|
374
|
+
# rubocop:enable Lint/UnderscorePrefixedVariableName
|
375
|
+
|
376
|
+
def increase_indent
|
377
|
+
@indent += 1
|
378
|
+
yield
|
379
|
+
ensure
|
380
|
+
@indent -= 1
|
381
|
+
end
|
382
|
+
|
383
|
+
def swap_io
|
384
|
+
old_io = @io
|
385
|
+
@io = IndentedStringIo.new
|
386
|
+
yield
|
387
|
+
@io
|
388
|
+
ensure
|
389
|
+
@io = old_io
|
390
|
+
end
|
391
|
+
|
392
|
+
def module_label(*modules)
|
393
|
+
return if modules.empty?
|
394
|
+
|
395
|
+
"cluster_#{modules.join(MODULE_DELIMITER)}"
|
396
|
+
end
|
397
|
+
end
|
398
|
+
end
|
399
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rack/proxy'
|
4
|
+
require 'websocket/driver'
|
5
|
+
require 'eventmachine'
|
6
|
+
|
7
|
+
module DiverDown
|
8
|
+
class Web
|
9
|
+
# For vite
|
10
|
+
class DevServerMiddleware
|
11
|
+
class HttpProxy < ::Rack::Proxy
|
12
|
+
def initialize(_app = nil, host:, port:)
|
13
|
+
@host = host
|
14
|
+
@port = port
|
15
|
+
|
16
|
+
super(nil, backend: "http://#{@host}:#{@port}", proxy_host: @host, proxy_port: @port, proxy_scheme: 'http')
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class WebSocketProxy
|
21
|
+
attr_reader :env, :url
|
22
|
+
|
23
|
+
def initialize(env, host:, port:)
|
24
|
+
@env = env
|
25
|
+
@url = "ws://#{host}:#{port}#{env['REQUEST_URI']}"
|
26
|
+
@driver = WebSocket::Driver.rack(self)
|
27
|
+
|
28
|
+
env['rack.hijack'].call
|
29
|
+
@io = env['rack.hijack_io']
|
30
|
+
|
31
|
+
EM.attach(@io, Reader) { |conn| conn.driver = @driver }
|
32
|
+
|
33
|
+
@driver.start
|
34
|
+
end
|
35
|
+
|
36
|
+
# @param string [String]
|
37
|
+
def write(string)
|
38
|
+
@io.write(string)
|
39
|
+
end
|
40
|
+
|
41
|
+
module Reader
|
42
|
+
attr_writer :driver
|
43
|
+
|
44
|
+
# @param string [String]
|
45
|
+
def receive_data(string)
|
46
|
+
@driver.parse(string)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def initialize(app, host:, port:)
|
52
|
+
@app = app
|
53
|
+
@host = host
|
54
|
+
@port = port
|
55
|
+
@http_proxy = HttpProxy.new(@app, host: @host, port: @port)
|
56
|
+
end
|
57
|
+
|
58
|
+
# @param env [Hash]
|
59
|
+
def call(env)
|
60
|
+
request = Rack::Request.new(env)
|
61
|
+
|
62
|
+
if WebSocket::Driver.websocket?(env)
|
63
|
+
WebSocketProxy.new(env, host: @host, port: @port)
|
64
|
+
elsif request.path.start_with?('/api')
|
65
|
+
@app.call(env)
|
66
|
+
else
|
67
|
+
@http_proxy.call(env)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stringio'
|
4
|
+
require 'forwardable'
|
5
|
+
|
6
|
+
module DiverDown
|
7
|
+
class Web
|
8
|
+
class IndentedStringIo
|
9
|
+
extend ::Forwardable
|
10
|
+
|
11
|
+
def_delegators :@io, :rewind, :string
|
12
|
+
|
13
|
+
attr_accessor :indent
|
14
|
+
|
15
|
+
# @param tab [String]
|
16
|
+
def initialize(tab: ' ')
|
17
|
+
@io = StringIO.new
|
18
|
+
@indent = 0
|
19
|
+
@tab = tab
|
20
|
+
end
|
21
|
+
|
22
|
+
# @param contents [Array<String>]
|
23
|
+
# @param indent [Boolean] Enable or disable indentation
|
24
|
+
# @return [void]
|
25
|
+
def write(*contents, indent: true)
|
26
|
+
indent_string = if indent
|
27
|
+
@tab * @indent
|
28
|
+
else
|
29
|
+
''
|
30
|
+
end
|
31
|
+
|
32
|
+
string = contents.join
|
33
|
+
lines = string.lines
|
34
|
+
lines.each do |line|
|
35
|
+
if line == "\n"
|
36
|
+
@io.write "\n"
|
37
|
+
else
|
38
|
+
@io.write "#{indent_string}#{line}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# @param content [String]
|
44
|
+
# @return [void]
|
45
|
+
def puts(*contents, indent: true)
|
46
|
+
write("#{contents.join("\n")}\n", indent:)
|
47
|
+
nil
|
48
|
+
end
|
49
|
+
|
50
|
+
# increase the indent level for the block
|
51
|
+
def indented
|
52
|
+
@indent += 1
|
53
|
+
yield
|
54
|
+
ensure
|
55
|
+
@indent -= 1
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module DiverDown
|
6
|
+
class Web
|
7
|
+
class ModuleStore
|
8
|
+
BLANK_ARRAY = [].freeze
|
9
|
+
BLANK_RE = /\A\s*\z/
|
10
|
+
|
11
|
+
private_constant(:BLANK_RE)
|
12
|
+
|
13
|
+
def initialize(path)
|
14
|
+
@path = path
|
15
|
+
@store = load
|
16
|
+
end
|
17
|
+
|
18
|
+
# @param source_name [String]
|
19
|
+
# @param module_names [Array<String>]
|
20
|
+
def set(source_name, module_names)
|
21
|
+
@store[source_name] = module_names.dup.reject do
|
22
|
+
BLANK_RE.match?(_1)
|
23
|
+
end.freeze
|
24
|
+
end
|
25
|
+
|
26
|
+
# @param source_name [String]
|
27
|
+
# @return [Array<Module>]
|
28
|
+
def get(source_name)
|
29
|
+
@store[source_name] || BLANK_ARRAY
|
30
|
+
end
|
31
|
+
|
32
|
+
# @return [Hash]
|
33
|
+
def to_h
|
34
|
+
@store.dup
|
35
|
+
end
|
36
|
+
|
37
|
+
# Write store to file
|
38
|
+
# @return [void]
|
39
|
+
def flush
|
40
|
+
File.write(@path, to_h.to_yaml)
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def load
|
46
|
+
store = {}
|
47
|
+
|
48
|
+
begin
|
49
|
+
loaded = YAML.load_file(@path)
|
50
|
+
store.merge!(loaded) if loaded
|
51
|
+
rescue StandardError
|
52
|
+
# Ignore error
|
53
|
+
end
|
54
|
+
|
55
|
+
store
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rack'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
module DiverDown
|
7
|
+
class Web
|
8
|
+
WEB_DIR = File.expand_path('../../web', __dir__)
|
9
|
+
|
10
|
+
require 'diver_down/web/action'
|
11
|
+
require 'diver_down/web/definition_to_dot'
|
12
|
+
require 'diver_down/web/definition_enumerator'
|
13
|
+
require 'diver_down/web/bit_id'
|
14
|
+
require 'diver_down/web/module_store'
|
15
|
+
require 'diver_down/web/indented_string_io'
|
16
|
+
require 'diver_down/web/definition_store'
|
17
|
+
require 'diver_down/web/definition_loader'
|
18
|
+
|
19
|
+
# For development
|
20
|
+
autoload :DevServerMiddleware, 'diver_down/web/dev_server_middleware'
|
21
|
+
|
22
|
+
# @param definition_dir [String]
|
23
|
+
# @param module_store [DiverDown::ModuleStore]
|
24
|
+
# @param store [DiverDown::Web::DefinitionStore]
|
25
|
+
def initialize(definition_dir:, module_store:, store: DiverDown::Web::DefinitionStore.new)
|
26
|
+
@store = store
|
27
|
+
@module_store = module_store
|
28
|
+
@files_server = Rack::Files.new(File.join(WEB_DIR))
|
29
|
+
|
30
|
+
definition_files = ::Dir["#{definition_dir}/**/*.{yml,yaml,msgpack,json}"].sort
|
31
|
+
@total_definition_files_size = definition_files.size
|
32
|
+
|
33
|
+
load_definition_files_on_thread(definition_files)
|
34
|
+
end
|
35
|
+
|
36
|
+
# @param env [Hash]
|
37
|
+
# @return [Array[Integer, Hash, Array]]
|
38
|
+
def call(env)
|
39
|
+
request = Rack::Request.new(env)
|
40
|
+
action = DiverDown::Web::Action.new(store: @store, module_store: @module_store, request:)
|
41
|
+
|
42
|
+
case [request.request_method, request.path]
|
43
|
+
in ['GET', %r{\A/api/definitions\.json\z}]
|
44
|
+
action.definitions(
|
45
|
+
page: request.params['page']&.to_i || 1,
|
46
|
+
per: request.params['per']&.to_i || 100,
|
47
|
+
title: request.params['title'] || '',
|
48
|
+
source: request.params['source'] || ''
|
49
|
+
)
|
50
|
+
in ['GET', %r{\A/api/sources\.json\z}]
|
51
|
+
action.sources
|
52
|
+
in ['GET', %r{\A/api/modules\.json\z}]
|
53
|
+
action.modules
|
54
|
+
in ['GET', %r{\A/api/modules/(?<module_names>.+)\.json\z}]
|
55
|
+
module_names = Regexp.last_match[:module_names].split('/')
|
56
|
+
action.module(module_names)
|
57
|
+
in ['GET', %r{\A/api/definitions/(?<bit_id>\d+)\.json\z}]
|
58
|
+
bit_id = Regexp.last_match[:bit_id].to_i
|
59
|
+
compound = request.params['compound'] == '1'
|
60
|
+
concentrate = request.params['concentrate'] == '1'
|
61
|
+
only_module = request.params['only_module'] == '1'
|
62
|
+
action.combine_definitions(bit_id, compound, concentrate, only_module)
|
63
|
+
in ['GET', %r{\A/api/sources/(?<source>[^/]+)\.json\z}]
|
64
|
+
source = Regexp.last_match[:source]
|
65
|
+
action.source(source)
|
66
|
+
in ['POST', %r{\A/api/sources/(?<source>[^/]+)/modules.json\z}]
|
67
|
+
source = Regexp.last_match[:source]
|
68
|
+
modules = request.params['modules'] || []
|
69
|
+
action.set_modules(source, modules)
|
70
|
+
in ['GET', %r{\A/api/pid\.json\z}]
|
71
|
+
action.pid
|
72
|
+
in ['GET', %r{\A/api/initialization_status\.json\z}]
|
73
|
+
action.initialization_status(@total_definition_files_size)
|
74
|
+
in ['GET', %r{\A/assets/}]
|
75
|
+
@files_server.call(env)
|
76
|
+
in ['GET', /\.json\z/], ['POST', /\.json\z/]
|
77
|
+
action.not_found
|
78
|
+
else
|
79
|
+
@files_server.call(env.merge('PATH_INFO' => '/index.html'))
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def load_definition_files_on_thread(definition_files)
|
86
|
+
definition_loader = DiverDown::Web::DefinitionLoader.new
|
87
|
+
|
88
|
+
Thread.new do
|
89
|
+
loop do
|
90
|
+
break if definition_files.empty?
|
91
|
+
|
92
|
+
definition_file = definition_files.shift
|
93
|
+
definition = definition_loader.load_file(definition_file)
|
94
|
+
|
95
|
+
# No needed to synchronize because this is executed on a single thread.
|
96
|
+
@store.set(definition)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|