StrictDoc Documentation
strictdoc/backend/reqif/p01_sdoc/sdoc_to_reqif_converter.py
Source file coverage
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
    @classmethod
100
    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
                                continue
153
                            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
                                continue
178
                            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 be
200
                                # 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 be
231
                                # 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
                            field
254
                        ) 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 the
265
            #        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
                    continue
286
                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
                    continue
346
                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_uid
352
                ]
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
    @classmethod
420
    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
                continue
452
 
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
                                break
478
 
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_field
552
                ) 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_object
570
            )
571
        return spec_object
572
 
573
    @classmethod
574
    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
                        field
656
                    ) 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