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.
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