igniter-contracts 0.5.2

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 (64) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +239 -0
  3. data/lib/igniter/contracts/api.rb +92 -0
  4. data/lib/igniter/contracts/assembly/baseline_pack.rb +141 -0
  5. data/lib/igniter/contracts/assembly/const_pack.rb +29 -0
  6. data/lib/igniter/contracts/assembly/dsl_keyword.rb +21 -0
  7. data/lib/igniter/contracts/assembly/hook_result_policies.rb +47 -0
  8. data/lib/igniter/contracts/assembly/hook_spec.rb +73 -0
  9. data/lib/igniter/contracts/assembly/hook_specs.rb +74 -0
  10. data/lib/igniter/contracts/assembly/kernel.rb +220 -0
  11. data/lib/igniter/contracts/assembly/node_type.rb +26 -0
  12. data/lib/igniter/contracts/assembly/ordered_registry.rb +55 -0
  13. data/lib/igniter/contracts/assembly/pack.rb +13 -0
  14. data/lib/igniter/contracts/assembly/pack_manifest.rb +131 -0
  15. data/lib/igniter/contracts/assembly/path_access.rb +76 -0
  16. data/lib/igniter/contracts/assembly/profile.rb +133 -0
  17. data/lib/igniter/contracts/assembly/project_pack.rb +42 -0
  18. data/lib/igniter/contracts/assembly/registry.rb +57 -0
  19. data/lib/igniter/contracts/assembly/step_result_pack.rb +42 -0
  20. data/lib/igniter/contracts/assembly.rb +18 -0
  21. data/lib/igniter/contracts/contract.rb +135 -0
  22. data/lib/igniter/contracts/contractable.rb +288 -0
  23. data/lib/igniter/contracts/environment.rb +51 -0
  24. data/lib/igniter/contracts/errors.rb +47 -0
  25. data/lib/igniter/contracts/execution/baseline_normalizers.rb +23 -0
  26. data/lib/igniter/contracts/execution/baseline_runtime.rb +55 -0
  27. data/lib/igniter/contracts/execution/baseline_validators.rb +113 -0
  28. data/lib/igniter/contracts/execution/builder.rb +43 -0
  29. data/lib/igniter/contracts/execution/compilation_report.rb +46 -0
  30. data/lib/igniter/contracts/execution/compiled_graph.rb +21 -0
  31. data/lib/igniter/contracts/execution/compiler.rb +66 -0
  32. data/lib/igniter/contracts/execution/const_runtime.rb +15 -0
  33. data/lib/igniter/contracts/execution/diagnostics.rb +24 -0
  34. data/lib/igniter/contracts/execution/diagnostics_report.rb +40 -0
  35. data/lib/igniter/contracts/execution/diagnostics_section.rb +37 -0
  36. data/lib/igniter/contracts/execution/effect_invocation.rb +26 -0
  37. data/lib/igniter/contracts/execution/execution_request.rb +28 -0
  38. data/lib/igniter/contracts/execution/execution_result.rb +32 -0
  39. data/lib/igniter/contracts/execution/inline_executor.rb +19 -0
  40. data/lib/igniter/contracts/execution/mutable_named_values.rb +52 -0
  41. data/lib/igniter/contracts/execution/named_values.rb +48 -0
  42. data/lib/igniter/contracts/execution/operation.rb +42 -0
  43. data/lib/igniter/contracts/execution/runtime.rb +43 -0
  44. data/lib/igniter/contracts/execution/step_result.rb +51 -0
  45. data/lib/igniter/contracts/execution/step_result_diagnostics.rb +35 -0
  46. data/lib/igniter/contracts/execution/step_result_runtime.rb +51 -0
  47. data/lib/igniter/contracts/execution/step_result_validators.rb +44 -0
  48. data/lib/igniter/contracts/execution/structured_dump.rb +49 -0
  49. data/lib/igniter/contracts/execution/validation_finding.rb +28 -0
  50. data/lib/igniter/contracts/execution/validation_report.rb +46 -0
  51. data/lib/igniter/contracts/execution.rb +28 -0
  52. data/lib/igniter/contracts.rb +54 -0
  53. data/lib/igniter/lang/backend.rb +19 -0
  54. data/lib/igniter/lang/backends/ruby.rb +42 -0
  55. data/lib/igniter/lang/diagnostic_payload.rb +174 -0
  56. data/lib/igniter/lang/metadata_carrier_manifest.rb +112 -0
  57. data/lib/igniter/lang/metadata_manifest.rb +128 -0
  58. data/lib/igniter/lang/receipt_payload.rb +152 -0
  59. data/lib/igniter/lang/schema_compatibility_diagnostic.rb +300 -0
  60. data/lib/igniter/lang/types.rb +84 -0
  61. data/lib/igniter/lang/verification_report.rb +226 -0
  62. data/lib/igniter/lang.rb +27 -0
  63. data/lib/igniter-contracts.rb +3 -0
  64. metadata +103 -0
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Contracts
5
+ module Assembly
6
+ module HookSpecs
7
+ module_function
8
+
9
+ REGISTRY_SPECS = {
10
+ dsl_keywords: HookSpec.new(
11
+ registry: :dsl_keywords,
12
+ method_name: :call,
13
+ required_keywords: %i[builder],
14
+ role: :dsl_keyword,
15
+ return_policy: :opaque
16
+ ),
17
+ normalizers: HookSpec.new(
18
+ registry: :normalizers,
19
+ method_name: :call,
20
+ required_keywords: %i[operations profile],
21
+ role: :graph_transformer,
22
+ return_policy: :operations_array,
23
+ result_validator: HookResultPolicies.method(:operations_array)
24
+ ),
25
+ validators: HookSpec.new(
26
+ registry: :validators,
27
+ method_name: :call,
28
+ required_keywords: %i[operations profile],
29
+ role: :validator,
30
+ return_policy: :validation_findings,
31
+ result_validator: HookResultPolicies.method(:validation_findings)
32
+ ),
33
+ runtime_handlers: HookSpec.new(
34
+ registry: :runtime_handlers,
35
+ method_name: :call,
36
+ required_keywords: %i[operation state outputs inputs profile],
37
+ role: :runtime_handler,
38
+ return_policy: :value
39
+ ),
40
+ diagnostics_contributors: HookSpec.new(
41
+ registry: :diagnostics_contributors,
42
+ method_name: :augment,
43
+ required_keywords: %i[report result profile],
44
+ role: :diagnostics_contributor,
45
+ return_policy: :ignored
46
+ ),
47
+ effects: HookSpec.new(
48
+ registry: :effects,
49
+ method_name: :call,
50
+ required_keywords: %i[invocation],
51
+ role: :effect_adapter,
52
+ return_policy: :opaque
53
+ ),
54
+ executors: HookSpec.new(
55
+ registry: :executors,
56
+ method_name: :call,
57
+ required_keywords: %i[invocation],
58
+ role: :executor,
59
+ return_policy: :execution_result,
60
+ result_validator: HookResultPolicies.method(:execution_result)
61
+ )
62
+ }.freeze
63
+
64
+ def fetch(registry_name)
65
+ REGISTRY_SPECS.fetch(registry_name.to_sym)
66
+ end
67
+
68
+ def registry_names
69
+ REGISTRY_SPECS.keys
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Contracts
5
+ module Assembly
6
+ class Kernel
7
+ attr_reader :nodes,
8
+ :dsl_keywords,
9
+ :validators,
10
+ :normalizers,
11
+ :runtime_handlers,
12
+ :diagnostics_contributors,
13
+ :pack_manifests,
14
+ :effects,
15
+ :executors
16
+
17
+ def initialize(
18
+ nodes: Registry.new(name: :nodes),
19
+ dsl_keywords: Registry.new(name: :dsl_keywords),
20
+ validators: OrderedRegistry.new(name: :validators),
21
+ normalizers: OrderedRegistry.new(name: :normalizers),
22
+ runtime_handlers: Registry.new(name: :runtime_handlers),
23
+ diagnostics_contributors: OrderedRegistry.new(name: :diagnostics_contributors),
24
+ effects: Registry.new(name: :effects),
25
+ executors: Registry.new(name: :executors)
26
+ )
27
+ @nodes = nodes
28
+ @dsl_keywords = dsl_keywords
29
+ @validators = validators
30
+ @normalizers = normalizers
31
+ @runtime_handlers = runtime_handlers
32
+ @diagnostics_contributors = diagnostics_contributors
33
+ @pack_manifests = []
34
+ @effects = effects
35
+ @executors = executors
36
+ @finalized = false
37
+ end
38
+
39
+ def install(pack, installing: [])
40
+ raise FrozenKernelError, "kernel already finalized" if finalized?
41
+
42
+ manifest = pack.respond_to?(:manifest) ? pack.manifest : nil
43
+ pack_name = manifest&.name
44
+
45
+ return self if pack_name && pack_installed?(pack_name)
46
+
47
+ if pack_name && installing.include?(pack_name)
48
+ cycle = (installing + [pack_name]).join(" -> ")
49
+ raise CircularPackDependencyError, "circular pack dependency detected: #{cycle}"
50
+ end
51
+
52
+ install_required_packs(manifest, installing: [*installing, pack_name].compact) if manifest
53
+ register_pack_manifest(pack, manifest: manifest)
54
+ pack.install_into(self)
55
+ self
56
+ end
57
+
58
+ def finalize
59
+ validate_completeness!
60
+ validate_hook_implementations!
61
+ freeze_registries!
62
+ @finalized = true
63
+ Profile.build_from(self)
64
+ end
65
+
66
+ def finalized?
67
+ @finalized
68
+ end
69
+
70
+ private
71
+
72
+ REGISTRY_LABELS = {
73
+ dsl_keywords: "DSL keywords",
74
+ validators: "validators",
75
+ normalizers: "normalizers",
76
+ runtime_handlers: "runtime handlers",
77
+ diagnostics_contributors: "diagnostics contributors",
78
+ effects: "effects",
79
+ executors: "executors"
80
+ }.freeze
81
+
82
+ def register_pack_manifest(pack, manifest: nil)
83
+ manifest ||= pack.respond_to?(:manifest) ? pack.manifest : nil
84
+ return unless manifest
85
+
86
+ pack_manifests << manifest
87
+ end
88
+
89
+ def pack_installed?(name)
90
+ pack_manifests.any? { |manifest| manifest.name == name.to_sym }
91
+ end
92
+
93
+ def install_required_packs(manifest, installing:)
94
+ manifest.requires_packs.each do |dependency|
95
+ next if pack_installed?(dependency.name)
96
+
97
+ unless dependency.pack
98
+ raise UnknownPackDependencyError,
99
+ "pack #{manifest.name} requires pack #{dependency.name}, but no pack implementation was provided"
100
+ end
101
+
102
+ install(dependency.pack, installing: installing)
103
+ end
104
+ end
105
+
106
+ def validate_completeness!
107
+ manifest_contracts = pack_manifests.flat_map(&:node_contracts)
108
+ undeclared_nodes = nodes.to_h.values.reject do |node|
109
+ manifest_contracts.any? do |contract|
110
+ contract.kind == node.kind
111
+ end
112
+ end
113
+ undeclared_contracts = undeclared_nodes.map do |node|
114
+ PackManifest.node(
115
+ node.kind,
116
+ requires_dsl: node.requires_dsl?,
117
+ requires_runtime: node.requires_runtime?
118
+ )
119
+ end
120
+ contracts = manifest_contracts + undeclared_contracts
121
+
122
+ missing_node_definitions = manifest_contracts.map(&:kind).reject { |kind| nodes.registered?(kind) }
123
+ missing_dsl = contracts.select(&:requires_dsl).map(&:kind).reject { |kind| dsl_keywords.registered?(kind) }
124
+ missing_runtime = contracts.select(&:requires_runtime).map(&:kind).reject do |kind|
125
+ runtime_handlers.registered?(kind)
126
+ end
127
+ missing_registry_contracts = collect_missing_registry_contracts
128
+ return if missing_node_definitions.empty? &&
129
+ missing_dsl.empty? &&
130
+ missing_runtime.empty? &&
131
+ missing_registry_contracts.empty?
132
+
133
+ parts = []
134
+ parts << "missing node definitions for: #{missing_node_definitions.map(&:to_s).join(", ")}" unless missing_node_definitions.empty?
135
+ parts << "missing DSL keywords for: #{missing_dsl.map(&:to_s).join(", ")}" unless missing_dsl.empty?
136
+ parts << "missing runtime handlers for: #{missing_runtime.map(&:to_s).join(", ")}" unless missing_runtime.empty?
137
+ parts.concat(missing_registry_contracts)
138
+
139
+ raise IncompletePackError, parts.join("; ")
140
+ end
141
+
142
+ def validate_hook_implementations!
143
+ HookSpecs.registry_names.each do |registry_name|
144
+ hook_spec = HookSpecs.fetch(registry_name)
145
+
146
+ each_registry_entry(registry_name) do |key, implementation|
147
+ hook_spec.validate!(key, implementation)
148
+ end
149
+ end
150
+ end
151
+
152
+ def collect_missing_registry_contracts
153
+ pack_manifests
154
+ .flat_map(&:registry_contracts)
155
+ .group_by(&:registry)
156
+ .filter_map do |registry_name, contracts|
157
+ registry = registry_for(registry_name)
158
+ next if registry.nil?
159
+
160
+ missing = contracts.map(&:key).reject { |key| registry.registered?(key) }
161
+ next if missing.empty?
162
+
163
+ missing_registry_message(registry_name, missing)
164
+ end
165
+ end
166
+
167
+ def missing_registry_message(registry_name, missing)
168
+ label = REGISTRY_LABELS.fetch(registry_name, registry_name.to_s.tr("_", " "))
169
+ "missing #{label} for: #{missing.map(&:to_s).join(", ")}"
170
+ end
171
+
172
+ def registry_for(name)
173
+ case name.to_sym
174
+ when :dsl_keywords
175
+ dsl_keywords
176
+ when :validators
177
+ validators
178
+ when :normalizers
179
+ normalizers
180
+ when :runtime_handlers
181
+ runtime_handlers
182
+ when :diagnostics_contributors
183
+ diagnostics_contributors
184
+ when :effects
185
+ effects
186
+ when :executors
187
+ executors
188
+ end
189
+ end
190
+
191
+ def each_registry_entry(registry_name, &block)
192
+ registry = registry_for(registry_name)
193
+ return unless registry
194
+
195
+ case registry
196
+ when Registry
197
+ registry.to_h.each(&block)
198
+ when OrderedRegistry
199
+ registry.entries.each do |entry|
200
+ yield(entry.key, entry.value)
201
+ end
202
+ end
203
+ end
204
+
205
+ def freeze_registries!
206
+ [
207
+ nodes,
208
+ dsl_keywords,
209
+ validators,
210
+ normalizers,
211
+ runtime_handlers,
212
+ diagnostics_contributors,
213
+ effects,
214
+ executors
215
+ ].each(&:freeze!)
216
+ end
217
+ end
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Contracts
5
+ module Assembly
6
+ NodeType = Struct.new(:kind, :metadata, keyword_init: true) do
7
+ def initialize(kind:, metadata: {})
8
+ normalized_metadata = {
9
+ requires_dsl: true,
10
+ requires_runtime: true
11
+ }.merge(metadata).freeze
12
+
13
+ super(kind: kind.to_sym, metadata: normalized_metadata)
14
+ end
15
+
16
+ def requires_dsl?
17
+ metadata.fetch(:requires_dsl, true)
18
+ end
19
+
20
+ def requires_runtime?
21
+ metadata.fetch(:requires_runtime, true)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Contracts
5
+ module Assembly
6
+ class OrderedRegistry
7
+ Entry = Struct.new(:key, :value, keyword_init: true)
8
+
9
+ def initialize(name:)
10
+ @name = name
11
+ @entries = []
12
+ @keys = {}
13
+ @frozen = false
14
+ end
15
+
16
+ def register(key, value)
17
+ raise FrozenRegistryError, "#{@name} is frozen" if frozen?
18
+
19
+ normalized_key = normalize_key(key)
20
+ raise DuplicateRegistrationError, "#{@name} already has #{normalized_key}" if @keys.key?(normalized_key)
21
+
22
+ entry = Entry.new(key: normalized_key, value: value)
23
+ @entries << entry
24
+ @keys[normalized_key] = true
25
+ entry
26
+ end
27
+
28
+ def registered?(key)
29
+ @keys.key?(normalize_key(key))
30
+ end
31
+
32
+ def entries
33
+ @entries.dup
34
+ end
35
+
36
+ def freeze!
37
+ @frozen = true
38
+ @entries.freeze
39
+ @keys.freeze
40
+ self
41
+ end
42
+
43
+ def frozen?
44
+ @frozen
45
+ end
46
+
47
+ private
48
+
49
+ def normalize_key(key)
50
+ key.to_sym
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Contracts
5
+ module Assembly
6
+ module Pack
7
+ def install_into(_kernel)
8
+ raise NotImplementedError
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Contracts
5
+ module Assembly
6
+ class PackManifest
7
+ RegistryContract = Struct.new(:registry, :key, keyword_init: true) do
8
+ def initialize(registry:, key:)
9
+ super(
10
+ registry: registry.to_sym,
11
+ key: key.to_sym
12
+ )
13
+ end
14
+ end
15
+
16
+ NodeContract = Struct.new(:kind, :requires_dsl, :requires_runtime, keyword_init: true) do
17
+ def initialize(kind:, requires_dsl: true, requires_runtime: true)
18
+ super(
19
+ kind: kind.to_sym,
20
+ requires_dsl: requires_dsl,
21
+ requires_runtime: requires_runtime
22
+ )
23
+ end
24
+ end
25
+
26
+ PackDependency = Struct.new(:name, :pack, keyword_init: true) do
27
+ def initialize(name:, pack: nil)
28
+ super(
29
+ name: name.to_sym,
30
+ pack: pack
31
+ )
32
+ end
33
+ end
34
+
35
+ class << self
36
+ def node(kind, requires_dsl: true, requires_runtime: true)
37
+ NodeContract.new(
38
+ kind: kind,
39
+ requires_dsl: requires_dsl,
40
+ requires_runtime: requires_runtime
41
+ )
42
+ end
43
+
44
+ def registry(registry, key)
45
+ RegistryContract.new(registry: registry, key: key)
46
+ end
47
+
48
+ def dsl_keyword(key)
49
+ registry(:dsl_keywords, key)
50
+ end
51
+
52
+ def runtime_handler(key)
53
+ registry(:runtime_handlers, key)
54
+ end
55
+
56
+ def validator(key)
57
+ registry(:validators, key)
58
+ end
59
+
60
+ def normalizer(key)
61
+ registry(:normalizers, key)
62
+ end
63
+
64
+ def diagnostic(key)
65
+ registry(:diagnostics_contributors, key)
66
+ end
67
+
68
+ def effect(key)
69
+ registry(:effects, key)
70
+ end
71
+
72
+ def executor(key)
73
+ registry(:executors, key)
74
+ end
75
+
76
+ def pack_dependency(pack_or_name, pack: nil)
77
+ return PackDependency.new(name: pack_or_name, pack: pack) if pack
78
+
79
+ if pack_or_name.respond_to?(:manifest)
80
+ manifest = pack_or_name.manifest
81
+ return PackDependency.new(name: manifest.name, pack: pack_or_name)
82
+ end
83
+
84
+ PackDependency.new(name: pack_or_name)
85
+ end
86
+ end
87
+
88
+ attr_reader :name, :node_contracts, :registry_contracts, :metadata,
89
+ :requires_packs, :provides_capabilities, :requires_capabilities
90
+
91
+ def initialize(name:, node_contracts: [], registry_contracts: [], diagnostics: [], metadata: {},
92
+ requires_packs: [], provides_capabilities: [], requires_capabilities: [])
93
+ @name = name.to_sym
94
+ @node_contracts = node_contracts.freeze
95
+ @registry_contracts = (
96
+ registry_contracts +
97
+ diagnostics.map { |key| self.class.diagnostic(key) }
98
+ ).uniq.freeze
99
+ @metadata = metadata.freeze
100
+ @requires_packs = normalize_pack_dependencies(requires_packs)
101
+ @provides_capabilities = normalize_capabilities(provides_capabilities)
102
+ @requires_capabilities = normalize_capabilities(requires_capabilities)
103
+ freeze
104
+ end
105
+
106
+ def declared_keys_for(registry)
107
+ registry_contracts
108
+ .select { |contract| contract.registry == registry.to_sym }
109
+ .map(&:key)
110
+ end
111
+
112
+ def diagnostics
113
+ declared_keys_for(:diagnostics_contributors)
114
+ end
115
+
116
+ private
117
+
118
+ def normalize_pack_dependencies(dependencies)
119
+ Array(dependencies)
120
+ .map { |entry| entry.is_a?(PackDependency) ? entry : self.class.pack_dependency(entry) }
121
+ .uniq(&:name)
122
+ .freeze
123
+ end
124
+
125
+ def normalize_capabilities(capabilities)
126
+ Array(capabilities).flatten.compact.map(&:to_sym).uniq.freeze
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Contracts
5
+ module Assembly
6
+ module PathAccess
7
+ module_function
8
+
9
+ NO_DEFAULT = Object.new.freeze
10
+
11
+ def normalize_path(keyword_name:, key: nil, dig: nil)
12
+ raise ArgumentError, "#{keyword_name} accepts either key: or dig:, not both" if !key.nil? && !dig.nil?
13
+
14
+ raw_path =
15
+ if !key.nil?
16
+ [key]
17
+ elsif !dig.nil?
18
+ Array(dig)
19
+ else
20
+ raise ArgumentError, "#{keyword_name} requires key: or dig:"
21
+ end
22
+
23
+ raise ArgumentError, "#{keyword_name} dig: path cannot be empty" if raw_path.empty?
24
+
25
+ raw_path.map { |segment| normalize_segment(segment) }
26
+ end
27
+
28
+ def fetch_path(source, path, source_name:, keyword_name:, default: NO_DEFAULT)
29
+ current = source
30
+
31
+ path.each do |segment|
32
+ if segment_present?(current, segment)
33
+ current = fetch_segment(current, segment)
34
+ else
35
+ return default unless default.equal?(NO_DEFAULT)
36
+
37
+ raise KeyError, "#{keyword_name} path #{format_path(path)} not present in #{source_name}"
38
+ end
39
+ end
40
+
41
+ current
42
+ end
43
+
44
+ def normalize_segment(segment)
45
+ return segment if segment.is_a?(Integer)
46
+
47
+ segment.to_sym
48
+ end
49
+
50
+ def segment_present?(value, segment)
51
+ if value.respond_to?(:key?)
52
+ value.key?(segment) ||
53
+ (segment.is_a?(Symbol) && value.key?(segment.to_s)) ||
54
+ (segment.is_a?(String) && value.key?(segment.to_sym))
55
+ elsif value.is_a?(Array) && segment.is_a?(Integer)
56
+ segment >= 0 && segment < value.length
57
+ else
58
+ false
59
+ end
60
+ end
61
+
62
+ def fetch_segment(value, segment)
63
+ return value.fetch(segment) if value.respond_to?(:key?) && value.key?(segment)
64
+ return value.fetch(segment.to_s) if value.respond_to?(:key?) && segment.is_a?(Symbol) && value.key?(segment.to_s)
65
+ return value.fetch(segment.to_sym) if value.respond_to?(:key?) && segment.is_a?(String) && value.key?(segment.to_sym)
66
+
67
+ value.fetch(segment)
68
+ end
69
+
70
+ def format_path(path)
71
+ path.map(&:inspect).join(" -> ")
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end