Path:
strictdoc/backend/reqif/p01_sdoc/sdoc_to_reqif_converter.py
Lines:
699
Non-empty lines:
632
Non-empty lines covered with requirements:
632 / 632 (100.0%)
Functions:
8
Functions covered by requirements:
8 / 8 (100.0%)
1
"""2
@relation(SDOC-SRS-72, scope=file)3
"""4
5
import datetime
6
import uuid
7
from collections import defaultdict
8
from enum import Enum
9
from typing import Dict, List, Optional, Tuple, Union
10
11
from reqif.helpers.string.xhtml_indent import reqif_indent_xhtml_string
12
from reqif.models.reqif_core_content import ReqIFCoreContent
13
from reqif.models.reqif_data_type import (
14
ReqIFDataTypeDefinitionEnumeration,
15
ReqIFDataTypeDefinitionString,
16
ReqIFDataTypeDefinitionXHTML,
17
ReqIFEnumValue,
18
)19
from reqif.models.reqif_namespace_info import ReqIFNamespaceInfo
20
from reqif.models.reqif_req_if_content import ReqIFReqIFContent
21
from reqif.models.reqif_reqif_header import ReqIFReqIFHeader
22
from reqif.models.reqif_spec_hierarchy import ReqIFSpecHierarchy
23
from reqif.models.reqif_spec_object import ReqIFSpecObject, SpecObjectAttribute
24
from reqif.models.reqif_spec_object_type import (
25
ReqIFSpecObjectType,
26
SpecAttributeDefinition,
27
)28
from reqif.models.reqif_spec_relation import ReqIFSpecRelation
29
from reqif.models.reqif_spec_relation_type import ReqIFSpecRelationType
30
from reqif.models.reqif_specification import ReqIFSpecification
31
from reqif.models.reqif_specification_type import ReqIFSpecificationType
32
from reqif.models.reqif_types import SpecObjectAttributeType
33
from reqif.object_lookup import ReqIFObjectLookup
34
from reqif.reqif_bundle import ReqIFBundle
35
36
from strictdoc.backend.reqif.sdoc_reqif_fields import (
37
SDOC_SPECIFICATION_TYPE_SINGLETON,
38
map_sdoc_field_title_to_reqif_field_title,
39
)40
from strictdoc.backend.sdoc.models.document import SDocDocument
41
from strictdoc.backend.sdoc.models.document_grammar import DocumentGrammar
42
from strictdoc.backend.sdoc.models.grammar_element import (
43
GrammarElementField,
44
GrammarElementFieldMultipleChoice,
45
GrammarElementFieldSingleChoice,
46
GrammarElementFieldString,
47
)48
from strictdoc.backend.sdoc.models.node import SDocNode
49
from strictdoc.backend.sdoc.models.reference import (
50
ChildReqReference,
51
ParentReqReference,
52
Reference,
53
)54
from strictdoc.core.document_iterator import SDocDocumentIterator
55
from strictdoc.core.document_tree import DocumentTree
56
from strictdoc.helpers.cast import assert_cast
57
from strictdoc.helpers.ordered_set import OrderedSet
58
from strictdoc.helpers.string import escape
59
60
61
class StrictDocReqIFTypes(Enum):
62
SINGLE_LINE_STRING = "SDOC_DATATYPE_SINGLE_LINE_STRING"
63
MULTI_LINE_STRING = "SDOC_DATATYPE_MULTI_LINE_STRING"
64
SINGLE_CHOICE = "SDOC_DATATYPE_SINGLE_CHOICE"
65
MULTI_CHOICE = "SDOC_DATATYPE_MULTI_CHOICE"
66
67
68
REQIF_SINGLELINE_MAX_LENGTH = "10000"
69
70
71
def generate_unique_identifier(element_type: str) -> str:
72
return f"{element_type}-{uuid.uuid4()}"
73
74
75
class P01_SDocToReqIFBuildContext:
76
def __init__(self, *, multiline_is_xhtml: bool, enable_mid: bool):
77
self.multiline_is_xhtml: bool = multiline_is_xhtml
78
self.enable_mid: bool = enable_mid
79
self.map_uid_to_spec_objects: Dict[str, ReqIFSpecObject] = {}
80
self.map_node_uids_to_their_relations: Dict[str, List[Reference]] = (
81
defaultdict(list)
82
)83
84
# Maps SDoc field titles to ReqiF Data Type identifiers.85
self.data_types_lookup: Dict[str, str] = {}
86
87
self.map_grammar_node_tags_to_spec_object_type: Dict[
88
SDocDocument, Dict[str, ReqIFSpecObjectType]
89
] = defaultdict(dict)
90
self.map_spec_relation_tuple_to_spec_relation_type: Dict[
91
Tuple[str, Optional[str]], ReqIFSpecRelationType
92
] = {}
93
self.export_date_str: str = datetime.datetime.now(
94
datetime.timezone.utc
95
).strftime("%Y-%m-%dT%H:%M:%SZ")
96
97
98
class P01_SDocToReqIFObjectConverter:
99
@classmethod100
def convert_document_tree(
101
cls,
102
document_tree: DocumentTree,
103
multiline_is_xhtml: bool,
104
enable_mid: bool,
105
) -> ReqIFBundle:
106
creation_time = datetime.datetime.now(
107
datetime.datetime.now().astimezone().tzinfo
108
).isoformat()
109
110
namespace = "http://www.omg.org/spec/ReqIF/20110401/reqif.xsd"
111
112
context: P01_SDocToReqIFBuildContext = P01_SDocToReqIFBuildContext(
113
multiline_is_xhtml=multiline_is_xhtml, enable_mid=enable_mid
114
)115
116
spec_types: List[ReqIFSpecificationType] = []
117
spec_objects: List[ReqIFSpecObject] = []
118
spec_relations: List[ReqIFSpecRelation] = []
119
specifications: List[ReqIFSpecification] = []
120
data_types: List[
121
Union[
122
ReqIFDataTypeDefinitionString,
123
ReqIFDataTypeDefinitionXHTML,
124
ReqIFDataTypeDefinitionEnumeration,
125
]126
] = []
127
128
specification_type = ReqIFSpecificationType(
129
identifier=SDOC_SPECIFICATION_TYPE_SINGLETON,
130
description=None,
131
last_change=creation_time,
132
long_name=SDOC_SPECIFICATION_TYPE_SINGLETON,
133
spec_attributes=None,
134
spec_attribute_map={},
135
is_self_closed=True,
136
)137
spec_types.append(specification_type)
138
139
document: SDocDocument
140
for document in document_tree.document_list:
141
assert document.grammar is not None
142
for element in document.grammar.elements:
143
for field in element.fields:
144
multiline = element.is_field_multiline(field.title)
145
146
if isinstance(field, GrammarElementFieldString):
147
if multiline:
148
if (
149
StrictDocReqIFTypes.MULTI_LINE_STRING.value
150
in context.data_types_lookup
151
):152
continue153
if multiline_is_xhtml:
154
data_type = ReqIFDataTypeDefinitionXHTML(
155
identifier=(
156
StrictDocReqIFTypes.MULTI_LINE_STRING.value
157
),158
last_change=context.export_date_str,
159
is_self_closed=True,
160
)161
else:
162
data_type = ReqIFDataTypeDefinitionString(
163
identifier=(
164
StrictDocReqIFTypes.MULTI_LINE_STRING.value
165
),166
last_change=context.export_date_str,
167
max_length=REQIF_SINGLELINE_MAX_LENGTH,
168
)169
context.data_types_lookup[
170
StrictDocReqIFTypes.MULTI_LINE_STRING.value
171
] = data_type.identifier
172
else:
173
if (
174
StrictDocReqIFTypes.SINGLE_LINE_STRING.value
175
in context.data_types_lookup
176
):177
continue178
data_type = ReqIFDataTypeDefinitionString(
179
identifier=(
180
StrictDocReqIFTypes.SINGLE_LINE_STRING.value
181
),182
last_change=context.export_date_str,
183
max_length=REQIF_SINGLELINE_MAX_LENGTH,
184
)185
context.data_types_lookup[
186
StrictDocReqIFTypes.SINGLE_LINE_STRING.value
187
] = data_type.identifier
188
data_types.append(data_type)
189
elif isinstance(field, GrammarElementFieldSingleChoice):
190
values = []
191
values_map = {}
192
for option_idx_, option in enumerate(field.options):
193
value = ReqIFEnumValue(
194
identifier=generate_unique_identifier(
195
"ENUM-VALUE"196
),197
key=str(option_idx_),
198
last_change=context.export_date_str,
199
# ReqIF XML validator wants OTHER-CONTENT to be200
# present, even if empty.201
other_content="",
202
long_name=option,
203
)204
values.append(value)
205
values_map[option] = option
206
207
data_type = ReqIFDataTypeDefinitionEnumeration(
208
identifier=(
209
generate_unique_identifier(
210
StrictDocReqIFTypes.SINGLE_CHOICE.value
211
)212
),213
last_change=context.export_date_str,
214
values=values,
215
)216
data_types.append(data_type)
217
context.data_types_lookup[field.title] = (
218
data_type.identifier
219
)220
elif isinstance(field, GrammarElementFieldMultipleChoice):
221
values = []
222
values_map = {}
223
for option_idx_, option in enumerate(field.options):
224
value = ReqIFEnumValue(
225
identifier=generate_unique_identifier(
226
"ENUM-VALUE"227
),228
key=str(option_idx_),
229
last_change=context.export_date_str,
230
# ReqIF XML validator wants OTHER-CONTENT to be231
# present, even if empty.232
other_content="",
233
long_name=option,
234
)235
values.append(value)
236
values_map[option] = option
237
238
data_type = ReqIFDataTypeDefinitionEnumeration(
239
identifier=(
240
generate_unique_identifier(
241
StrictDocReqIFTypes.MULTI_CHOICE.value
242
)243
),244
last_change=context.export_date_str,
245
values=values,
246
)247
data_types.append(data_type)
248
context.data_types_lookup[field.title] = (
249
data_type.identifier
250
)251
else:
252
raise NotImplementedError( # pragma: no cover
253
field254
) from None
255
256
document_spec_types = cls._convert_document_grammar_to_spec_types(
257
grammar=assert_cast(document.grammar, DocumentGrammar),
258
context=context,
259
)260
spec_types.extend(document_spec_types)
261
262
document_iterator = SDocDocumentIterator(document)
263
264
# FIXME: This is a throw-away object. It gets discarded when the265
# iteration is over. Find a way to do without it.266
root_hierarchy = ReqIFSpecHierarchy(
267
xml_node=None,
268
is_self_closed=False,
269
identifier="NOT_USED",
270
last_change=None,
271
long_name=None,
272
spec_object="NOT_USED",
273
children=[],
274
ref_then_children_order=True,
275
level=0,
276
)277
278
node_stack: List[ReqIFSpecHierarchy] = [root_hierarchy]
279
280
# FIXME: ReqIF must export complete documents including fragments.281
for node_, node_context_ in document_iterator.all_content(
282
print_fragments=False
283
):284
if not isinstance(node_, SDocNode):
285
continue286
leaf_or_composite_node = assert_cast(node_, SDocNode)
287
while len(node_stack) > assert_cast(
288
node_context_.get_level(), int
289
):290
node_stack.pop()
291
292
current_hierarchy = node_stack[-1]
293
294
spec_object = cls._convert_requirement_to_spec_object(
295
requirement=leaf_or_composite_node,
296
grammar=assert_cast(document.grammar, DocumentGrammar),
297
context=context,
298
data_types=data_types,
299
)300
spec_objects.append(spec_object)
301
hierarchy = ReqIFSpecHierarchy(
302
xml_node=None,
303
is_self_closed=False,
304
identifier=generate_unique_identifier("SPEC-IDENTIFIER"),
305
last_change=context.export_date_str,
306
long_name=None,
307
spec_object=spec_object.identifier,
308
children=None,
309
ref_then_children_order=True,
310
level=node_context_.get_level(),
311
)312
current_hierarchy.add_child(hierarchy)
313
if leaf_or_composite_node.is_composite:
314
node_stack.append(hierarchy)
315
316
specification_identifier: str
317
if context.enable_mid and document.reserved_mid is not None:
318
specification_identifier = document.reserved_mid
319
else:
320
specification_identifier = generate_unique_identifier(
321
"SPECIFICATION"322
)323
specification = ReqIFSpecification(
324
xml_node=None,
325
description=None,
326
identifier=specification_identifier,
327
last_change=context.export_date_str,
328
long_name=document.title,
329
values=None,
330
specification_type=specification_type.identifier,
331
children=root_hierarchy.children,
332
)333
specifications.append(specification)
334
335
for (
336
requirement_id,
337
node_relations_,
338
) in context.map_node_uids_to_their_relations.items():
339
spec_object = context.map_uid_to_spec_objects[requirement_id]
340
for node_relation_ in node_relations_:
341
# For now, the File-relations are not supported.342
if not isinstance(
343
node_relation_, (ParentReqReference, ChildReqReference)
344
):345
continue346
parent_or_child_relation = assert_cast(
347
node_relation_, (ParentReqReference, ChildReqReference)
348
)349
related_node_uid = parent_or_child_relation.ref_uid
350
parent_spec_object = context.map_uid_to_spec_objects[
351
related_node_uid352
]353
spec_relation_type: ReqIFSpecRelationType = (
354
context.map_spec_relation_tuple_to_spec_relation_type[
355
(node_relation_.ref_type, node_relation_.role)
356
]357
)358
spec_relations.append(
359
ReqIFSpecRelation(
360
xml_node=None,
361
description=None,
362
identifier=generate_unique_identifier("SPEC-RELATION"),
363
last_change=context.export_date_str,
364
relation_type_ref=spec_relation_type.identifier,
365
source=spec_object.identifier,
366
target=parent_spec_object.identifier,
367
values_attribute=None,
368
)369
)370
371
reqif_reqif_content = ReqIFReqIFContent(
372
data_types=data_types,
373
spec_types=spec_types,
374
spec_objects=spec_objects,
375
spec_relations=spec_relations,
376
specifications=specifications,
377
spec_relation_groups=None,
378
)379
core_content_or_none = ReqIFCoreContent(reqif_reqif_content)
380
381
namespace_info: ReqIFNamespaceInfo = ReqIFNamespaceInfo(
382
original_reqif_tag_dump=None,
383
doctype_is_present=True,
384
encoding="UTF-8",
385
namespace=namespace,
386
configuration=None,
387
namespace_id=None,
388
namespace_xhtml="http://www.w3.org/1999/xhtml",
389
schema_namespace=None,
390
schema_location=None,
391
language=None,
392
)393
req_reqif_header = ReqIFReqIFHeader(
394
identifier=generate_unique_identifier("REQ-IF-HEADER"),
395
creation_time=creation_time,
396
title="Documentation export by StrictDoc",
397
req_if_tool_id="strictdoc",
398
req_if_version="1.0",
399
source_tool_id="strictdoc",
400
repository_id=None,
401
comment=None,
402
)403
404
reqif_bundle = ReqIFBundle(
405
namespace_info=namespace_info,
406
req_if_header=req_reqif_header,
407
core_content=core_content_or_none,
408
tool_extensions_tag_exists=False,
409
lookup=ReqIFObjectLookup(
410
data_types_lookup={},
411
spec_types_lookup={},
412
spec_objects_lookup={},
413
spec_relations_parent_lookup={},
414
),415
exceptions=[],
416
)417
return reqif_bundle
418
419
@classmethod420
def _convert_requirement_to_spec_object(
421
cls,
422
requirement: SDocNode,
423
grammar: DocumentGrammar,
424
context: P01_SDocToReqIFBuildContext,
425
data_types: List[ReqIFDataTypeDefinitionString],
426
) -> ReqIFSpecObject:
427
node_document = assert_cast(requirement.get_document(), SDocDocument)
428
429
spec_object_type: ReqIFSpecObjectType = (
430
context.map_grammar_node_tags_to_spec_object_type[node_document][
431
requirement.node_type
432
]433
)434
435
enable_mid = context.enable_mid and node_document.config.enable_mid
436
437
requirement_identifier: str
438
if enable_mid and requirement.reserved_mid is not None:
439
requirement_identifier = requirement.reserved_mid
440
else:
441
requirement_identifier = generate_unique_identifier(
442
requirement.node_type
443
)444
445
grammar_element = grammar.elements_by_type[requirement.node_type]
446
447
attributes: List[SpecObjectAttribute] = []
448
for field in requirement.fields_as_parsed:
449
# The MID field, if exists, is extracted separately as a ReqIF Identifier.450
if field.field_name == "MID":
451
continue452
453
field_name = map_sdoc_field_title_to_reqif_field_title(
454
field.field_name,
455
grammar_element.property_is_composite == True,
456
)457
458
field_identifier = spec_object_type.identifier + "_" + field_name
459
460
grammar_field = grammar_element.fields_map[field.field_name]
461
if isinstance(grammar_field, GrammarElementFieldSingleChoice):
462
data_type_ref = context.data_types_lookup[field.field_name]
463
464
enum_ref_value = None
465
for data_type in data_types:
466
if data_type_ref == data_type.identifier:
467
assert isinstance(
468
data_type, ReqIFDataTypeDefinitionEnumeration
469
)470
for data_type_value in data_type.values:
471
if data_type_value.long_name is not None:
472
data_type_sdoc_value = data_type_value.long_name
473
else:
474
data_type_sdoc_value = data_type_value.key
475
if data_type_sdoc_value == field.get_text_value():
476
enum_ref_value = data_type_value.identifier
477
break478
479
assert enum_ref_value is not None
480
481
attribute = SpecObjectAttribute(
482
xml_node=None,
483
attribute_type=SpecObjectAttributeType.ENUMERATION,
484
definition_ref=field_identifier,
485
value=[enum_ref_value],
486
)487
elif isinstance(grammar_field, GrammarElementFieldMultipleChoice):
488
field_values: List[str] = field.get_text_value().split(",")
489
field_values = list(map(lambda v: v.strip(), field_values))
490
491
data_type_ref = context.data_types_lookup[field.field_name]
492
493
data_type_lookup = {}
494
for data_type in data_types:
495
if data_type_ref == data_type.identifier:
496
assert isinstance(
497
data_type, ReqIFDataTypeDefinitionEnumeration
498
)499
for data_type_value in data_type.values:
500
if data_type_value.long_name is not None:
501
data_type_sdoc_value = data_type_value.long_name
502
else:
503
data_type_sdoc_value = data_type_value.key
504
data_type_lookup[data_type_sdoc_value] = (
505
data_type_value.identifier
506
)507
508
field_values_refs = []
509
for field_value_ in field_values:
510
field_values_refs.append(data_type_lookup[field_value_])
511
512
attribute = SpecObjectAttribute(
513
xml_node=None,
514
attribute_type=SpecObjectAttributeType.ENUMERATION,
515
definition_ref=field_identifier,
516
value=field_values_refs,
517
)518
elif isinstance(grammar_field, GrammarElementFieldString):
519
is_multiline_field = field.is_multiline()
520
521
field_value: str = field.get_text_value().rstrip()
522
523
attribute_type: SpecObjectAttributeType
524
if is_multiline_field:
525
if context.multiline_is_xhtml:
526
attribute_type = SpecObjectAttributeType.XHTML
527
field_value = (
528
"<xhtml:div>\n "
529
+ "\n ".join(
530
f"<xhtml:p>{line}</xhtml:p>"
531
for line in field_value.split("\n\n")
532
)533
+ "\n</xhtml:div>"
534
)535
field_value = reqif_indent_xhtml_string(field_value)
536
else:
537
attribute_type = SpecObjectAttributeType.STRING
538
field_value = escape(field_value)
539
else:
540
field_value = escape(field_value)
541
attribute_type = SpecObjectAttributeType.STRING
542
543
attribute = SpecObjectAttribute(
544
xml_node=None,
545
attribute_type=attribute_type,
546
definition_ref=field_identifier,
547
value=field_value,
548
)549
else:
550
raise NotImplementedError( # pragma: no cover
551
grammar_field552
) from None
553
attributes.append(attribute)
554
555
if requirement.reserved_uid is not None:
556
if len(requirement.relations) > 0:
557
context.map_node_uids_to_their_relations[
558
requirement.reserved_uid
559
] = requirement.relations
560
561
spec_object = ReqIFSpecObject(
562
identifier=requirement_identifier,
563
spec_object_type=spec_object_type.identifier,
564
attributes=attributes,
565
last_change=context.export_date_str,
566
)567
if requirement.reserved_uid is not None:
568
context.map_uid_to_spec_objects[requirement.reserved_uid] = (
569
spec_object570
)571
return spec_object
572
573
@classmethod574
def _convert_document_grammar_to_spec_types(
575
cls,
576
grammar: DocumentGrammar,
577
context: P01_SDocToReqIFBuildContext,
578
) -> List[ReqIFSpecificationType]:
579
spec_object_types: List[ReqIFSpecificationType] = []
580
581
grammar_document: SDocDocument = assert_cast(
582
grammar.parent, SDocDocument
583
)584
585
for element in grammar.elements:
586
spec_object_type_identifier = element.tag + "_" + uuid.uuid4().hex
587
588
attribute_definitions = []
589
590
field: GrammarElementField
591
for field in element.fields:
592
multiline = element.is_field_multiline(field.title)
593
field_title = map_sdoc_field_title_to_reqif_field_title(
594
field.title, element.property_is_composite == True
595
)596
field_human_title = (
597
field.human_title
598
if field.human_title is not None
599
else field_title
600
)601
field_identifier = (
602
spec_object_type_identifier + "_" + field_title
603
)604
605
if isinstance(field, GrammarElementFieldString):
606
if multiline:
607
attribute_type = (
608
SpecObjectAttributeType.XHTML
609
if context.multiline_is_xhtml
610
else SpecObjectAttributeType.STRING
611
)612
attribute = SpecAttributeDefinition(
613
attribute_type=attribute_type,
614
identifier=field_identifier,
615
datatype_definition=(
616
StrictDocReqIFTypes.MULTI_LINE_STRING.value
617
),618
long_name=field_human_title,
619
last_change=context.export_date_str,
620
)621
else:
622
attribute = SpecAttributeDefinition(
623
attribute_type=SpecObjectAttributeType.STRING,
624
identifier=field_identifier,
625
datatype_definition=(
626
StrictDocReqIFTypes.SINGLE_LINE_STRING.value
627
),628
long_name=field_human_title,
629
last_change=context.export_date_str,
630
)631
elif isinstance(field, GrammarElementFieldSingleChoice):
632
attribute = SpecAttributeDefinition(
633
attribute_type=SpecObjectAttributeType.ENUMERATION,
634
identifier=field_identifier,
635
datatype_definition=context.data_types_lookup[
636
field.title
637
],638
long_name=field_human_title,
639
multi_valued=False,
640
last_change=context.export_date_str,
641
)642
elif isinstance(field, GrammarElementFieldMultipleChoice):
643
attribute = SpecAttributeDefinition(
644
attribute_type=SpecObjectAttributeType.ENUMERATION,
645
identifier=field_identifier,
646
datatype_definition=context.data_types_lookup[
647
field.title
648
],649
long_name=field_human_title,
650
multi_valued=True,
651
last_change=context.export_date_str,
652
)653
else:
654
raise NotImplementedError( # pragma: no cover
655
field656
) from None
657
attribute_definitions.append(attribute)
658
659
spec_object_type = ReqIFSpecObjectType.create(
660
identifier=spec_object_type_identifier,
661
long_name=element.tag,
662
attribute_definitions=attribute_definitions,
663
last_change=context.export_date_str,
664
)665
spec_object_types.append(spec_object_type)
666
context.map_grammar_node_tags_to_spec_object_type[grammar_document][
667
element.tag
668
] = spec_object_type
669
670
assert grammar.parent is not None
671
672
# Using dict as an ordered set.673
spec_relation_tuples: OrderedSet[Tuple[str, Optional[str]]] = (
674
OrderedSet()
675
)676
for element_ in grammar.elements:
677
for relation_ in element_.relations:
678
spec_relation_tuples.add(
679
(relation_.relation_type, relation_.relation_role)
680
)681
682
spec_relation_types: List[ReqIFSpecRelationType] = []
683
for spec_relation_tuple_ in spec_relation_tuples:
684
spec_relation_type_name = (
685
spec_relation_tuple_[1]
686
if spec_relation_tuple_[1] is not None
687
else spec_relation_tuple_[0]
688
)689
spec_relation_type = ReqIFSpecRelationType(
690
identifier=generate_unique_identifier(spec_relation_type_name),
691
last_change=context.export_date_str,
692
long_name=spec_relation_type_name,
693
)694
spec_relation_types.append(spec_relation_type)
695
context.map_spec_relation_tuple_to_spec_relation_type[
696
spec_relation_tuple_697
] = spec_relation_type
698
699
return spec_object_types + spec_relation_types