boxwerk 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -3
- data/README.md +70 -226
- data/exe/boxwerk +0 -8
- data/lib/boxwerk/cli.rb +12 -44
- data/lib/boxwerk/graph.rb +8 -66
- data/lib/boxwerk/loader.rb +33 -161
- data/lib/boxwerk/package.rb +5 -14
- data/lib/boxwerk/registry.rb +1 -12
- data/lib/boxwerk/setup.rb +5 -26
- data/lib/boxwerk/version.rb +1 -1
- data/lib/boxwerk.rb +0 -5
- metadata +4 -17
- data/example/Gemfile +0 -6
- data/example/Gemfile.lock +0 -66
- data/example/README.md +0 -130
- data/example/app.rb +0 -101
- data/example/package.yml +0 -6
- data/example/packages/finance/lib/invoice.rb +0 -51
- data/example/packages/finance/lib/tax_calculator.rb +0 -26
- data/example/packages/finance/package.yml +0 -10
- data/example/packages/util/lib/calculator.rb +0 -21
- data/example/packages/util/lib/geometry.rb +0 -26
- data/example/packages/util/package.yml +0 -5
- data/sig/boxwerk.rbs +0 -4
data/lib/boxwerk/graph.rb
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Boxwerk
|
|
4
|
-
#
|
|
4
|
+
# Graph builds a directed acyclic graph (DAG) of package dependencies.
|
|
5
|
+
# Validates no circular dependencies and provides topological ordering for boot sequence.
|
|
5
6
|
class Graph
|
|
6
7
|
attr_reader :packages, :root
|
|
7
8
|
|
|
@@ -10,102 +11,43 @@ module Boxwerk
|
|
|
10
11
|
@packages = {}
|
|
11
12
|
@root = load_package('root', root_path)
|
|
12
13
|
resolve_dependencies(@root, [])
|
|
13
|
-
validate!
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
# Returns packages in topological order (leaves first)
|
|
17
16
|
def topological_order
|
|
18
17
|
visited = {}
|
|
19
18
|
order = []
|
|
20
|
-
|
|
21
|
-
@packages.each_value { |package| visit(package, visited, order, []) }
|
|
22
|
-
|
|
19
|
+
@packages.each_value { |pkg| visit(pkg, visited, order, []) }
|
|
23
20
|
order
|
|
24
21
|
end
|
|
25
22
|
|
|
26
23
|
private
|
|
27
24
|
|
|
28
|
-
# Validate that the graph is acyclic
|
|
29
|
-
def validate!
|
|
30
|
-
visited = {}
|
|
31
|
-
rec_stack = {}
|
|
32
|
-
|
|
33
|
-
@packages.each_value do |package|
|
|
34
|
-
if has_cycle?(package, visited, rec_stack, [])
|
|
35
|
-
raise 'Circular dependency detected in package graph'
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
true
|
|
40
|
-
end
|
|
41
|
-
|
|
42
25
|
def load_package(name, path)
|
|
43
26
|
return @packages[name] if @packages[name]
|
|
44
27
|
|
|
45
|
-
|
|
46
|
-
@packages[name] = package
|
|
47
|
-
package
|
|
28
|
+
@packages[name] = Package.new(name, path)
|
|
48
29
|
end
|
|
49
30
|
|
|
50
31
|
def resolve_dependencies(package, path)
|
|
51
|
-
if path.include?(package.name)
|
|
52
|
-
cycle = (path + [package.name]).join(' -> ')
|
|
53
|
-
raise "Circular dependency detected: #{cycle}"
|
|
54
|
-
end
|
|
32
|
+
raise "Circular dependency: #{(path + [package.name]).join(' -> ')}" if path.include?(package.name)
|
|
55
33
|
|
|
56
34
|
package.dependencies.each do |dep_path|
|
|
57
|
-
dep_name = File.basename(dep_path)
|
|
58
35
|
full_path = File.join(@root_path, dep_path)
|
|
36
|
+
raise "Package not found: #{dep_path}" unless File.directory?(full_path)
|
|
59
37
|
|
|
60
|
-
|
|
61
|
-
raise "Package not found: #{dep_path} (expected at #{full_path})"
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
dep_package = load_package(dep_name, full_path)
|
|
65
|
-
resolve_dependencies(dep_package, path + [package.name])
|
|
38
|
+
resolve_dependencies(load_package(File.basename(dep_path), full_path), path + [package.name])
|
|
66
39
|
end
|
|
67
40
|
end
|
|
68
41
|
|
|
69
|
-
# DFS for topological sort
|
|
70
42
|
def visit(package, visited, order, path)
|
|
71
43
|
return if visited[package.name]
|
|
72
44
|
|
|
73
|
-
if path.include?(package.name)
|
|
74
|
-
raise "Circular dependency: #{(path + [package.name]).join(' -> ')}"
|
|
75
|
-
end
|
|
76
|
-
|
|
77
45
|
visited[package.name] = true
|
|
78
|
-
|
|
79
46
|
package.dependencies.each do |dep_path|
|
|
80
|
-
|
|
81
|
-
dep_package = @packages[dep_name]
|
|
47
|
+
dep_package = @packages[File.basename(dep_path)]
|
|
82
48
|
visit(dep_package, visited, order, path + [package.name]) if dep_package
|
|
83
49
|
end
|
|
84
|
-
|
|
85
50
|
order << package
|
|
86
51
|
end
|
|
87
|
-
|
|
88
|
-
# Cycle detection
|
|
89
|
-
def has_cycle?(package, visited, rec_stack, path)
|
|
90
|
-
return false if visited[package.name]
|
|
91
|
-
|
|
92
|
-
return true if rec_stack[package.name]
|
|
93
|
-
|
|
94
|
-
visited[package.name] = true
|
|
95
|
-
rec_stack[package.name] = true
|
|
96
|
-
|
|
97
|
-
package.dependencies.each do |dep_path|
|
|
98
|
-
dep_name = File.basename(dep_path)
|
|
99
|
-
dep_package = @packages[dep_name]
|
|
100
|
-
|
|
101
|
-
if dep_package &&
|
|
102
|
-
has_cycle?(dep_package, visited, rec_stack, path + [package.name])
|
|
103
|
-
return true
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
rec_stack[package.name] = false
|
|
108
|
-
false
|
|
109
|
-
end
|
|
110
52
|
end
|
|
111
53
|
end
|
data/lib/boxwerk/loader.rb
CHANGED
|
@@ -1,45 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Boxwerk
|
|
4
|
-
# Loader
|
|
4
|
+
# Loader creates isolated Ruby::Box instances for packages and wires imports.
|
|
5
|
+
# Lazily loads exports using Zeitwerk naming conventions and injects constants.
|
|
5
6
|
class Loader
|
|
6
7
|
class << self
|
|
7
|
-
# Boot all packages in topological order
|
|
8
|
-
# @param graph [Boxwerk::Graph] The dependency graph
|
|
9
|
-
# @param registry [Boxwerk::Registry] The registry instance
|
|
10
8
|
def boot_all(graph, registry)
|
|
11
9
|
order = graph.topological_order
|
|
12
10
|
|
|
13
11
|
order.each { |package| boot(package, graph, registry) }
|
|
14
12
|
end
|
|
15
13
|
|
|
16
|
-
# Boot a single package
|
|
17
|
-
# @param package [Boxwerk::Package] The package to boot
|
|
18
|
-
# @param graph [Boxwerk::Graph] The dependency graph
|
|
19
|
-
# @param registry [Boxwerk::Registry] The registry instance
|
|
20
14
|
def boot(package, graph, registry)
|
|
21
15
|
return package if package.booted?
|
|
22
16
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
raise 'Boxwerk requires RUBY_BOX=1 environment variable to enable Ruby::Box support'
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# Check if we have Ruby::Box support
|
|
29
|
-
unless defined?(Ruby::Box)
|
|
30
|
-
raise 'Boxwerk requires Ruby 4.0+ with Ruby::Box support'
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# All packages (including root) get their own isolated boxes
|
|
34
|
-
box = Ruby::Box.new
|
|
35
|
-
|
|
36
|
-
# Store the box reference first so it's available during wiring
|
|
37
|
-
package.box = box
|
|
38
|
-
|
|
39
|
-
# Wire imports based on configuration (exports loaded lazily on-demand)
|
|
40
|
-
wire_imports(box, package, graph)
|
|
41
|
-
|
|
42
|
-
# Register in the registry
|
|
17
|
+
package.box = Ruby::Box.new
|
|
18
|
+
wire_imports(package.box, package, graph)
|
|
43
19
|
registry.register(package.name, package)
|
|
44
20
|
|
|
45
21
|
package
|
|
@@ -47,10 +23,6 @@ module Boxwerk
|
|
|
47
23
|
|
|
48
24
|
private
|
|
49
25
|
|
|
50
|
-
# Load a specific exported constant from package's lib directory using Zeitwerk conventions
|
|
51
|
-
# This enforces strict isolation - only requested exports are loaded, lazily
|
|
52
|
-
# @param package [Boxwerk::Package] The package
|
|
53
|
-
# @param const_name [String] The constant name to load
|
|
54
26
|
def load_export(package, const_name)
|
|
55
27
|
# Skip if already loaded (cached)
|
|
56
28
|
return if package.loaded_exports.key?(const_name)
|
|
@@ -58,91 +30,33 @@ module Boxwerk
|
|
|
58
30
|
lib_path = File.join(package.path, 'lib')
|
|
59
31
|
return unless File.directory?(lib_path)
|
|
60
32
|
|
|
61
|
-
# Find the file path for this constant using Zeitwerk conventions
|
|
62
33
|
file_path = find_file_for_constant(lib_path, const_name)
|
|
63
|
-
|
|
64
34
|
unless file_path
|
|
65
|
-
raise "Cannot find file for exported constant '#{const_name}' in package '#{package.name}'
|
|
35
|
+
raise "Cannot find file for exported constant '#{const_name}' in package '#{package.name}'"
|
|
66
36
|
end
|
|
67
37
|
|
|
68
|
-
# Load the file in the package's box
|
|
69
38
|
package.box.require(file_path)
|
|
70
|
-
|
|
71
|
-
# Cache the mapping AFTER successful load
|
|
72
39
|
package.loaded_exports[const_name] = file_path
|
|
73
40
|
end
|
|
74
41
|
|
|
75
|
-
# Load only exported constants from package's lib directory using Zeitwerk conventions
|
|
76
|
-
# @param box [Ruby::Box] The box instance
|
|
77
|
-
# @param package [Boxwerk::Package] The package
|
|
78
|
-
def load_exports(box, package)
|
|
79
|
-
lib_path = File.join(package.path, 'lib')
|
|
80
|
-
return unless File.directory?(lib_path)
|
|
81
|
-
|
|
82
|
-
# Find all files that might contain exported constants and map them to constants
|
|
83
|
-
files_to_load = {} # file_path => [const_name, ...]
|
|
84
|
-
|
|
85
|
-
package.exports.each do |const_name|
|
|
86
|
-
# Skip if already loaded (cached)
|
|
87
|
-
next if package.loaded_exports.key?(const_name)
|
|
88
|
-
|
|
89
|
-
# Find the file path for this constant using Zeitwerk conventions
|
|
90
|
-
file_path = find_file_for_constant(lib_path, const_name)
|
|
91
|
-
|
|
92
|
-
unless file_path
|
|
93
|
-
raise "Cannot find file for exported constant '#{const_name}' in package '#{package.name}' at #{lib_path}"
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
files_to_load[file_path] ||= []
|
|
97
|
-
files_to_load[file_path] << const_name
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
# Load only the discovered files in the box (strict mode - no fallback)
|
|
101
|
-
files_to_load.keys.sort.each do |file|
|
|
102
|
-
box.require(file)
|
|
103
|
-
|
|
104
|
-
# Cache the mapping AFTER successful load
|
|
105
|
-
files_to_load[file].each do |const_name|
|
|
106
|
-
package.loaded_exports[const_name] = file
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
# Find the file that should define a constant using Zeitwerk's conventions
|
|
112
|
-
# Handles nested constants: Foo::Bar can be in lib/foo/bar.rb OR lib/foo.rb
|
|
113
|
-
# @param lib_path [String] The lib directory path
|
|
114
|
-
# @param const_name [String] The constant name (can include ::)
|
|
115
|
-
# @return [String, nil] The file path or nil if not found
|
|
116
42
|
def find_file_for_constant(lib_path, const_name)
|
|
117
|
-
# For nested constants like Foo::Bar, try the nested path first
|
|
118
43
|
if const_name.include?('::')
|
|
119
|
-
# Try conventional nested path: Foo::Bar -> lib/foo/bar.rb
|
|
120
44
|
nested_path = File.join(lib_path, "#{underscore(const_name)}.rb")
|
|
121
45
|
return nested_path if File.exist?(nested_path)
|
|
122
46
|
|
|
123
|
-
# Fall back to parent path: Foo::Bar -> lib/foo.rb
|
|
124
|
-
# The parent file might define the nested constant
|
|
125
47
|
parts = const_name.split('::')
|
|
126
48
|
parent_name = parts[0..-2].join('::')
|
|
127
|
-
parent_file =
|
|
128
|
-
# Top-level nested constant (shouldn't happen, but handle it)
|
|
129
|
-
File.join(lib_path, "#{underscore(parts[-1])}.rb")
|
|
130
|
-
else
|
|
131
|
-
File.join(lib_path, "#{underscore(parent_name)}.rb")
|
|
132
|
-
end
|
|
49
|
+
parent_file = File.join(lib_path, "#{underscore(parent_name)}.rb")
|
|
133
50
|
return parent_file if File.exist?(parent_file)
|
|
134
51
|
else
|
|
135
|
-
|
|
136
|
-
|
|
52
|
+
conventional_path =
|
|
53
|
+
File.join(lib_path, "#{underscore(const_name)}.rb")
|
|
137
54
|
return conventional_path if File.exist?(conventional_path)
|
|
138
55
|
end
|
|
139
56
|
|
|
140
57
|
nil
|
|
141
58
|
end
|
|
142
59
|
|
|
143
|
-
# Convert CamelCase to snake_case (Zeitwerk-compatible)
|
|
144
|
-
# @param string [String] The string to convert
|
|
145
|
-
# @return [String] The underscored string
|
|
146
60
|
def underscore(string)
|
|
147
61
|
string
|
|
148
62
|
.gsub(/::/, '/')
|
|
@@ -152,28 +66,22 @@ module Boxwerk
|
|
|
152
66
|
.downcase
|
|
153
67
|
end
|
|
154
68
|
|
|
155
|
-
# Wire imports according to YAML configuration (list format)
|
|
156
|
-
# @param box [Ruby::Box] The box instance
|
|
157
|
-
# @param package [Boxwerk::Package] The package being booted
|
|
158
|
-
# @param graph [Boxwerk::Graph] The dependency graph
|
|
159
69
|
def wire_imports(box, package, graph)
|
|
160
70
|
package.imports.each do |import_item|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
71
|
+
path, config =
|
|
72
|
+
(
|
|
73
|
+
if import_item.is_a?(String)
|
|
74
|
+
[import_item, nil]
|
|
75
|
+
else
|
|
76
|
+
[import_item.keys.first, import_item.values.first]
|
|
77
|
+
end
|
|
78
|
+
)
|
|
170
79
|
dep_name = File.basename(path)
|
|
171
80
|
dependency = graph.packages[dep_name]
|
|
172
81
|
|
|
173
82
|
unless dependency
|
|
174
83
|
raise "Cannot resolve dependency '#{path}' for package '#{package.name}'"
|
|
175
84
|
end
|
|
176
|
-
|
|
177
85
|
unless dependency.booted?
|
|
178
86
|
raise "Dependency '#{dep_name}' not booted yet"
|
|
179
87
|
end
|
|
@@ -182,93 +90,57 @@ module Boxwerk
|
|
|
182
90
|
end
|
|
183
91
|
end
|
|
184
92
|
|
|
185
|
-
# Execute the appropriate wiring strategy based on config type
|
|
186
|
-
# @param box [Ruby::Box] The box instance
|
|
187
|
-
# @param package [Boxwerk::Package] The current package being wired
|
|
188
|
-
# @param path [String] The dependency path
|
|
189
|
-
# @param config [nil, String, Array, Hash] The import configuration
|
|
190
|
-
# @param dependency [Boxwerk::Package] The dependency package
|
|
191
93
|
def wire_import_strategy(box, package, path, config, dependency)
|
|
192
94
|
case config
|
|
193
95
|
when nil
|
|
194
|
-
|
|
195
|
-
name = camelize(File.basename(path))
|
|
196
|
-
create_namespace(package, name, dependency)
|
|
96
|
+
create_namespace(package, camelize(File.basename(path)), dependency)
|
|
197
97
|
when String
|
|
198
|
-
# Strategy 2: Aliased Namespace ("packages/identity": "Auth")
|
|
199
98
|
create_namespace(package, config, dependency)
|
|
200
99
|
when Array
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
value = get_constant(dependency, const_name)
|
|
204
|
-
set_constant(package, const_name, value)
|
|
100
|
+
config.each do |name|
|
|
101
|
+
set_constant(package, name, get_constant(dependency, name))
|
|
205
102
|
end
|
|
206
103
|
when Hash
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
value = get_constant(dependency, remote_name)
|
|
210
|
-
set_constant(package, local_alias, value)
|
|
104
|
+
config.each do |remote, local|
|
|
105
|
+
set_constant(package, local, get_constant(dependency, remote))
|
|
211
106
|
end
|
|
212
107
|
end
|
|
213
108
|
end
|
|
214
109
|
|
|
215
|
-
# Create a namespace module and populate with all exports
|
|
216
|
-
# Or import directly if single export (optimization)
|
|
217
|
-
# @param package [Boxwerk::Package] The current package
|
|
218
|
-
# @param namespace_name [String] The module name to create
|
|
219
|
-
# @param dependency [Boxwerk::Package] The dependency package
|
|
220
110
|
def create_namespace(package, namespace_name, dependency)
|
|
221
111
|
if dependency.exports.size == 1
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
112
|
+
set_constant(
|
|
113
|
+
package,
|
|
114
|
+
namespace_name,
|
|
115
|
+
get_constant(dependency, dependency.exports.first),
|
|
116
|
+
)
|
|
225
117
|
else
|
|
226
|
-
# Multiple exports: create namespace module
|
|
227
118
|
mod = create_module(package.box, namespace_name)
|
|
228
|
-
dependency.exports.each do |
|
|
229
|
-
|
|
230
|
-
mod.const_set(export_name.to_sym, value)
|
|
119
|
+
dependency.exports.each do |name|
|
|
120
|
+
mod.const_set(name.to_sym, get_constant(dependency, name))
|
|
231
121
|
end
|
|
232
122
|
end
|
|
233
123
|
end
|
|
234
124
|
|
|
235
|
-
# Get a constant from a package's box, loading it lazily if needed
|
|
236
|
-
# @param package [Boxwerk::Package] The package to get the constant from
|
|
237
|
-
# @param name [String] The constant name
|
|
238
|
-
# @return [Object] The constant value
|
|
239
125
|
def get_constant(package, name)
|
|
240
|
-
# Load the export lazily
|
|
241
126
|
load_export(package, name)
|
|
242
|
-
|
|
243
127
|
package.box.const_get(name.to_sym)
|
|
244
128
|
end
|
|
245
129
|
|
|
246
|
-
# Set a constant in a package's box
|
|
247
|
-
# @param package [Boxwerk::Package] The package to set the constant in
|
|
248
|
-
# @param name [String] The constant name
|
|
249
|
-
# @param value [Object] The constant value
|
|
250
130
|
def set_constant(package, name, value)
|
|
251
131
|
package.box.const_set(name.to_sym, value)
|
|
252
132
|
end
|
|
253
133
|
|
|
254
|
-
# Create a module in the box
|
|
255
134
|
def create_module(box, name)
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
box.const_set(name.to_sym, mod)
|
|
259
|
-
mod
|
|
260
|
-
else
|
|
261
|
-
box.const_get(name.to_sym)
|
|
135
|
+
if box.const_defined?(name.to_sym, false)
|
|
136
|
+
return box.const_get(name.to_sym)
|
|
262
137
|
end
|
|
263
|
-
end
|
|
264
138
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
mod
|
|
268
|
-
mod.const_set(const_name.to_sym, value)
|
|
139
|
+
mod = Module.new
|
|
140
|
+
box.const_set(name.to_sym, mod)
|
|
141
|
+
mod
|
|
269
142
|
end
|
|
270
143
|
|
|
271
|
-
# Simple camelization (underscore to CamelCase)
|
|
272
144
|
def camelize(string)
|
|
273
145
|
string.split('_').map(&:capitalize).join
|
|
274
146
|
end
|
data/lib/boxwerk/package.rb
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
require 'yaml'
|
|
4
4
|
|
|
5
5
|
module Boxwerk
|
|
6
|
-
#
|
|
6
|
+
# Package represents a single package loaded from package.yml.
|
|
7
|
+
# Tracks exports, imports, dependencies, and the isolated Ruby::Box instance.
|
|
7
8
|
class Package
|
|
8
|
-
attr_reader :name, :path, :exports, :imports, :
|
|
9
|
+
attr_reader :name, :path, :exports, :imports, :loaded_exports
|
|
10
|
+
attr_accessor :box
|
|
9
11
|
|
|
10
12
|
def initialize(name, path)
|
|
11
13
|
@name = name
|
|
@@ -22,18 +24,8 @@ module Boxwerk
|
|
|
22
24
|
!@box.nil?
|
|
23
25
|
end
|
|
24
26
|
|
|
25
|
-
def box=(box_instance)
|
|
26
|
-
@box = box_instance
|
|
27
|
-
end
|
|
28
|
-
|
|
29
27
|
def dependencies
|
|
30
|
-
@imports.map
|
|
31
|
-
if item.is_a?(String)
|
|
32
|
-
item
|
|
33
|
-
else
|
|
34
|
-
item.keys.first
|
|
35
|
-
end
|
|
36
|
-
end
|
|
28
|
+
@imports.map { |item| item.is_a?(String) ? item : item.keys.first }
|
|
37
29
|
end
|
|
38
30
|
|
|
39
31
|
private
|
|
@@ -43,7 +35,6 @@ module Boxwerk
|
|
|
43
35
|
return unless File.exist?(config_path)
|
|
44
36
|
|
|
45
37
|
config = YAML.load_file(config_path)
|
|
46
|
-
|
|
47
38
|
@exports = config['exports'] || []
|
|
48
39
|
@imports = config['imports'] || []
|
|
49
40
|
end
|
data/lib/boxwerk/registry.rb
CHANGED
|
@@ -1,35 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Boxwerk
|
|
4
|
-
# Registry
|
|
5
|
-
# This allows packages to be booted once and reused
|
|
4
|
+
# Registry tracks booted package instances to ensure each package boots only once.
|
|
6
5
|
class Registry
|
|
7
6
|
def initialize
|
|
8
7
|
@registry = {}
|
|
9
8
|
end
|
|
10
9
|
|
|
11
|
-
# Register a booted package
|
|
12
|
-
# @param name [Symbol] Package name
|
|
13
|
-
# @param instance [Object] The booted package instance
|
|
14
10
|
def register(name, instance)
|
|
15
11
|
@registry[name] = instance
|
|
16
12
|
end
|
|
17
13
|
|
|
18
|
-
# Retrieve a booted package
|
|
19
|
-
# @param name [Symbol] Package name
|
|
20
|
-
# @return [Object, nil] The package instance or nil
|
|
21
14
|
def get(name)
|
|
22
15
|
@registry[name]
|
|
23
16
|
end
|
|
24
17
|
|
|
25
|
-
# Check if a package is registered
|
|
26
|
-
# @param name [Symbol] Package name
|
|
27
|
-
# @return [Boolean]
|
|
28
18
|
def registered?(name)
|
|
29
19
|
@registry.key?(name)
|
|
30
20
|
end
|
|
31
21
|
|
|
32
|
-
# Clear the registry (useful for testing)
|
|
33
22
|
def clear!
|
|
34
23
|
@registry = {}
|
|
35
24
|
end
|
data/lib/boxwerk/setup.rb
CHANGED
|
@@ -1,48 +1,31 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Boxwerk
|
|
4
|
-
# Setup
|
|
4
|
+
# Setup finds the root package, builds the dependency graph, and boots all packages.
|
|
5
|
+
# Orchestrates the entire Boxwerk initialization process.
|
|
5
6
|
module Setup
|
|
6
7
|
class << self
|
|
7
|
-
# Main entry point for setup
|
|
8
|
-
# @param start_dir [String] Directory to start searching for package.yml
|
|
9
|
-
# @return [Boxwerk::Graph] The loaded and validated graph
|
|
10
8
|
def run!(start_dir: Dir.pwd)
|
|
11
|
-
# Find the root package.yml
|
|
12
9
|
root_path = find_package_yml(start_dir)
|
|
13
|
-
unless root_path
|
|
14
|
-
raise 'Cannot find package.yml in current directory or ancestors'
|
|
15
|
-
end
|
|
10
|
+
raise 'Cannot find package.yml in current directory or ancestors' unless root_path
|
|
16
11
|
|
|
17
|
-
# Build and validate dependency graph (happens automatically in constructor)
|
|
18
12
|
graph = Boxwerk::Graph.new(root_path)
|
|
19
|
-
|
|
20
|
-
# Create a registry instance for tracking booted packages
|
|
21
13
|
registry = Boxwerk::Registry.new
|
|
22
|
-
|
|
23
|
-
# Boot all packages in topological order (all in isolated boxes)
|
|
24
14
|
Boxwerk::Loader.boot_all(graph, registry)
|
|
25
15
|
|
|
26
|
-
# Store graph for introspection
|
|
27
16
|
@graph = graph
|
|
28
17
|
@booted = true
|
|
29
|
-
|
|
30
18
|
graph
|
|
31
19
|
end
|
|
32
20
|
|
|
33
|
-
# Returns the loaded graph (for introspection)
|
|
34
|
-
# @return [Boxwerk::Graph, nil]
|
|
35
21
|
def graph
|
|
36
22
|
@graph
|
|
37
23
|
end
|
|
38
24
|
|
|
39
|
-
# Check if Boxwerk has been booted
|
|
40
|
-
# @return [Boolean]
|
|
41
25
|
def booted?
|
|
42
26
|
@booted || false
|
|
43
27
|
end
|
|
44
28
|
|
|
45
|
-
# Reset the setup state (useful for testing)
|
|
46
29
|
def reset!
|
|
47
30
|
@graph = nil
|
|
48
31
|
@booted = false
|
|
@@ -50,17 +33,13 @@ module Boxwerk
|
|
|
50
33
|
|
|
51
34
|
private
|
|
52
35
|
|
|
53
|
-
# Find package.yml by searching up the directory tree
|
|
54
|
-
# @param start_dir [String] Directory to start searching from
|
|
55
|
-
# @return [String, nil] Path to directory containing package.yml, or nil
|
|
56
36
|
def find_package_yml(start_dir)
|
|
57
37
|
current = File.expand_path(start_dir)
|
|
58
38
|
loop do
|
|
59
|
-
|
|
60
|
-
return current if File.exist?(package_yml)
|
|
39
|
+
return current if File.exist?(File.join(current, 'package.yml'))
|
|
61
40
|
|
|
62
41
|
parent = File.dirname(current)
|
|
63
|
-
break if parent == current
|
|
42
|
+
break if parent == current
|
|
64
43
|
|
|
65
44
|
current = parent
|
|
66
45
|
end
|
data/lib/boxwerk/version.rb
CHANGED
data/lib/boxwerk.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: boxwerk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Cristofaro
|
|
@@ -23,10 +23,9 @@ dependencies:
|
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '1.16'
|
|
26
|
-
description: Boxwerk
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
a static package system.
|
|
26
|
+
description: Boxwerk is an experimental Ruby package system with Box-powered constant
|
|
27
|
+
isolation. It is used at runtime to organize code into packages with explicit dependencies
|
|
28
|
+
and strict constant access using Ruby::Box. Inspired by Packwerk.
|
|
30
29
|
email:
|
|
31
30
|
- david@dtcristo.com
|
|
32
31
|
executables:
|
|
@@ -38,17 +37,6 @@ files:
|
|
|
38
37
|
- LICENSE.txt
|
|
39
38
|
- README.md
|
|
40
39
|
- Rakefile
|
|
41
|
-
- example/Gemfile
|
|
42
|
-
- example/Gemfile.lock
|
|
43
|
-
- example/README.md
|
|
44
|
-
- example/app.rb
|
|
45
|
-
- example/package.yml
|
|
46
|
-
- example/packages/finance/lib/invoice.rb
|
|
47
|
-
- example/packages/finance/lib/tax_calculator.rb
|
|
48
|
-
- example/packages/finance/package.yml
|
|
49
|
-
- example/packages/util/lib/calculator.rb
|
|
50
|
-
- example/packages/util/lib/geometry.rb
|
|
51
|
-
- example/packages/util/package.yml
|
|
52
40
|
- exe/boxwerk
|
|
53
41
|
- lib/boxwerk.rb
|
|
54
42
|
- lib/boxwerk/cli.rb
|
|
@@ -58,7 +46,6 @@ files:
|
|
|
58
46
|
- lib/boxwerk/registry.rb
|
|
59
47
|
- lib/boxwerk/setup.rb
|
|
60
48
|
- lib/boxwerk/version.rb
|
|
61
|
-
- sig/boxwerk.rbs
|
|
62
49
|
homepage: https://github.com/dtcristo/boxwerk
|
|
63
50
|
licenses:
|
|
64
51
|
- MIT
|