Rework list ending data paths

There are now four modes to NodeParser. This mode is specified by the
NodeParserMode enum via a template argument.

Modes:
SchemaNode: will parse schema nodes.

CompleteDataNode: works exactly as the dataNode_ parser before. It will
parse data nodes.

IncompleteDataNode: works the same as CompleteDataNode, but allows
parsing a list without keys.

CompletionsOnly: in this mode, the parser only creates completions, and
doesn't do any actual parsing. This mode is needed when parsing a path
ending with a list without keys, with a trailing slash.
Example: /module:list/
After the path fragment and the trailing slash are parsed, no more node
parsing happens (because there's no more input). However, I still need
to generate completions. That's when I need to use CompletionsOnly.

PathParser now also has modes, they are similar to the NodeParser modes.
They are used to state what kind of path is supposed to be parsed.

Change-Id: I65b6de45c452ece37002a5cfe4048b9042562d2a
diff --git a/src/ast_handlers.hpp b/src/ast_handlers.hpp
index ab9958a..735700a 100644
--- a/src/ast_handlers.hpp
+++ b/src/ast_handlers.hpp
@@ -120,18 +120,6 @@
         return x3::error_handler_result::rethrow;
     }
 };
-struct list_class {
-    template <typename T, typename Iterator, typename Context>
-    void on_success(Iterator const&, Iterator const&, T& ast, Context const& context)
-    {
-        auto& parserContext = x3::get<parser_context_tag>(context);
-        const Schema& schema = parserContext.m_schema;
-
-        if (!schema.isList(parserContext.currentSchemaPath(), {parserContext.m_curModule, ast.m_name})) {
-            _pass(context) = false;
-        }
-    }
-};
 
 struct module_class {
     template <typename T, typename Iterator, typename Context>
@@ -150,15 +138,6 @@
     }
 };
 
-struct dataNodeList_class {
-    template <typename T, typename Iterator, typename Context>
-    void on_success(Iterator const&, Iterator const&, T& ast, Context const& context)
-    {
-        auto& parserContext = x3::get<parser_context_tag>(context);
-        parserContext.pushPathFragment(ast);
-    }
-};
-
 struct absoluteStart_class {
     template <typename T, typename Iterator, typename Context>
     void on_success(Iterator const&, Iterator const&, T&, Context const& context)
@@ -168,10 +147,6 @@
     }
 };
 
-struct dataNodesListEnd_class;
-
-struct dataPathListEnd_class;
-
 struct discard_class;
 
 struct ls_class;
@@ -341,36 +316,6 @@
 
 struct trailingSlash_class;
 
-struct createPathSuggestions_class {
-    template <typename T, typename Iterator, typename Context>
-    void on_success(Iterator const& begin, Iterator const&, T&, Context const& context)
-    {
-        auto& parserContext = x3::get<parser_context_tag>(context);
-        const auto& schema = parserContext.m_schema;
-
-        parserContext.m_completionIterator = begin;
-        auto suggestions = schema.availableNodes(parserContext.currentSchemaPath(), Recursion::NonRecursive);
-        std::set<Completion> suffixesAdded;
-        std::transform(suggestions.begin(), suggestions.end(),
-                std::inserter(suffixesAdded, suffixesAdded.end()),
-                [&parserContext, &schema](const ModuleNodePair& node) {
-            std::string completion = (node.first ? *node.first + ":" : "") + node.second;
-
-            if (schema.isLeaf(parserContext.currentSchemaPath(), node)) {
-                return Completion{completion + " "};
-            }
-            if (schema.isContainer(parserContext.currentSchemaPath(), node)) {
-                return Completion{completion + "/"};
-            }
-            if (schema.isList(parserContext.currentSchemaPath(), node)) {
-                return Completion{completion, "[", Completion::WhenToAdd::IfFullMatch};
-            }
-            return Completion{completion};
-        });
-        parserContext.m_suggestions = suffixesAdded;
-    }
-};
-
 std::set<Completion> generateMissingKeyCompletionSet(std::set<std::string> keysNeeded, std::map<std::string, leaf_data_> currentSet);
 
 struct createKeySuggestions_class {
diff --git a/src/grammars.hpp b/src/grammars.hpp
index 74a6a17..859d5b8 100644
--- a/src/grammars.hpp
+++ b/src/grammars.hpp
@@ -47,7 +47,7 @@
 } const ls_options;
 
 auto const ls_def =
-    ls_::name >> *(space_separator >> ls_options) >> -(space_separator >> (dataPathListEnd | anyPath | (module >> "*")));
+    ls_::name >> *(space_separator >> ls_options) >> -(space_separator >> (anyPath | (module >> "*")));
 
 auto const cd_def =
     cd_::name >> space_separator > dataPath;
diff --git a/src/parser_context.hpp b/src/parser_context.hpp
index f837934..87706a5 100644
--- a/src/parser_context.hpp
+++ b/src/parser_context.hpp
@@ -10,6 +10,7 @@
 #include "completion.hpp"
 #include "data_query.hpp"
 #include "schema.hpp"
+
 struct ParserContext {
     ParserContext(const Schema& schema, const std::shared_ptr<const DataQuery> dataQuery, const dataPath_& curDir);
     schemaPath_ currentSchemaPath();
diff --git a/src/path_parser.hpp b/src/path_parser.hpp
index 6360912..6b894d8 100644
--- a/src/path_parser.hpp
+++ b/src/path_parser.hpp
@@ -14,42 +14,68 @@
 
 namespace x3 = boost::spirit::x3;
 
-x3::rule<dataNodeList_class, decltype(dataPath_::m_nodes)::value_type> const dataNodeList = "dataNodeList";
-x3::rule<dataNodesListEnd_class, decltype(dataPath_::m_nodes)> const dataNodesListEnd = "dataNodesListEnd";
-x3::rule<dataPathListEnd_class, dataPath_> const dataPathListEnd = "dataPathListEnd";
 x3::rule<leaf_path_class, dataPath_> const leafPath = "leafPath";
 x3::rule<presenceContainerPath_class, dataPath_> const presenceContainerPath = "presenceContainerPath";
 x3::rule<listInstancePath_class, dataPath_> const listInstancePath = "listInstancePath";
 x3::rule<initializePath_class, x3::unused_type> const initializePath = "initializePath";
-x3::rule<createPathSuggestions_class, x3::unused_type> const createPathSuggestions = "createPathSuggestions";
 x3::rule<trailingSlash_class, TrailingSlash> const trailingSlash = "trailingSlash";
 x3::rule<absoluteStart_class, Scope> const absoluteStart = "absoluteStart";
 x3::rule<keyValue_class, keyValue_> const keyValue = "keyValue";
 x3::rule<key_identifier_class, std::string> const key_identifier = "key_identifier";
 x3::rule<listSuffix_class, std::vector<keyValue_>> const listSuffix = "listSuffix";
-x3::rule<list_class, list_> const list = "list";
 x3::rule<createKeySuggestions_class, x3::unused_type> const createKeySuggestions = "createKeySuggestions";
 x3::rule<createValueSuggestions_class, x3::unused_type> const createValueSuggestions = "createValueSuggestions";
 x3::rule<suggestKeysEnd_class, x3::unused_type> const suggestKeysEnd = "suggestKeysEnd";
 
-template <typename NodeType>
-struct NodeParser : x3::parser<NodeParser<NodeType>> {
-    using attribute_type = NodeType;
+enum class NodeParserMode {
+    CompleteDataNode,
+    IncompleteDataNode,
+    CompletionsOnly,
+    SchemaNode
+};
+
+template <auto>
+struct ModeToAttribute;
+template <>
+struct ModeToAttribute<NodeParserMode::CompleteDataNode> {
+    using type = dataNode_;
+};
+template <>
+struct ModeToAttribute<NodeParserMode::IncompleteDataNode> {
+    using type = dataNode_;
+};
+template <>
+struct ModeToAttribute<NodeParserMode::SchemaNode> {
+    using type = schemaNode_;
+};
+// The CompletionsOnly attribute is dataNode_ only because of convenience:
+// having the same return type means we can get by without a ton of `if constexpr` stanzas.
+// So the code will still "parse data into the target attr" for simplicity.
+template <>
+struct ModeToAttribute<NodeParserMode::CompletionsOnly> {
+    using type = dataNode_;
+};
+
+template <NodeParserMode PARSER_MODE>
+struct NodeParser : x3::parser<NodeParser<PARSER_MODE>> {
+    using attribute_type = typename ModeToAttribute<PARSER_MODE>::type;
+    // GCC complains that `end` isn't used when doing completions only
+    // FIXME: GCC 10.1 doesn't emit a warning here. Remove [[maybe_unused]] when GCC 10 is available
     template <typename It, typename Ctx, typename RCtx, typename Attr>
-    bool parse(It& begin, It end, Ctx const& ctx, RCtx& rctx, Attr& attr) const
+    bool parse(It& begin, [[maybe_unused]] It end, Ctx const& ctx, RCtx& rctx, Attr& attr) const
     {
         std::string tableName;
-        if constexpr (std::is_same<NodeType, schemaNode_>()) {
+        if constexpr (std::is_same<attribute_type, schemaNode_>()) {
             tableName = "schemaNode";
         } else {
             tableName = "dataNode";
         }
-        x3::symbols<NodeType> table(tableName);
+        x3::symbols<attribute_type> table(tableName);
 
         ParserContext& parserContext = x3::get<parser_context_tag>(ctx);
         parserContext.m_suggestions.clear();
         for (const auto& child : parserContext.m_schema.availableNodes(parserContext.currentSchemaPath(), Recursion::NonRecursive)) {
-            NodeType out;
+            attribute_type out;
             std::string parseString;
             if (child.first) {
                 out.m_prefix = module_{*child.first};
@@ -67,7 +93,7 @@
                     parserContext.m_suggestions.emplace(Completion{parseString + " "});
                     break;
                 case yang::NodeTypes::List:
-                    if constexpr (std::is_same<NodeType, schemaNode_>()) {
+                    if constexpr (std::is_same<attribute_type, schemaNode_>()) {
                         out.m_suffix = list_{child.second};
                     } else {
                         out.m_suffix = listElement_{child.second, {}};
@@ -82,7 +108,7 @@
                     continue;
             }
             table.add(parseString, out);
-            table.add("..", NodeType{nodeup_{}});
+            table.add("..", attribute_type{nodeup_{}});
             if (!child.first) {
                 auto topLevelModule = parserContext.currentSchemaPath().m_nodes.begin()->m_prefix;
                 out.m_prefix = topLevelModule;
@@ -90,73 +116,143 @@
             }
         }
         parserContext.m_completionIterator = begin;
-        auto res = table.parse(begin, end, ctx, rctx, attr);
 
-        if (attr.m_prefix) {
-            parserContext.m_curModule = attr.m_prefix->m_name;
-        }
-
-        if (attr.m_suffix.type() == typeid(leaf_)) {
-            parserContext.m_tmpListKeyLeafPath.m_location = parserContext.currentSchemaPath();
-            ModuleNodePair node{attr.m_prefix.flat_map([](const auto& it) {
-                return boost::optional<std::string>{it.m_name};
-            }), boost::get<leaf_>(attr.m_suffix).m_name};
-            parserContext.m_tmpListKeyLeafPath.m_node = node;
-        }
-
-        if constexpr (std::is_same<NodeType, dataNode_>()) {
-            if (attr.m_suffix.type() == typeid(listElement_)) {
-                parserContext.m_tmpListName = boost::get<listElement_>(attr.m_suffix).m_name;
-                res = listSuffix.parse(begin, end, ctx, rctx, boost::get<listElement_>(attr.m_suffix).m_keys);
+        if constexpr (PARSER_MODE == NodeParserMode::CompletionsOnly) {
+            return true;
+        } else {
+            It saveIter;
+            // GCC complains that I assign saveIter because I use it only if NodeType is dataNode_
+            // FIXME: GCC 10.1 doesn't emit a warning here. Make this unconditional when GCC 10 is available.
+            if constexpr (std::is_same<attribute_type, dataNode_>()) {
+                saveIter = begin;
             }
-        }
 
-        if (res) {
-            parserContext.pushPathFragment(attr);
-            parserContext.m_topLevelModulePresent = true;
-        }
+            auto res = table.parse(begin, end, ctx, rctx, attr);
 
-        if (attr.m_prefix) {
-            parserContext.m_curModule = boost::none;
+            if (attr.m_prefix) {
+                parserContext.m_curModule = attr.m_prefix->m_name;
+            }
+
+            if (attr.m_suffix.type() == typeid(leaf_)) {
+                parserContext.m_tmpListKeyLeafPath.m_location = parserContext.currentSchemaPath();
+                ModuleNodePair node{attr.m_prefix.flat_map([](const auto& it) {
+                                        return boost::optional<std::string>{it.m_name};
+                                    }),
+                                    boost::get<leaf_>(attr.m_suffix).m_name};
+                parserContext.m_tmpListKeyLeafPath.m_node = node;
+            }
+
+            if constexpr (std::is_same<attribute_type, dataNode_>()) {
+                if (attr.m_suffix.type() == typeid(listElement_)) {
+                    parserContext.m_tmpListName = boost::get<listElement_>(attr.m_suffix).m_name;
+                    res = listSuffix.parse(begin, end, ctx, rctx, boost::get<listElement_>(attr.m_suffix).m_keys);
+
+                    // FIXME: think of a better way to do this, that is, get rid of manual iterator reverting
+                    if (!res) {
+                        // If listSuffix didn't succeed, we check, if we allow incomplete nodes. If we do, then we replace listElement_ with list_.
+                        // If we don't, we fail the whole symbol table.
+                        if constexpr (PARSER_MODE == NodeParserMode::IncompleteDataNode) {
+                            res = true;
+                            attr.m_suffix = list_{boost::get<listElement_>(attr.m_suffix).m_name};
+                        } else {
+                            begin = saveIter;
+                        }
+                    }
+                }
+            }
+
+            if (res) {
+                parserContext.pushPathFragment(attr);
+                parserContext.m_topLevelModulePresent = true;
+            }
+
+            if (attr.m_prefix) {
+                parserContext.m_curModule = boost::none;
+            }
+            return res;
         }
-        return res;
     }
 };
 
-NodeParser<schemaNode_> schemaNode;
-NodeParser<dataNode_> dataNode;
+NodeParser<NodeParserMode::SchemaNode> schemaNode;
+NodeParser<NodeParserMode::CompleteDataNode> dataNode;
+NodeParser<NodeParserMode::IncompleteDataNode> dataNodeAllowList;
+NodeParser<NodeParserMode::CompletionsOnly> pathCompletions;
 
 using AnyPath = boost::variant<schemaPath_, dataPath_>;
 
-template <typename PathType>
-struct PathParser : x3::parser<PathParser<PathType>> {
-    using attribute_type = PathType;
+enum class PathParserMode {
+    AnyPath,
+    DataPath,
+    DataPathListEnd
+};
+
+template <>
+struct ModeToAttribute<PathParserMode::AnyPath> {
+    using type = AnyPath;
+};
+
+template <>
+struct ModeToAttribute<PathParserMode::DataPath> {
+    using type = dataPath_;
+};
+
+template <>
+struct ModeToAttribute<PathParserMode::DataPathListEnd> {
+    using type = dataPath_;
+};
+
+template <PathParserMode PARSER_MODE>
+struct PathParser : x3::parser<PathParser<PARSER_MODE>> {
+    using attribute_type = ModeToAttribute<PARSER_MODE>;
     template <typename It, typename Ctx, typename RCtx, typename Attr>
     bool parse(It& begin, It end, Ctx const& ctx, RCtx& rctx, Attr& attr) const
     {
         initializePath.parse(begin, end, ctx, rctx, x3::unused);
         dataPath_ attrData;
 
+        auto pathEnd = x3::rule<class PathEnd>{"pathEnd"} = &space_separator | x3::eoi;
         // absoluteStart has to be separate from the dataPath parser,
         // otherwise, if the "dataNode % '/'" parser fails, the begin iterator
         // gets reverted to before the starting slash.
-        auto res = -absoluteStart.parse(begin, end, ctx, rctx, attrData.m_scope);
-        auto dataPath = x3::attr(attrData.m_scope) >> dataNode % '/' >> -trailingSlash;
+        auto res = (-absoluteStart).parse(begin, end, ctx, rctx, attrData.m_scope);
+        auto dataPath = x3::attr(attrData.m_scope)
+            >> (dataNode % '/' | pathEnd >> x3::attr(std::vector<dataNode_>{}))
+            >> -trailingSlash;
         res = dataPath.parse(begin, end, ctx, rctx, attrData);
-        attr = attrData;
 
-        if constexpr (std::is_same<Attr, AnyPath>()) {
-            auto pathEnd = x3::rule<class PathEnd>{"pathEnd"} = &space_separator | x3::eoi;
-            // If parsing failed, or if there's more input we try parsing schema nodes
+        // If we allow data paths with a list at the end, we just try to parse that separately.
+        if constexpr (PARSER_MODE == PathParserMode::DataPathListEnd || PARSER_MODE == PathParserMode::AnyPath) {
             if (!res || !pathEnd.parse(begin, end, ctx, rctx, x3::unused)) {
-                // If dataPath parsed some nodes, they will be saved in `attrData`. We have to keep these.
-                schemaPath_ attrSchema = dataPathToSchemaPath(attrData);
-                auto schemaPath = schemaNode % '/';
-                // The schemaPath parser continues where the dataPath parser ended.
-                res = schemaPath.parse(begin, end, ctx, rctx, attrSchema.m_nodes);
-                auto trailing = -trailingSlash >> pathEnd;
-                res = trailing.parse(begin, end, ctx, rctx, attrSchema.m_trailingSlash);
-                attr = attrSchema;
+                dataNode_ attrNodeList;
+                res = dataNodeAllowList.parse(begin, end, ctx, rctx, attrNodeList);
+                if (res) {
+                    attrData.m_nodes.push_back(attrNodeList);
+                    // If the trailing slash matches, no more nodes are parsed.
+                    // That means no more completion. So, I generate them
+                    // manually.
+                    res = (-(trailingSlash >> x3::omit[pathCompletions])).parse(begin, end, ctx, rctx, attrData.m_trailingSlash);
+                }
+            }
+        }
+
+        attr = attrData;
+        if constexpr (PARSER_MODE == PathParserMode::AnyPath) {
+            // If our data path already has some listElement_ fragments, we can't parse rest of the path as a schema path
+            auto hasLists = std::any_of(attrData.m_nodes.begin(), attrData.m_nodes.end(),
+                [] (const auto& node) { return node.m_suffix.type() == typeid(listElement_); });
+            // If parsing failed, or if there's more input we try parsing schema nodes.
+            if (!hasLists) {
+                if (!res || !pathEnd.parse(begin, end, ctx, rctx, x3::unused)) {
+                    // If dataPath parsed some nodes, they will be saved in `attrData`. We have to keep these.
+                    schemaPath_ attrSchema = dataPathToSchemaPath(attrData);
+                    auto schemaPath = schemaNode % '/';
+                    // The schemaPath parser continues where the dataPath parser ended.
+                    res = schemaPath.parse(begin, end, ctx, rctx, attrSchema.m_nodes);
+                    auto trailing = -trailingSlash >> pathEnd;
+                    res = trailing.parse(begin, end, ctx, rctx, attrSchema.m_trailingSlash);
+                    attr = attrSchema;
+                }
             }
         }
         return res;
@@ -170,8 +266,9 @@
 // The PathParser class would get a boost::variant as the attribute, but I
 // don't want to deal with that, so I use these wrappers to ensure the
 // attribute I want (and let Spirit deal with boost::variant).
-auto const anyPath = x3::rule<class anyPath_class, AnyPath>{"anyPath"} = PathParser<AnyPath>{};
-auto const dataPath = x3::rule<class dataPath_class, dataPath_>{"dataPath"} = PathParser<dataPath_>{};
+auto const anyPath = x3::rule<class anyPath_class, AnyPath>{"anyPath"} = PathParser<PathParserMode::AnyPath>{};
+auto const dataPath = x3::rule<class dataPath_class, dataPath_>{"dataPath"} = PathParser<PathParserMode::DataPath>{};
+auto const dataPathListEnd = x3::rule<class dataPath_class, dataPath_>{"dataPath"} = PathParser<PathParserMode::DataPathListEnd>{};
 
 #if __clang__
 #pragma GCC diagnostic push
@@ -214,22 +311,6 @@
 auto const trailingSlash_def =
     x3::omit['/'] >> x3::attr(TrailingSlash::Present);
 
-auto const createPathSuggestions_def =
-    x3::eps;
-
-auto const dataNodeList_def =
-    createPathSuggestions >> -(module) >> list;
-
-// This intermediate rule is mandatory, because we need the first alternative
-// to be collapsed to a vector. If we didn't use the intermediate rule,
-// Spirit wouldn't know we want it to collapse.
-// https://github.com/boostorg/spirit/issues/408
-auto const dataNodesListEnd_def =
-    dataNode % '/' >> '/' >> dataNodeList >> -(&char_('/') >> createPathSuggestions) |
-    x3::attr(decltype(dataPath_::m_nodes)()) >> dataNodeList;
-
-auto const dataPathListEnd_def = initializePath >> absoluteStart >> createPathSuggestions >> x3::attr(decltype(dataPath_::m_nodes)()) >> x3::attr(TrailingSlash::NonPresent) >> x3::eoi | initializePath >> -(absoluteStart >> createPathSuggestions) >> dataNodesListEnd >> (-(trailingSlash >> createPathSuggestions) >> -(completing >> rest) >> (&space_separator | x3::eoi));
-
 auto const leafPath_def =
     dataPath;
 
@@ -252,16 +333,11 @@
 BOOST_SPIRIT_DEFINE(keyValue)
 BOOST_SPIRIT_DEFINE(key_identifier)
 BOOST_SPIRIT_DEFINE(listSuffix)
-BOOST_SPIRIT_DEFINE(list)
-BOOST_SPIRIT_DEFINE(dataNodeList)
-BOOST_SPIRIT_DEFINE(dataNodesListEnd)
 BOOST_SPIRIT_DEFINE(leafPath)
 BOOST_SPIRIT_DEFINE(presenceContainerPath)
 BOOST_SPIRIT_DEFINE(listInstancePath)
-BOOST_SPIRIT_DEFINE(dataPathListEnd)
 BOOST_SPIRIT_DEFINE(initializePath)
 BOOST_SPIRIT_DEFINE(createKeySuggestions)
-BOOST_SPIRIT_DEFINE(createPathSuggestions)
 BOOST_SPIRIT_DEFINE(createValueSuggestions)
 BOOST_SPIRIT_DEFINE(suggestKeysEnd)
 BOOST_SPIRIT_DEFINE(absoluteStart)
diff --git a/tests/ls.cpp b/tests/ls.cpp
index 93ec75b..6d70414 100644
--- a/tests/ls.cpp
+++ b/tests/ls.cpp
@@ -9,6 +9,7 @@
 #include "trompeloeil_doctest.hpp"
 #include "ast_commands.hpp"
 #include "parser.hpp"
+#include "pretty_printers.hpp"
 #include "static_schema.hpp"
 
 TEST_CASE("ls")
diff --git a/tests/pretty_printers.hpp b/tests/pretty_printers.hpp
index fb8aebf..7f59adc 100644
--- a/tests/pretty_printers.hpp
+++ b/tests/pretty_printers.hpp
@@ -110,3 +110,21 @@
     }
     return s;
 }
+
+std::ostream& operator<<(std::ostream& s, const boost::optional<boost::variant<dataPath_, schemaPath_, module_>>& path)
+{
+    if (path) {
+        s << *path;
+    } else {
+        s << "boost::none";
+    }
+
+    return s;
+}
+
+
+std::ostream& operator<<(std::ostream& s, const ls_& ls)
+{
+    s << "\nls_ {\n    " << ls.m_path << "}\n";
+    return s;
+}