archspec 1.0.1 → 1.1.0.rc2
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/README.md +12 -6
- data/lib/archspec/analyzer.rb +639 -261
- data/lib/archspec/architectures.rb +7 -3
- data/lib/archspec/component_spec.rb +13 -2
- data/lib/archspec/diagnostic.rb +19 -3
- data/lib/archspec/dsl.rb +69 -38
- data/lib/archspec/formatters/explanation.rb +33 -5
- data/lib/archspec/formatters/json.rb +1 -0
- data/lib/archspec/formatters/text.rb +19 -1
- data/lib/archspec/model.rb +266 -65
- data/lib/archspec/rubydex_index.rb +379 -0
- data/lib/archspec/rules/component_rules.rb +3 -4
- data/lib/archspec/rules/concern_rules.rb +7 -1
- data/lib/archspec/rules/cycle_rule.rb +130 -33
- data/lib/archspec/rules/naming_rules.rb +11 -22
- data/lib/archspec/rules/protocol_rules.rb +117 -22
- data/lib/archspec/rules/reasoned.rb +37 -0
- data/lib/archspec/source_location.rb +1 -3
- data/lib/archspec/version.rb +1 -1
- data/lib/archspec.rb +4 -2
- metadata +21 -6
- data/lib/archspec/value_object.rb +0 -46
|
@@ -74,31 +74,22 @@ module ArchSpec
|
|
|
74
74
|
|
|
75
75
|
# Forbids any selected method from existing. Rule id +naming.forbidden+.
|
|
76
76
|
class Forbidden
|
|
77
|
-
def initialize(because: nil)
|
|
78
|
-
@because = because
|
|
79
|
-
end
|
|
80
|
-
|
|
81
77
|
def id
|
|
82
78
|
'naming.forbidden'
|
|
83
79
|
end
|
|
84
80
|
|
|
85
81
|
def diagnostics(selected, rule, _graph)
|
|
86
82
|
selected.map do |definition, _match|
|
|
83
|
+
evidence = "#{definition.owner} defines #{definition.scope} method #{definition.name} " \
|
|
84
|
+
"(#{rule.selector.describe})"
|
|
87
85
|
Diagnostic.new(
|
|
88
86
|
rule: id,
|
|
89
|
-
message:
|
|
87
|
+
message: "#{definition.owner} must not define ##{definition.name}",
|
|
90
88
|
location: definition.location,
|
|
91
|
-
evidence:
|
|
89
|
+
evidence: evidence
|
|
92
90
|
)
|
|
93
91
|
end
|
|
94
92
|
end
|
|
95
|
-
|
|
96
|
-
private
|
|
97
|
-
|
|
98
|
-
def message_for(definition)
|
|
99
|
-
base = "#{definition.owner} must not define ##{definition.name}"
|
|
100
|
-
@because ? "#{base}: #{@because}" : base
|
|
101
|
-
end
|
|
102
93
|
end
|
|
103
94
|
|
|
104
95
|
# Requires each selected method to have a sibling named by a template, in
|
|
@@ -106,7 +97,7 @@ module ArchSpec
|
|
|
106
97
|
# interpolates the selector's named captures, as in
|
|
107
98
|
# <tt>requires("without_%{base}")</tt>. Rule id +naming.requires+.
|
|
108
99
|
class Requires
|
|
109
|
-
def initialize(template, on: nil, scope: :instance
|
|
100
|
+
def initialize(template, on: nil, scope: :instance)
|
|
110
101
|
unless NamingRule::VALID_SCOPES.include?(scope)
|
|
111
102
|
raise Error, "requires scope: must be :instance or :class, got #{scope.inspect}"
|
|
112
103
|
end
|
|
@@ -115,7 +106,6 @@ module ArchSpec
|
|
|
115
106
|
@template = template
|
|
116
107
|
@on = on
|
|
117
108
|
@target_scope = scope
|
|
118
|
-
@because = because
|
|
119
109
|
end
|
|
120
110
|
|
|
121
111
|
def id
|
|
@@ -163,8 +153,7 @@ module ArchSpec
|
|
|
163
153
|
else
|
|
164
154
|
"a matching ##{sibling}"
|
|
165
155
|
end
|
|
166
|
-
|
|
167
|
-
@because ? "#{base}: #{@because}" : base
|
|
156
|
+
"#{definition.owner}##{definition.name} requires #{clause}"
|
|
168
157
|
end
|
|
169
158
|
end
|
|
170
159
|
|
|
@@ -190,17 +179,17 @@ module ArchSpec
|
|
|
190
179
|
end
|
|
191
180
|
|
|
192
181
|
def forbidden(except: [], because: nil)
|
|
193
|
-
add(Forbidden.new
|
|
182
|
+
add(Forbidden.new, except, because)
|
|
194
183
|
end
|
|
195
184
|
|
|
196
185
|
def requires(template, on: nil, scope: :instance, except: [], because: nil)
|
|
197
186
|
validate_template!(template)
|
|
198
|
-
add(Requires.new(template, on: component_name(on), scope: scope,
|
|
187
|
+
add(Requires.new(template, on: component_name(on), scope: scope), except, because)
|
|
199
188
|
end
|
|
200
189
|
|
|
201
190
|
private
|
|
202
191
|
|
|
203
|
-
def add(constraint, except)
|
|
192
|
+
def add(constraint, except, because)
|
|
204
193
|
rule = NamingRule.new(
|
|
205
194
|
source: @component.name,
|
|
206
195
|
selector: @selector,
|
|
@@ -208,7 +197,7 @@ module ArchSpec
|
|
|
208
197
|
scope: @scope,
|
|
209
198
|
except: except
|
|
210
199
|
)
|
|
211
|
-
@component.definition.add_rule(rule)
|
|
200
|
+
@component.definition.add_rule(Rules.with_reason(rule, because))
|
|
212
201
|
@component
|
|
213
202
|
end
|
|
214
203
|
|
|
@@ -224,7 +213,7 @@ module ArchSpec
|
|
|
224
213
|
end
|
|
225
214
|
|
|
226
215
|
def validate_template!(template)
|
|
227
|
-
return unless template.is_a?(String)
|
|
216
|
+
return unless template.is_a?(String) && template.include?('%')
|
|
228
217
|
|
|
229
218
|
captures = @selector.regex.names.to_h { |name| [name.to_sym, name] }
|
|
230
219
|
template % captures
|
|
@@ -8,13 +8,13 @@ module ArchSpec
|
|
|
8
8
|
attr_reader :source, :method_names, :receiver
|
|
9
9
|
|
|
10
10
|
def initialize(source, methods, receiver: :any)
|
|
11
|
-
unless %i[any none].include?(receiver)
|
|
12
|
-
raise Error, "cannot_call receiver: must be :any or
|
|
11
|
+
unless %i[any none].include?(receiver) || receiver.is_a?(String)
|
|
12
|
+
raise Error, "cannot_call receiver: must be :any, :none or a constant name, got #{receiver.inspect}"
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
@source = source.to_sym
|
|
16
16
|
@method_names = Array(methods).flatten.map(&:to_sym)
|
|
17
|
-
@receiver = receiver
|
|
17
|
+
@receiver = receiver.is_a?(String) ? receiver.sub(/\A::/, '') : receiver
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def merge_key
|
|
@@ -33,22 +33,42 @@ module ArchSpec
|
|
|
33
33
|
def evaluate(graph)
|
|
34
34
|
graph.edges.filter_map do |edge|
|
|
35
35
|
next unless edge.type == :calls_named_method
|
|
36
|
-
|
|
36
|
+
forbidden = forbidden_name(edge)
|
|
37
|
+
next unless forbidden
|
|
37
38
|
next if receiver == :none && edge.receiver != :none
|
|
39
|
+
next if receiver.is_a?(String) && !receiver_matches?(graph, edge)
|
|
38
40
|
next unless graph.source_components_for(edge).include?(source)
|
|
39
41
|
next if own_method_call?(graph, edge)
|
|
40
42
|
|
|
41
43
|
Diagnostic.new(
|
|
42
44
|
rule: id,
|
|
43
|
-
message: "#{source} must not call ##{
|
|
45
|
+
message: "#{source} must not call ##{forbidden}",
|
|
44
46
|
location: edge.location,
|
|
45
|
-
evidence:
|
|
47
|
+
evidence: evidence_for(graph, edge)
|
|
46
48
|
)
|
|
47
49
|
end
|
|
48
50
|
end
|
|
49
51
|
|
|
50
52
|
private
|
|
51
53
|
|
|
54
|
+
def forbidden_name(edge)
|
|
55
|
+
return edge.to.to_sym if method_names.include?(edge.to.to_sym)
|
|
56
|
+
return edge.resolved_method if edge.resolved_method && method_names.include?(edge.resolved_method)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def receiver_matches?(graph, edge)
|
|
60
|
+
return false unless edge.resolved_receiver
|
|
61
|
+
return true if edge.resolved_receiver == receiver
|
|
62
|
+
|
|
63
|
+
graph.ancestor_names(edge.resolved_receiver, scope: edge.receiver_scope || :instance).first.include?(receiver)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def evidence_for(graph, edge)
|
|
67
|
+
evidence = "#{graph.edge_source_name(edge)} calls #{edge.to}"
|
|
68
|
+
evidence = "#{evidence} (alias of #{edge.resolved_method})" if edge.resolved_method
|
|
69
|
+
receiver.is_a?(String) ? "#{evidence} on #{edge.resolved_receiver}" : evidence
|
|
70
|
+
end
|
|
71
|
+
|
|
52
72
|
# A receiverless call to a method the class itself defines (directly,
|
|
53
73
|
# inherited, or via attr_*/attribute/delegate) is a call to its own API.
|
|
54
74
|
def own_method_call?(graph, edge)
|
|
@@ -63,15 +83,23 @@ module ArchSpec
|
|
|
63
83
|
# component that do not implement the method, counting inherited and
|
|
64
84
|
# mixed-in methods.
|
|
65
85
|
class MustImplementRule
|
|
66
|
-
attr_reader :source, :method_name
|
|
86
|
+
attr_reader :source, :method_name, :scope, :arity, :keywords
|
|
87
|
+
|
|
88
|
+
def initialize(source, method_name, scope: :instance, arity: nil, keywords: nil)
|
|
89
|
+
ProtocolEvidence.validate_scope!(scope, for_rule: 'must_implement')
|
|
90
|
+
if arity && (!arity.is_a?(Integer) || arity.negative?)
|
|
91
|
+
raise Error, "must_implement arity: must be a non-negative Integer, got #{arity.inspect}"
|
|
92
|
+
end
|
|
67
93
|
|
|
68
|
-
def initialize(source, method_name)
|
|
69
94
|
@source = source.to_sym
|
|
70
95
|
@method_name = method_name.to_sym
|
|
96
|
+
@scope = scope
|
|
97
|
+
@arity = arity
|
|
98
|
+
@keywords = Array(keywords).compact.map(&:to_sym).to_set
|
|
71
99
|
end
|
|
72
100
|
|
|
73
101
|
def merge_key
|
|
74
|
-
[self.class, source, method_name]
|
|
102
|
+
[self.class, source, method_name, scope, arity, keywords]
|
|
75
103
|
end
|
|
76
104
|
|
|
77
105
|
def id
|
|
@@ -80,15 +108,16 @@ module ArchSpec
|
|
|
80
108
|
|
|
81
109
|
def evaluate(graph)
|
|
82
110
|
constants_for(graph).filter_map do |constant|
|
|
83
|
-
|
|
84
|
-
|
|
111
|
+
definitions, unresolved = graph.effective_method_definitions(constant.name, scope)
|
|
112
|
+
implementations = definitions.select { |definition| definition.name == method_name }
|
|
113
|
+
next if requirement_met?(implementations)
|
|
85
114
|
|
|
86
115
|
Diagnostic.new(
|
|
87
116
|
rule: id,
|
|
88
|
-
message: "#{constant.name} must implement
|
|
117
|
+
message: "#{constant.name} must implement #{method_label}#{requirement_clause}",
|
|
89
118
|
location: constant.location,
|
|
90
|
-
evidence:
|
|
91
|
-
confidence: unresolved.empty? ? :high : :medium
|
|
119
|
+
evidence: evidence_for(constant, definitions, implementations, unresolved),
|
|
120
|
+
confidence: unresolved.empty? && signature_known?(implementations) ? :high : :medium
|
|
92
121
|
)
|
|
93
122
|
end
|
|
94
123
|
end
|
|
@@ -98,23 +127,73 @@ module ArchSpec
|
|
|
98
127
|
def constants_for(graph)
|
|
99
128
|
ProtocolEvidence.constants_for(graph, source)
|
|
100
129
|
end
|
|
130
|
+
|
|
131
|
+
def requirement_met?(implementations)
|
|
132
|
+
return false if implementations.empty?
|
|
133
|
+
return true unless signature_required?
|
|
134
|
+
|
|
135
|
+
implementations.any? do |definition|
|
|
136
|
+
definition.signatures.any? do |signature|
|
|
137
|
+
(arity.nil? || signature.accepts_arity?(arity)) && signature.accepts_keywords?(keywords)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def signature_required?
|
|
143
|
+
!arity.nil? || keywords.any?
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def signature_known?(implementations)
|
|
147
|
+
!signature_required? || implementations.any? { |definition| definition.signatures.any? }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def method_label
|
|
151
|
+
"#{scope == :class ? '.' : '#'}#{method_name}"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def requirement_clause
|
|
155
|
+
requirements = []
|
|
156
|
+
requirements << "#{arity} positional #{arity == 1 ? 'argument' : 'arguments'}" if arity
|
|
157
|
+
requirements << "keywords #{keywords.map { |name| "#{name}:" }.sort.join(', ')}" if keywords.any?
|
|
158
|
+
requirements.empty? ? '' : " accepting #{requirements.join(' and ')}"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def evidence_for(constant, definitions, implementations, unresolved)
|
|
162
|
+
if implementations.empty?
|
|
163
|
+
return ProtocolEvidence.for(constant, definitions.map(&:name).to_set, unresolved, scope: scope)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
signatures = implementations.flat_map(&:signatures)
|
|
167
|
+
detail =
|
|
168
|
+
if signatures.empty?
|
|
169
|
+
'has no recorded signature'
|
|
170
|
+
else
|
|
171
|
+
"accepts #{signatures.map(&:describe).uniq.join(' or ')}"
|
|
172
|
+
end
|
|
173
|
+
evidence = "#{constant.name} #{method_label} #{detail}"
|
|
174
|
+
return evidence if unresolved.empty?
|
|
175
|
+
|
|
176
|
+
"#{evidence}; unresolved ancestors: #{unresolved.to_a.sort.join(', ')}"
|
|
177
|
+
end
|
|
101
178
|
end
|
|
102
179
|
|
|
103
180
|
# Backs ArchSpec::DSL::ComponentProxy#must_implement_one_of. Flags classes
|
|
104
181
|
# that implement none of the named methods.
|
|
105
182
|
class MustImplementOneOfRule
|
|
106
|
-
attr_reader :source, :method_names
|
|
183
|
+
attr_reader :source, :method_names, :scope
|
|
107
184
|
|
|
108
|
-
def initialize(source, method_names)
|
|
185
|
+
def initialize(source, method_names, scope: :instance)
|
|
186
|
+
ProtocolEvidence.validate_scope!(scope, for_rule: 'must_implement_one_of')
|
|
109
187
|
@source = source.to_sym
|
|
110
188
|
names = Array(method_names).flatten.compact
|
|
111
189
|
raise Error, 'must_implement_one_of requires at least one method' if names.empty?
|
|
112
190
|
|
|
113
191
|
@method_names = names.map(&:to_sym)
|
|
192
|
+
@scope = scope
|
|
114
193
|
end
|
|
115
194
|
|
|
116
195
|
def merge_key
|
|
117
|
-
[self.class, source]
|
|
196
|
+
[self.class, source, scope]
|
|
118
197
|
end
|
|
119
198
|
|
|
120
199
|
def merge!(other)
|
|
@@ -128,14 +207,21 @@ module ArchSpec
|
|
|
128
207
|
|
|
129
208
|
def evaluate(graph)
|
|
130
209
|
constants_for(graph).filter_map do |constant|
|
|
131
|
-
methods, unresolved =
|
|
210
|
+
methods, unresolved =
|
|
211
|
+
if scope == :class
|
|
212
|
+
graph.effective_class_methods(constant.name)
|
|
213
|
+
else
|
|
214
|
+
graph.effective_instance_methods(constant.name)
|
|
215
|
+
end
|
|
132
216
|
next if method_names.any? { |method_name| methods.include?(method_name) }
|
|
133
217
|
|
|
218
|
+
sigil = scope == :class ? '.' : '#'
|
|
219
|
+
choices = method_names.map { |name| "#{sigil}#{name}" }.join(', ')
|
|
134
220
|
Diagnostic.new(
|
|
135
221
|
rule: id,
|
|
136
|
-
message: "#{constant.name} must implement one of #{
|
|
222
|
+
message: "#{constant.name} must implement one of #{choices}",
|
|
137
223
|
location: constant.location,
|
|
138
|
-
evidence: ProtocolEvidence.for(constant, methods, unresolved),
|
|
224
|
+
evidence: ProtocolEvidence.for(constant, methods, unresolved, scope: scope),
|
|
139
225
|
confidence: unresolved.empty? ? :high : :medium
|
|
140
226
|
)
|
|
141
227
|
end
|
|
@@ -153,6 +239,14 @@ module ArchSpec
|
|
|
153
239
|
module ProtocolEvidence
|
|
154
240
|
module_function
|
|
155
241
|
|
|
242
|
+
VALID_SCOPES = %i[instance class].freeze
|
|
243
|
+
|
|
244
|
+
def validate_scope!(scope, for_rule:)
|
|
245
|
+
return if VALID_SCOPES.include?(scope)
|
|
246
|
+
|
|
247
|
+
raise Error, "#{for_rule} scope: must be :instance or :class, got #{scope.inspect}"
|
|
248
|
+
end
|
|
249
|
+
|
|
156
250
|
def constants_for(graph, source)
|
|
157
251
|
component = graph.components[source]
|
|
158
252
|
return [] unless component
|
|
@@ -160,8 +254,9 @@ module ArchSpec
|
|
|
160
254
|
graph.constants_for_component(source).select(&:class?).uniq(&:name)
|
|
161
255
|
end
|
|
162
256
|
|
|
163
|
-
def for(constant, methods, unresolved)
|
|
164
|
-
|
|
257
|
+
def for(constant, methods, unresolved, scope: :instance)
|
|
258
|
+
label = scope == :class ? 'class methods' : 'methods'
|
|
259
|
+
evidence = "#{constant.name} #{label}: #{methods.empty? ? '(none)' : methods.to_a.sort.join(', ')}"
|
|
165
260
|
return evidence if unresolved.empty?
|
|
166
261
|
|
|
167
262
|
"#{evidence}; unresolved ancestors: #{unresolved.to_a.sort.join(', ')}"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ArchSpec
|
|
4
|
+
module Rules
|
|
5
|
+
# Attaches a human reason to any rule without coupling that metadata to the
|
|
6
|
+
# rule's implementation or to diagnostic fingerprints.
|
|
7
|
+
module Reasoned
|
|
8
|
+
attr_reader :archspec_because
|
|
9
|
+
|
|
10
|
+
def archspec_because=(reason)
|
|
11
|
+
return unless reason
|
|
12
|
+
|
|
13
|
+
reason = reason.to_s.strip
|
|
14
|
+
raise Error, 'because: must not be empty' if reason.empty?
|
|
15
|
+
if archspec_because && archspec_because != reason
|
|
16
|
+
raise Error, "the same rule cannot have two reasons: #{archspec_because.inspect} and #{reason.inspect}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
@archspec_because = reason
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def evaluate(graph)
|
|
23
|
+
super.map { |diagnostic| diagnostic.with_reason(archspec_because) }
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
def with_reason(rule, because)
|
|
30
|
+
return rule unless because
|
|
31
|
+
|
|
32
|
+
rule.extend(Reasoned) unless rule.is_a?(Reasoned)
|
|
33
|
+
rule.archspec_because = because
|
|
34
|
+
rule
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -2,10 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require 'pathname'
|
|
4
4
|
|
|
5
|
-
require_relative 'value_object'
|
|
6
|
-
|
|
7
5
|
module ArchSpec
|
|
8
|
-
SourceLocation =
|
|
6
|
+
SourceLocation = Data.define(:path, :line, :column, :end_line, :end_column) do
|
|
9
7
|
def self.from_prism(path, location)
|
|
10
8
|
new(path, location.start_line, location.start_column + 1, location.end_line, location.end_column + 1)
|
|
11
9
|
end
|
data/lib/archspec/version.rb
CHANGED
data/lib/archspec.rb
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'archspec/error'
|
|
4
4
|
require_relative 'archspec/version'
|
|
5
|
-
require_relative 'archspec/value_object'
|
|
6
5
|
require_relative 'archspec/source_location'
|
|
7
6
|
require_relative 'archspec/diagnostic'
|
|
8
7
|
require_relative 'archspec/component_spec'
|
|
@@ -10,9 +9,11 @@ require_relative 'archspec/model'
|
|
|
10
9
|
require_relative 'archspec/definition'
|
|
11
10
|
require_relative 'archspec/todo'
|
|
12
11
|
require_relative 'archspec/dsl'
|
|
12
|
+
require_relative 'archspec/rubydex_index'
|
|
13
13
|
require_relative 'archspec/analyzer'
|
|
14
14
|
require_relative 'archspec/evaluator'
|
|
15
15
|
require_relative 'archspec/architectures'
|
|
16
|
+
require_relative 'archspec/rules/reasoned'
|
|
16
17
|
require_relative 'archspec/rules/component_rules'
|
|
17
18
|
require_relative 'archspec/rules/concern_rules'
|
|
18
19
|
require_relative 'archspec/rules/dependency_rules'
|
|
@@ -30,7 +31,8 @@ require_relative 'archspec/cli'
|
|
|
30
31
|
#
|
|
31
32
|
# You describe components, dependencies, and boundaries in an +Archspec.rb+
|
|
32
33
|
# file written in the ArchSpec::DSL, then run <tt>archspec check</tt> to verify
|
|
33
|
-
# every change. ArchSpec
|
|
34
|
+
# every change. ArchSpec indexes Ruby source with Rubydex, uses Prism for a
|
|
35
|
+
# handful of syntax-specific facts, and never boots the app.
|
|
34
36
|
#
|
|
35
37
|
# The DSL is the public API. An +Archspec.rb+ file is evaluated directly:
|
|
36
38
|
#
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: archspec
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.1.0.rc2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Carmine Paolino
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: prism
|
|
@@ -24,6 +24,20 @@ dependencies:
|
|
|
24
24
|
- - ">="
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '1.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rubydex
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: 0.4.0
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: 0.4.0
|
|
27
41
|
- !ruby/object:Gem::Dependency
|
|
28
42
|
name: minitest
|
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -73,8 +87,8 @@ dependencies:
|
|
|
73
87
|
- !ruby/object:Gem::Version
|
|
74
88
|
version: '6.6'
|
|
75
89
|
description: A static architecture linter for Ruby and Rails. Declare your components,
|
|
76
|
-
dependencies, and boundaries in one file, then check every change in CI. It
|
|
77
|
-
source with
|
|
90
|
+
dependencies, and boundaries in one file, then check every change in CI. It indexes
|
|
91
|
+
source with Rubydex and never boots the app.
|
|
78
92
|
email:
|
|
79
93
|
- carmine@paolino.me
|
|
80
94
|
executables:
|
|
@@ -100,6 +114,7 @@ files:
|
|
|
100
114
|
- lib/archspec/formatters/style.rb
|
|
101
115
|
- lib/archspec/formatters/text.rb
|
|
102
116
|
- lib/archspec/model.rb
|
|
117
|
+
- lib/archspec/rubydex_index.rb
|
|
103
118
|
- lib/archspec/rules/component_rules.rb
|
|
104
119
|
- lib/archspec/rules/concern_rules.rb
|
|
105
120
|
- lib/archspec/rules/cycle_rule.rb
|
|
@@ -107,9 +122,9 @@ files:
|
|
|
107
122
|
- lib/archspec/rules/naming_rules.rb
|
|
108
123
|
- lib/archspec/rules/privacy_rule.rb
|
|
109
124
|
- lib/archspec/rules/protocol_rules.rb
|
|
125
|
+
- lib/archspec/rules/reasoned.rb
|
|
110
126
|
- lib/archspec/source_location.rb
|
|
111
127
|
- lib/archspec/todo.rb
|
|
112
|
-
- lib/archspec/value_object.rb
|
|
113
128
|
- lib/archspec/version.rb
|
|
114
129
|
homepage: https://archspecrb.dev
|
|
115
130
|
licenses:
|
|
@@ -129,7 +144,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
129
144
|
requirements:
|
|
130
145
|
- - ">="
|
|
131
146
|
- !ruby/object:Gem::Version
|
|
132
|
-
version: 3.
|
|
147
|
+
version: '3.2'
|
|
133
148
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
134
149
|
requirements:
|
|
135
150
|
- - ">="
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ArchSpec
|
|
4
|
-
# A frozen value type with positional or keyword construction and #with,
|
|
5
|
-
# mirroring Ruby 3.2's Data.define. Deliberately hand-rolled: the gem
|
|
6
|
-
# supports Ruby 3.1, where Data does not exist. Do not replace this with
|
|
7
|
-
# Data until the required Ruby version reaches 3.2.
|
|
8
|
-
module ValueObject
|
|
9
|
-
def self.define(*members, &block)
|
|
10
|
-
klass = Struct.new(*members) do
|
|
11
|
-
members.each { |member| undef_method :"#{member}=" }
|
|
12
|
-
|
|
13
|
-
def initialize(*values, **keywords)
|
|
14
|
-
member_names = self.class.members
|
|
15
|
-
|
|
16
|
-
if keywords.any?
|
|
17
|
-
raise ArgumentError, 'expected positional arguments or keyword arguments, not both' unless values.empty?
|
|
18
|
-
|
|
19
|
-
unknown = keywords.keys - member_names
|
|
20
|
-
raise ArgumentError, "unknown keyword: #{unknown.first.inspect}" if unknown.any?
|
|
21
|
-
|
|
22
|
-
missing = member_names - keywords.keys
|
|
23
|
-
raise ArgumentError, "missing keyword: #{missing.first.inspect}" if missing.any?
|
|
24
|
-
|
|
25
|
-
values = member_names.map { |member| keywords.fetch(member) }
|
|
26
|
-
elsif values.length != member_names.length
|
|
27
|
-
raise ArgumentError, "wrong number of arguments (given #{values.length}, expected #{member_names.length})"
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
super(*values)
|
|
31
|
-
freeze
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def with(**changes)
|
|
35
|
-
unknown = changes.keys - self.class.members
|
|
36
|
-
raise ArgumentError, "unknown keyword: #{unknown.first.inspect}" if unknown.any?
|
|
37
|
-
|
|
38
|
-
self.class.new(**to_h.merge(changes))
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
klass.class_eval(&block) if block
|
|
43
|
-
klass
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|