diver_down 0.0.1.alpha1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +132 -0
  5. data/exe/diver_down_web +55 -0
  6. data/lib/diver_down/definition/dependency.rb +107 -0
  7. data/lib/diver_down/definition/method_id.rb +83 -0
  8. data/lib/diver_down/definition/source.rb +90 -0
  9. data/lib/diver_down/definition.rb +112 -0
  10. data/lib/diver_down/helper.rb +81 -0
  11. data/lib/diver_down/trace/call_stack.rb +45 -0
  12. data/lib/diver_down/trace/ignored_method_ids.rb +136 -0
  13. data/lib/diver_down/trace/module_set/array_module_set.rb +31 -0
  14. data/lib/diver_down/trace/module_set/const_source_location_module_set.rb +28 -0
  15. data/lib/diver_down/trace/module_set.rb +78 -0
  16. data/lib/diver_down/trace/redefine_ruby_methods.rb +64 -0
  17. data/lib/diver_down/trace/tracer/session.rb +121 -0
  18. data/lib/diver_down/trace/tracer.rb +96 -0
  19. data/lib/diver_down/trace.rb +27 -0
  20. data/lib/diver_down/version.rb +5 -0
  21. data/lib/diver_down/web/action.rb +344 -0
  22. data/lib/diver_down/web/bit_id.rb +41 -0
  23. data/lib/diver_down/web/definition_enumerator.rb +54 -0
  24. data/lib/diver_down/web/definition_loader.rb +37 -0
  25. data/lib/diver_down/web/definition_store.rb +89 -0
  26. data/lib/diver_down/web/definition_to_dot.rb +399 -0
  27. data/lib/diver_down/web/dev_server_middleware.rb +72 -0
  28. data/lib/diver_down/web/indented_string_io.rb +59 -0
  29. data/lib/diver_down/web/module_store.rb +59 -0
  30. data/lib/diver_down/web.rb +101 -0
  31. data/lib/diver_down-trace.rb +4 -0
  32. data/lib/diver_down-web.rb +4 -0
  33. data/lib/diver_down.rb +14 -0
  34. data/web/assets/CjLq7LhZ.css +1 -0
  35. data/web/assets/bundle.js +978 -0
  36. data/web/index.html +13 -0
  37. 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
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2024-03-05
4
+
5
+ - Initial release
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).
@@ -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