Path:
strictdoc/backend/reqif/p01_sdoc/reqif_to_sdoc_converter.py
Lines:
626
Non-empty lines:
567
Non-empty lines covered with requirements:
567 / 567 (100.0%)
Functions:
10
Functions covered by requirements:
10 / 10 (100.0%)
- "8.2.1. Export/import from/to ReqIF" (REQUIREMENT)
- "10.1. ReqIF-to-SDoc import" (DESIGN)
1
"""2
@relation(SDOC-SRS-72, SDOC-SRS-153, scope=file)3
"""4
5
import re
6
from typing import Any, Dict, List, Optional, Tuple, Union
7
8
from reqif.models.reqif_data_type import ReqIFDataTypeDefinitionEnumeration
9
from reqif.models.reqif_spec_object import ReqIFSpecObject
10
from reqif.models.reqif_spec_object_type import (
11
ReqIFSpecObjectType,
12
SpecAttributeDefinition,
13
)14
from reqif.models.reqif_specification import ReqIFSpecification
15
from reqif.models.reqif_types import SpecObjectAttributeType
16
from reqif.reqif_bundle import ReqIFBundle
17
18
from strictdoc.backend.reqif.sdoc_reqif_fields import (
19
ReqIFReservedField,
20
map_reqif_field_title_to_sdoc_field_title,
21
)22
from strictdoc.backend.sdoc.document_reference import DocumentReference
23
from strictdoc.backend.sdoc.models.document import SDocDocument
24
from strictdoc.backend.sdoc.models.document_config import DocumentConfig
25
from strictdoc.backend.sdoc.models.document_grammar import (
26
DocumentGrammar,
27
)28
from strictdoc.backend.sdoc.models.grammar_element import (
29
GrammarElement,
30
GrammarElementFieldMultipleChoice,
31
GrammarElementFieldSingleChoice,
32
GrammarElementFieldString,
33
GrammarElementFieldType,
34
GrammarElementRelationParent,
35
)36
from strictdoc.backend.sdoc.models.model import (
37
SDocDocumentIF,
38
SDocNodeIF,
39
)40
from strictdoc.backend.sdoc.models.node import SDocNode, SDocNodeField
41
from strictdoc.backend.sdoc.models.reference import (
42
ParentReqReference,
43
Reference,
44
)45
from strictdoc.helpers.lxml import convert_xhtml_to_multiline_string
46
from strictdoc.helpers.mid import MID
47
from strictdoc.helpers.ordered_set import OrderedSet
48
from strictdoc.helpers.string import (
49
create_safe_requirement_tag_string,
50
ensure_newline,
51
unescape,
52
)53
54
55
class P01_ReqIFToSDocBuildContext:
56
def __init__(self, *, enable_mid: bool, import_markup: Optional[str]):
57
self.enable_mid: bool = enable_mid
58
self.import_markup: Optional[str] = import_markup
59
self.map_spec_object_type_identifier_to_grammar_node_tags: Dict[
60
str, GrammarElement
61
] = {}
62
self.map_source_target_pairs_to_spec_relation_types: Dict[
63
Tuple[str, str], Any
64
] = {}
65
self.unique_grammar_element_relations: Dict[
66
GrammarElement, OrderedSet[Tuple[str, Optional[str]]]
67
] = {}
68
69
70
class P01_ReqIFToSDocConverter:
71
SAFE_SDOC_FIELD_NAME_REGEX = re.compile(r"[^A-Za-z0-9]")
72
73
@staticmethod74
def convert_reqif_bundle(
75
reqif_bundle: ReqIFBundle,
76
enable_mid: bool,
77
import_markup: Optional[str],
78
) -> List[SDocDocument]:
79
context = P01_ReqIFToSDocBuildContext(
80
enable_mid=enable_mid, import_markup=import_markup
81
)82
83
if (
84
reqif_bundle.core_content is None
85
or reqif_bundle.core_content.req_if_content is None
86
or len(reqif_bundle.core_content.req_if_content.specifications) == 0
87
):88
return []
89
90
if reqif_bundle.core_content.req_if_content.spec_relations is not None:
91
for (
92
spec_relation_93
) in reqif_bundle.core_content.req_if_content.spec_relations:
94
spec_relation_type_ = reqif_bundle.lookup.get_spec_type_by_ref(
95
spec_relation_.relation_type_ref
96
)97
context.map_source_target_pairs_to_spec_relation_types[
98
(spec_relation_.source, spec_relation_.target)
99
] = spec_relation_type_
100
101
documents: List[SDocDocument] = []
102
for (
103
specification104
) in reqif_bundle.core_content.req_if_content.specifications:
105
document = P01_ReqIFToSDocConverter._create_document_from_reqif_specification(
106
specification=specification,
107
reqif_bundle=reqif_bundle,
108
context=context,
109
)110
documents.append(document)
111
return documents
112
113
@staticmethod114
def convert_requirement_field_from_reqif(field_name: str) -> str:
115
return map_reqif_field_title_to_sdoc_field_title(field_name)
116
117
@staticmethod118
def _create_document_from_reqif_specification(
119
*,
120
specification: ReqIFSpecification,
121
reqif_bundle: ReqIFBundle,
122
context: P01_ReqIFToSDocBuildContext,
123
) -> SDocDocument:
124
"""
125
Convert a single ReqIF Specification to a SDoc document.126
"""127
128
#129
# This lookup object is used to first collect the spec object type identifiers130
# that are actually used by this document. This is needed to ensure that a131
# StrictDoc document is not created with irrelevant grammar elements that132
# actually belong to other Specifications in this ReqIF bundle.133
# Using Dict as an ordered set.134
#135
spec_object_type_identifiers_used_by_this_document: OrderedSet[str] = (
136
OrderedSet()
137
)138
139
# This variable tracks spec types of elements that can nest other elements.140
# Usually, these elements are document sections/chapters.141
composite_spec_types = set()
142
143
#144
# Iterate this ReqIF specification's hierarchy to get information145
# about the used spec types and the spec types that are composite.146
#147
for hierarchy_ in reqif_bundle.iterate_specification_hierarchy(
148
specification,
149
):150
spec_object = reqif_bundle.get_spec_object_by_ref(
151
hierarchy_.spec_object
152
)153
spec_object_type_identifiers_used_by_this_document.add(
154
spec_object.spec_object_type
155
)156
if hierarchy_.children is not None:
157
composite_spec_types.add(spec_object.spec_object_type)
158
159
#160
# Iterate over the collected Spec Object types and create their161
# corresponding SDoc Grammar Elements.162
#163
elements: List[GrammarElement] = []
164
for (
165
spec_object_type_166
) in reqif_bundle.core_content.req_if_content.spec_types:
167
if not isinstance(spec_object_type_, ReqIFSpecObjectType):
168
continue169
170
spec_object_type_identifier_ = spec_object_type_.identifier
171
if (
172
spec_object_type_identifier_173
not in spec_object_type_identifiers_used_by_this_document
174
):175
continue176
177
grammar_element: GrammarElement = P01_ReqIFToSDocConverter.create_grammar_element_from_spec_object_type(
178
spec_object_type=spec_object_type_,
179
reqif_bundle=reqif_bundle,
180
is_composite=spec_object_type_.identifier
181
in composite_spec_types,
182
)183
184
elements.append(grammar_element)
185
context.map_spec_object_type_identifier_to_grammar_node_tags[
186
spec_object_type_identifier_187
] = grammar_element
188
189
#190
# Create an empty SDoc document and iterate the complete ReqIF191
# Specification one more time, creating a corresponding SDoc Node for192
# each ReqIF Spec Object.193
# Use the previously created map of composite Spec Object Types, i.e.,194
# those that are section/chapters and can nest other elements.195
#196
197
document: SDocDocument = P01_ReqIFToSDocConverter.create_document(
198
specification=specification, context=context
199
)200
document.section_contents = []
201
202
document_reference = DocumentReference()
203
document_reference.set_document(document)
204
205
grammar: DocumentGrammar
206
if len(elements) > 0:
207
grammar = DocumentGrammar(parent=document, elements=elements)
208
grammar.is_default = False
209
else:
210
# This case is mainly a placeholder for simple edge cases such as211
# an empty [DOCUMENT] where there are no grammar or nodes declared.212
grammar = DocumentGrammar.create_default(parent=document)
213
214
document.grammar = grammar
215
216
node_stack: List[Union[SDocDocumentIF, SDocNodeIF]] = [document]
217
218
for hierarchy_ in reqif_bundle.iterate_specification_hierarchy(
219
specification,
220
):221
while len(node_stack) > hierarchy_.level:
222
node_stack.pop()
223
224
parent_node = node_stack[-1]
225
226
spec_object = reqif_bundle.get_spec_object_by_ref(
227
hierarchy_.spec_object
228
)229
converted_node: SDocNode = (
230
P01_ReqIFToSDocConverter.create_requirement_from_spec_object(
231
spec_object=spec_object,
232
context=context,
233
parent_section=parent_node,
234
document_reference=document_reference,
235
reqif_bundle=reqif_bundle,
236
level=hierarchy_.level,
237
)238
)239
parent_node.section_contents.append(converted_node)
240
241
if spec_object.spec_object_type in composite_spec_types:
242
node_stack.append(converted_node)
243
244
return document
245
246
@staticmethod247
def create_grammar_element_from_spec_object_type(
248
*,
249
spec_object_type: ReqIFSpecObjectType,
250
reqif_bundle: ReqIFBundle,
251
is_composite: bool,
252
) -> GrammarElement:
253
fields: List[GrammarElementFieldType] = []
254
255
unique_safe_field_names: OrderedSet[str] = OrderedSet()
256
for attribute in spec_object_type.attribute_definitions:
257
field_name = (
258
P01_ReqIFToSDocConverter.convert_requirement_field_from_reqif(
259
attribute.long_name
260
)261
)262
sdoc_safe_field_name = (
263
P01_ReqIFToSDocConverter._create_sdoc_safe_field_name(
264
field_name265
)266
)267
assert sdoc_safe_field_name not in unique_safe_field_names, (
268
"ReqIF Spec Object type attributes translate to "269
f"non unique fields in SDoc: {sdoc_safe_field_name}. "
270
f"Unique fields: {unique_safe_field_names}."
271
)272
unique_safe_field_names.add(sdoc_safe_field_name)
273
274
sdoc_field_human_title = (
275
field_name if field_name != sdoc_safe_field_name else None
276
)277
if attribute.attribute_type == SpecObjectAttributeType.STRING:
278
fields.append(
279
GrammarElementFieldString(
280
parent=None,
281
title=sdoc_safe_field_name,
282
human_title=sdoc_field_human_title,
283
required="False",
284
)285
)286
elif attribute.attribute_type == SpecObjectAttributeType.INTEGER:
287
fields.append(
288
GrammarElementFieldString(
289
parent=None,
290
title=sdoc_safe_field_name,
291
human_title=sdoc_field_human_title,
292
required="False",
293
)294
)295
elif attribute.attribute_type == SpecObjectAttributeType.XHTML:
296
fields.append(
297
GrammarElementFieldString(
298
parent=None,
299
title=sdoc_safe_field_name,
300
human_title=sdoc_field_human_title,
301
required="False",
302
)303
)304
elif (
305
attribute.attribute_type == SpecObjectAttributeType.ENUMERATION
306
):307
enum_data_type: ReqIFDataTypeDefinitionEnumeration = (
308
reqif_bundle.lookup.get_data_type_by_ref(
309
attribute.datatype_definition
310
)311
)312
313
options = []
314
for value_ in enum_data_type.values:
315
if value_.long_name is not None:
316
assert len(value_.long_name) > 0, (
317
"Empty enum values are not allowed. "318
f"Invalid enum data type: {enum_data_type}"
319
)320
options.append(value_.long_name)
321
else:
322
options.append(value_.key)
323
324
if attribute.multi_valued is True:
325
fields.append(
326
GrammarElementFieldMultipleChoice(
327
parent=None,
328
title=sdoc_safe_field_name,
329
human_title=sdoc_field_human_title,
330
options=options,
331
required="False",
332
)333
)334
else:
335
fields.append(
336
GrammarElementFieldSingleChoice(
337
parent=None,
338
title=sdoc_safe_field_name,
339
human_title=sdoc_field_human_title,
340
options=options,
341
required="False",
342
)343
)344
elif attribute.attribute_type == SpecObjectAttributeType.DATE:
345
# Recognize the DATE attributes but do nothing about them,346
# since StrictDoc has no concept of "date" for its grammar347
# fields.348
pass349
else:
350
raise NotImplementedError( # pragma: no cover
351
attribute.attribute_type, attribute
352
) from None
353
354
requirement_element = GrammarElement(
355
parent=None,
356
tag=create_safe_requirement_tag_string(spec_object_type.long_name),
357
property_is_composite="True" if is_composite else "",
358
property_prefix="",
359
property_view_style="",
360
fields=fields,
361
relations=[],
362
)363
return requirement_element
364
365
@staticmethod366
def create_document(
367
*,
368
specification: ReqIFSpecification,
369
context: P01_ReqIFToSDocBuildContext,
370
) -> SDocDocument:
371
document_config = DocumentConfig.default_config(None)
372
document_config.enable_mid = (
373
context.enable_mid if context.enable_mid else None
374
)375
document_title = (
376
specification.long_name
377
if specification.long_name is not None
378
else "<No title>"
379
)380
document = SDocDocument(
381
mid=None,
382
title=document_title,
383
config=document_config,
384
view=None,
385
grammar=None,
386
section_contents=[],
387
)388
if context.enable_mid:
389
document.reserved_mid = MID(specification.identifier)
390
if context.import_markup is not None:
391
document_config.markup = context.import_markup
392
393
document.grammar = DocumentGrammar.create_default(document)
394
return document
395
396
@staticmethod397
def create_requirement_from_spec_object(
398
*,
399
spec_object: ReqIFSpecObject,
400
context: P01_ReqIFToSDocBuildContext,
401
parent_section: Union[SDocDocumentIF, SDocNodeIF],
402
document_reference: DocumentReference,
403
reqif_bundle: ReqIFBundle,
404
level: int,
405
) -> SDocNode:
406
fields = []
407
spec_object_type = reqif_bundle.lookup.get_spec_type_by_ref(
408
spec_object.spec_object_type
409
)410
attribute_map: Dict[str, SpecAttributeDefinition] = (
411
spec_object_type.attribute_map
412
)413
414
foreign_key_id_or_none: Optional[str] = None
415
for attribute in spec_object.attributes:
416
long_name_or_none = attribute_map[
417
attribute.definition_ref
418
].long_name
419
if long_name_or_none is None:
420
raise NotImplementedError
421
field_name: str = long_name_or_none
422
423
sdoc_field_name = (
424
P01_ReqIFToSDocConverter.convert_requirement_field_from_reqif(
425
field_name,
426
)427
)428
sdoc_field_name = (
429
P01_ReqIFToSDocConverter._create_sdoc_safe_field_name(
430
sdoc_field_name431
)432
)433
434
if attribute.attribute_type == SpecObjectAttributeType.ENUMERATION:
435
enum_values_resolved = []
436
for (
437
attribute_definition_438
) in spec_object_type.attribute_definitions:
439
if (
440
attribute.definition_ref
441
== attribute_definition_.identifier
442
):443
datatype_definition = (
444
attribute_definition_.datatype_definition
445
)446
447
datatype: ReqIFDataTypeDefinitionEnumeration = (
448
reqif_bundle.lookup.get_data_type_by_ref(
449
datatype_definition450
)451
)452
453
enum_values_list = list(attribute.value)
454
for enum_value in enum_values_list:
455
reqif_enum_value = datatype.values_map[enum_value]
456
reqif_enum_value_value = (
457
reqif_enum_value.long_name
458
if reqif_enum_value.long_name is not None
459
and len(reqif_enum_value.long_name) > 0
460
else reqif_enum_value.key
461
)462
assert len(reqif_enum_value_value) > 0
463
enum_values_resolved.append(reqif_enum_value_value)
464
465
break466
else:
467
raise NotImplementedError
468
469
enum_values = ", ".join(enum_values_resolved)
470
fields.append(
471
SDocNodeField.create_from_string(
472
parent=None,
473
field_name=sdoc_field_name,
474
field_value=enum_values,
475
multiline=False,
476
)477
)478
continue479
assert isinstance(attribute.value, str)
480
if long_name_or_none == "ReqIF.ForeignID":
481
foreign_key_id_or_none = attribute.definition_ref
482
attribute_value: str = unescape(attribute.value)
483
multiline: bool = False
484
if (
485
"\n" in attribute_value
486
or attribute.attribute_type == SpecObjectAttributeType.XHTML
487
or field_name
488
in (
489
ReqIFReservedField.TEXT,
490
ReqIFReservedField.COMMENT_NOTES,
491
)492
):493
attribute_value = attribute_value.lstrip()
494
multiline = True
495
if attribute.attribute_type == SpecObjectAttributeType.XHTML:
496
attribute_value = attribute.value_stripped_xhtml
497
# Another strip() is hidden in .value_stripped_xhtml498
# but doing this anyway to highlight the intention.499
attribute_value = attribute_value.strip()
500
501
if context.import_markup != "HTML":
502
attribute_value = convert_xhtml_to_multiline_string(
503
attribute_value504
)505
506
# We saw ReqIF examples where tools produce ReqIF.ChapterName507
# as XHTML, not String. Assuming this is a wrong/legacy508
# behavior but still supporting it.509
# See tests/integration/features/reqif/profiles/p01_sdoc/examples/01_sample510
# for an example.511
if field_name in (
512
ReqIFReservedField.NAME,
513
ReqIFReservedField.CHAPTER_NAME,
514
):515
multiline = False
516
517
if multiline:
518
attribute_value = ensure_newline(attribute_value)
519
fields.append(
520
SDocNodeField.create_from_string(
521
parent=None,
522
field_name=sdoc_field_name,
523
field_value=attribute_value,
524
multiline=multiline,
525
)526
)527
528
requirement_mid = spec_object.identifier if context.enable_mid else None
529
530
grammar_element: GrammarElement = (
531
context.map_spec_object_type_identifier_to_grammar_node_tags[
532
spec_object_type.identifier
533
]534
)535
if requirement_mid is not None:
536
fields.insert(
537
0,
538
SDocNodeField.create_from_string(
539
None, "MID", requirement_mid, multiline=False
540
),541
)542
requirement = SDocNode(
543
parent=parent_section,
544
node_type=grammar_element.tag,
545
fields=fields,
546
relations=[],
547
is_composite=grammar_element.property_is_composite is True,
548
node_type_close=grammar_element.tag
549
if grammar_element.property_is_composite
550
else None,
551
)552
requirement.context.ng_level = level
553
requirement.ng_document_reference = document_reference
554
requirement.ng_including_document_reference = DocumentReference()
555
556
for field_ in fields:
557
field_.parent = requirement
558
if foreign_key_id_or_none is not None:
559
spec_object_parents = reqif_bundle.get_spec_object_parents(
560
spec_object.identifier
561
)562
parent_refs: List[Reference] = []
563
for spec_object_parent in spec_object_parents:
564
spec_relation_type = (
565
context.map_source_target_pairs_to_spec_relation_types[
566
(spec_object.identifier, spec_object_parent)
567
]568
)569
570
relation_role = (
571
spec_relation_type.long_name
572
if spec_relation_type.long_name is not None
573
else None
574
)575
if relation_role == "Parent":
576
relation_role = None
577
578
if (
579
grammar_element580
not in context.unique_grammar_element_relations
581
):582
context.unique_grammar_element_relations[
583
grammar_element584
] = OrderedSet()
585
586
if (
587
"Parent",
588
relation_role,
589
) not in context.unique_grammar_element_relations[
590
grammar_element591
]:592
context.unique_grammar_element_relations[
593
grammar_element594
].add(("Parent", relation_role))
595
grammar_element.relations.append(
596
GrammarElementRelationParent(
597
parent=grammar_element,
598
relation_type="Parent",
599
relation_role=relation_role,
600
)601
)602
603
parent_spec_object_parent = (
604
reqif_bundle.lookup.get_spec_object_by_ref(
605
spec_object_parent606
)607
)608
609
parent_refs.append(
610
ParentReqReference(
611
requirement,
612
parent_spec_object_parent.attribute_map[
613
foreign_key_id_or_none614
].value,
615
role=relation_role,
616
)617
)618
if len(parent_refs) > 0:
619
requirement.relations = parent_refs
620
return requirement
621
622
@staticmethod623
def _create_sdoc_safe_field_name(reqif_field_long_name: str) -> str:
624
return P01_ReqIFToSDocConverter.SAFE_SDOC_FIELD_NAME_REGEX.sub(
625
"_", reqif_field_long_name
626
).upper()