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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: be24104efe3c4cc931f29552c5922681622e5efcc29d74edd26ea0058d68823b
|
4
|
+
data.tar.gz: 9813779ca1e2451ed7cb97f983e198c2f749fc2f3dc88b54db0f33813e938255
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 583e816d1ff590d7e42e5c02f3546e67802ea1046c5bb8183e973cf65ae1ce37b2c7e1549578b4d848383ff2a5affd1f51455ad78cd3799e73cfecbc40b1c3ec
|
7
|
+
data.tar.gz: 150a56d11ff08e5f0a43e5483c3d5d59a6ac94dde414af484183a4b6b265c8310b8583a10488e28b1bf3f5b6a9bd205e39e2b77b5a15666bf1b03c7958d94ca8
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2024 alpaca-tc
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
# DiverDown
|
2
|
+
|
3
|
+
`divertdown` is a tool that dynamically analyzes application dependencies and creates a dependency map.
|
4
|
+
This tool was created to analyze Ruby applications for use in large-scale refactoring such as moduler monolith.
|
5
|
+
|
6
|
+
The results of the analysis can be viewed the dependency map graph, and and since the file-by-file association can be categorized into specific groups, you can deepen your architectural consideration.
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
Add this line to your application's Gemfile:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
gem 'diver_down'
|
14
|
+
```
|
15
|
+
|
16
|
+
And then execute:
|
17
|
+
|
18
|
+
$ bundle
|
19
|
+
|
20
|
+
Or install it yourself as:
|
21
|
+
|
22
|
+
$ gem install diver_down
|
23
|
+
|
24
|
+
## Usage
|
25
|
+
|
26
|
+
`divert_down` is mainly divided into the `DiverDown::Trace` for dynamic analysing ruby code, and the `DiverDown::Web` for viewing the result on the browser.
|
27
|
+
|
28
|
+
### `DiverDown::Trace`
|
29
|
+
|
30
|
+
Analyzes the processing of ruby code and outputs the analysis results as `DiverDown::Definition`.
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
tracer = DiverDown::Trace::Tracer.new
|
34
|
+
|
35
|
+
# Analyze the processing in the block.
|
36
|
+
definition = tracer.trace do
|
37
|
+
# do something
|
38
|
+
end
|
39
|
+
|
40
|
+
# Or, analyze the processing without block.
|
41
|
+
session = tracer.new_session
|
42
|
+
|
43
|
+
session.start
|
44
|
+
# do something
|
45
|
+
session.stop
|
46
|
+
|
47
|
+
definition = session.definition
|
48
|
+
```
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
# Analysis results can be titled.
|
52
|
+
# And if specified the `definition_group`, the results can be grouped when viewed in a browser.
|
53
|
+
tracer = DiverDown::Trace::Tracer.new
|
54
|
+
|
55
|
+
tracer.trace(title: 'title', definition_group: 'group name') do
|
56
|
+
# do something
|
57
|
+
end
|
58
|
+
```
|
59
|
+
|
60
|
+
The analysis results should be output to a specific directory.
|
61
|
+
Files saved in `.msgpack`, `.json`, or `.yaml` can be read by `DiverDown::Web`.
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
dir = 'tmp/diver_down'
|
65
|
+
|
66
|
+
definition = tracer.trace do
|
67
|
+
# do something
|
68
|
+
end
|
69
|
+
|
70
|
+
File.binwrite(File.join(dir, "#{definition.title}.msgpack"), definition.to_msgpack)
|
71
|
+
File.write(File.join(dir, "#{definition.title}.json"), definition.to_h.to_json)
|
72
|
+
File.write(File.join(dir, "#{definition.title}.yaml"), definition.to_h.to_yaml)
|
73
|
+
```
|
74
|
+
|
75
|
+
**Options**
|
76
|
+
|
77
|
+
TODO
|
78
|
+
|
79
|
+
### `DiverDown::Web`
|
80
|
+
|
81
|
+
View the analysis results in a browser.
|
82
|
+
|
83
|
+
This gem is designed to consider large application with a modular monolithic architecture.
|
84
|
+
Each file in the analysis can be specified to belong to a module you specify on the browser.
|
85
|
+
|
86
|
+
- `--definition-dir` specifies the directory where the analysis results are stored.
|
87
|
+
- `--module-store-path` will store the results specifying which module each file belongs to. If not specified, the specified results are stored in tempfile.
|
88
|
+
|
89
|
+
```sh
|
90
|
+
bundle exec diver_down_web --definition-dir tmp/diver_down --module-store-path tmp/module_store.yml
|
91
|
+
open http://localhost:8080
|
92
|
+
```
|
93
|
+
|
94
|
+
## Development
|
95
|
+
|
96
|
+
1. Checking out the repo `git clone https://github.com/alpaca-tc/diver_down`
|
97
|
+
1. Install dependencies.
|
98
|
+
- [Install pnpm](https://pnpm.io/installation)
|
99
|
+
- [Install Ruby](https://www.ruby-lang.org/en/documentation/installation/)
|
100
|
+
- `pnpm install`
|
101
|
+
- `bundle install`
|
102
|
+
1. Run the tests/static code analyzer.
|
103
|
+
- Ruby: `bundle exec rspec`, `bundle exec rubocop`
|
104
|
+
- TypeScript: `pnpm run test`, `pnpm run lint`
|
105
|
+
|
106
|
+
### Development DiverDown::Web
|
107
|
+
|
108
|
+
If you want to develop `DiverDown::Web` locally, set up a server for development.
|
109
|
+
|
110
|
+
```
|
111
|
+
# Start server for frontend
|
112
|
+
$ pnpm install
|
113
|
+
$ pnpm run dev
|
114
|
+
|
115
|
+
# Start server for backend
|
116
|
+
$ bundle install
|
117
|
+
# DIVER_DOWN_DIR specifies the directory where the analysis results are stored.
|
118
|
+
# DIVER_DOWN_MODULE_STORE specifies a yaml file that defines which module the file belongs to, but this file is newly created, so it works even if the file does not exist.
|
119
|
+
$ DIVER_DOWN_DIR=/path/to/definitions_dir DIVER_DOWN_MODULE_STORE=/path/to/module_store.yml bundle exec puma
|
120
|
+
```
|
121
|
+
|
122
|
+
## Contributing
|
123
|
+
|
124
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/alpaca-tc/diver_down. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/alpaca-tc/diver_down/blob/main/CODE_OF_CONDUCT.md).
|
125
|
+
|
126
|
+
## License
|
127
|
+
|
128
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
129
|
+
|
130
|
+
## Code of Conduct
|
131
|
+
|
132
|
+
Everyone interacting in the DiverDown project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/alpaca-tc/diver_down/blob/main/CODE_OF_CONDUCT.md).
|
data/exe/diver_down_web
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'rack/contrib'
|
6
|
+
require 'webrick'
|
7
|
+
require 'diver_down'
|
8
|
+
require 'diver_down-web'
|
9
|
+
require 'optparse'
|
10
|
+
|
11
|
+
options = {}
|
12
|
+
option_parser = OptionParser.new do |opts|
|
13
|
+
opts.banner = <<~BANNER
|
14
|
+
Usage: diver_down_web [options]
|
15
|
+
|
16
|
+
Example:
|
17
|
+
diver_down_web --definition-dir /path/to/definitions --module-store /path/to/module_store.yml
|
18
|
+
|
19
|
+
Options:
|
20
|
+
BANNER
|
21
|
+
|
22
|
+
opts.on('--definition-dir PATH', 'Path to the definition directory') do |path|
|
23
|
+
options[:definition_dir] = path
|
24
|
+
end
|
25
|
+
|
26
|
+
opts.on('--module-store PATH', 'Path to the module store') do |path|
|
27
|
+
options[:module_store] = path
|
28
|
+
end
|
29
|
+
end
|
30
|
+
option_parser.parse!(ARGV)
|
31
|
+
|
32
|
+
unless options[:definition_dir]
|
33
|
+
puts 'Missing --definition-dir'
|
34
|
+
puts
|
35
|
+
puts option_parser.help
|
36
|
+
exit 1
|
37
|
+
end
|
38
|
+
|
39
|
+
app = Rack::JSONBodyParser.new(
|
40
|
+
DiverDown::Web.new(
|
41
|
+
definition_dir: options.fetch(:definition_dir),
|
42
|
+
module_store: DiverDown::Web::ModuleStore.new(options[:module_store] || Tempfile.new(['module_store', '.yaml']))
|
43
|
+
)
|
44
|
+
)
|
45
|
+
|
46
|
+
begin
|
47
|
+
# Rack 2.0
|
48
|
+
require 'rack'
|
49
|
+
require 'rack/server'
|
50
|
+
Rack::Server.new(app:, server: :webrick).start
|
51
|
+
rescue LoadError
|
52
|
+
# Rack 3.0
|
53
|
+
require 'rackup'
|
54
|
+
Rackup::Server.new(app:, server: :webrick).start
|
55
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DiverDown
|
4
|
+
class Definition
|
5
|
+
class Dependency
|
6
|
+
include Comparable
|
7
|
+
|
8
|
+
# @param hash [Hash]
|
9
|
+
# @return [DiverDown::Definition::Dependency]
|
10
|
+
def self.from_hash(hash)
|
11
|
+
method_ids = (hash[:method_ids] || hash['method_ids'] || []).map do
|
12
|
+
DiverDown::Definition::MethodId.from_hash(_1)
|
13
|
+
end
|
14
|
+
|
15
|
+
new(
|
16
|
+
source_name: hash[:source_name] || hash['source_name'],
|
17
|
+
method_ids:
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
# @param dependencies [Array<DiverDown::Definition::Dependency>]
|
22
|
+
# @return [Array<DiverDown::Definition::Dependency>]
|
23
|
+
def self.combine(*dependencies)
|
24
|
+
dependencies.group_by(&:source_name).map do |source_name, same_source_dependencies|
|
25
|
+
new_dependency = new(source_name:)
|
26
|
+
|
27
|
+
same_source_dependencies.each do |dependency|
|
28
|
+
dependency.method_ids.each do |method_id|
|
29
|
+
new_method_id = new_dependency.find_or_build_method_id(name: method_id.name, context: method_id.context)
|
30
|
+
new_method_id.add_path(*method_id.paths)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
new_dependency
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
attr_reader :source_name
|
39
|
+
|
40
|
+
# @param source_name [String]
|
41
|
+
# @param method_ids [Array<DiverDown::Definition::MethodId>]
|
42
|
+
def initialize(source_name:, method_ids: [])
|
43
|
+
@source_name = source_name
|
44
|
+
@method_id_map = {
|
45
|
+
'class' => {},
|
46
|
+
'instance' => {},
|
47
|
+
}
|
48
|
+
|
49
|
+
method_ids.each do |method_id|
|
50
|
+
@method_id_map[method_id.context][method_id.name] = method_id
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# @param name [String]
|
55
|
+
# @param context ['instance', 'class']
|
56
|
+
# @return [DiverDown::Definition::MethodId]
|
57
|
+
def find_or_build_method_id(name:, context:)
|
58
|
+
@method_id_map[context.to_s][name.to_s] ||= DiverDown::Definition::MethodId.new(name:, context:)
|
59
|
+
end
|
60
|
+
|
61
|
+
# @param name [String, Symbol]
|
62
|
+
# @param context ['instance', 'class']
|
63
|
+
# @return [DiverDown::Definition::MethodId, nil]
|
64
|
+
def method_id(name:, context:)
|
65
|
+
@method_id_map[context.to_s][name.to_s]
|
66
|
+
end
|
67
|
+
|
68
|
+
# @return [Array<DiverDown::Definition::MethodId>]
|
69
|
+
def method_ids
|
70
|
+
(@method_id_map['class'].values + @method_id_map['instance'].values).sort
|
71
|
+
end
|
72
|
+
|
73
|
+
# @return [Hash]
|
74
|
+
def to_h
|
75
|
+
{
|
76
|
+
source_name:,
|
77
|
+
method_ids: method_ids.map(&:to_h),
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
# @return [Integer]
|
82
|
+
def <=>(other)
|
83
|
+
source_name <=> other.source_name
|
84
|
+
end
|
85
|
+
|
86
|
+
# @param other [Object, DiverDown::Definition::Source]
|
87
|
+
# @return [Boolean]
|
88
|
+
def ==(other)
|
89
|
+
other.is_a?(self.class) &&
|
90
|
+
source_name == other.source_name &&
|
91
|
+
method_ids == other.method_ids
|
92
|
+
end
|
93
|
+
alias eq? ==
|
94
|
+
alias eql? ==
|
95
|
+
|
96
|
+
# @return [Integer]
|
97
|
+
def hash
|
98
|
+
[self.class, source_name, method_ids].hash
|
99
|
+
end
|
100
|
+
|
101
|
+
# @return [String]
|
102
|
+
def inspect
|
103
|
+
%(#<#{self.class} source_name="#{source_name}" method_ids=#{method_ids}>")
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DiverDown
|
4
|
+
class Definition
|
5
|
+
class MethodId
|
6
|
+
VALID_CONTEXT = %w[instance class].freeze
|
7
|
+
private_constant :VALID_CONTEXT
|
8
|
+
|
9
|
+
include Comparable
|
10
|
+
|
11
|
+
attr_reader :name, :context, :paths
|
12
|
+
|
13
|
+
# @param hash [Hash]
|
14
|
+
def self.from_hash(hash)
|
15
|
+
new(
|
16
|
+
name: hash[:name] || hash['name'],
|
17
|
+
context: hash[:context] || hash['context'],
|
18
|
+
paths: hash[:paths] || hash['paths'] || []
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
# @param name [String, Symbol]
|
23
|
+
# @param context ['instance', 'class']
|
24
|
+
# @param paths [Set<String>]
|
25
|
+
def initialize(name:, context:, paths: Set.new)
|
26
|
+
raise ArgumentError, "invalid context: #{context}" unless VALID_CONTEXT.include?(context)
|
27
|
+
|
28
|
+
@name = name.to_s
|
29
|
+
@context = context.to_s
|
30
|
+
@paths = paths.to_set
|
31
|
+
end
|
32
|
+
|
33
|
+
# @param path [Array<String>]
|
34
|
+
def add_path(*paths)
|
35
|
+
paths.each do
|
36
|
+
@paths.add(_1)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# @return [String]
|
41
|
+
def human_method_name
|
42
|
+
prefix = context == 'instance' ? '#' : '.'
|
43
|
+
"#{prefix}#{name}"
|
44
|
+
end
|
45
|
+
|
46
|
+
# @return [Hash]
|
47
|
+
def to_h
|
48
|
+
{
|
49
|
+
name:,
|
50
|
+
context:,
|
51
|
+
paths: @paths.sort,
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
# @return [Integer]
|
56
|
+
def hash
|
57
|
+
[self.class, name, context, paths].hash
|
58
|
+
end
|
59
|
+
|
60
|
+
# @param other [DiverDown::Definition::MethodId]
|
61
|
+
# @return [Integer]
|
62
|
+
def <=>(other)
|
63
|
+
[context, name] <=> [other.context, other.name]
|
64
|
+
end
|
65
|
+
|
66
|
+
# @param other [Object, DiverDown::Definition::Source]
|
67
|
+
# @return [Boolean]
|
68
|
+
def ==(other)
|
69
|
+
other.is_a?(self.class) &&
|
70
|
+
name == other.name &&
|
71
|
+
context == other.context &&
|
72
|
+
paths == other.paths
|
73
|
+
end
|
74
|
+
alias eq? ==
|
75
|
+
alias eql? ==
|
76
|
+
|
77
|
+
# @return [String]
|
78
|
+
def inspect
|
79
|
+
%(#<#{self.class} #{human_method_name} paths=#{paths.inspect}>")
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DiverDown
|
4
|
+
class Definition
|
5
|
+
class Source
|
6
|
+
include Comparable
|
7
|
+
|
8
|
+
# @param hash [Hash]
|
9
|
+
def self.from_hash(hash)
|
10
|
+
new(
|
11
|
+
source_name: hash[:source_name] || hash['source_name'],
|
12
|
+
dependencies: (hash[:dependencies] || hash['dependencies'] || []).map { DiverDown::Definition::Dependency.from_hash(_1) }
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
# @param sources [Array<DiverDown::Definition::Source>]
|
17
|
+
# @return [DiverDown::Definition::Source]
|
18
|
+
def self.combine(*sources)
|
19
|
+
raise ArgumentError, 'sources are empty' if sources.empty?
|
20
|
+
|
21
|
+
unique_sources = sources.map(&:source_name).uniq
|
22
|
+
raise ArgumentError, "sources are unmatched. (#{unique_sources})" unless unique_sources.length == 1
|
23
|
+
|
24
|
+
all_dependencies = sources.flat_map(&:dependencies)
|
25
|
+
|
26
|
+
new(
|
27
|
+
source_name: unique_sources[0],
|
28
|
+
dependencies: DiverDown::Definition::Dependency.combine(*all_dependencies)
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
attr_reader :source_name
|
33
|
+
|
34
|
+
# @param source_name [String] filename of the source file
|
35
|
+
# @param dependencies [Array<DiverDown::Definition::Dependency>]
|
36
|
+
def initialize(source_name:, dependencies: [])
|
37
|
+
@source_name = source_name
|
38
|
+
@dependency_map = dependencies.map { [_1.source_name, _1] }.to_h
|
39
|
+
end
|
40
|
+
|
41
|
+
# @param source [String]
|
42
|
+
# @return [DiverDown::Definition::Dependency, nil] return nil if source is self.source
|
43
|
+
def find_or_build_dependency(dependency_source_name)
|
44
|
+
return if source_name == dependency_source_name
|
45
|
+
|
46
|
+
@dependency_map[dependency_source_name] ||= DiverDown::Definition::Dependency.new(source_name: dependency_source_name)
|
47
|
+
end
|
48
|
+
|
49
|
+
# @param dependency_source_name [String]
|
50
|
+
# @return [DiverDown::Definition::Dependency, nil]
|
51
|
+
def dependency(dependency_source_name)
|
52
|
+
@dependency_map[dependency_source_name]
|
53
|
+
end
|
54
|
+
|
55
|
+
# @return [Array<DiverDown::Definition::Dependency>]
|
56
|
+
def dependencies
|
57
|
+
@dependency_map.values.sort
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return [Hash]
|
61
|
+
def to_h
|
62
|
+
{
|
63
|
+
source_name:,
|
64
|
+
dependencies: dependencies.map(&:to_h),
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
# @param other [DiverDown::Definition::Source]
|
69
|
+
# @return [Integer]
|
70
|
+
def <=>(other)
|
71
|
+
source_name <=> other.source_name
|
72
|
+
end
|
73
|
+
|
74
|
+
# @param other [Object, DiverDown::Definition::Source]
|
75
|
+
# @return [Boolean]
|
76
|
+
def ==(other)
|
77
|
+
other.is_a?(self.class) &&
|
78
|
+
source_name == other.source_name &&
|
79
|
+
dependencies == other.dependencies
|
80
|
+
end
|
81
|
+
alias eq? ==
|
82
|
+
alias eql? ==
|
83
|
+
|
84
|
+
# @return [Integer]
|
85
|
+
def hash
|
86
|
+
[self.class, source_name, dependencies].hash
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
module DiverDown
|
6
|
+
class Definition
|
7
|
+
require 'diver_down/definition/source'
|
8
|
+
require 'diver_down/definition/dependency'
|
9
|
+
require 'diver_down/definition/method_id'
|
10
|
+
|
11
|
+
# @param hash [Hash]
|
12
|
+
# @return [DiverDown::Definition]
|
13
|
+
def self.from_hash(hash)
|
14
|
+
new(
|
15
|
+
title: hash[:title] || hash['title'] || '',
|
16
|
+
definition_group: hash[:definition_group] || hash['definition_group'],
|
17
|
+
sources: (hash[:sources] || hash['sources'] || []).map do |source_hash|
|
18
|
+
DiverDown::Definition::Source.from_hash(source_hash)
|
19
|
+
end
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
# @param definition_group [String, nil]
|
24
|
+
# @param title [String]
|
25
|
+
# @param definitions [Array<DiverDown::Definition>]
|
26
|
+
def self.combine(definition_group:, title:, definitions: [])
|
27
|
+
all_sources = definitions.flat_map(&:sources)
|
28
|
+
|
29
|
+
sources = all_sources.group_by(&:source_name).map do |_, same_sources|
|
30
|
+
DiverDown::Definition::Source.combine(*same_sources)
|
31
|
+
end
|
32
|
+
|
33
|
+
new(
|
34
|
+
definition_group:,
|
35
|
+
title:,
|
36
|
+
sources:
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
attr_reader :definition_group, :title
|
41
|
+
|
42
|
+
# ID issued when stored in DiverDown::Web::DefinitionStore
|
43
|
+
# I want to manage ID in DiverDown::Web::DefinitionStore, but for performance reasons, I have to set Definition#id to determine its identity
|
44
|
+
# because naive comparing the identity by instance variables of Definitions is slow.
|
45
|
+
# @attr_accessor [Integer]
|
46
|
+
attr_accessor :store_id
|
47
|
+
|
48
|
+
# @param title [String]
|
49
|
+
# @param sources [Array<DiverDown::Definition::Source>]
|
50
|
+
def initialize(definition_group: nil, title: '', sources: [])
|
51
|
+
@definition_group = definition_group
|
52
|
+
@title = title
|
53
|
+
@source_map = sources.map { [_1.source_name, _1] }.to_h
|
54
|
+
end
|
55
|
+
|
56
|
+
# @param source_name [String]
|
57
|
+
# @return [DiverDown::Definition::Source]
|
58
|
+
def find_or_build_source(source_name)
|
59
|
+
@source_map[source_name] ||= DiverDown::Definition::Source.new(source_name:)
|
60
|
+
end
|
61
|
+
|
62
|
+
# @param source_name [String]
|
63
|
+
# @return [DiverDown::Definition::Source, nil]
|
64
|
+
def source(source_name)
|
65
|
+
@source_map[source_name]
|
66
|
+
end
|
67
|
+
|
68
|
+
# @return [Array<DiverDown::Definition::Source>]
|
69
|
+
def sources
|
70
|
+
@source_map.values.sort
|
71
|
+
end
|
72
|
+
|
73
|
+
# @return [String]
|
74
|
+
def to_h
|
75
|
+
{
|
76
|
+
definition_group:,
|
77
|
+
title:,
|
78
|
+
sources: sources.map(&:to_h),
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
# @return [String]
|
83
|
+
def to_msgpack
|
84
|
+
MessagePack.pack(to_h)
|
85
|
+
end
|
86
|
+
|
87
|
+
# @param other [Object, DiverDown::Definition::Source]
|
88
|
+
# @return [Boolean]
|
89
|
+
def ==(other)
|
90
|
+
if store_id
|
91
|
+
other.is_a?(self.class) &&
|
92
|
+
store_id == other.store_id
|
93
|
+
else
|
94
|
+
other.is_a?(self.class) &&
|
95
|
+
definition_group == other.definition_group &&
|
96
|
+
title == other.title &&
|
97
|
+
sources.sort == other.sources.sort
|
98
|
+
end
|
99
|
+
end
|
100
|
+
alias eq? ==
|
101
|
+
alias eql? ==
|
102
|
+
|
103
|
+
# @return [Integer]
|
104
|
+
def hash
|
105
|
+
if store_id
|
106
|
+
[self.class, store_id].hash
|
107
|
+
else
|
108
|
+
[self.class, definition_group, title, sources].hash
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DiverDown
|
4
|
+
module Helper
|
5
|
+
CLASS_NAME_QUERY = Module.method(:name).unbind.freeze
|
6
|
+
|
7
|
+
INSTANCE_CLASS_QUERY = Class.instance_method(:class).freeze
|
8
|
+
private_constant :INSTANCE_CLASS_QUERY
|
9
|
+
|
10
|
+
# @param obj [Object, String, Module, Class]
|
11
|
+
# @return [String, nil] if obj is anonymous module, return nil
|
12
|
+
def self.normalize_module_name(obj)
|
13
|
+
if String === obj
|
14
|
+
obj
|
15
|
+
else
|
16
|
+
mod = resolve_module(obj)
|
17
|
+
|
18
|
+
# Do not call the original method as much as possible
|
19
|
+
CLASS_NAME_QUERY.bind_call(mod) ||
|
20
|
+
(mod.name if mod.respond_to?(:name))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# @note The object passed as an argument may be a Proxied BasicObject.
|
25
|
+
# For example, the DSL of FactoryBot's factory will add a new method in response to the invoked method name,
|
26
|
+
# so passed as an argument methods are not called within this method.
|
27
|
+
#
|
28
|
+
# @return [Module, Class]
|
29
|
+
def self.resolve_module(obj)
|
30
|
+
if module?(obj) # Do not call method of this
|
31
|
+
resolve_singleton_class(obj)
|
32
|
+
else
|
33
|
+
k = INSTANCE_CLASS_QUERY.bind_call(obj)
|
34
|
+
resolve_singleton_class(k)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# @param obj [Module, Class]
|
39
|
+
# @return [Module, Class]
|
40
|
+
def self.resolve_singleton_class(obj)
|
41
|
+
if obj.singleton_class?
|
42
|
+
obj.attached_object
|
43
|
+
else
|
44
|
+
obj
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# @param obj [Object]
|
49
|
+
# @return [Boolean]
|
50
|
+
def self.module?(obj)
|
51
|
+
Module === obj
|
52
|
+
end
|
53
|
+
|
54
|
+
# @param obj [Object]
|
55
|
+
# @return [Boolean]
|
56
|
+
def self.class?(obj)
|
57
|
+
Class === obj
|
58
|
+
end
|
59
|
+
|
60
|
+
# @param str [String]
|
61
|
+
# @return [Module]
|
62
|
+
def self.constantize(str)
|
63
|
+
::ActiveSupport::Inflector.constantize(str)
|
64
|
+
end
|
65
|
+
|
66
|
+
# @param hash [Hash]
|
67
|
+
# @return [Hash]
|
68
|
+
def self.deep_symbolize_keys(object)
|
69
|
+
case object
|
70
|
+
when Hash
|
71
|
+
object.each_with_object({}) do |(key, value), memo|
|
72
|
+
memo[key.to_sym] = deep_symbolize_keys(value)
|
73
|
+
end
|
74
|
+
when Array
|
75
|
+
object.map { deep_symbolize_keys(_1) }
|
76
|
+
else
|
77
|
+
object
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|