From 1a3b51ef414f15c58f13fe7708ccff77346ef6cb Mon Sep 17 00:00:00 2001
From: Tobias Deiminger <tdmg@linutronix.de>
Date: Wed, 6 Nov 2024 14:14:35 +0100
Subject: [PATCH] Track UID_TO_NODE for SDocDocument to resolve LINK to
 DOCUMENT

We can now use [LINK: DOC] to link to whole documents, where DOC is

 [DOCUMENT]
 TITLE: Document
 UID: DOC

in the same or another sdoc.
---
 strictdoc/core/traceability_index.py          | 17 ++++++----
 strictdoc/core/traceability_index_builder.py  | 11 ++++--
 .../core/transforms/update_document_config.py | 34 ++++++++++++++++++-
 .../input1.sdoc                               | 10 ++++++
 .../input2.sdoc                               |  8 +++++
 .../test.itest                                | 10 ++++++
 .../52_link_to_document/expected/input1.rst   | 18 ++++++++++
 .../52_link_to_document/expected/input2.rst   |  6 ++++
 .../rst/52_link_to_document/input1.sdoc       | 10 ++++++
 .../rst/52_link_to_document/input2.sdoc       |  8 +++++
 .../export/rst/52_link_to_document/test.itest |  6 ++++
 11 files changed, 128 insertions(+), 10 deletions(-)
 create mode 100644 tests/integration/commands/export/html/markup/11_node_LINK_references_a_document/input1.sdoc
 create mode 100644 tests/integration/commands/export/html/markup/11_node_LINK_references_a_document/input2.sdoc
 create mode 100644 tests/integration/commands/export/html/markup/11_node_LINK_references_a_document/test.itest
 create mode 100644 tests/integration/commands/export/rst/52_link_to_document/expected/input1.rst
 create mode 100644 tests/integration/commands/export/rst/52_link_to_document/expected/input2.rst
 create mode 100644 tests/integration/commands/export/rst/52_link_to_document/input1.sdoc
 create mode 100644 tests/integration/commands/export/rst/52_link_to_document/input2.sdoc
 create mode 100644 tests/integration/commands/export/rst/52_link_to_document/test.itest

diff --git a/strictdoc/core/traceability_index.py b/strictdoc/core/traceability_index.py
index c5aa43661..fcbc5d90f 100644
--- a/strictdoc/core/traceability_index.py
+++ b/strictdoc/core/traceability_index.py
@@ -354,9 +354,10 @@ def get_node_by_uid_weak2(self, uid: str):
 
     def get_linkable_node_by_uid(
         self, uid
-    ) -> Union[SDocNode, SDocSection, Anchor]:
+    ) -> Union[SDocDocument, SDocNode, SDocSection, Anchor]:
         return assert_cast(
-            self.get_node_by_uid(uid), (SDocNode, SDocSection, Anchor)
+            self.get_node_by_uid(uid),
+            (SDocDocument, SDocNode, SDocSection, Anchor),
         )
 
     def get_node_by_uid_weak(
@@ -383,16 +384,16 @@ def get_node_by_uid_weak(
 
     def get_linkable_node_by_uid_weak(
         self, uid
-    ) -> Union[SDocNode, SDocSection, Anchor, None]:
+    ) -> Union[SDocDocument, SDocNode, SDocSection, Anchor, None]:
         return assert_optional_cast(
             self.graph_database.get_link_value_weak(
                 link_type=GraphLinkType.UID_TO_NODE, lhs_node=uid
             ),
-            (SDocNode, SDocSection, Anchor),
+            (SDocDocument, SDocNode, SDocSection, Anchor),
         )
 
     def get_incoming_links(
-        self, node: Union[SDocNode, SDocSection, Anchor]
+        self, node: Union[SDocDocument, SDocNode, SDocSection, Anchor]
     ) -> Optional[List[InlineLink]]:
         incoming_links = self.graph_database.get_link_values_weak(
             link_type=GraphLinkType.NODE_TO_INCOMING_LINKS,
@@ -469,12 +470,14 @@ def create_inline_link(self, new_link: InlineLink):
         if self.graph_database.has_link(
             link_type=GraphLinkType.UID_TO_NODE, lhs_node=new_link.link
         ):
-            node_or_anchor: Union[SDocNode, SDocSection, Anchor] = assert_cast(
+            node_or_anchor: Union[
+                SDocDocument, SDocNode, SDocSection, Anchor
+            ] = assert_cast(
                 self.graph_database.get_link_value(
                     link_type=GraphLinkType.UID_TO_NODE,
                     lhs_node=new_link.link,
                 ),
-                (SDocNode, SDocSection, Anchor),
+                (SDocDocument, SDocNode, SDocSection, Anchor),
             )
             self.graph_database.create_link(
                 link_type=GraphLinkType.NODE_TO_INCOMING_LINKS,
diff --git a/strictdoc/core/traceability_index_builder.py b/strictdoc/core/traceability_index_builder.py
index ddacb6898..d1d645fd2 100644
--- a/strictdoc/core/traceability_index_builder.py
+++ b/strictdoc/core/traceability_index_builder.py
@@ -258,7 +258,9 @@ def create_from_document_tree(
                 ),
                 (
                     GraphLinkType.UID_TO_NODE,
-                    OneToOneDictionary(str, (SDocNode, SDocSection, Anchor)),
+                    OneToOneDictionary(
+                        str, (SDocDocument, SDocNode, SDocSection, Anchor)
+                    ),
                 ),
                 (
                     GraphLinkType.UID_TO_REQUIREMENT_CONNECTIONS,
@@ -378,7 +380,12 @@ def create_from_document_tree(
                 lhs_node=document.reserved_mid,
                 rhs_node=document,
             )
-            # FIXME: Register Document with UID_TO_NODE
+            if document.uid:
+                graph_database.create_link(
+                    link_type=GraphLinkType.UID_TO_NODE,
+                    lhs_node=document.uid,
+                    rhs_node=document,
+                )
 
             document_tags: Dict[str, int] = {}
             graph_database.create_link(
diff --git a/strictdoc/core/transforms/update_document_config.py b/strictdoc/core/transforms/update_document_config.py
index 59e293d4d..0441cbd5d 100644
--- a/strictdoc/core/transforms/update_document_config.py
+++ b/strictdoc/core/transforms/update_document_config.py
@@ -1,8 +1,12 @@
 # mypy: disable-error-code="arg-type,no-untyped-call,no-untyped-def"
 from collections import defaultdict
-from typing import Dict, List
+
+# neue imports
+from typing import Dict, List, Optional
 
 from strictdoc.backend.sdoc.models.document import SDocDocument
+from strictdoc.backend.sdoc.models.inline_link import InlineLink
+from strictdoc.backend.sdoc.models.node import SDocNode
 from strictdoc.core.traceability_index import (
     TraceabilityIndex,
 )
@@ -12,6 +16,7 @@
 from strictdoc.export.html.form_objects.document_config_form_object import (
     DocumentConfigFormObject,
 )
+from strictdoc.helpers.mid import MID
 
 
 class UpdateDocumentConfigTransform:
@@ -59,9 +64,36 @@ def validate(
     ):
         errors: Dict[str, List[str]] = defaultdict(list)
         assert isinstance(document, SDocDocument)
+
         if len(form_object.document_title) == 0:
             errors["TITLE"].append("Document title must not be empty.")
 
+        # Ensure that UID doesn't have any incoming links if it is going to be
+        # renamed or removed
+        existing_node: SDocNode = self.traceability_index.get_node_by_mid(
+            MID(form_object.document_mid)
+        )
+        existing_uid = existing_node.reserved_uid
+        if existing_uid is not None:
+            if (
+                form_object.document_uid is None
+                or existing_uid != form_object.document_uid
+            ):
+                existing_incoming_links: Optional[List[InlineLink]] = (
+                    self.traceability_index.get_incoming_links(existing_node)
+                )
+                if (
+                    existing_incoming_links is not None
+                    and len(existing_incoming_links) > 0
+                ):
+                    errors["UID"].append(
+                        (
+                            "Renaming a node UID when the node has "
+                            "incoming links is not supported yet. "
+                            "Please delete all incoming links first."
+                        ),
+                    )
+
         if len(errors):
             raise MultipleValidationError(
                 "Document form has not passed validation.", errors=errors
diff --git a/tests/integration/commands/export/html/markup/11_node_LINK_references_a_document/input1.sdoc b/tests/integration/commands/export/html/markup/11_node_LINK_references_a_document/input1.sdoc
new file mode 100644
index 000000000..1c06a23dc
--- /dev/null
+++ b/tests/integration/commands/export/html/markup/11_node_LINK_references_a_document/input1.sdoc
@@ -0,0 +1,10 @@
+[DOCUMENT]
+TITLE: Document 1
+UID: DOC-1
+
+[REQUIREMENT]
+UID: REQ-2
+TITLE: Foo Bar
+STATEMENT: >>>
+Read [LINK: DOC-1] then [LINK: DOC-2].
+<<<
diff --git a/tests/integration/commands/export/html/markup/11_node_LINK_references_a_document/input2.sdoc b/tests/integration/commands/export/html/markup/11_node_LINK_references_a_document/input2.sdoc
new file mode 100644
index 000000000..07cc08ec1
--- /dev/null
+++ b/tests/integration/commands/export/html/markup/11_node_LINK_references_a_document/input2.sdoc
@@ -0,0 +1,8 @@
+[DOCUMENT]
+TITLE: Document 2
+UID: DOC-2
+
+[TEXT]
+STATEMENT: >>>
+Read [LINK: DOC-2] then [LINK: DOC-1].
+<<<
diff --git a/tests/integration/commands/export/html/markup/11_node_LINK_references_a_document/test.itest b/tests/integration/commands/export/html/markup/11_node_LINK_references_a_document/test.itest
new file mode 100644
index 000000000..7085c4b97
--- /dev/null
+++ b/tests/integration/commands/export/html/markup/11_node_LINK_references_a_document/test.itest
@@ -0,0 +1,10 @@
+RUN: %strictdoc export %S --output-dir Output | filecheck %s --dump-input=fail
+CHECK: Published: Document 1
+
+RUN: %cat %S/Output/html/%THIS_TEST_FOLDER/input1.html | filecheck %s --dump-input=fail --check-prefix CHECK-HTML-DOC1
+
+CHECK-HTML-DOC1: Read <a href="../11_node_LINK_references_a_document/input1.html#_TOP">🔗&nbsp;Document 1</a> then <a href="../11_node_LINK_references_a_document/input2.html#_TOP">🔗&nbsp;Document 2</a>.
+
+RUN: %cat %S/Output/html/%THIS_TEST_FOLDER/input2.html | filecheck %s --dump-input=fail --check-prefix CHECK-HTML-DOC2
+
+CHECK-HTML-DOC2: Read <a href="../11_node_LINK_references_a_document/input2.html#_TOP">🔗&nbsp;Document 2</a> then <a href="../11_node_LINK_references_a_document/input1.html#_TOP">🔗&nbsp;Document 1</a>.
diff --git a/tests/integration/commands/export/rst/52_link_to_document/expected/input1.rst b/tests/integration/commands/export/rst/52_link_to_document/expected/input1.rst
new file mode 100644
index 000000000..d67bf1864
--- /dev/null
+++ b/tests/integration/commands/export/rst/52_link_to_document/expected/input1.rst
@@ -0,0 +1,18 @@
+.. _DOC-1:
+
+Document 1
+$$$$$$$$$$
+
+.. _REQ-2:
+
+Foo Bar
+=======
+
+.. list-table::
+    :align: left
+    :header-rows: 0
+
+    * - **UID:**
+      - REQ-2
+
+Read :ref:`Document 1 <DOC-1>` then :ref:`Document 2 <DOC-2>`.
diff --git a/tests/integration/commands/export/rst/52_link_to_document/expected/input2.rst b/tests/integration/commands/export/rst/52_link_to_document/expected/input2.rst
new file mode 100644
index 000000000..54e8d3202
--- /dev/null
+++ b/tests/integration/commands/export/rst/52_link_to_document/expected/input2.rst
@@ -0,0 +1,6 @@
+.. _DOC-2:
+
+Document 2
+$$$$$$$$$$
+
+Read :ref:`Document 2 <DOC-2>` then :ref:`Document 1 <DOC-1>`.
diff --git a/tests/integration/commands/export/rst/52_link_to_document/input1.sdoc b/tests/integration/commands/export/rst/52_link_to_document/input1.sdoc
new file mode 100644
index 000000000..1c06a23dc
--- /dev/null
+++ b/tests/integration/commands/export/rst/52_link_to_document/input1.sdoc
@@ -0,0 +1,10 @@
+[DOCUMENT]
+TITLE: Document 1
+UID: DOC-1
+
+[REQUIREMENT]
+UID: REQ-2
+TITLE: Foo Bar
+STATEMENT: >>>
+Read [LINK: DOC-1] then [LINK: DOC-2].
+<<<
diff --git a/tests/integration/commands/export/rst/52_link_to_document/input2.sdoc b/tests/integration/commands/export/rst/52_link_to_document/input2.sdoc
new file mode 100644
index 000000000..07cc08ec1
--- /dev/null
+++ b/tests/integration/commands/export/rst/52_link_to_document/input2.sdoc
@@ -0,0 +1,8 @@
+[DOCUMENT]
+TITLE: Document 2
+UID: DOC-2
+
+[TEXT]
+STATEMENT: >>>
+Read [LINK: DOC-2] then [LINK: DOC-1].
+<<<
diff --git a/tests/integration/commands/export/rst/52_link_to_document/test.itest b/tests/integration/commands/export/rst/52_link_to_document/test.itest
new file mode 100644
index 000000000..fd136ab42
--- /dev/null
+++ b/tests/integration/commands/export/rst/52_link_to_document/test.itest
@@ -0,0 +1,6 @@
+RUN: %strictdoc export %S --formats=rst --output-dir Output
+
+RUN: %check_exists --file "%S/Output/rst/input1.rst"
+
+RUN: %diff "%S/Output/rst/input1.rst" "%S/expected/input1.rst"
+RUN: %diff "%S/Output/rst/input2.rst" "%S/expected/input2.rst"