modspec 0.2.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 925062f2457c7aa978de3d2eabc679f4a33b9177e8ba2f3a80494c46377ff0b8
4
- data.tar.gz: '083c357ffd578b297ae28753e48dccacd08a10d202f336299b037d1d6b08fe9a'
3
+ metadata.gz: 2ca0254068b698f935d90fbfb963362136ae975df3f2dea02d946bd38b53d07b
4
+ data.tar.gz: 0f24f91cbfdd1674e39a1b37a2cd7ac736771d2edefbfc7930dac858a17d03f5
5
5
  SHA512:
6
- metadata.gz: ce9246f0f6b4ce811bf498d52a59c25949ad0016b024ab215c332ce7e1e34fa311c2a78267a3a1b07cb1a2f0f468559c1aeaedb638c4552e9500016ad8a3235b
7
- data.tar.gz: 7132a3d0194c87222d077752784d1d1d7197481d1db1ae356c9f959d8cccd8163f3ef268a2e4e6b75812095768e133ab6da81c629fd3d33b0122f7f2789079fe
6
+ metadata.gz: 5a467d5f5cf70c4df7f20e1b17960f9fc169cd793bfab169f639fee73a4a9838af1f4b6778c7b1e045546ccd3a87fb07f0c3f7a8d4427ea8405acf5aba1c63b4
7
+ data.tar.gz: a2b9ba6b8545bcb5cf7af9a8118f079d3758be7a4342d2aab7342078962308967cfed9b8eb139b86e5ee84aa9f12e9d11a2799b719a12bdb82b3920f06f0b57d
data/.rubocop.yml CHANGED
@@ -17,3 +17,9 @@ plugins:
17
17
 
18
18
  AllCops:
19
19
  TargetRubyVersion: 3.0
20
+
21
+ RSpec/ExampleLength:
22
+ Max: 30
23
+
24
+ RSpec/MultipleMemoizedHelpers:
25
+ Max: 14
data/.rubocop_todo.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2026-05-05 14:09:35 UTC using RuboCop version 1.86.1.
3
+ # on 2026-05-06 04:01:19 UTC using RuboCop version 1.86.1.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
@@ -11,16 +11,13 @@ Gemspec/RequiredRubyVersion:
11
11
  Exclude:
12
12
  - 'modspec.gemspec'
13
13
 
14
- # Offense count: 33
14
+ # Offense count: 31
15
15
  # This cop supports safe autocorrection (--autocorrect).
16
16
  # Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
17
17
  # URISchemes: http, https
18
18
  Layout/LineLength:
19
19
  Exclude:
20
- - 'lib/modspec/conformance_class.rb'
21
20
  - 'lib/modspec/conformance_test.rb'
22
- - 'lib/modspec/normative_statement.rb'
23
- - 'lib/modspec/normative_statements_class.rb'
24
21
  - 'lib/modspec/suite.rb'
25
22
  - 'spec/modspec/conformance_class_spec.rb'
26
23
  - 'spec/modspec/conformance_test_spec.rb'
@@ -28,28 +25,14 @@ Layout/LineLength:
28
25
  - 'spec/modspec/normative_statements_class_spec.rb'
29
26
  - 'spec/modspec/suite_spec.rb'
30
27
 
31
- # Offense count: 4
32
- # Configuration parameters: AllowComments, AllowEmptyLambdas.
33
- Lint/EmptyBlock:
34
- Exclude:
35
- - 'spec/modspec/conformance_class_spec.rb'
36
- - 'spec/modspec/suite_spec.rb'
37
-
38
- # Offense count: 1
39
- # This cop supports safe autocorrection (--autocorrect).
40
- # Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods, NotImplementedExceptions.
41
- # NotImplementedExceptions: NotImplementedError
42
- Lint/UnusedMethodArgument:
43
- Exclude:
44
- - 'lib/modspec/normative_statement.rb'
45
-
46
- # Offense count: 7
28
+ # Offense count: 6
47
29
  # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
48
30
  Metrics/AbcSize:
49
31
  Exclude:
32
+ - 'lib/modspec/child_container.rb'
50
33
  - 'lib/modspec/suite.rb'
51
34
 
52
- # Offense count: 5
35
+ # Offense count: 4
53
36
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
54
37
  Metrics/CyclomaticComplexity:
55
38
  Exclude:
@@ -58,9 +41,9 @@ Metrics/CyclomaticComplexity:
58
41
  # Offense count: 9
59
42
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
60
43
  Metrics/MethodLength:
61
- Max: 21
44
+ Max: 20
62
45
 
63
- # Offense count: 3
46
+ # Offense count: 2
64
47
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
65
48
  Metrics/PerceivedComplexity:
66
49
  Exclude:
@@ -72,11 +55,6 @@ Performance/CollectionLiteralInLoop:
72
55
  Exclude:
73
56
  - 'lib/modspec/suite.rb'
74
57
 
75
- # Offense count: 5
76
- # Configuration parameters: CountAsOne.
77
- RSpec/ExampleLength:
78
- Max: 15
79
-
80
58
  # Offense count: 1
81
59
  # This cop supports safe autocorrection (--autocorrect).
82
60
  RSpec/ExpectActual:
@@ -90,15 +68,10 @@ RSpec/IndexedLet:
90
68
  - 'spec/modspec/normative_statements_class_spec.rb'
91
69
  - 'spec/modspec/suite_spec.rb'
92
70
 
93
- # Offense count: 7
71
+ # Offense count: 13
94
72
  RSpec/MultipleExpectations:
95
73
  Max: 9
96
74
 
97
- # Offense count: 7
98
- # Configuration parameters: AllowSubject.
99
- RSpec/MultipleMemoizedHelpers:
100
- Max: 12
101
-
102
75
  # Offense count: 2
103
76
  RSpec/RepeatedExample:
104
77
  Exclude:
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Modspec
4
+ module ChildContainer
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+ def validates_children(collection_name, empty_label:, child_label:)
11
+ define_method(:validate_children_presence) do
12
+ children = send(collection_name)
13
+ if children.nil? || children.empty?
14
+ ["#{empty_label} #{identifier} has no child #{child_label}"]
15
+ else
16
+ []
17
+ end
18
+ end
19
+
20
+ define_method(:validate_children_identifier_prefix) do
21
+ children = send(collection_name)
22
+ return [] unless children
23
+
24
+ expected_prefix = "#{identifier}/"
25
+ children.filter_map do |child|
26
+ unless child.identifier.to_s.start_with?(expected_prefix)
27
+ msg = "#{child_label} #{child.identifier} "
28
+ msg += "does not share the expected prefix #{expected_prefix}"
29
+ msg
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "lutaml/model"
4
- require_relative "conformance_test"
5
- require_relative "identifier"
6
4
 
7
5
  module Modspec
8
6
  class ConformanceClass < Lutaml::Model::Serializable
7
+ include ChildContainer
8
+
9
9
  attribute :identifier, Identifier
10
10
  attribute :name, :string
11
11
  attribute :description, :string
@@ -17,6 +17,9 @@ module Modspec
17
17
  attribute :belongs_to, Identifier, collection: true
18
18
  attribute :reference, :string
19
19
 
20
+ validates_children :tests, empty_label: "Conformance class",
21
+ child_label: "conformance tests"
22
+
20
23
  xml do
21
24
  element "conformance-class"
22
25
  map_attribute "identifier", to: :identifier
@@ -33,29 +36,10 @@ module Modspec
33
36
 
34
37
  def validate
35
38
  errors = super
36
- errors.concat(validate_identifier_prefix)
37
- errors.concat(validate_class_children_mapping)
39
+ errors.concat(validate_children_identifier_prefix)
40
+ errors.concat(validate_children_presence)
38
41
  errors.concat(tests.flat_map(&:validate))
39
42
  errors
40
43
  end
41
-
42
- private
43
-
44
- def validate_class_children_mapping
45
- if tests.nil? || tests.empty?
46
- ["Conformance class #{identifier} has no child conformance tests"]
47
- else
48
- []
49
- end
50
- end
51
-
52
- def validate_identifier_prefix
53
- errors = []
54
- expected_prefix = "#{identifier}/"
55
- tests&.each do |test|
56
- errors << "Conformance test #{test.identifier} does not share the expected prefix #{expected_prefix}" unless test.identifier.to_s.start_with?(expected_prefix)
57
- end
58
- errors
59
- end
60
44
  end
61
45
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "lutaml/model"
4
- require_relative "identifier"
5
4
 
6
5
  module Modspec
7
6
  class ConformanceTest < Lutaml::Model::Serializable
@@ -4,6 +4,5 @@ require "lutaml/model"
4
4
 
5
5
  module Modspec
6
6
  class Identifier < Lutaml::Model::Type::String
7
- # attribute :identifier, :string
8
7
  end
9
8
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "lutaml/model"
4
- require_relative "identifier"
5
4
 
6
5
  module Modspec
7
6
  class NormativeStatementPart < Lutaml::Model::Serializable
@@ -46,43 +45,8 @@ module Modspec
46
45
  map_element "parts", to: :parts
47
46
  end
48
47
 
49
- def validate(suite = nil, register: Lutaml::Model::Config.default_register)
50
- errors = super()
51
- errors.concat(validate_dependencies(suite)) if suite
52
- errors.concat(validate_nested_requirement)
53
- errors
54
- end
55
-
56
- private
57
-
58
- def all_dependencies
59
- (
60
- (dependencies || []) +
61
- (indirect_dependency || []) +
62
- (implements || [])
63
- ).flatten.compact
64
- end
65
-
66
- def validate_dependencies(suite)
67
- errors = []
68
- all_identifiers = suite.all_identifiers.map(&:to_s)
69
- all_dependencies.each do |dep|
70
- errors << "Requirement #{identifier} has an invalid dependency: #{dep}" unless all_identifiers.include?(dep)
71
- end
72
- errors
73
- end
74
-
75
- def validate_nested_requirement
76
- if has_parent_requirement?
77
- ["Nested requirement detected: #{identifier}"]
78
- else
79
- []
80
- end
81
- end
82
-
83
- def has_parent_requirement?
84
- # Implementation depends on how you determine if a requirement is nested
85
- false
48
+ def validate(register: Lutaml::Model::Config.default_register)
49
+ super
86
50
  end
87
51
  end
88
52
  end
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "lutaml/model"
4
- require_relative "normative_statement"
5
- require_relative "identifier"
6
4
 
7
5
  module Modspec
8
6
  class NormativeStatementsClass < Lutaml::Model::Serializable
7
+ include ChildContainer
8
+
9
9
  attribute :identifier, Identifier
10
10
  attribute :name, :string
11
11
  attribute :description, :string
@@ -18,6 +18,9 @@ module Modspec
18
18
  attribute :reference, :string
19
19
  attribute :source, :string
20
20
 
21
+ validates_children :normative_statements, empty_label: "Requirement class",
22
+ child_label: "requirements"
23
+
21
24
  xml do
22
25
  element "normative-statements-class"
23
26
  map_attribute "identifier", to: :identifier
@@ -33,31 +36,12 @@ module Modspec
33
36
  map_element "source", to: :source
34
37
  end
35
38
 
36
- def validate(suite = nil)
37
- errors = super()
38
- errors.concat(validate_identifier_prefix)
39
- errors.concat(validate_class_children_mapping)
40
- errors.concat(normative_statements.flat_map { |n| n.validate(suite) })
39
+ def validate
40
+ errors = super
41
+ errors.concat(validate_children_identifier_prefix)
42
+ errors.concat(validate_children_presence)
43
+ errors.concat(normative_statements.flat_map(&:validate))
41
44
  errors
42
45
  end
43
-
44
- private
45
-
46
- def validate_identifier_prefix
47
- return [] if normative_statements.nil? || normative_statements.empty?
48
-
49
- expected_prefix = "#{identifier}/"
50
- normative_statements.each_with_object([]) do |statement, errors|
51
- errors << "Normative statement #{statement.identifier} does not share the expected prefix #{expected_prefix}" unless statement.identifier.to_s.start_with?(expected_prefix)
52
- end
53
- end
54
-
55
- def validate_class_children_mapping
56
- if normative_statements.nil? || normative_statements.empty?
57
- ["Requirement class #{identifier} has no child requirements"]
58
- else
59
- []
60
- end
61
- end
62
46
  end
63
47
  end
data/lib/modspec/suite.rb CHANGED
@@ -21,15 +21,13 @@ module Modspec
21
21
 
22
22
  def validate
23
23
  setup_relationships
24
- self.all_identifiers = nil
24
+ reset_statement_index
25
25
  errors = super
26
26
  errors.concat(validate_cycles)
27
27
  errors.concat(validate_label_uniqueness)
28
28
  errors.concat(validate_dependencies)
29
29
  unless normative_statements_classes.nil?
30
- errors.concat(normative_statements_classes.flat_map do |n|
31
- n.validate(self)
32
- end)
30
+ errors.concat(normative_statements_classes.flat_map(&:validate))
33
31
  end
34
32
  errors.concat(conformance_classes.flat_map(&:validate)) unless conformance_classes.nil?
35
33
  errors
@@ -42,7 +40,7 @@ module Modspec
42
40
  end
43
41
 
44
42
  combined_suite = dup
45
- combined_suite.all_identifiers = nil
43
+ combined_suite.reset_statement_index
46
44
  if other_suite.normative_statements_classes
47
45
  combined_suite.normative_statements_classes ||= []
48
46
  combined_suite.normative_statements_classes += other_suite.normative_statements_classes
@@ -63,19 +61,17 @@ module Modspec
63
61
  combined_suite
64
62
  end
65
63
 
66
- def all_identifiers
67
- return @all_identifiers if @all_identifiers
68
-
69
- nsc = normative_statements_classes || []
70
- cc = conformance_classes || []
64
+ def statement_index
65
+ @statement_index ||= build_statement_index
66
+ end
71
67
 
72
- @all_identifiers = (nsc.flat_map(&:normative_statements) +
73
- cc.flat_map(&:tests) +
74
- nsc +
75
- cc).map(&:identifier)
68
+ def reset_statement_index
69
+ @statement_index = nil
76
70
  end
77
71
 
78
- attr_writer :all_identifiers
72
+ def all_identifiers
73
+ statement_index.keys
74
+ end
79
75
 
80
76
  def resolve_conflicts(other_suite)
81
77
  resolve_conflicts_for(normative_statements_classes,
@@ -95,18 +91,17 @@ module Modspec
95
91
  end
96
92
 
97
93
  def setup_relationships
98
- all_requirements = if normative_statements_classes
99
- normative_statements_classes.flat_map(&:normative_statements)
100
- else
101
- []
102
- end
94
+ return unless normative_statements_classes && conformance_classes
103
95
 
104
- return unless conformance_classes
96
+ req_index = normative_statements_classes
97
+ .flat_map(&:normative_statements)
98
+ .to_h { |r| [r.identifier.to_s, r] }
105
99
 
106
100
  conformance_classes.each do |cc|
107
101
  cc.tests.each do |ct|
108
- ct.corresponding_requirements = all_requirements.select do |r|
109
- Array(ct.targets).map(&:to_s).include?(r.identifier.to_s)
102
+ targets = Array(ct.targets).map(&:to_s)
103
+ ct.corresponding_requirements = targets.filter_map do |t|
104
+ req_index[t]
110
105
  end
111
106
  ct.parent_class = cc
112
107
  end
@@ -132,13 +127,16 @@ module Modspec
132
127
  end
133
128
 
134
129
  def merge_attributes(existing_item, other_item)
135
- existing_item.class.attribute_names.each do |attr|
130
+ existing_item.class.attributes.each_key do |attr|
136
131
  next if %i[identifier name].include?(attr)
137
132
 
138
- if existing_item.send(attr).is_a?(Array)
139
- existing_item.send(attr).concat(other_item.send(attr)).uniq!
140
- elsif existing_item.send(attr).nil?
141
- existing_item.send("#{attr}=", other_item.send(attr))
133
+ existing_val = existing_item.send(attr)
134
+ other_val = other_item.send(attr)
135
+
136
+ if existing_val.is_a?(Array)
137
+ existing_item.send(attr).concat(other_val).uniq!
138
+ elsif existing_val.nil? && !other_val.nil?
139
+ existing_item.send("#{attr}=", other_val)
142
140
  end
143
141
  end
144
142
  end
@@ -149,21 +147,20 @@ module Modspec
149
147
  cycles.map { |cycle| "Cycle detected: #{cycle.join(' -> ')}" }
150
148
  end
151
149
 
152
- # Combine all statements into a single array
153
- # This includes both normative statements and conformance tests
154
150
  def all_statements
155
- nsc = if normative_statements_classes
156
- normative_statements_classes.flat_map(&:normative_statements)
157
- else
158
- []
159
- end
160
- cc = if conformance_classes
161
- conformance_classes.flat_map(&:tests)
162
- else
163
- []
164
- end
165
-
166
- nsc + cc
151
+ statement_index.values
152
+ end
153
+
154
+ def each_statement(&block)
155
+ normative_statements_classes&.each do |nsc|
156
+ yield nsc
157
+ nsc.normative_statements.each(&block)
158
+ end
159
+
160
+ conformance_classes&.each do |cc|
161
+ yield cc
162
+ cc.tests.each(&block)
163
+ end
167
164
  end
168
165
 
169
166
  def build_dependency_graph
@@ -171,15 +168,14 @@ module Modspec
171
168
 
172
169
  all_statements.each do |statement|
173
170
  id = statement.identifier.to_s
174
- graph[id] = Set.new
175
-
176
- # Define all dependency-like properties to check
177
- dependency_properties = %i[dependencies indirect_dependency implements
178
- targets]
171
+ deps = Set.new
179
172
 
180
- dependency_properties.each do |property|
181
- graph[id].merge(statement.send(property).map(&:to_s)) if statement.respond_to?(property) && !statement.send(property).nil?
173
+ %i[dependencies indirect_dependency implements targets].each do |prop|
174
+ refs = statement.send(prop) if statement.respond_to?(prop)
175
+ deps.merge(refs.map(&:to_s)) if refs
182
176
  end
177
+
178
+ graph[id] = deps
183
179
  end
184
180
 
185
181
  graph
@@ -205,20 +201,14 @@ module Modspec
205
201
  recursion_stack.add(node)
206
202
  path.push(node)
207
203
 
208
- # Check if the node exists in the graph and has dependencies
209
- if graph[node]
210
- graph[node].each do |neighbor|
211
- if !visited.include?(neighbor)
212
- cycle = detect_cycle_util(neighbor, graph, visited,
213
- recursion_stack, path)
214
- return cycle if cycle
215
- elsif recursion_stack.include?(neighbor)
216
- return path[path.index(neighbor)..] + [neighbor]
217
- end
204
+ graph[node]&.each do |neighbor|
205
+ if !visited.include?(neighbor)
206
+ cycle = detect_cycle_util(neighbor, graph, visited,
207
+ recursion_stack, path)
208
+ return cycle if cycle
209
+ elsif recursion_stack.include?(neighbor)
210
+ return path[path.index(neighbor)..] + [neighbor]
218
211
  end
219
- else
220
- # If the node doesn't exist in the graph, log a warning
221
- puts "Warning: Node #{node} referenced but not found in the graph"
222
212
  end
223
213
 
224
214
  path.pop
@@ -227,81 +217,60 @@ module Modspec
227
217
  end
228
218
 
229
219
  def validate_label_uniqueness
230
- labels = {}
220
+ seen = {}
231
221
  errors = []
232
- all_statements.each do |statement|
233
- if labels[statement.identifier]
222
+ each_statement do |statement|
223
+ id = statement.identifier.to_s
224
+ if seen[id]
234
225
  errors << "Duplicate identifier found: #{statement.identifier}"
235
226
  else
236
- labels[statement.identifier] = true
227
+ seen[id] = true
237
228
  end
238
229
  end
239
230
  errors
240
231
  end
241
232
 
242
233
  def validate_dependencies
243
- all_identifiers = collect_all_identifiers
234
+ all_ids = statement_index
244
235
 
245
236
  errors = []
246
237
  normative_statements_classes&.each do |nsc|
247
- errors.concat(validate_class_dependencies(nsc, all_identifiers))
238
+ errors.concat(validate_refs(nsc, all_ids, :dependencies))
239
+ errors.concat(validate_refs(nsc, all_ids, :implements))
248
240
  nsc.normative_statements.each do |ns|
249
- errors.concat(validate_statement_dependencies(ns, all_identifiers))
241
+ errors.concat(validate_refs(ns, all_ids, :dependencies))
242
+ errors.concat(validate_refs(ns, all_ids, :indirect_dependency))
243
+ errors.concat(validate_refs(ns, all_ids, :implements))
250
244
  end
251
245
  end
252
246
 
253
247
  conformance_classes&.each do |cc|
254
- errors.concat(validate_class_dependencies(cc, all_identifiers))
248
+ errors.concat(validate_refs(cc, all_ids, :dependencies))
255
249
  cc.tests.each do |ct|
256
- errors.concat(validate_test_targets(ct, all_identifiers))
250
+ errors.concat(validate_refs(ct, all_ids, :dependencies))
251
+ errors.concat(validate_refs(ct, all_ids, :targets))
257
252
  end
258
253
  end
259
254
 
260
255
  errors
261
256
  end
262
257
 
263
- def collect_all_identifiers
264
- identifiers = {}
265
-
266
- normative_statements_classes&.each do |nsc|
267
- identifiers[nsc.identifier.to_s] = nsc
268
- nsc.normative_statements.each do |ns|
269
- identifiers[ns.identifier.to_s] = ns
270
- end
271
- end
272
-
273
- conformance_classes&.each do |cc|
274
- identifiers[cc.identifier.to_s] = cc
275
- cc.tests.each do |ct|
276
- identifiers[ct.identifier.to_s] = ct
277
- end
278
- end
279
-
280
- identifiers
281
- end
282
-
283
- def validate_class_dependencies(klass, all_identifiers)
284
- errors = []
285
- klass.dependencies&.each do |dep|
286
- errors << "Invalid dependency #{dep} in #{klass.identifier}" unless all_identifiers.key?(dep.to_s)
287
- end
288
- errors
258
+ def build_statement_index
259
+ index = {}
260
+ each_statement { |s| index[s.identifier.to_s] = s }
261
+ index
289
262
  end
290
263
 
291
- def validate_statement_dependencies(statement, all_identifiers)
292
- errors = []
293
- statement.dependencies&.each do |dep|
294
- errors << "Invalid dependency #{dep} in #{statement.identifier}" unless all_identifiers.key?(dep.to_s)
295
- end
296
- errors
297
- end
264
+ def validate_refs(obj, all_ids, property)
265
+ refs = obj.send(property)
266
+ return [] unless refs
298
267
 
299
- def validate_test_targets(test, all_identifiers)
300
- errors = []
301
- test.targets&.each do |target|
302
- errors << "Invalid target #{target} in #{test.identifier}" unless all_identifiers.key?(target.to_s)
268
+ refs.filter_map do |ref|
269
+ unless all_ids.key?(ref.to_s)
270
+ "Invalid #{property.to_s.tr('_',
271
+ ' ')} #{ref} in #{obj.identifier}"
272
+ end
303
273
  end
304
- errors
305
274
  end
306
275
  end
307
276
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Modspec
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.2"
5
5
  end
data/lib/modspec.rb CHANGED
@@ -5,14 +5,12 @@ require "lutaml/model"
5
5
  require "set"
6
6
 
7
7
  module Modspec
8
- class Error < StandardError; end
9
-
10
- # Your code goes here...
8
+ autoload :ChildContainer, "modspec/child_container"
9
+ autoload :Identifier, "modspec/identifier"
10
+ autoload :NormativeStatement, "modspec/normative_statement"
11
+ autoload :NormativeStatementPart, "modspec/normative_statement"
12
+ autoload :NormativeStatementsClass, "modspec/normative_statements_class"
13
+ autoload :ConformanceTest, "modspec/conformance_test"
14
+ autoload :ConformanceClass, "modspec/conformance_class"
15
+ autoload :Suite, "modspec/suite"
11
16
  end
12
-
13
- require_relative "modspec/identifier"
14
- require_relative "modspec/normative_statement"
15
- require_relative "modspec/normative_statements_class"
16
- require_relative "modspec/conformance_test"
17
- require_relative "modspec/conformance_class"
18
- require_relative "modspec/suite"
@@ -11,12 +11,12 @@ RSpec.describe Modspec::ConformanceClass do
11
11
  Modspec::NormativeStatement.new(
12
12
  identifier: "/req/basic-ypr/position",
13
13
  name: "Expression of outer frame",
14
- statement: "The `Basic_YPR.position` attribute shall represent the outer frame, specified by an implicit WGS-84 CRS and an implicit EPSG 4461-CS (LTP-ENU) coordinate system and explicit parameters to define the tangent point.",
14
+ statement: "The `Basic_YPR.position` attribute shall represent the outer frame.",
15
15
  ),
16
16
  Modspec::NormativeStatement.new(
17
17
  identifier: "/req/basic-ypr/angles",
18
18
  name: "Expression of inner frame",
19
- statement: "The `Basic_YPR.angles` attribute shall represent the inner frame, which is a rotation-only transformation with Yaw, Pitch, and Roll (YPR) angles.",
19
+ statement: "The `Basic_YPR.angles` attribute shall represent the inner frame.",
20
20
  ),
21
21
  ],
22
22
  )
@@ -35,7 +35,7 @@ RSpec.describe Modspec::ConformanceClass do
35
35
  identifier: "/conf/basic-ypr/position",
36
36
  name: "Verify expression of outer frame",
37
37
  targets: ["/req/basic-ypr/position"],
38
- description: "To confirm that an implementation of a Basic-YPR consists of an Outer Frame specified by an implicit WGS-84 CRS and an implicit EPSG 4461-CS (LTP-ENU) coordinate system and explicit parameters to define the tangent point.",
38
+ description: "To confirm outer frame.",
39
39
  purpose: "Verify that this requirement is satisfied.",
40
40
  test_method: "Inspection",
41
41
  ),
@@ -43,7 +43,7 @@ RSpec.describe Modspec::ConformanceClass do
43
43
  identifier: "/conf/basic-ypr/angles",
44
44
  name: "Verify expression of inner frame",
45
45
  targets: ["/req/basic-ypr/angles"],
46
- description: "To confirm that the Inner Frame is expressed as a rotation-only transformation using Yaw, Pitch, and Roll angles.",
46
+ description: "To confirm inner frame.",
47
47
  purpose: "Verify that this requirement is satisfied.",
48
48
  test_method: "Inspection",
49
49
  ),
@@ -88,7 +88,7 @@ RSpec.describe Modspec::ConformanceClass do
88
88
  identifier: "/conf/global/sdu",
89
89
  name: "Verify SDU conformance",
90
90
  targets: ["/req/global/sdu"],
91
- description: "To confirm that an implementation of an SDU conforms to the logical model.",
91
+ description: "To confirm SDU conformance.",
92
92
  purpose: "Verify that this requirement is satisfied.",
93
93
  test_method: "Inspection",
94
94
  ),
@@ -105,11 +105,10 @@ RSpec.describe Modspec::ConformanceClass do
105
105
  identifier: "/conf/tangent-point/height",
106
106
  name: "Verify tangent point height",
107
107
  targets: ["/req/tangent-point/height"],
108
- description: "To confirm that an implementation of a Tangent Point specifies the height of the Tangent Point.",
108
+ description: "To confirm tangent point height.",
109
109
  purpose: "Verify that this requirement is satisfied.",
110
110
  test_method: "Inspection",
111
111
  ),
112
-
113
112
  ],
114
113
  )
115
114
  end
@@ -140,17 +139,25 @@ RSpec.describe Modspec::ConformanceClass do
140
139
  describe "#validate" do
141
140
  it "returns no errors for a valid conformance class" do
142
141
  errors = suite.validate
143
- if errors.any?
144
-
145
- errors.each { |error| }
146
- end
147
142
  expect(errors).to be_empty
148
143
  end
149
144
 
150
145
  it "returns errors if there are no conformance tests" do
151
146
  conformance_class.tests = []
152
147
  errors = conformance_class.validate
153
- expect(errors).not_to be_empty
148
+ expect(errors).to include(a_string_matching(/no child conformance tests/))
149
+ end
150
+
151
+ it "returns errors if test identifier does not share prefix" do
152
+ conformance_class.tests = [
153
+ Modspec::ConformanceTest.new(
154
+ identifier: "/conf/other/test",
155
+ name: "Mismatched test",
156
+ targets: ["/req/basic-ypr/position"],
157
+ ),
158
+ ]
159
+ errors = conformance_class.validate
160
+ expect(errors).to include(a_string_matching(/does not share the expected prefix/))
154
161
  end
155
162
  end
156
163
  end
@@ -5,7 +5,7 @@ RSpec.describe Modspec::ConformanceTest do
5
5
  Modspec::NormativeStatement.new(
6
6
  identifier: "/req/basic-ypr/position",
7
7
  name: "Expression of outer frame",
8
- statement: "The `Basic_YPR.position` attribute shall represent the outer frame, specified by an implicit WGS-84 CRS and an implicit EPSG 4461-CS (LTP-ENU) coordinate system and explicit parameters to define the tangent point.",
8
+ statement: "The `Basic_YPR.position` attribute shall represent the outer frame.",
9
9
  )
10
10
  end
11
11
 
@@ -22,7 +22,7 @@ RSpec.describe Modspec::ConformanceTest do
22
22
  identifier: "/conf/basic-ypr/position",
23
23
  name: "Verify expression of outer frame",
24
24
  targets: ["/req/basic-ypr/position"],
25
- description: "To confirm that an implementation of a Basic-YPR consists of an Outer Frame specified by an implicit WGS-84 CRS and an implicit EPSG 4461-CS (LTP-ENU) coordinate system and explicit parameters to define the tangent point.",
25
+ description: "To confirm outer frame.",
26
26
  purpose: "Verify that this requirement is satisfied.",
27
27
  test_method: "Inspection",
28
28
  )
@@ -45,7 +45,7 @@ RSpec.describe Modspec::ConformanceTest do
45
45
  end
46
46
 
47
47
  before do
48
- suite # Ensure the suite is created and relationships are set up
48
+ suite
49
49
  end
50
50
 
51
51
  it "has an identifier" do
@@ -65,6 +65,24 @@ RSpec.describe Modspec::ConformanceTest do
65
65
  errors = conformance_test.validate
66
66
  expect(errors).to be_empty
67
67
  end
68
+
69
+ it "returns errors when corresponding_requirements is nil" do
70
+ conformance_test.corresponding_requirements = nil
71
+ errors = conformance_test.validate
72
+ expect(errors).to include(a_string_matching(/no corresponding requirements/))
73
+ end
74
+
75
+ it "returns errors when corresponding_requirements is empty" do
76
+ conformance_test.corresponding_requirements = []
77
+ errors = conformance_test.validate
78
+ expect(errors).to include(a_string_matching(/no corresponding requirements/))
79
+ end
80
+
81
+ it "returns errors when parent_class is nil" do
82
+ conformance_test.parent_class = nil
83
+ errors = conformance_test.validate
84
+ expect(errors).to include(a_string_matching(/does not belong to its parent class/))
85
+ end
68
86
  end
69
87
 
70
88
  it "has a corresponding requirement" do
@@ -5,7 +5,7 @@ RSpec.describe Modspec::NormativeStatementsClass do
5
5
  Modspec::NormativeStatement.new(
6
6
  identifier: "/req/basic-ypr/position",
7
7
  name: "Expression of outer frame",
8
- statement: "The `Basic_YPR.position` attribute shall represent the outer frame, specified by an implicit WGS-84 CRS and an implicit EPSG 4461-CS (LTP-ENU) coordinate system and explicit parameters to define the tangent point.",
8
+ statement: "The `Basic_YPR.position` attribute shall represent the outer frame.",
9
9
  )
10
10
  end
11
11
 
@@ -13,7 +13,7 @@ RSpec.describe Modspec::NormativeStatementsClass do
13
13
  Modspec::NormativeStatement.new(
14
14
  identifier: "/req/basic-ypr/angles",
15
15
  name: "Expression of inner frame",
16
- statement: "The `Basic_YPR.angles` attribute shall represent the inner frame, which is a rotation-only transformation with Yaw, Pitch, and Roll (YPR) angles.",
16
+ statement: "The `Basic_YPR.angles` attribute shall represent the inner frame.",
17
17
  )
18
18
  end
19
19
 
@@ -21,7 +21,7 @@ RSpec.describe Modspec::NormativeStatementsClass do
21
21
  described_class.new(
22
22
  identifier: "/req/basic-ypr",
23
23
  name: "Basic-YPR logical model SDU",
24
- description: "The Basic-YPR Target has a simple structure with no options. Position is specified as a point in an LTP-ENU frame and rotation is specified by yaw, pitch, and roll angles specified in decimal degrees.",
24
+ description: "The Basic-YPR Target has a simple structure.",
25
25
  dependencies: ["/req/global", "/req/tangent-point"],
26
26
  normative_statements: [normative_statement1, normative_statement2],
27
27
  )
@@ -55,7 +55,19 @@ RSpec.describe Modspec::NormativeStatementsClass do
55
55
  it "returns errors if there are no normative statements" do
56
56
  normative_statements_class.normative_statements = []
57
57
  errors = normative_statements_class.validate
58
- expect(errors).not_to be_empty
58
+ expect(errors).to include(a_string_matching(/no child requirements/))
59
+ end
60
+
61
+ it "returns errors if statement identifier does not share prefix" do
62
+ normative_statements_class.normative_statements = [
63
+ Modspec::NormativeStatement.new(
64
+ identifier: "/req/other/mismatch",
65
+ name: "Mismatched",
66
+ statement: "stmt",
67
+ ),
68
+ ]
69
+ errors = normative_statements_class.validate
70
+ expect(errors).to include(a_string_matching(/does not share the expected prefix/))
59
71
  end
60
72
  end
61
73
  end
@@ -38,12 +38,204 @@ RSpec.describe Modspec::Suite do
38
38
  .combine(frame_spec_suite)
39
39
 
40
40
  errors = combined_suite.validate
41
- if errors.any?
42
-
43
- errors.each { |error| }
44
- end
45
41
  expect(errors).to be_empty
46
42
  end
43
+
44
+ it "detects duplicate identifiers" do
45
+ suite = described_class.new(
46
+ identifier: "/suite",
47
+ name: "Test",
48
+ normative_statements_classes: [
49
+ Modspec::NormativeStatementsClass.new(
50
+ identifier: "/req/test",
51
+ normative_statements: [
52
+ Modspec::NormativeStatement.new(identifier: "/req/test/a",
53
+ name: "A", statement: "a"),
54
+ Modspec::NormativeStatement.new(identifier: "/req/test/a",
55
+ name: "A2", statement: "a2"),
56
+ ],
57
+ ),
58
+ ],
59
+ conformance_classes: [],
60
+ )
61
+
62
+ errors = suite.validate
63
+ expect(errors).to include(a_string_matching(/Duplicate identifier/))
64
+ end
65
+
66
+ it "detects cross-type identifier collisions" do
67
+ suite = described_class.new(
68
+ identifier: "/suite",
69
+ name: "Test",
70
+ normative_statements_classes: [
71
+ Modspec::NormativeStatementsClass.new(
72
+ identifier: "/req/test",
73
+ normative_statements: [
74
+ Modspec::NormativeStatement.new(identifier: "/req/test/a",
75
+ name: "A", statement: "a"),
76
+ ],
77
+ ),
78
+ ],
79
+ conformance_classes: [
80
+ Modspec::ConformanceClass.new(
81
+ identifier: "/conf/test",
82
+ tests: [
83
+ Modspec::ConformanceTest.new(
84
+ identifier: "/req/test/a",
85
+ name: "CT collision",
86
+ ),
87
+ ],
88
+ ),
89
+ ],
90
+ )
91
+
92
+ errors = suite.validate
93
+ expect(errors).to include(a_string_matching(/Duplicate identifier.*\/req\/test\/a/))
94
+ end
95
+
96
+ it "detects dependency cycles" do
97
+ ns_a = Modspec::NormativeStatement.new(
98
+ identifier: "/req/test/a", name: "A", statement: "a",
99
+ dependencies: ["/req/test/b"]
100
+ )
101
+ ns_b = Modspec::NormativeStatement.new(
102
+ identifier: "/req/test/b", name: "B", statement: "b",
103
+ dependencies: ["/req/test/a"]
104
+ )
105
+
106
+ suite = described_class.new(
107
+ identifier: "/suite",
108
+ name: "Test",
109
+ normative_statements_classes: [
110
+ Modspec::NormativeStatementsClass.new(
111
+ identifier: "/req/test",
112
+ normative_statements: [ns_a, ns_b],
113
+ ),
114
+ ],
115
+ conformance_classes: [],
116
+ )
117
+
118
+ errors = suite.validate
119
+ expect(errors).to include(a_string_matching(/Cycle detected/))
120
+ end
121
+
122
+ it "detects invalid dependencies" do
123
+ suite = described_class.new(
124
+ identifier: "/suite",
125
+ name: "Test",
126
+ normative_statements_classes: [
127
+ Modspec::NormativeStatementsClass.new(
128
+ identifier: "/req/test",
129
+ dependencies: ["/req/nonexistent"],
130
+ normative_statements: [
131
+ Modspec::NormativeStatement.new(
132
+ identifier: "/req/test/a", name: "A", statement: "a",
133
+ dependencies: ["/req/missing"]
134
+ ),
135
+ ],
136
+ ),
137
+ ],
138
+ conformance_classes: [],
139
+ )
140
+
141
+ errors = suite.validate
142
+ expect(errors).to include(a_string_matching(/Invalid dependencies .* in \/req\/test\b/))
143
+ expect(errors).to include(a_string_matching(/Invalid dependencies .* in \/req\/test\/a/))
144
+ end
145
+
146
+ it "detects invalid conformance test targets" do
147
+ suite = described_class.new(
148
+ identifier: "/suite",
149
+ name: "Test",
150
+ normative_statements_classes: [
151
+ Modspec::NormativeStatementsClass.new(
152
+ identifier: "/req/test",
153
+ normative_statements: [
154
+ Modspec::NormativeStatement.new(identifier: "/req/test/a",
155
+ name: "A", statement: "a"),
156
+ ],
157
+ ),
158
+ ],
159
+ conformance_classes: [
160
+ Modspec::ConformanceClass.new(
161
+ identifier: "/conf/test",
162
+ tests: [
163
+ Modspec::ConformanceTest.new(
164
+ identifier: "/conf/test/a",
165
+ name: "CT-A",
166
+ targets: ["/req/nonexistent"],
167
+ ),
168
+ ],
169
+ ),
170
+ ],
171
+ )
172
+
173
+ errors = suite.validate
174
+ expect(errors).to include(a_string_matching(/Invalid targets .* in \/conf\/test\/a/))
175
+ end
176
+
177
+ it "detects invalid indirect_dependency references" do
178
+ suite = described_class.new(
179
+ identifier: "/suite",
180
+ name: "Test",
181
+ normative_statements_classes: [
182
+ Modspec::NormativeStatementsClass.new(
183
+ identifier: "/req/test",
184
+ normative_statements: [
185
+ Modspec::NormativeStatement.new(
186
+ identifier: "/req/test/a", name: "A", statement: "a",
187
+ indirect_dependency: ["/req/ghost"]
188
+ ),
189
+ ],
190
+ ),
191
+ ],
192
+ conformance_classes: [],
193
+ )
194
+
195
+ errors = suite.validate
196
+ expect(errors).to include(a_string_matching(/indirect dependency.*in \/req\/test\/a/))
197
+ end
198
+
199
+ it "detects invalid implements references" do
200
+ suite = described_class.new(
201
+ identifier: "/suite",
202
+ name: "Test",
203
+ normative_statements_classes: [
204
+ Modspec::NormativeStatementsClass.new(
205
+ identifier: "/req/test",
206
+ implements: ["/req/phantom"],
207
+ normative_statements: [],
208
+ ),
209
+ ],
210
+ conformance_classes: [],
211
+ )
212
+
213
+ errors = suite.validate
214
+ expect(errors).to include(a_string_matching(/implements.*in \/req\/test/))
215
+ end
216
+
217
+ it "detects invalid conformance test dependencies" do
218
+ suite = described_class.new(
219
+ identifier: "/suite",
220
+ name: "Test",
221
+ normative_statements_classes: [],
222
+ conformance_classes: [
223
+ Modspec::ConformanceClass.new(
224
+ identifier: "/conf/test",
225
+ tests: [
226
+ Modspec::ConformanceTest.new(
227
+ identifier: "/conf/test/a",
228
+ name: "CT-A",
229
+ dependencies: ["/conf/nonexistent"],
230
+ ),
231
+ ],
232
+ ),
233
+ ],
234
+ )
235
+
236
+ errors = suite.validate
237
+ expect(errors).to include(a_string_matching(/Invalid dependencies .* in \/conf\/test\/a/))
238
+ end
47
239
  end
48
240
 
49
241
  describe "#combine" do
@@ -63,6 +255,16 @@ RSpec.describe Modspec::Suite do
63
255
  )
64
256
  end
65
257
 
258
+ it "raises ArgumentError for non-Suite argument" do
259
+ expect { suite1.combine("not a suite") }.to raise_error(ArgumentError)
260
+ end
261
+
262
+ it "deduplicates by identifier" do
263
+ combined = suite1.combine(suite1)
264
+ nsc_count = combined.normative_statements_classes.count
265
+ expect(nsc_count).to eq(suite1.normative_statements_classes.count)
266
+ end
267
+
66
268
  it "resolves conflicts when combining suites" do
67
269
  combined_suite = suite1
68
270
  .combine(suite2)
@@ -72,10 +274,6 @@ RSpec.describe Modspec::Suite do
72
274
  .combine(frame_spec_suite)
73
275
 
74
276
  errors = combined_suite.validate
75
- if errors.any?
76
-
77
- errors.each { |error| }
78
- end
79
277
  expect(errors).to be_empty
80
278
  end
81
279
  end
@@ -89,25 +287,212 @@ RSpec.describe Modspec::Suite do
89
287
  expect(combined_suite).to be_a(described_class)
90
288
  expect(combined_suite.name).to eq("Combined Suite")
91
289
 
92
- # Ensure the combined suite has content
93
290
  expect(combined_suite.normative_statements_classes).not_to be_empty
94
291
  expect(combined_suite.conformance_classes).not_to be_empty
95
292
 
96
- # Validate the combined suite
97
293
  errors = combined_suite.validate
294
+ expect(errors).not_to include(a_string_matching(/has no corresponding requirement/))
295
+ expect(errors).not_to include(a_string_matching(/Cycle detected/))
296
+ expect(errors).not_to include(a_string_matching(/has an invalid dependency/))
297
+ expect(errors).to be_empty
298
+ end
299
+ end
300
+
301
+ describe "#setup_relationships" do
302
+ it "links conformance tests to their target normative statements" do
303
+ suite = described_class.new(
304
+ identifier: "/suite",
305
+ name: "Test",
306
+ normative_statements_classes: [
307
+ Modspec::NormativeStatementsClass.new(
308
+ identifier: "/req/test",
309
+ normative_statements: [
310
+ Modspec::NormativeStatement.new(identifier: "/req/test/a",
311
+ name: "A", statement: "a"),
312
+ ],
313
+ ),
314
+ ],
315
+ conformance_classes: [
316
+ Modspec::ConformanceClass.new(
317
+ identifier: "/conf/test",
318
+ tests: [
319
+ Modspec::ConformanceTest.new(
320
+ identifier: "/conf/test/a",
321
+ name: "CT-A",
322
+ targets: ["/req/test/a"],
323
+ ),
324
+ ],
325
+ ),
326
+ ],
327
+ )
98
328
 
99
- if errors.any?
329
+ suite.setup_relationships
330
+ ct = suite.conformance_classes.first.tests.first
331
+ expect(ct.corresponding_requirements.map(&:identifier)).to eq(["/req/test/a"])
332
+ expect(ct.parent_class).to eq(suite.conformance_classes.first)
333
+ end
100
334
 
101
- errors.each { |error| }
102
- end
335
+ it "handles missing target gracefully (no matching requirement)" do
336
+ suite = described_class.new(
337
+ identifier: "/suite",
338
+ name: "Test",
339
+ normative_statements_classes: [
340
+ Modspec::NormativeStatementsClass.new(
341
+ identifier: "/req/test",
342
+ normative_statements: [
343
+ Modspec::NormativeStatement.new(identifier: "/req/test/a",
344
+ name: "A", statement: "a"),
345
+ ],
346
+ ),
347
+ ],
348
+ conformance_classes: [
349
+ Modspec::ConformanceClass.new(
350
+ identifier: "/conf/test",
351
+ tests: [
352
+ Modspec::ConformanceTest.new(
353
+ identifier: "/conf/test/a",
354
+ name: "CT-A",
355
+ targets: ["/req/test/missing"],
356
+ ),
357
+ ],
358
+ ),
359
+ ],
360
+ )
103
361
 
104
- # Check for specific error types
105
- expect(errors).not_to include(a_string_matching(/Conformance test .* has no corresponding requirement/))
106
- expect(errors).not_to include(a_string_matching(/Cycle detected/))
107
- expect(errors).not_to include(a_string_matching(/Requirement .* has an invalid dependency/))
362
+ suite.setup_relationships
363
+ ct = suite.conformance_classes.first.tests.first
364
+ expect(ct.corresponding_requirements).to be_empty
365
+ end
108
366
 
109
- # If there are still errors, they should be of a different nature
110
- expect(errors).to be_empty
367
+ it "returns early when conformance_classes is nil" do
368
+ suite = described_class.new(
369
+ identifier: "/suite",
370
+ name: "Test",
371
+ normative_statements_classes: [
372
+ Modspec::NormativeStatementsClass.new(
373
+ identifier: "/req/test",
374
+ normative_statements: [],
375
+ ),
376
+ ],
377
+ conformance_classes: nil,
378
+ )
379
+
380
+ expect { suite.setup_relationships }.not_to raise_error
381
+ end
382
+ end
383
+
384
+ describe "#resolve_conflicts" do
385
+ let(:nsc_with_data) do
386
+ Modspec::NormativeStatementsClass.new(
387
+ identifier: "/req/test",
388
+ name: "Original",
389
+ description: "First description",
390
+ normative_statements: [
391
+ Modspec::NormativeStatement.new(identifier: "/req/test/a",
392
+ name: "A", statement: "a"),
393
+ ],
394
+ )
395
+ end
396
+
397
+ let(:nsc_partial) do
398
+ Modspec::NormativeStatementsClass.new(
399
+ identifier: "/req/test",
400
+ name: "Original",
401
+ description: "Second description",
402
+ subject: "Added subject",
403
+ normative_statements: [],
404
+ )
405
+ end
406
+
407
+ let(:nsc_other) do
408
+ Modspec::NormativeStatementsClass.new(
409
+ identifier: "/req/test2",
410
+ name: "Two",
411
+ normative_statements: [
412
+ Modspec::NormativeStatement.new(identifier: "/req/test2/b",
413
+ name: "B", statement: "b"),
414
+ ],
415
+ )
416
+ end
417
+
418
+ it "merges attributes of items with matching identifiers" do
419
+ suite1 = described_class.new(
420
+ identifier: "/suite", name: "Test1",
421
+ normative_statements_classes: [nsc_with_data],
422
+ conformance_classes: []
423
+ )
424
+ suite2 = described_class.new(
425
+ identifier: "/suite", name: "Test2",
426
+ normative_statements_classes: [nsc_partial],
427
+ conformance_classes: []
428
+ )
429
+
430
+ suite1.resolve_conflicts(suite2)
431
+ nsc = suite1.normative_statements_classes.first
432
+ expect(nsc.description).to eq("First description")
433
+ expect(nsc.subject).to eq("Added subject")
434
+ end
435
+
436
+ it "appends items with new identifiers" do
437
+ suite1 = described_class.new(
438
+ identifier: "/suite", name: "Test1",
439
+ normative_statements_classes: [nsc_with_data],
440
+ conformance_classes: []
441
+ )
442
+ suite2 = described_class.new(
443
+ identifier: "/suite", name: "Test2",
444
+ normative_statements_classes: [nsc_other],
445
+ conformance_classes: []
446
+ )
447
+
448
+ suite1.resolve_conflicts(suite2)
449
+ expect(suite1.normative_statements_classes.map(&:identifier)).to eq(
450
+ ["/req/test", "/req/test2"],
451
+ )
452
+ end
453
+ end
454
+
455
+ describe "YAML round-trip" do
456
+ it "serializes and deserializes a normative statements class" do
457
+ original = described_class.from_yaml(rc_yaml)
458
+ yaml_out = original.to_yaml
459
+ round_tripped = described_class.from_yaml(yaml_out)
460
+
461
+ expect(round_tripped.identifier).to eq(original.identifier)
462
+ expect(round_tripped.name).to eq(original.name)
463
+ expect(round_tripped.normative_statements_classes.count).to eq(
464
+ original.normative_statements_classes.count,
465
+ )
466
+ end
467
+
468
+ it "serializes and deserializes a conformance class" do
469
+ original = described_class.from_yaml(cc_yaml)
470
+ yaml_out = original.to_yaml
471
+ round_tripped = described_class.from_yaml(yaml_out)
472
+
473
+ expect(round_tripped.identifier).to eq(original.identifier)
474
+ expect(round_tripped.conformance_classes.count).to eq(
475
+ original.conformance_classes.count,
476
+ )
477
+ end
478
+ end
479
+
480
+ describe "JSON round-trip" do
481
+ let(:json_rc) { File.read("spec/fixtures/basic-ypr-json-rc.yaml") }
482
+ let(:json_cc) { File.read("spec/fixtures/basic-ypr-json-cc.yaml") }
483
+
484
+ it "parses JSON-format YAML fixtures" do
485
+ suite = described_class.from_yaml(json_rc)
486
+ expect(suite.normative_statements_classes).not_to be_empty
487
+ end
488
+
489
+ it "round-trips through JSON serialization" do
490
+ original = described_class.from_yaml(rc_yaml)
491
+ json_out = original.to_json
492
+ round_tripped = described_class.from_json(json_out)
493
+
494
+ expect(round_tripped.identifier).to eq(original.identifier)
495
+ expect(round_tripped.name).to eq(original.name)
111
496
  end
112
497
  end
113
498
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: modspec
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-05-05 00:00:00.000000000 Z
11
+ date: 2026-05-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lutaml-model
@@ -71,6 +71,7 @@ files:
71
71
  - README.adoc
72
72
  - Rakefile
73
73
  - lib/modspec.rb
74
+ - lib/modspec/child_container.rb
74
75
  - lib/modspec/conformance_class.rb
75
76
  - lib/modspec/conformance_test.rb
76
77
  - lib/modspec/identifier.rb