library_tree 1.0.0

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.
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LibraryTree
4
+ # Represents a tracked module and its relationships
5
+ # Parents are modules that include this module
6
+ # Children are modules that this module is included into.
7
+ # In Ruby, when A includes B, A depends on B.
8
+ # We will model edges parent -> child as: including_module -> included_module
9
+ class Node
10
+ # @return [String] the name of the underlying Ruby module being tracked
11
+ # @return [Array<LibraryTree::Node>] parents of this node (including modules)
12
+ # @return [Array<LibraryTree::Node>] children of this node (included modules)
13
+ attr_reader :name, :parents, :children
14
+
15
+ # Create a node for a Ruby module
16
+ # @param mod [Module] the module to track
17
+ def initialize(mod)
18
+ @name = mod.name || mod.inspect
19
+ @parents = [] # Array<Node>
20
+ @children = [] # Array<Node>
21
+ end
22
+
23
+ # Add a child node (an included module)
24
+ # @param child [LibraryTree::Node]
25
+ # @return [void]
26
+ def add_child(child)
27
+ return if @children.include?(child)
28
+ @children << child
29
+ end
30
+
31
+ # Add a parent node (a module that includes this module)
32
+ # @param parent [LibraryTree::Node]
33
+ # @return [void]
34
+ def add_parent(parent)
35
+ return if @parents.include?(parent)
36
+ @parents << parent
37
+ end
38
+
39
+ # Whether this node has no parents (it is a root)
40
+ # @return [Boolean]
41
+ def root?
42
+ @parents.empty?
43
+ end
44
+
45
+ # Return a Hash representation of this node and its children
46
+ # @param seen [Hash{Integer=>true}] a map of visited node object_ids to avoid cycles
47
+ # @return [Hash{Symbol=>Object}] a tree structure with :name and :children
48
+ def to_h(seen = {})
49
+ return {name: name} if seen[object_id]
50
+ seen[object_id] = true
51
+ {
52
+ name: name,
53
+ children: children.map { |c| c.to_h(seen) },
54
+ }
55
+ end
56
+
57
+ # Render a tree view starting from this node.
58
+ # @param indent [Integer] number of levels to indent (default: 0)
59
+ # @param seen [Hash{Integer=>true}] a map of visited node object_ids to avoid cycles
60
+ # @return [String] a multi-line string representing the subtree
61
+ def render(indent = 0, seen = {})
62
+ return "#{" " * indent}* #{name} (…cycle…)\n" if seen[object_id]
63
+ seen[object_id] = true
64
+ out = "#{" " * indent}* #{name}\n"
65
+ children.each do |child|
66
+ out += child.render(indent + 1, seen.dup)
67
+ end
68
+ out
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "node"
4
+
5
+ module LibraryTree
6
+ # Thread-safe registry of tracked modules and their relationships
7
+ module Registry
8
+ # Return the registry mutex
9
+ # @return [Mutex]
10
+ def mutex
11
+ @mutex ||= Mutex.new # rubocop:disable ThreadSafety/ClassInstanceVariable
12
+ end
13
+ module_function :mutex
14
+
15
+ # Internal storage of nodes keyed by Module
16
+ # @return [Hash{Module=>LibraryTree::Node}]
17
+ def nodes
18
+ @nodes ||= {} # rubocop:disable ThreadSafety/ClassInstanceVariable
19
+ end
20
+ module_function :nodes
21
+
22
+ # Check if a module is being tracked
23
+ # @param mod [Module]
24
+ # @return [Boolean]
25
+ def tracked?(mod)
26
+ nodes.key?(mod)
27
+ end
28
+ module_function :tracked?
29
+
30
+ # Ensure a Node exists for the given module, creating it if needed
31
+ # @param mod [Module]
32
+ # @return [LibraryTree::Node]
33
+ def ensure_node(mod)
34
+ nodes[mod] ||= Node.new(mod)
35
+ end
36
+ module_function :ensure_node
37
+
38
+ # Mark a module as tracked
39
+ # @param mod [Module]
40
+ # @return [void]
41
+ def mark_tracked(mod)
42
+ mutex.synchronize do
43
+ ensure_node(mod)
44
+ end
45
+ end
46
+ module_function :mark_tracked
47
+
48
+ # Create a link parent -> child (including_module -> included_module)
49
+ # Ruby 1.9.2+ compatible: use positional parameters instead of keyword arguments
50
+ # @param parent [Module] including module
51
+ # @param child [Module] included module
52
+ # @return [void]
53
+ def link(parent, child)
54
+ mutex.synchronize do
55
+ pnode = ensure_node(parent)
56
+ cnode = ensure_node(child)
57
+ pnode.add_child(cnode)
58
+ cnode.add_parent(pnode)
59
+ end
60
+ end
61
+ module_function :link
62
+
63
+ # Return all root nodes
64
+ # @return [Array<LibraryTree::Node>]
65
+ def roots
66
+ mutex.synchronize do
67
+ nodes.values.select(&:root?)
68
+ end
69
+ end
70
+ module_function :roots
71
+
72
+ # Return all known nodes
73
+ # @return [Array<LibraryTree::Node>]
74
+ def all
75
+ mutex.synchronize do
76
+ nodes.values
77
+ end
78
+ end
79
+ module_function :all
80
+
81
+ # Reset the registry to an empty state
82
+ # @return [void]
83
+ def reset!
84
+ mutex.synchronize do
85
+ @nodes = {}
86
+ end
87
+ end
88
+ module_function :reset!
89
+ end
90
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LibraryTree
4
+ # Contains version information for LibraryTree
5
+ module Version
6
+ # The current version of the LibraryTree gem
7
+ VERSION = "1.0.0"
8
+ end
9
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "registry"
4
+
5
+ module LibraryTree
6
+ # Public API module to enable watching on modules that include it.
7
+ # When a watched module is included into another watched module, a link is recorded.
8
+ module Watcher
9
+ module ClassMethods
10
+ # Called whenever the watched module (`self`) is included into another module (`base`).
11
+ # @param base [Module] the including module
12
+ # @return [void]
13
+ def included(base)
14
+ super
15
+ # Link whenever the included module (self) is tracked, regardless of whether the includer (base) is tracked.
16
+ if LibraryTree::Registry.tracked?(self)
17
+ LibraryTree::Registry.link(base, self)
18
+ end
19
+ end
20
+ end
21
+
22
+ class << self
23
+ # Mark the including module as tracked and extend it to observe future inclusions
24
+ # @param mod [Module]
25
+ # @return [void]
26
+ def included(mod)
27
+ # Mark the module itself as tracked
28
+ LibraryTree::Registry.mark_tracked(mod)
29
+ # Ensure we observe future inclusions of this module into other modules
30
+ mod.extend(ClassMethods)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "library_tree/version"
4
+ require_relative "library_tree/watcher"
5
+ require_relative "library_tree/registry"
6
+
7
+ module LibraryTree
8
+ # Main namespace for LibraryTree. Provides convenience accessors to the registry
9
+ # and a renderer for the forest of tracked modules.
10
+ # @see LibraryTree::Watcher
11
+ # @see LibraryTree::Registry
12
+ # @see LibraryTree::Node
13
+ # @see LibraryTree::Version
14
+ # Generic error class for LibraryTree
15
+ class Error < StandardError; end
16
+
17
+ class << self
18
+ # Return array of root nodes (modules without parents)
19
+ # @return [Array<LibraryTree::Node>] an array of nodes that have no parents
20
+ def roots
21
+ Registry.roots
22
+ end
23
+
24
+ # Return all nodes currently tracked in the registry
25
+ # @return [Array<LibraryTree::Node>] all known nodes
26
+ def nodes
27
+ Registry.all
28
+ end
29
+
30
+ # Render the forest as a string
31
+ # @return [String] a textual tree representation of all roots and their descendants
32
+ def render
33
+ roots.map { |r| r.render }.join
34
+ end
35
+
36
+ # Testing/utility: reset the registry
37
+ # @return [void]
38
+ def reset!
39
+ Registry.reset!
40
+ end
41
+ end
42
+ end
data.tar.gz.sig ADDED
Binary file
metadata ADDED
@@ -0,0 +1,234 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: library_tree
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Peter H. Boling
8
+ bindir: exe
9
+ cert_chain:
10
+ - |
11
+ -----BEGIN CERTIFICATE-----
12
+ MIIEgDCCAuigAwIBAgIBATANBgkqhkiG9w0BAQsFADBDMRUwEwYDVQQDDAxwZXRl
13
+ ci5ib2xpbmcxFTATBgoJkiaJk/IsZAEZFgVnbWFpbDETMBEGCgmSJomT8ixkARkW
14
+ A2NvbTAeFw0yNTA1MDQxNTMzMDlaFw00NTA0MjkxNTMzMDlaMEMxFTATBgNVBAMM
15
+ DHBldGVyLmJvbGluZzEVMBMGCgmSJomT8ixkARkWBWdtYWlsMRMwEQYKCZImiZPy
16
+ LGQBGRYDY29tMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAruUoo0WA
17
+ uoNuq6puKWYeRYiZekz/nsDeK5x/0IEirzcCEvaHr3Bmz7rjo1I6On3gGKmiZs61
18
+ LRmQ3oxy77ydmkGTXBjruJB+pQEn7UfLSgQ0xa1/X3kdBZt6RmabFlBxnHkoaGY5
19
+ mZuZ5+Z7walmv6sFD9ajhzj+oIgwWfnEHkXYTR8I6VLN7MRRKGMPoZ/yvOmxb2DN
20
+ coEEHWKO9CvgYpW7asIihl/9GMpKiRkcYPm9dGQzZc6uTwom1COfW0+ZOFrDVBuV
21
+ FMQRPswZcY4Wlq0uEBLPU7hxnCL9nKK6Y9IhdDcz1mY6HZ91WImNslOSI0S8hRpj
22
+ yGOWxQIhBT3fqCBlRIqFQBudrnD9jSNpSGsFvbEijd5ns7Z9ZMehXkXDycpGAUj1
23
+ to/5cuTWWw1JqUWrKJYoifnVhtE1o1DZ+LkPtWxHtz5kjDG/zR3MG0Ula0UOavlD
24
+ qbnbcXPBnwXtTFeZ3C+yrWpE4pGnl3yGkZj9SMTlo9qnTMiPmuWKQDatAgMBAAGj
25
+ fzB9MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQWBBQE8uWvNbPVNRXZ
26
+ HlgPbc2PCzC4bjAhBgNVHREEGjAYgRZwZXRlci5ib2xpbmdAZ21haWwuY29tMCEG
27
+ A1UdEgQaMBiBFnBldGVyLmJvbGluZ0BnbWFpbC5jb20wDQYJKoZIhvcNAQELBQAD
28
+ ggGBAJbnUwfJQFPkBgH9cL7hoBfRtmWiCvdqdjeTmi04u8zVNCUox0A4gT982DE9
29
+ wmuN12LpdajxZONqbXuzZvc+nb0StFwmFYZG6iDwaf4BPywm2e/Vmq0YG45vZXGR
30
+ L8yMDSK1cQXjmA+ZBKOHKWavxP6Vp7lWvjAhz8RFwqF9GuNIdhv9NpnCAWcMZtpm
31
+ GUPyIWw/Cw/2wZp74QzZj6Npx+LdXoLTF1HMSJXZ7/pkxLCsB8m4EFVdb/IrW/0k
32
+ kNSfjtAfBHO8nLGuqQZVH9IBD1i9K6aSs7pT6TW8itXUIlkIUI2tg5YzW6OFfPzq
33
+ QekSkX3lZfY+HTSp/o+YvKkqWLUV7PQ7xh1ZYDtocpaHwgxe/j3bBqHE+CUPH2vA
34
+ 0V/FwdTRWcwsjVoOJTrYcff8pBZ8r2MvtAc54xfnnhGFzeRHfcltobgFxkAXdE6p
35
+ DVjBtqT23eugOqQ73umLcYDZkc36vnqGxUBSsXrzY9pzV5gGr2I8YUxMqf6ATrZt
36
+ L9nRqA==
37
+ -----END CERTIFICATE-----
38
+ date: 2025-08-12 00:00:00.000000000 Z
39
+ dependencies:
40
+ - !ruby/object:Gem::Dependency
41
+ name: stone_checksums
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: appraisal2
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '3.0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '3.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rspec
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '3.13'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '3.13'
82
+ - !ruby/object:Gem::Dependency
83
+ name: rspec-block_is_expected
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '1.0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '1.0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: rspec_junit_formatter
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '0.6'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '0.6'
110
+ - !ruby/object:Gem::Dependency
111
+ name: rspec-stubbed_env
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '1.0'
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '1.0'
124
+ - !ruby/object:Gem::Dependency
125
+ name: silent_stream
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '1.0'
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: 1.0.11
134
+ type: :development
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - "~>"
139
+ - !ruby/object:Gem::Version
140
+ version: '1.0'
141
+ - - ">="
142
+ - !ruby/object:Gem::Version
143
+ version: 1.0.11
144
+ - !ruby/object:Gem::Dependency
145
+ name: rake
146
+ requirement: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - "~>"
149
+ - !ruby/object:Gem::Version
150
+ version: '13.0'
151
+ type: :development
152
+ prerelease: false
153
+ version_requirements: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - "~>"
156
+ - !ruby/object:Gem::Version
157
+ version: '13.0'
158
+ description: Builds a tree of module inclusion of a target module into other modules
159
+ email:
160
+ - floss@galtzo.com
161
+ executables: []
162
+ extensions: []
163
+ extra_rdoc_files:
164
+ - CHANGELOG.md
165
+ - CITATION.cff
166
+ - CODE_OF_CONDUCT.md
167
+ - CONTRIBUTING.md
168
+ - LICENSE.txt
169
+ - README.md
170
+ - REEK
171
+ - RUBOCOP.md
172
+ - SECURITY.md
173
+ files:
174
+ - CHANGELOG.md
175
+ - CITATION.cff
176
+ - CODE_OF_CONDUCT.md
177
+ - CONTRIBUTING.md
178
+ - LICENSE.txt
179
+ - README.md
180
+ - REEK
181
+ - RUBOCOP.md
182
+ - SECURITY.md
183
+ - lib/library_tree.rb
184
+ - lib/library_tree/node.rb
185
+ - lib/library_tree/registry.rb
186
+ - lib/library_tree/version.rb
187
+ - lib/library_tree/watcher.rb
188
+ homepage: https://github.com/galtzo-floss/library_tree
189
+ licenses:
190
+ - MIT
191
+ metadata:
192
+ homepage_uri: https://library-tree.galtzo.com/
193
+ source_code_uri: https://github.com/galtzo-floss/library_tree/tree/v1.0.0
194
+ changelog_uri: https://github.com/galtzo-floss/library_tree/blob/v1.0.0/CHANGELOG.md
195
+ bug_tracker_uri: https://github.com/galtzo-floss/library_tree/issues
196
+ documentation_uri: https://www.rubydoc.info/gems/library_tree/1.0.0
197
+ funding_uri: https://github.com/sponsors/pboling
198
+ wiki_uri: https://github.com/galtzo-floss/library_tree/wiki
199
+ news_uri: https://www.railsbling.com/tags/library_tree
200
+ discord_uri: https://discord.gg/3qme4XHNKN
201
+ rubygems_mfa_required: 'true'
202
+ rdoc_options:
203
+ - "--title"
204
+ - library_tree - Six-degrees of (Kevin Bacon's?) Module
205
+ - "--main"
206
+ - CHANGELOG.md
207
+ - CITATION.cff
208
+ - CODE_OF_CONDUCT.md
209
+ - CONTRIBUTING.md
210
+ - LICENSE.txt
211
+ - README.md
212
+ - REEK
213
+ - RUBOCOP.md
214
+ - SECURITY.md
215
+ - "--line-numbers"
216
+ - "--inline-source"
217
+ - "--quiet"
218
+ require_paths:
219
+ - lib
220
+ required_ruby_version: !ruby/object:Gem::Requirement
221
+ requirements:
222
+ - - ">="
223
+ - !ruby/object:Gem::Version
224
+ version: 1.9.2
225
+ required_rubygems_version: !ruby/object:Gem::Requirement
226
+ requirements:
227
+ - - ">="
228
+ - !ruby/object:Gem::Version
229
+ version: '0'
230
+ requirements: []
231
+ rubygems_version: 3.7.1
232
+ specification_version: 4
233
+ summary: Six-degrees of (Kevin Bacon's?) Module
234
+ test_files: []
metadata.gz.sig ADDED
@@ -0,0 +1,2 @@
1
+ ��6���,�F�&_��F�a��|T }Soણ��� E7xj7��B_F
2
+ S>��>ք��S�`#Ɠ��=IU$6����h��56�rT��r9 �r�oR��̜Yc��A��cm��2f�C.������fH��@���l}~-wٖ䟌&�^����M� �\H�����x�̠rD�������L�%�y�