]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[65-libyang-adaptor] Imported adaptor code from kea-yang
authorFrancis Dupont <fdupont@isc.org>
Fri, 31 Aug 2018 19:49:53 +0000 (21:49 +0200)
committerFrancis Dupont <fdupont@isc.org>
Mon, 3 Sep 2018 16:01:39 +0000 (18:01 +0200)
configure.ac
src/lib/Makefile.am
src/lib/yang/Makefile.am [new file with mode: 0644]
src/lib/yang/adaptor.cc [new file with mode: 0644]
src/lib/yang/adaptor.h [new file with mode: 0644]
src/lib/yang/tests/Makefile.am [new file with mode: 0644]
src/lib/yang/tests/adaptor_unittests.cc [new file with mode: 0644]
src/lib/yang/tests/run_unittests.cc [new file with mode: 0644]

index 3d76e5b9f5fbe686d4ead2f25172d0eaae5d3739..e9f2b0b486a725f7262edfa1fbe06ca2bb179d1e 100644 (file)
@@ -74,6 +74,9 @@ if test "$cross_compiling" = "yes"; then
 fi
 AM_CONDITIONAL([CROSS_COMPILING], [test "$cross_compiling" = "yes"])
 
+# pkg-config can be required.
+AC_PATH_PROG([PKG_CONFIG], [pkg-config])
+
 # Enable low-performing debugging facilities? This option optionally
 # enables some debugging aids that perform slowly and hence aren't built
 # by default.
@@ -817,7 +820,6 @@ AC_ARG_WITH([cql],
     [cql_config="$withval"])
 
 if test "${cql_config}" = "yes" ; then
-    AC_PATH_PROG([PKG_CONFIG], [pkg-config])
     CQL_CONFIG="$PKG_CONFIG"
 elif test "${cql_config}" != "no" ; then
     CQL_CONFIG="${cql_config}"
@@ -1597,6 +1599,8 @@ AC_CONFIG_FILES([Makefile
                  src/lib/util/threads/Makefile
                  src/lib/util/threads/tests/Makefile
                  src/lib/util/unittests/Makefile
+                 src/lib/yang/Makefile
+                 src/lib/yang/tests/Makefile
                  src/share/Makefile
                  src/share/database/Makefile
                  src/share/database/scripts/Makefile
index 0373e3f820d1df48289b5b6e8c2fdb9b7318fc72..fcaa33931e1ebda2e8d6602a5e8968204c66cbce 100644 (file)
@@ -13,6 +13,10 @@ if HAVE_CQL
 SUBDIRS += cql
 endif
 
-SUBDIRS += testutils hooks dhcp config stats asiodns dhcp_ddns eval \
-       dhcpsrv cfgrpt \
-       process http
+SUBDIRS += testutils hooks dhcp config stats
+
+if HAVE_SYSREPO
+SUBDIRS += yang
+endif
+
+SUBDIRS += asiodns dhcp_ddns eval dhcpsrv cfgrpt process http
diff --git a/src/lib/yang/Makefile.am b/src/lib/yang/Makefile.am
new file mode 100644 (file)
index 0000000..8eb5b2e
--- /dev/null
@@ -0,0 +1,27 @@
+SUBDIRS = . tests
+
+AM_CPPFLAGS = -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib
+AM_CPPFLAGS += $(BOOST_INCLUDES) $(SYSREPO_CPPFLAGS)
+AM_CXXFLAGS = $(KEA_CXXFLAGS)
+
+lib_LTLIBRARIES = libkea-yang.la
+libkea_yang_la_SOURCES = adaptor.cc adaptor.h
+
+libkea_yang_la_LIBADD =  $(top_builddir)/src/lib/asiolink/libkea-asiolink.la
+libkea_yang_la_LIBADD += $(top_builddir)/src/lib/cc/libkea-cc.la
+libkea_yang_la_LIBADD += $(top_builddir)/src/lib/log/libkea-log.la
+libkea_yang_la_LIBADD += $(top_builddir)/src/lib/util/threads/libkea-threads.la
+libkea_yang_la_LIBADD += $(top_builddir)/src/lib/util/libkea-util.la
+libkea_yang_la_LIBADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la
+libkea_yang_la_LIBADD += $(LOG4CPLUS_LIBS) $(BOOST_LIBS) $(SYSREPO_LIBS)
+
+libkea_yang_la_LDFLAGS = -no-undefined -version-info 0:0:0
+
+# Specify the headers for copying into the installation directory tree.
+libkea_yang_includedir = $(pkgincludedir)/yang
+libkea_yang_include_HEADERS = \
+       adaptor.h
+
+EXTRA_DIST = yang.dox
+
+CLEANFILES = *.gcno *.gcda
diff --git a/src/lib/yang/adaptor.cc b/src/lib/yang/adaptor.cc
new file mode 100644 (file)
index 0000000..96bb3b1
--- /dev/null
@@ -0,0 +1,280 @@
+// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <yang/adaptor.h>
+#include <boost/foreach.hpp>
+
+#include <iostream>
+
+using namespace std;
+using namespace isc::data;
+
+namespace isc {
+namespace yang {
+
+Adaptor::Adaptor() {
+}
+
+Adaptor::~Adaptor() {
+}
+
+ConstElementPtr
+Adaptor::getContext(ConstElementPtr parent)
+{
+    ConstElementPtr context = parent->get("user-context");
+    ConstElementPtr comment = parent->get("comment");
+    if (!comment) {
+        return (context);
+    }
+    ElementPtr result;
+    if (context) {
+        result = copy(context);
+    } else {
+        result = Element::createMap();
+    }
+    result->set("comment", comment);
+    return (result);
+}
+
+void
+Adaptor::fromParent(const string& name, ConstElementPtr parent,
+                    ConstElementPtr list) {
+    ConstElementPtr param = parent->get(name);
+    if (!param) {
+        return;
+    }
+    BOOST_FOREACH(ElementPtr item, list->listValue()) {
+        // don't override?
+        if (item->contains(name)) {
+            continue;
+        }
+        item->set(name, param);
+    }
+}
+
+void
+Adaptor::toParent(const string& name, ElementPtr parent,
+                  ConstElementPtr list) {
+    ConstElementPtr param;
+    bool first = true;
+    BOOST_FOREACH(ElementPtr item, list->listValue()) {
+        if (first) {
+            first = false;
+            param = item->get(name);
+        } else if ((!param && item->contains(name)) ||
+                   (param && !item->contains(name)) ||
+                   (param && item->contains(name) &&
+                    !param->equals(*item->get(name)))) {
+            isc_throw(BadValue,
+                      "inconsistent value of " << name
+                      << " in " << list->str());
+        }
+    }
+    if (!first && param) {
+        BOOST_FOREACH(ElementPtr item, list->listValue()) {
+            if (param) {
+                item->remove(name);
+            }
+        }
+        parent->set(name, param);
+    }
+}
+                      
+namespace {
+
+/// @brief Apply insert.
+///
+/// @param key The key of the modification.
+/// @param value The value of the modification.
+/// @param scope The place to apply the insert.
+void apply_insert(ConstElementPtr key, ConstElementPtr value,
+                  ElementPtr scope) {
+    if (scope->getType() == Element::map) {
+        if (!key || !value || (key->getType() != Element::string)) {
+            return;
+        }
+        string name = key->stringValue();
+        if (!name.empty() && !scope->contains(name)) {
+            scope->set(name, copy(value));
+        }
+    } else if (scope->getType() == Element::list) {
+        if (value) {
+            scope->add(copy(value));
+        }
+    }
+}
+
+/// @brief Apply replace.
+///
+/// For maps same than insert but the new value is set even if the key
+/// already exists.
+///
+/// @param key The key of the modification.
+/// @param value The value of the modification.
+/// @param scope The place to apply the replace.
+void apply_replace(ConstElementPtr key, ConstElementPtr value,
+                   ElementPtr scope) {
+    if ((scope->getType() != Element::map) ||
+        !key || !value || (key->getType() != Element::string)) {
+            return;
+    }
+    string name = key->stringValue();
+    if (!name.empty()) {
+        scope->set(name, copy(value));
+    }
+}
+
+/// @brief Apply delete.
+///
+/// @param last The last item of the path.
+/// @param scope The place to apply the delete.
+void apply_delete(ConstElementPtr last, ElementPtr scope) {
+    if (scope->getType() == Element::map) {
+        if (!last || (last->getType() != Element::string)) {
+            return;
+        }
+        string name = last->stringValue();
+        if (!name.empty()) {
+            scope->remove(name);
+        }
+    } else if (scope->getType() == Element::list) {
+        if (!last) {
+            return;
+        } else if (last->getType() == Element::integer) {
+            int index = last->intValue();
+            if ((index >= 0) && (index < scope->size())) {
+                scope->remove(index);
+            }
+        } else if (last->getType() == Element::map) {
+            ConstElementPtr key = last->get("key");
+            ConstElementPtr value = last->get("value");
+            if (!key || !value || (key->getType() != Element::string)) {
+                return;
+            }
+            string name = key->stringValue();
+            if (name.empty()) {
+                return;
+            }
+            for (int i = 0; i < scope->size(); ++i) {
+                ConstElementPtr item = scope->get(i);
+                if (!item || (item->getType() != Element::map)) {
+                    continue;
+                }
+                ConstElementPtr compare = item->get(name);
+                if (compare && value->equals(*compare)) {
+                    scope->remove(i);
+                    return;
+                }
+            }
+        }
+    }
+}
+
+/// @brief Apply action.
+///
+/// @param actions The action list.
+/// @param scope The current scope.
+/// @param next The index of the next action.
+void apply_action(ConstElementPtr actions, ElementPtr scope, size_t next) {
+    if (next == actions->size()) {
+        return;
+    }
+    ConstElementPtr action = actions->get(next);
+    ++next;
+    if (!action || (action->getType() != Element::map) ||
+        !action->contains("action")) {
+        apply_action(actions, scope, next);
+        return;
+    }
+    string name = action->get("action")->stringValue();
+    if (name == "insert") {
+        apply_insert(action->get("key"), action->get("value"), scope);
+    } else if (name == "replace") {
+        apply_replace(action->get("key"), action->get("value"), scope);
+    } else if (name == "delete") {
+        apply_delete(action->get("last"), scope);
+    }
+    apply_action(actions, scope, next);
+}
+
+/// @brief Modify down.
+///
+/// @param path The search list.
+/// @param actions The action list.
+/// @param scope The current scope.
+/// @param next The index of the next item to use in the path.
+void path_down(ConstElementPtr path, ConstElementPtr actions, ElementPtr scope,
+               size_t next) {
+    if (!scope) {
+        return;
+    }
+    if (next == path->size()) {
+        apply_action(actions, scope, 0);
+        return;
+    }
+    ConstElementPtr step = path->get(next);
+    ++next;
+    if (scope->getType() == Element::map) {
+        if (!step || (step->getType() != Element::string)) {
+            return;
+        }
+        string name = step->stringValue();
+        if (name.empty() || !scope->contains(name)) {
+            return;
+        }
+        ElementPtr down = boost::const_pointer_cast<Element>(scope->get(name));
+        if (down) {
+            path_down(path, actions, down, next);
+        }
+    } else if (scope->getType() == Element::list) {
+        if (!step) {
+            return;
+        }
+        auto downs = scope->listValue();
+        if (step->getType() == Element::map) {
+            ConstElementPtr key = step->get("key");
+            ConstElementPtr value = step->get("value");
+            if (!key || !value || (key->getType() != Element::string)) {
+                return;
+            }
+            string name = key->stringValue();
+            if (name.empty()) {
+                return;
+            }
+            for (ElementPtr down : downs) {
+                if (!down || (down->getType() != Element::map)) {
+                    continue;
+                }
+                ConstElementPtr compare = down->get(name);
+                if (compare && value->equals(*compare)) {
+                    path_down(path, actions, down, next);
+                    return;
+                }
+            }
+        } else if (step->getType() != Element::integer) {
+            return;
+        }
+        int index = step->intValue();
+        if (index == -1) {
+            for (ElementPtr down : downs) {
+                path_down(path, actions, down, next);
+            }
+        } else if ((index >= 0) && (index < scope->size())) {
+            path_down(path, actions, scope->getNonConst(index), next);
+        }
+    }
+}
+
+} // end of anonymous namespace
+
+void
+Adaptor::modify(ConstElementPtr path, ConstElementPtr actions,
+                ElementPtr config) {
+    path_down(path, actions, config, 0);
+}
+
+}; // end of namespace isc::yang
+}; // end of namespace isc
diff --git a/src/lib/yang/adaptor.h b/src/lib/yang/adaptor.h
new file mode 100644 (file)
index 0000000..d8c05b2
--- /dev/null
@@ -0,0 +1,87 @@
+// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#ifndef ISC_ADAPTOR_H
+#define ISC_ADAPTOR_H 1
+
+#include <cc/data.h>
+
+namespace isc {
+namespace yang {
+
+/// @brief JSON adaptor between canonical Kea and Yang models.
+///
+/// An adaptor slightly modifies a JSON configuration between canonical Kea
+/// what required or rendered by a Yang model, e.g. moving a parameter
+/// to/from a parent.
+/// The basic adaptor provides a set of tools.
+class Adaptor {
+public:
+
+    /// @brief Constructor.
+    Adaptor();
+
+    /// @brief Destructor.
+    virtual ~Adaptor();
+
+    /// @brief Get user context.
+    ///
+    /// Get user-context and/or comment and return it with the comment
+    /// if exists moved inside the user-context (without checking if
+    /// there is already a comment as it should never be the case).
+    static isc::data::ConstElementPtr
+    getContext(isc::data::ConstElementPtr parent);
+
+    /// @brief From parent.
+    ///
+    /// Move a parameter from the parent to each item in a list.
+    ///
+    /// @param name The parameter name.
+    /// @param parent The parent element.
+    /// @param list The children list.
+    static void fromParent(const std::string& name,
+                           isc::data::ConstElementPtr parent,
+                           isc::data::ConstElementPtr list);
+
+    /// @brief To parent.
+    ///
+    /// Move a parameter from children to the parent.
+    ///
+    /// @param name The parameter name.
+    /// @param parent The parent element.
+    /// @param list The children list.
+    static void toParent(const std::string& name,
+                         isc::data::ElementPtr parent,
+                         isc::data::ConstElementPtr list);
+
+    /// @brief Modify.
+    ///
+    /// Smart merging tool, e.g. completing a from yang configuration.
+    ///
+    /// A modification is a path and actions:
+    ///  - path item can be:
+    ///   * a string: current scope is a map, go down following the string
+    ///     as a key.
+    ///   * a number: current scope is a list, go down the number as an index.
+    ///   * special value -1: current scope is a list, apply to all items.
+    ///   * map { "<key>": <value> }: current scope is a list, go down to
+    ///     the item using the key / value pair.
+    ///  - an action can be: insert, replace or delete.
+    ///
+    /// @param path The search list to follow down to the place to
+    ///             apply the action list.
+    /// @param actions The action list
+    /// @param config The configuration (JSON map) to modify.
+    static void modify(isc::data::ConstElementPtr path,
+                       isc::data::ConstElementPtr actions,
+                       isc::data::ElementPtr config);
+
+};
+
+}; // end of namespace isc::yang
+}; // end of namespace isc
+
+#endif // ISC_ADAPTOR_H
diff --git a/src/lib/yang/tests/Makefile.am b/src/lib/yang/tests/Makefile.am
new file mode 100644 (file)
index 0000000..a4ebb5d
--- /dev/null
@@ -0,0 +1,37 @@
+AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib
+AM_CPPFLAGS += $(BOOST_INCLUDES) $(SYSREPO_CPPFLAGS)
+AM_CPPFLAGS += -DCFG_EXAMPLES=\"$(abs_top_srcdir)/doc/examples\"
+AM_CXXFLAGS = $(KEA_CXXFLAGS)
+
+if USE_STATIC_LINK
+AM_LDFLAGS = -static
+endif
+
+CLEANFILES = *.gcno *.gcda
+
+TESTS_ENVIRONMENT = \
+       $(LIBTOOL) --mode=execute $(VALGRIND_COMMAND)
+
+TESTS =
+if HAVE_GTEST
+TESTS += run_unittests
+run_unittests_SOURCES  = adaptor_unittests.cc
+run_unittests_SOURCES += run_unittests.cc
+run_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
+run_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS)
+
+run_unittests_LDADD  = $(top_builddir)/src/lib/yang/libkea-yang.la
+run_unittests_LDADD += $(top_builddir)/src/lib/testutils/libkea-testutils.la
+run_unittests_LDADD += $(top_builddir)/src/lib/cc/libkea-cc.la
+run_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la
+run_unittests_LDADD += $(top_builddir)/src/lib/log/libkea-log.la
+run_unittests_LDADD += $(top_builddir)/src/lib/util/threads/libkea-threads.la
+run_unittests_LDADD += $(top_builddir)/src/lib/util/unittests/libutil_unittests.la
+run_unittests_LDADD += $(top_builddir)/src/lib/util/libkea-util.la
+run_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la
+run_unittests_LDADD += $(LOG4CPLUS_LIBS) $(BOOST_LIBS)
+run_unittests_LDADD += $(SYSREPO_LIBS) $(GTEST_LDADD)
+
+endif
+
+noinst_PROGRAMS = $(TESTS)
diff --git a/src/lib/yang/tests/adaptor_unittests.cc b/src/lib/yang/tests/adaptor_unittests.cc
new file mode 100644 (file)
index 0000000..816b848
--- /dev/null
@@ -0,0 +1,390 @@
+// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <yang/adaptor.h>
+
+#include <boost/scoped_ptr.hpp>
+#include <gtest/gtest.h>
+
+using namespace std;
+using namespace isc;
+using namespace isc::data;
+using namespace isc::yang;
+
+namespace {
+
+// Test get context.
+TEST(AdaptorTest, getContext) {
+    // Empty.
+    string config = "{\n"
+        "}\n";
+    ConstElementPtr json = Element::fromJSON(config);
+    ConstElementPtr context;
+    ASSERT_NO_THROW(context = Adaptor::getContext(json));
+    EXPECT_FALSE(context);
+
+    // No relevant.
+    config = "{\n"
+        " \"foo\": 1\n"
+        "}\n";
+    json = Element::fromJSON(config);
+    ASSERT_NO_THROW(context = Adaptor::getContext(json));
+    EXPECT_FALSE(context);
+
+    // User context.
+    config = "{\n"
+        " \"foo\": 1,\n"
+        " \"user-context\": { \"bar\": 2 }\n"
+        "}\n";
+    json = Element::fromJSON(config);
+    ASSERT_NO_THROW(context = Adaptor::getContext(json));
+    ASSERT_TRUE(context);
+    EXPECT_EQ("{ \"bar\": 2 }", context->str());
+
+    // Comment.
+    config = "{\n"
+        " \"foo\": 1,\n"
+        " \"comment\": \"a comment\"\n"
+        "}\n";
+    json = Element::fromJSON(config);
+    ASSERT_NO_THROW(context = Adaptor::getContext(json));
+    ASSERT_TRUE(context);
+    EXPECT_EQ("{ \"comment\": \"a comment\" }", context->str());
+
+    // User context and comment.
+    config = "{\n"
+        " \"foo\": 1,\n"
+        " \"user-context\": { \"bar\": 2 },\n"
+        " \"comment\": \"a comment\"\n"
+        "}\n";
+    json = Element::fromJSON(config);
+    ASSERT_NO_THROW(context = Adaptor::getContext(json));
+    ASSERT_TRUE(context);
+    EXPECT_EQ("{ \"bar\": 2, \"comment\": \"a comment\" }", context->str());
+
+    // User context with conflicting comment and comment.
+    config = "{\n"
+        " \"foo\": 1,\n"
+        " \"user-context\": {\n"
+        "   \"bar\": 2,\n"
+        "   \"comment\": \"conflicting\"\n"
+        "  },\n"
+        " \"comment\": \"a comment\"\n"
+        "}\n";
+    json = Element::fromJSON(config);
+    ASSERT_NO_THROW(context = Adaptor::getContext(json));
+    ASSERT_TRUE(context);
+    EXPECT_EQ("{ \"bar\": 2, \"comment\": \"a comment\" }", context->str());
+}
+
+// Test from parent.
+TEST(AdaptorTest, fromParent) {
+    string config = "{\n"
+        " \"param1\": 123,\n"
+        " \"param2\": \"foo\",\n"
+        " \"list\": [\n"
+        "  {\n"
+        "   \"param1\": 234\n"
+        "  },{\n"
+        "   \"another\": \"entry\"\n"
+        "  }\n"
+        " ]\n"
+        "}\n";
+
+    ConstElementPtr json = Element::fromJSON(config);
+    EXPECT_NO_THROW(Adaptor::fromParent("param1", json, json->get("list")));
+    EXPECT_NO_THROW(Adaptor::fromParent("param2", json, json->get("list")));
+    EXPECT_NO_THROW(Adaptor::fromParent("param3", json, json->get("list")));
+
+    string expected = "{\n"
+        " \"param1\": 123,\n"
+        " \"param2\": \"foo\",\n"
+        " \"list\": [\n"
+        "  {\n"
+        "   \"param1\": 234,\n"
+        "   \"param2\": \"foo\"\n"
+        "  },{\n"
+        "   \"another\": \"entry\",\n"
+        "   \"param1\": 123,\n"
+        "   \"param2\": \"foo\"\n"
+        "  }\n"
+        " ]\n"
+        "}\n";
+    EXPECT_TRUE(json->equals(*Element::fromJSON(expected)));
+}
+
+// Test to parent.
+TEST(AdaptorTest, toParent) {
+    string config = "{\n"
+        " \"list\": [\n"
+        "  {\n"
+        "   \"param2\": \"foo\",\n"
+        "   \"param3\": 234,\n"
+        "   \"param4\": true\n"
+        "  },{\n"
+        "   \"another\": \"entry\",\n"
+        "   \"param2\": \"foo\",\n"
+        "   \"param3\": 123,\n"
+        "   \"param5\": false\n"
+        "  }\n"
+        " ]\n"
+        "}\n";
+
+    ElementPtr json = Element::fromJSON(config);
+    EXPECT_NO_THROW(Adaptor::toParent("param1", json, json->get("list")));
+    EXPECT_TRUE(json->equals(*Element::fromJSON(config)));
+
+    string expected = "{\n"
+        " \"param2\": \"foo\",\n"
+        " \"list\": [\n"
+        "  {\n"
+        "   \"param3\": 234,\n"
+        "   \"param4\": true\n"
+        "  },{\n"
+        "   \"another\": \"entry\",\n"
+        "   \"param3\": 123,\n"
+        "   \"param5\": false\n"
+        "  }\n"
+        " ]\n"
+        "}\n";
+
+    EXPECT_NO_THROW(Adaptor::toParent("param2",json, json->get("list")));
+    EXPECT_TRUE(json->equals(*Element::fromJSON(expected)));
+
+    // param1 has different value so it should throw.
+    EXPECT_THROW(Adaptor::toParent("param3",json, json->get("list")),
+                 BadValue);
+    EXPECT_THROW(Adaptor::toParent("param4",json, json->get("list")),
+                 BadValue);
+    EXPECT_THROW(Adaptor::toParent("param5",json, json->get("list")),
+                 BadValue);
+    // And not modify the value.
+    EXPECT_TRUE(json->equals(*Element::fromJSON(expected)));
+}
+
+// Test for modify (maps & insert).
+TEST(AdaptorTest, modifyMapInsert) {
+    string config = "{\n"
+        " \"foo\": {\n"
+        "   \"bar\": {\n"
+        "}}}\n";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    string spath = "[ \"foo\", \"bar\" ]";
+    ConstElementPtr path;
+    ASSERT_NO_THROW(path = Element::fromJSON(spath));
+    string sactions = "[\n"
+        "{\n"
+        "  \"action\": \"insert\",\n"
+        "  \"key\": \"test\",\n"
+        "  \"value\": 1234\n"
+        "}]\n";
+    ConstElementPtr actions;
+    ASSERT_NO_THROW(actions = Element::fromJSON(sactions));
+    string result = "{\n"
+        " \"foo\": {\n"
+        "   \"bar\": {\n"
+        "     \"test\": 1234\n"
+        "}}}\n";
+    ConstElementPtr expected;
+    ASSERT_NO_THROW(expected = Element::fromJSON(result));
+    ASSERT_NO_THROW(Adaptor::modify(path, actions, json));
+    EXPECT_TRUE(expected->equals(*json));
+}
+
+// Test for modify (maps & replace).
+TEST(AdaptorTest, modifyMapReplace) {
+    string config = "{\n"
+        " \"foo\": {\n"
+        "   \"bar\": {\n"
+        "     \"test1\": 1234,\n"
+        "     \"test2\": 1234\n"
+        "}}}\n";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    string spath = "[ \"foo\", \"bar\" ]";
+    ConstElementPtr path;
+    ASSERT_NO_THROW(path = Element::fromJSON(spath));
+    string sactions = "[\n"
+        "{\n"
+        "  \"action\": \"insert\",\n"
+        "  \"key\": \"test1\",\n"
+        "  \"value\": 5678\n"
+        "},{\n"
+        "  \"action\": \"replace\",\n"
+        "  \"key\": \"test2\",\n"
+        "  \"value\": 5678\n"
+        "}]\n";
+    ConstElementPtr actions;
+    ASSERT_NO_THROW(actions = Element::fromJSON(sactions));
+    string result = "{\n"
+        " \"foo\": {\n"
+        "   \"bar\": {\n"
+        "     \"test1\": 1234,\n"
+        "     \"test2\": 5678\n"
+        "}}}\n";
+    ConstElementPtr expected;
+    ASSERT_NO_THROW(expected = Element::fromJSON(result));
+    ASSERT_NO_THROW(Adaptor::modify(path, actions, json));
+    EXPECT_TRUE(expected->equals(*json));
+}
+
+// Test for modify (maps & delete).
+TEST(AdaptorTest, modifyMapDelete) {
+    string config = "{\n"
+        " \"foo\": {\n"
+        "   \"bar\": {\n"
+        "     \"test\": 1234\n"
+        "}}}\n";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    string spath = "[ \"foo\", \"bar\" ]";
+    ConstElementPtr path;
+    ASSERT_NO_THROW(path = Element::fromJSON(spath));
+    string sactions = "[\n"
+        "{\n"
+        "  \"action\": \"delete\",\n"
+        "  \"last\": \"test\"\n"
+        "}]\n";
+    ConstElementPtr actions;
+    ASSERT_NO_THROW(actions = Element::fromJSON(sactions));
+    string result = "{\n"
+        " \"foo\": {\n"
+        "   \"bar\": {\n"
+        "}}}\n";
+    ConstElementPtr expected;
+    ASSERT_NO_THROW(expected = Element::fromJSON(result));
+    ASSERT_NO_THROW(Adaptor::modify(path, actions, json));
+    EXPECT_TRUE(expected->equals(*json));
+}
+
+// Test for modify (lists & insert).
+TEST(AdaptorTest, modifyListInsert) {
+    string config = "[\n"
+        "[{\n"
+        " \"foo\": \"bar\"\n"
+        "}]]\n";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    string spath = "[ 0, { \"key\": \"foo\", \"value\": \"bar\" }]";
+    ConstElementPtr path;
+    ASSERT_NO_THROW(path = Element::fromJSON(spath));
+    string sactions = "[\n"
+        "{\n"
+        "  \"action\": \"insert\",\n"
+        "  \"key\": \"test\",\n"
+        "  \"value\": 1234\n"
+        "}]\n";
+    ConstElementPtr actions;
+    ASSERT_NO_THROW(actions = Element::fromJSON(sactions));
+    string result = "[\n"
+        "[{\n"
+        " \"foo\": \"bar\",\n"
+        " \"test\": 1234\n"
+        "}]]\n";
+    ConstElementPtr expected;
+    ASSERT_NO_THROW(expected = Element::fromJSON(result));
+    ASSERT_NO_THROW(Adaptor::modify(path, actions, json));
+    EXPECT_TRUE(expected->equals(*json));
+}
+
+// Test for modify (list all & insert).
+TEST(AdaptorTest, modifyListAllInsert) {
+    string config = "[\n"
+        "{},\n"
+        "{},\n"
+        "{ \"test\": 1234 },\n"
+        "]\n";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    string spath = "[ -1 ]";
+    ConstElementPtr path;
+    ASSERT_NO_THROW(path = Element::fromJSON(spath));
+    string sactions = "[\n"
+        "{\n"
+        "  \"action\": \"insert\",\n"
+        "  \"key\": \"test\",\n"
+        "  \"value\": 5678\n"
+        "}]\n";
+    ConstElementPtr actions;
+    ASSERT_NO_THROW(actions = Element::fromJSON(sactions));
+    string result = "[\n"
+        "{ \"test\": 5678 },\n"
+        "{ \"test\": 5678 },\n"
+        "{ \"test\": 1234 }\n"
+        "]\n";
+    ConstElementPtr expected;
+    ASSERT_NO_THROW(expected = Element::fromJSON(result));
+    ASSERT_NO_THROW(Adaptor::modify(path, actions, json));
+    EXPECT_TRUE(expected->equals(*json));
+}
+
+TEST(AdaptorTest, modifyListDelete) {
+    string config = "[[\n"
+        "{\n"
+        " \"foo\": \"bar\"\n"
+        "},{\n"
+        "},[\n"
+        "0, 1, 2, 3\n"
+        "]]]\n";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    string spath = "[ 0 ]";
+    ConstElementPtr path;
+    ASSERT_NO_THROW(path = Element::fromJSON(spath));
+    // Put the positional first as it applies after previous actions...
+    string sactions = "[\n"
+        "{\n"
+        "  \"action\": \"delete\",\n"
+        "  \"last\": 2\n"
+        "},{\n"
+        "  \"action\": \"delete\",\n"
+        "  \"last\": { \"key\": \"foo\", \"value\": \"bar\" }\n"
+        "}]\n";
+    ConstElementPtr actions;
+    ASSERT_NO_THROW(actions = Element::fromJSON(sactions));
+    string result = "[[{}]]\n";
+    ConstElementPtr expected;
+    ASSERT_NO_THROW(expected = Element::fromJSON(result));
+    ASSERT_NO_THROW(Adaptor::modify(path, actions, json));
+    EXPECT_TRUE(expected->equals(*json));
+}
+
+TEST(AdaptorTest, modifyListAllDelete) {
+    string config = "[[\n"
+        "{\n"
+        " \"foo\": \"bar\"\n"
+        "},{\n"
+        "},[\n"
+        "0, 1, 2, 3\n"
+        "]]]\n";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    // The only change from the previous unit test is 0 -> -1.
+    string spath = "[ -1 ]";
+    ConstElementPtr path;
+    ASSERT_NO_THROW(path = Element::fromJSON(spath));
+    // Put the positional first as it applies after previous actions...
+    string sactions = "[\n"
+        "{\n"
+        "  \"action\": \"delete\",\n"
+        "  \"last\": 2\n"
+        "},{\n"
+        "  \"action\": \"delete\",\n"
+        "  \"last\": { \"key\": \"foo\", \"value\": \"bar\" }\n"
+        "}]\n";
+    ConstElementPtr actions;
+    ASSERT_NO_THROW(actions = Element::fromJSON(sactions));
+    string result = "[[{}]]\n";
+    ConstElementPtr expected;
+    ASSERT_NO_THROW(expected = Element::fromJSON(result));
+    ASSERT_NO_THROW(Adaptor::modify(path, actions, json));
+    EXPECT_TRUE(expected->equals(*json));
+}
+
+}; // end of anonymous namespace
diff --git a/src/lib/yang/tests/run_unittests.cc b/src/lib/yang/tests/run_unittests.cc
new file mode 100644 (file)
index 0000000..c0847ca
--- /dev/null
@@ -0,0 +1,20 @@
+// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <gtest/gtest.h>
+#include <util/unittests/run_all.h>
+#include <log/logger_support.h>
+
+int
+main(int argc, char* argv[]) {
+    ::testing::InitGoogleTest(&argc, argv);
+
+    isc::log::initLogger();
+
+    return (isc::util::unittests::run_all());
+}