Add support for instance-identifier

My goal was at least to make basic R/O operation work with ietf-alarms.
Then I didn't have an easy way of testing this code, so I made it work
for editing as well.

Limitations:

- You can set an instance-identifier which points to an input node of an
  RPC/action, which sounds fishy, but that could be a libyang bug.

- Non-config lists works funnily, which might point to a bug somewhere
  in our handling of data paths:

 /> set example-schema:iid-valid /example-schema:users/userList
 libyang[0]: Unexpected XPath token "]" ("]"). (path: Data location "/example-schema:iid-valid".)
 libyang[0]: Invalid instance-identifier "/example-schema:users/userList[]" value - syntax error. (path: Data location "/example-schema:iid-valid".)
 The following errors occured:
  Message: Invalid instance-identifier "/example-schema:users/userList[]" value - syntax error.
  XPath: Data location "/example-schema:iid-valid".

- XPath says that any key value must be quoted, but the CLI reuses the
  existing leaf_data_ parser for list keys (even prior to this patch),
  which means that a list indexed by, say, an enum looks like:

  /example-schema/list[val=666]

  ...which is not a valid XPath. A valid XPath would look like this:

  /example-schema/list[val='666']

  This new feature hits the same problem; when the instance-identifier
  gets parsed from the user's input and converted to a string, the usual
  netconf-cli rules are followed, and the list keys are not quoted
  unless they are strings. This means that these values cannot be stored
  or committed because libyang correctly rejects unquoted key values in,
  say, instance-identifier values.

  Quite frankly I'm not even sure how come that this has not been a
  problem during normal operation, but perhaps it's only because we've
  always tested with string-based lists and nothing else. Sure, we have
  partial unit tests for all sorts of data types, but nothing actually
  checks the full stack from the parser's input all the way to a
  backend.

Path completion was a challenge for me to get right. At first I wanted
to use a different, fresh ParserContext to make sure that we do not
affect the main (leaf) path's state at all. That did not work well even
when I tried to copy the "relevant" memebrs of ParserContext back to the
original parser. But it turns out that simply reusing the existing
ParserContext works perfectly well. The only caveat is the concept of
"cwd", where a leaf below a certain container would "expect" paths that
are relative to the original cwd (which means "not root"). That's just
broken. The easiest fix is to always enforce an absolute path. The
easiest way of getting there was a yet another "parser mode" for
absolute-paths-only.

Yay for the immediately-invoked-lambda, because I really wanted to use
an constexpr if in there.

Change-Id: I68302ad854eab43aa6c42dcb2693b71ca3d2bd48
Depends-on: https://gerrit.cesnet.cz/c/CzechLight/dependencies/+/6445
diff --git a/tests/leaf_editing.cpp b/tests/leaf_editing.cpp
index 5965110..302c8e4 100644
--- a/tests/leaf_editing.cpp
+++ b/tests/leaf_editing.cpp
@@ -72,6 +72,9 @@
     schema->addLeaf("/", "mod:readonly", yang::Int32{}, yang::AccessType::ReadOnly);
 
     schema->addLeaf("/", "mod:flags", yang::Bits{{"carry", "sign"}});
+    schema->addLeaf("/", "mod:iid", yang::InstanceIdentifier{});
+    schema->addLeaf("/mod:contA", "mod:x", yang::String{});
+    schema->addLeaf("/mod:contA", "mod:iid", yang::InstanceIdentifier{});
 
     Parser parser(schema);
     std::string input;
@@ -509,6 +512,47 @@
             }
         }
 
+        SECTION("instance-identifier") {
+            SECTION("toplevel")
+            {
+                input = "set mod:iid /mod:leafUint32";
+                expected.m_path.m_nodes.emplace_back(module_{"mod"}, leaf_{"iid"});
+                expected.m_data = instanceIdentifier_{"/mod:leafUint32"};
+            }
+
+            SECTION("deep to toplevel")
+            {
+                input = "set mod:contA/iid /mod:leafUint32";
+                expected.m_path.m_nodes.emplace_back(module_{"mod"}, container_{"contA"});
+                expected.m_path.m_nodes.emplace_back(leaf_{"iid"});
+                expected.m_data = instanceIdentifier_{"/mod:leafUint32"};
+            }
+
+            SECTION("deep to deep unprefixed")
+            {
+                input = "set mod:contA/iid /mod:contA/x";
+                expected.m_path.m_nodes.emplace_back(module_{"mod"}, container_{"contA"});
+                expected.m_path.m_nodes.emplace_back(leaf_{"iid"});
+                expected.m_data = instanceIdentifier_{"/mod:contA/x"};
+            }
+
+            SECTION("deep to deep mod-prefixed")
+            {
+                input = "set mod:contA/iid /mod:contA/mod:x";
+                expected.m_path.m_nodes.emplace_back(module_{"mod"}, container_{"contA"});
+                expected.m_path.m_nodes.emplace_back(leaf_{"iid"});
+                expected.m_data = instanceIdentifier_{"/mod:contA/mod:x"};
+            }
+
+            SECTION("absolute when nested")
+            {
+                cwd.m_nodes.emplace_back(module_{"mod"}, container_{"contA"});
+                input = "set iid /mod:contA/x";
+                expected.m_path.m_nodes.emplace_back(leaf_{"iid"});
+                expected.m_data = instanceIdentifier_{"/mod:contA/x"};
+            }
+        }
+
         parser.changeNode(cwd);
         command_ command = parser.parseCommand(input, errorStream);
         REQUIRE(command.type() == typeid(set_));
@@ -683,6 +727,39 @@
             input = "set mod:flags carry carry";
         }
 
+        SECTION("instance-identifier non-existing node")
+        {
+            input = "set mod:iid /mod:404";
+        }
+
+        SECTION("instance-identifier non-existing node without leading slash")
+        {
+            input = "set mod:iid mod:404";
+        }
+
+        SECTION("instance-identifier node without leading slash")
+        {
+            input = "set mod:iid mod:leafUint32";
+        }
+
+        SECTION("instance-identifier to pseudo-relative unprefixed")
+        {
+            cwd.m_nodes.emplace_back(module_{"mod"}, container_{"contA"});
+            input = "set iid x";
+        }
+
+        SECTION("instance-identifier to pseudo-relative prefixed")
+        {
+            cwd.m_nodes.emplace_back(module_{"mod"}, container_{"contA"});
+            input = "set iid mod:x";
+        }
+
+        SECTION("instance-identifier without leading slash when nested")
+        {
+            cwd.m_nodes.emplace_back(module_{"mod"}, container_{"contA"});
+            input = "set iid mod:contA/x";
+        }
+
         parser.changeNode(cwd);
         REQUIRE_THROWS_AS(parser.parseCommand(input, errorStream), InvalidCommandException);
         REQUIRE(errorStream.str().find(expectedError) != std::string::npos);