StrictDoc Documentation
strictdoc/backend/reqif/p01_sdoc/reqif_to_sdoc_converter.py
Source file coverage
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%)
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
    @staticmethod
74
    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
            specification
104
        ) 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
    @staticmethod
114
    def convert_requirement_field_from_reqif(field_name: str) -> str:
115
        return map_reqif_field_title_to_sdoc_field_title(field_name)
116
 
117
    @staticmethod
118
    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 identifiers
130
        # that are actually used by this document. This is needed to ensure that a
131
        # StrictDoc document is not created with irrelevant grammar elements that
132
        # 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 information
145
        # 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 their
161
        # 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
                continue
169
 
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
                continue
176
 
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 ReqIF
191
        # Specification one more time, creating a corresponding SDoc Node for
192
        # 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 as
211
            # 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
    @staticmethod
247
    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_name
265
                )
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 grammar
347
                # fields.
348
                pass
349
            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
    @staticmethod
366
    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
    @staticmethod
392
    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_name
425
                )
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_definition
444
                            )
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
                        break
460
                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
                continue
473
            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_xhtml
492
                    # 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_value
498
                        )
499
 
500
                    # We saw ReqIF examples where tools produce ReqIF.ChapterName
501
                    # as XHTML, not String. Assuming this is a wrong/legacy
502
                    # behavior but still supporting it.
503
                    # See tests/integration/features/reqif/profiles/p01_sdoc/examples/01_sample
504
                    # 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_element
574
                    not in context.unique_grammar_element_relations
575
                ):
576
                    context.unique_grammar_element_relations[
577
                        grammar_element
578
                    ] = OrderedSet()
579
 
580
                if (
581
                    "Parent",
582
                    relation_role,
583
                ) not in context.unique_grammar_element_relations[
584
                    grammar_element
585
                ]:
586
                    context.unique_grammar_element_relations[
587
                        grammar_element
588
                    ].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_parent
600
                    )
601
                )
602
 
603
                parent_refs.append(
604
                    ParentReqReference(
605
                        requirement,
606
                        parent_spec_object_parent.attribute_map[
607
                            foreign_key_id_or_none
608
                        ].value,
609
                        role=relation_role,
610
                    )
611
                )
612
            if len(parent_refs) > 0:
613
                requirement.relations = parent_refs
614
        return requirement
615
 
616
    @staticmethod
617
    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()