Path:
strictdoc/backend/reqif/p01_sdoc/reqif_to_sdoc_converter.py
Lines:
620
Non-empty lines:
561
Non-empty lines covered with requirements:
561 / 561 (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
None, document_title, document_config, None, None, []
382
)383
if context.enable_mid:
384
document.reserved_mid = MID(specification.identifier)
385
if context.import_markup is not None:
386
document_config.markup = context.import_markup
387
388
document.grammar = DocumentGrammar.create_default(document)
389
return document
390
391
@staticmethod392
def create_requirement_from_spec_object(
393
spec_object: ReqIFSpecObject,
394
context: P01_ReqIFToSDocBuildContext,
395
parent_section: Union[SDocDocumentIF, SDocNodeIF],
396
document_reference: DocumentReference,
397
reqif_bundle: ReqIFBundle,
398
level: int,
399
) -> SDocNode:
400
fields = []
401
spec_object_type = reqif_bundle.lookup.get_spec_type_by_ref(
402
spec_object.spec_object_type
403
)404
attribute_map: Dict[str, SpecAttributeDefinition] = (
405
spec_object_type.attribute_map
406
)407
408
foreign_key_id_or_none: Optional[str] = None
409
for attribute in spec_object.attributes:
410
long_name_or_none = attribute_map[
411
attribute.definition_ref
412
].long_name
413
if long_name_or_none is None:
414
raise NotImplementedError
415
field_name: str = long_name_or_none
416
417
sdoc_field_name = (
418
P01_ReqIFToSDocConverter.convert_requirement_field_from_reqif(
419
field_name,
420
)421
)422
sdoc_field_name = (
423
P01_ReqIFToSDocConverter._create_sdoc_safe_field_name(
424
sdoc_field_name425
)426
)427
428
if attribute.attribute_type == SpecObjectAttributeType.ENUMERATION:
429
enum_values_resolved = []
430
for (
431
attribute_definition_432
) in spec_object_type.attribute_definitions:
433
if (
434
attribute.definition_ref
435
== attribute_definition_.identifier
436
):437
datatype_definition = (
438
attribute_definition_.datatype_definition
439
)440
441
datatype: ReqIFDataTypeDefinitionEnumeration = (
442
reqif_bundle.lookup.get_data_type_by_ref(
443
datatype_definition444
)445
)446
447
enum_values_list = list(attribute.value)
448
for enum_value in enum_values_list:
449
reqif_enum_value = datatype.values_map[enum_value]
450
reqif_enum_value_value = (
451
reqif_enum_value.long_name
452
if reqif_enum_value.long_name is not None
453
and len(reqif_enum_value.long_name) > 0
454
else reqif_enum_value.key
455
)456
assert len(reqif_enum_value_value) > 0
457
enum_values_resolved.append(reqif_enum_value_value)
458
459
break460
else:
461
raise NotImplementedError
462
463
enum_values = ", ".join(enum_values_resolved)
464
fields.append(
465
SDocNodeField.create_from_string(
466
parent=None,
467
field_name=sdoc_field_name,
468
field_value=enum_values,
469
multiline=False,
470
)471
)472
continue473
assert isinstance(attribute.value, str)
474
if long_name_or_none == "ReqIF.ForeignID":
475
foreign_key_id_or_none = attribute.definition_ref
476
attribute_value: str = unescape(attribute.value)
477
multiline: bool = False
478
if (
479
"\n" in attribute_value
480
or attribute.attribute_type == SpecObjectAttributeType.XHTML
481
or field_name
482
in (
483
ReqIFReservedField.TEXT,
484
ReqIFReservedField.COMMENT_NOTES,
485
)486
):487
attribute_value = attribute_value.lstrip()
488
multiline = True
489
if attribute.attribute_type == SpecObjectAttributeType.XHTML:
490
attribute_value = attribute.value_stripped_xhtml
491
# Another strip() is hidden in .value_stripped_xhtml492
# but doing this anyway to highlight the intention.493
attribute_value = attribute_value.strip()
494
495
if context.import_markup != "HTML":
496
attribute_value = convert_xhtml_to_multiline_string(
497
attribute_value498
)499
500
# We saw ReqIF examples where tools produce ReqIF.ChapterName501
# as XHTML, not String. Assuming this is a wrong/legacy502
# behavior but still supporting it.503
# See tests/integration/features/reqif/profiles/p01_sdoc/examples/01_sample504
# for an example.505
if field_name in (
506
ReqIFReservedField.NAME,
507
ReqIFReservedField.CHAPTER_NAME,
508
):509
multiline = False
510
511
if multiline:
512
attribute_value = ensure_newline(attribute_value)
513
fields.append(
514
SDocNodeField.create_from_string(
515
parent=None,
516
field_name=sdoc_field_name,
517
field_value=attribute_value,
518
multiline=multiline,
519
)520
)521
522
requirement_mid = spec_object.identifier if context.enable_mid else None
523
524
grammar_element: GrammarElement = (
525
context.map_spec_object_type_identifier_to_grammar_node_tags[
526
spec_object_type.identifier
527
]528
)529
if requirement_mid is not None:
530
fields.insert(
531
0,
532
SDocNodeField.create_from_string(
533
None, "MID", requirement_mid, multiline=False
534
),535
)536
requirement = SDocNode(
537
parent=parent_section,
538
node_type=grammar_element.tag,
539
fields=fields,
540
relations=[],
541
is_composite=grammar_element.property_is_composite is True,
542
node_type_close=grammar_element.tag
543
if grammar_element.property_is_composite
544
else None,
545
)546
requirement.context.ng_level = level
547
requirement.ng_document_reference = document_reference
548
requirement.ng_including_document_reference = DocumentReference()
549
550
for field_ in fields:
551
field_.parent = requirement
552
if foreign_key_id_or_none is not None:
553
spec_object_parents = reqif_bundle.get_spec_object_parents(
554
spec_object.identifier
555
)556
parent_refs: List[Reference] = []
557
for spec_object_parent in spec_object_parents:
558
spec_relation_type = (
559
context.map_source_target_pairs_to_spec_relation_types[
560
(spec_object.identifier, spec_object_parent)
561
]562
)563
564
relation_role = (
565
spec_relation_type.long_name
566
if spec_relation_type.long_name is not None
567
else None
568
)569
if relation_role == "Parent":
570
relation_role = None
571
572
if (
573
grammar_element574
not in context.unique_grammar_element_relations
575
):576
context.unique_grammar_element_relations[
577
grammar_element578
] = OrderedSet()
579
580
if (
581
"Parent",
582
relation_role,
583
) not in context.unique_grammar_element_relations[
584
grammar_element585
]:586
context.unique_grammar_element_relations[
587
grammar_element588
].add(("Parent", relation_role))
589
grammar_element.relations.append(
590
GrammarElementRelationParent(
591
parent=grammar_element,
592
relation_type="Parent",
593
relation_role=relation_role,
594
)595
)596
597
parent_spec_object_parent = (
598
reqif_bundle.lookup.get_spec_object_by_ref(
599
spec_object_parent600
)601
)602
603
parent_refs.append(
604
ParentReqReference(
605
requirement,
606
parent_spec_object_parent.attribute_map[
607
foreign_key_id_or_none608
].value,
609
role=relation_role,
610
)611
)612
if len(parent_refs) > 0:
613
requirement.relations = parent_refs
614
return requirement
615
616
@staticmethod617
def _create_sdoc_safe_field_name(reqif_field_long_name: str) -> str:
618
return P01_ReqIFToSDocConverter.SAFE_SDOC_FIELD_NAME_REGEX.sub(
619
"_", reqif_field_long_name
620
).upper()