From fbe2f1af0792eef14b8bc5810c5cf4159faf5cda Mon Sep 17 00:00:00 2001
From: Cyrille Berger <cyrille.berger@liu.se>
Date: Fri, 5 May 2023 11:44:01 +0200
Subject: [PATCH] add a simple management query libary

---
 .vscode/launch.json                   |  26 ++++
 docs/doxygen/pages/kDBSMQuery.dox     |  19 +++
 docs/doxygen/pages/mainpage.dox       |   5 +
 kDB/CMakeLists.txt                    |   1 +
 kDB/Forward.h                         |   8 ++
 kDB/SMQuery/CMakeLists.txt            |  24 ++++
 kDB/SMQuery/Engine.cpp                |  46 +++++++
 kDB/SMQuery/Engine.h                  |  31 +++++
 kDB/SMQuery/Forward.h                 |   0
 kDB/SMQuery/Interfaces/Action.h       |  19 +++
 kDB/SMQuery/Interfaces/Interfaces.cpp |   6 +
 kDB/SMQuery/Parser.cpp                | 167 ++++++++++++++++++++++++++
 kDB/SMQuery/Parser.h                  |  18 +++
 kDB/SMQuery/tests/CMakeLists.txt      |   3 +
 kDB/SMQuery/tests/TestEngine.cpp      |  70 +++++++++++
 kDB/SMQuery/tests/TestEngine.h        |  10 ++
 kDBConfig.cmake.in                    |   1 +
 17 files changed, 454 insertions(+)
 create mode 100644 docs/doxygen/pages/kDBSMQuery.dox
 create mode 100644 kDB/SMQuery/CMakeLists.txt
 create mode 100644 kDB/SMQuery/Engine.cpp
 create mode 100644 kDB/SMQuery/Engine.h
 create mode 100644 kDB/SMQuery/Forward.h
 create mode 100644 kDB/SMQuery/Interfaces/Action.h
 create mode 100644 kDB/SMQuery/Interfaces/Interfaces.cpp
 create mode 100644 kDB/SMQuery/Parser.cpp
 create mode 100644 kDB/SMQuery/Parser.h
 create mode 100644 kDB/SMQuery/tests/CMakeLists.txt
 create mode 100644 kDB/SMQuery/tests/TestEngine.cpp
 create mode 100644 kDB/SMQuery/tests/TestEngine.h

diff --git a/.vscode/launch.json b/.vscode/launch.json
index a12ffb32..4c467e7a 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -291,6 +291,32 @@
             "visualizerFile": "${env:LRS_PKG_ROOT}/src/qt-natvis/qt5.natvis.xml",
             "showDisplayString": true
         },
+        {
+            "name": "TestEngine (kDB::SMQuery) (gdb)",
+            "type": "cppdbg",
+            "request": "launch",
+            "program": "${env:LRS_PKG_ROOT}/host-build-debug/kDB/kDB/SMQuery/tests/TestEngine",
+            "args": [],
+            "stopAtEntry": false,
+            "cwd": "${env:LRS_PKG_ROOT}",
+            "environment": [],
+            "externalConsole": false,
+            "MIMode": "gdb",
+            "setupCommands": [
+                {
+                    "description": "Enable pretty-printing for gdb",
+                    "text": "-enable-pretty-printing",
+                    "ignoreFailures": true
+                },
+                {
+                    "description": "Set Disassembly Flavor to Intel",
+                    "text": "-gdb-set disassembly-flavor intel",
+                    "ignoreFailures": true
+                }
+            ],
+            "visualizerFile": "${env:LRS_PKG_ROOT}/src/qt-natvis/qt5.natvis.xml",
+            "showDisplayString": true
+        },
         
     ]
 }
diff --git a/docs/doxygen/pages/kDBSMQuery.dox b/docs/doxygen/pages/kDBSMQuery.dox
new file mode 100644
index 00000000..b5c58212
--- /dev/null
+++ b/docs/doxygen/pages/kDBSMQuery.dox
@@ -0,0 +1,19 @@
+/**
+\page kDB_SMQuery kDB::SMQuery Library
+\defgroup kDB_SMQuery
+
+kDB::SMQuery is a kDB library which allows to execute Simple Management Query.
+
+The idea is for the engine to be able to execute query of the form
+
+@code
+SOME KEY name1: value1 name2: value2 ... 
+@endcode
+
+For instance:
+
+@code
+CREATE DATASETS uri: <uri_of_dataset>
+@endcode
+
+*/
\ No newline at end of file
diff --git a/docs/doxygen/pages/mainpage.dox b/docs/doxygen/pages/mainpage.dox
index 49d3b8f7..09e4d0e1 100644
--- a/docs/doxygen/pages/mainpage.dox
+++ b/docs/doxygen/pages/mainpage.dox
@@ -1,7 +1,12 @@
 /**
 \mainpage kDB: Knowledge DataBase
 \section kdb_overview General overview
+
+\section kDB_core Core Libraries
+ - \ref kDB_SMQuery Simple Management Query
+
 \section kdb_extensions Extensions
+ - \ref kDBDatasets extension to manage datasets
  - \ref kDBQueries extension to handle query templates in C++/Python
 
  */
diff --git a/kDB/CMakeLists.txt b/kDB/CMakeLists.txt
index df5f0291..178d975f 100644
--- a/kDB/CMakeLists.txt
+++ b/kDB/CMakeLists.txt
@@ -1,6 +1,7 @@
 add_subdirectory(RDFDB)
 add_subdirectory(Repository)
 add_subdirectory(RDFView)
+add_subdirectory(SMQuery)
 add_subdirectory(SPARQL)
 add_subdirectory(Utils)
 
diff --git a/kDB/Forward.h b/kDB/Forward.h
index bd61d9b6..11d12169 100644
--- a/kDB/Forward.h
+++ b/kDB/Forward.h
@@ -69,4 +69,12 @@ namespace kDB
   {
     class Database;
   }
+  namespace SMQuery
+  {
+    class Engine;
+    namespace Interfaces
+    {
+      class Action;
+    }
+  }
 }
diff --git a/kDB/SMQuery/CMakeLists.txt b/kDB/SMQuery/CMakeLists.txt
new file mode 100644
index 00000000..33c98b59
--- /dev/null
+++ b/kDB/SMQuery/CMakeLists.txt
@@ -0,0 +1,24 @@
+add_subdirectory(tests)
+
+# Create library
+
+set(KDBSMQUERY_SRCS
+  Engine.cpp
+  Parser.cpp
+  Interfaces/Interfaces.cpp
+  )
+
+add_library(kDBSMQuery SHARED ${KDBSMQUERY_SRCS})
+target_link_libraries(kDBSMQuery PUBLIC knowCore )
+
+# Install
+install(TARGETS kDBSMQuery EXPORT kDBTargets ${INSTALL_TARGETS_DEFAULT_ARGS} )
+set(PROJECT_EXPORTED_TARGETS kDBSMQuery  ${PROJECT_EXPORTED_TARGETS} CACHE INTERNAL "")
+
+install( FILES
+  Engine.h
+  DESTINATION ${INSTALL_INCLUDE_DIR}/kDBSMQuery )
+
+  install( FILES
+  Interfaces/Action.h
+  DESTINATION ${INSTALL_INCLUDE_DIR}/kDBSMQuery/Interfaces )
diff --git a/kDB/SMQuery/Engine.cpp b/kDB/SMQuery/Engine.cpp
new file mode 100644
index 00000000..8a923092
--- /dev/null
+++ b/kDB/SMQuery/Engine.cpp
@@ -0,0 +1,46 @@
+#include "Engine.h"
+
+#include <QHash>
+
+#include "Parser.h"
+#include "Interfaces/Action.h"
+
+using namespace kDB::SMQuery;
+
+struct Engine::Private
+{
+  QHash<QString, Interfaces::Action*> key2action;
+  QList<Interfaces::Action*> actions;
+};
+
+Engine::Engine() : d(new Private)
+{
+
+}
+
+Engine::~Engine()
+{
+  qDeleteAll(d->actions);
+  delete d;
+}
+
+void Engine::add(const QStringList& _keys, Interfaces::Action* _action)
+{
+  d->actions.append(_action);
+  for(const QString& key : _keys)
+  {
+    d->key2action[key.toUpper()] = _action;
+  }
+}
+
+knowCore::ReturnVoid Engine::execute(const QString& _text)
+{
+  KNOWCORE_RETURN_VALUE_TRY(query, Parser::parse(_text));
+  QString key = query.key.join(" ").toUpper();
+  if(d->key2action.contains(key))
+  {
+    return d->key2action.value(key)->execute(key, query.parameters);
+  } else {
+    return kCrvError("No action for '{}'", key);
+  }
+}
diff --git a/kDB/SMQuery/Engine.h b/kDB/SMQuery/Engine.h
new file mode 100644
index 00000000..86c390e2
--- /dev/null
+++ b/kDB/SMQuery/Engine.h
@@ -0,0 +1,31 @@
+#include <QStringList>
+
+#include <kDB/Forward.h>
+
+namespace kDB::SMQuery
+{
+  /**
+   * @ingroup kDB_SMQuery
+   * 
+   * Execution engine for Simple Management Query
+   */
+  class Engine
+  {
+  public:
+    Engine();
+    ~Engine();
+    /**
+     * Add an \ref _action for the list of \ref _keys.
+     * 
+     * _keys take the form of "SOME ACTION" and will react to query with "SOME ACTION".
+     */
+    void add(const QStringList& _keys, Interfaces::Action* _action);
+    /**
+     * Execute a query.
+     */
+    knowCore::ReturnVoid execute(const QString& _text);
+  private:
+    struct Private;
+    Private* const d;
+  };
+}
\ No newline at end of file
diff --git a/kDB/SMQuery/Forward.h b/kDB/SMQuery/Forward.h
new file mode 100644
index 00000000..e69de29b
diff --git a/kDB/SMQuery/Interfaces/Action.h b/kDB/SMQuery/Interfaces/Action.h
new file mode 100644
index 00000000..e5ca21a1
--- /dev/null
+++ b/kDB/SMQuery/Interfaces/Action.h
@@ -0,0 +1,19 @@
+#include <knowCore/Forward.h>
+
+namespace kDB::SMQuery::Interfaces
+{
+  /**
+   * @ingroup kDB_SMQuery
+   * 
+   * Define an action to be executed by the SMQuery engine
+   */
+  class Action
+  {
+  public:
+    virtual ~Action();
+    /**
+     * Execute the action with the given \ref _key and \ref _parameters
+     */
+    virtual knowCore::ReturnVoid execute(const QString& _key, const knowCore::ValueHash& _parameters) = 0;
+  };
+}
diff --git a/kDB/SMQuery/Interfaces/Interfaces.cpp b/kDB/SMQuery/Interfaces/Interfaces.cpp
new file mode 100644
index 00000000..c5f1cfda
--- /dev/null
+++ b/kDB/SMQuery/Interfaces/Interfaces.cpp
@@ -0,0 +1,6 @@
+#include "Action.h"
+
+using namespace kDB::SMQuery::Interfaces;
+
+Action::~Action()
+{}
diff --git a/kDB/SMQuery/Parser.cpp b/kDB/SMQuery/Parser.cpp
new file mode 100644
index 00000000..cdc69825
--- /dev/null
+++ b/kDB/SMQuery/Parser.cpp
@@ -0,0 +1,167 @@
+#include "Parser.h"
+
+#include <knowCore/BigNumber.h>
+#include <knowCore/LexerTextStream.h>
+
+using namespace kDB::SMQuery;
+
+struct Parser::Private
+{
+  struct Token
+  {
+    enum class Type {
+      END_OF_FILE,
+      IDENTIFIER,
+      COLON,
+      VALUE
+    };
+    Type type;
+    QString text;
+    knowCore::Value value;
+  };
+  QString getIdentifier(knowCore::LexerTextStream::Element lastChar);
+  knowCore::ReturnValue<Token> nextToken();
+
+  knowCore::LexerTextStream stream;
+};
+
+QString Parser::Private::getIdentifier(knowCore::LexerTextStream::Element lastChar)
+{
+  QString identifierStr;
+  if( lastChar.content != 0) {
+    identifierStr = lastChar.content;
+  }
+  while (not stream.eof() )
+  {
+    lastChar = stream.getNextChar();
+    if( lastChar.isLetter() or lastChar == '_')
+    {
+      identifierStr += lastChar.content;
+    } else {
+      stream.unget(lastChar);
+      break;
+    }
+  }
+  return identifierStr;
+}
+
+knowCore::ReturnValue<Parser::Private::Token> Parser::Private::nextToken()
+{
+  using Token = Private::Token;
+  knowCore::LexerTextStream::Element lastChar = stream.getNextNonSeparatorChar();
+  const knowCore::LexerTextStream::Element firstChar = lastChar;
+  if( lastChar == EOF ) return kCrvSuccess(Token{Token::Type::END_OF_FILE, QString(), knowCore::Value()});
+
+  KNOWCORE_ASSERT(lastChar.content.size() == 1);
+  switch(lastChar.content.at(0).toLatin1())
+  {
+    case '<':
+    { // This  an URI
+      auto [string, finished] = stream.getString(">");
+      if(finished)
+      {
+        return kCrvSuccess(Token{Token::Type::VALUE, QString(), knowCore::Value::fromValue(knowCore::Uri(string.content))});
+      } else {
+        return kCrvError("Unfinished uri: {}", string.content);
+      }
+    }
+    case '"':
+    { // This  a String
+      auto [string, finished] = stream.getString("\"");
+      if(finished)
+      {
+        return kCrvSuccess(Token{Token::Type::VALUE, QString(), knowCore::Value::fromValue(string.content)});
+      } else {
+        return kCrvError("Unfinished uri: {}", string.content);
+      }
+    }
+    case ':':
+      return kCrvSuccess(Token{Token::Type::COLON, QString(), knowCore::Value()});
+    default:
+    {
+      if(lastChar.isLetter())
+      {
+        return kCrvSuccess(Token{Token::Type::IDENTIFIER, getIdentifier(lastChar), knowCore::Value()});
+      } else if(lastChar.isDigit())
+      {
+        auto [number, isinteger] = stream.getDigit(lastChar);
+        KNOWCORE_RETURN_VALUE_TRY(bn, knowCore::BigNumber::fromString(number.content));
+        return kCrvSuccess(Token{Token::Type::VALUE, QString(), bn.toValue()});
+      } else {
+        return kCrvError("Invalid character: {}", lastChar.content);
+      }
+    }
+  }
+
+
+}
+
+knowCore::ReturnValue<Parser::Result> Parser::parse(const QString& _string)
+{
+  using Token = Private::Token;
+  Private d;
+  d.stream.setString(_string);
+  Result r;
+  bool parsingKey = true;
+  while(parsingKey)
+  {
+    KNOWCORE_RETURN_VALUE_TRY(token, d.nextToken());
+    switch(token.type)
+    {
+      case Token::Type::END_OF_FILE:
+       return kCrvSuccess(r);
+      case Token::Type::IDENTIFIER:
+      {
+        r.key.append(token.text);
+        break;
+      }
+      case Token::Type::COLON:
+      {
+        if(r.key.isEmpty())
+        {
+          return kCrvError("Unexpected ':'");
+        }
+        QString name = r.key.takeLast();
+        KNOWCORE_RETURN_VALUE_TRY(token, d.nextToken());
+        if(token.type != Token::Type::VALUE)
+        {
+          return kCrvError("Expected value after '{}:'", name);
+        }
+        r.parameters.add(name, token.value);
+        parsingKey = false;
+        break;
+      }
+      default:
+        return kCrvError("Unexpected token, expecting identifier, ':' or end of file.");
+    }
+  }
+  while(true)
+  {
+    KNOWCORE_RETURN_VALUE_TRY(token, d.nextToken());
+    switch(token.type)
+    {
+      case Token::Type::END_OF_FILE:
+       return kCrvSuccess(r);
+      case Token::Type::IDENTIFIER:
+      {
+        QString name = token.text;
+        KNOWCORE_RETURN_VALUE_TRY(token_colon, d.nextToken());
+        if(token_colon.type != Token::Type::COLON)
+        {
+          return kCrvError("Expected ':' after '{}'", name);
+        }
+        KNOWCORE_RETURN_VALUE_TRY(token_value, d.nextToken());
+        if(token_value.type != Token::Type::VALUE)
+        {
+          return kCrvError("Expected value after '{}:'", name);
+        }
+        r.parameters.add(name, token_value.value);
+        break;
+      }
+      default:
+        return kCrvError("Unexpected token, expecting identifier or end of file.");
+    }
+  }
+
+}
+
diff --git a/kDB/SMQuery/Parser.h b/kDB/SMQuery/Parser.h
new file mode 100644
index 00000000..a2f04fb4
--- /dev/null
+++ b/kDB/SMQuery/Parser.h
@@ -0,0 +1,18 @@
+#include <knowCore/ValueHash.h>
+
+namespace kDB::SMQuery
+{
+  class Parser
+  {
+  public:
+    struct Result
+    {
+      QStringList key;
+      knowCore::ValueHash parameters;
+    };
+  public:
+    static knowCore::ReturnValue<Result> parse(const QString& _query);
+  private:
+    struct Private;
+  };
+}
diff --git a/kDB/SMQuery/tests/CMakeLists.txt b/kDB/SMQuery/tests/CMakeLists.txt
new file mode 100644
index 00000000..233d8da7
--- /dev/null
+++ b/kDB/SMQuery/tests/CMakeLists.txt
@@ -0,0 +1,3 @@
+add_executable(TestEngine TestEngine.cpp )
+target_link_libraries(TestEngine PRIVATE kDBSMQuery Qt5::Test)
+add_test(NAME TEST-SMQuery-Engine COMMAND TestEngine)
diff --git a/kDB/SMQuery/tests/TestEngine.cpp b/kDB/SMQuery/tests/TestEngine.cpp
new file mode 100644
index 00000000..568edae8
--- /dev/null
+++ b/kDB/SMQuery/tests/TestEngine.cpp
@@ -0,0 +1,70 @@
+#include "TestEngine.h"
+
+#include "../Interfaces/Action.h"
+#include "../Engine.h"
+
+#include <knowCore/TypeDefinitions.h>
+#include <knowCore/Test.h>
+#include <knowCore/ValueHash.h>
+
+class TestFailAction : public kDB::SMQuery::Interfaces::Action
+{
+public:
+  virtual ~TestFailAction() {}
+  knowCore::ReturnVoid execute(const QString&, const knowCore::ValueHash&) override
+  {
+    return kCrvError("Expected failure.");
+  }
+
+};
+
+class TestAction : public kDB::SMQuery::Interfaces::Action
+{
+public:
+  virtual ~TestAction() {}
+  knowCore::ReturnVoid execute(const QString& _key, const knowCore::ValueHash& _parameters) override
+  {
+    if(_key == "ALSO FAIL")
+    {
+      return kCrvError("Expected failure.");
+    } else if(_key == "DO SUCCEED")
+    {
+      if(_parameters.contains("uri") and _parameters.value("uri") != knowCore::Value::fromValue("hello world"_kCu))
+      {
+        return kCrvError("Invalid uri.");
+      }
+      if(_parameters.contains("value") and _parameters.value("value") != knowCore::Value::fromValue(12))
+      {
+        return kCrvError("Invalid value.");
+      }
+      if(_parameters.contains("text") and _parameters.value("text") != knowCore::Value::fromValue("some text"_kCs))
+      {
+        return kCrvError("Invalid text.");
+      }
+      return kCrvSuccess();
+    } else {
+      return kCrvError("Should not happen.");
+    }
+  }
+
+};
+void TestEngine::testQuery()
+{
+  kDB::SMQuery::Engine e;
+  e.add({"TRIGGER FAIL"}, new TestFailAction);
+  e.add({"ALSO FAIL", "DO SUCCEED"}, new TestAction);
+
+  KNOWCORE_TEST_VERIFY_FAILURE_VOID(e.execute("RANDOM ACTION"));
+  KNOWCORE_TEST_VERIFY_FAILURE_VOID(e.execute("TRIGGER FAIL"));
+  KNOWCORE_TEST_VERIFY_FAILURE_VOID(e.execute("ALSO FAIL"));
+  KNOWCORE_TEST_VERIFY_SUCCESS_VOID(e.execute("DO SUCCEED uri: <hello world>"));
+  KNOWCORE_TEST_VERIFY_SUCCESS_VOID(e.execute("DO SUCCEED uri: <hello world> value: 12"));
+  KNOWCORE_TEST_VERIFY_SUCCESS_VOID(e.execute("DO SUCCEED uri: <hello world> value: 12 text: \"some text\""));
+
+  KNOWCORE_TEST_VERIFY_FAILURE_VOID(e.execute("DO SUCCEED uri: <hello>"));
+  KNOWCORE_TEST_VERIFY_FAILURE_VOID(e.execute("DO SUCCEED value: 121"));
+  KNOWCORE_TEST_VERIFY_FAILURE_VOID(e.execute("DO SUCCEED text: \"some wrong text\""));
+}
+
+QTEST_MAIN(TestEngine)
+
diff --git a/kDB/SMQuery/tests/TestEngine.h b/kDB/SMQuery/tests/TestEngine.h
new file mode 100644
index 00000000..a9d04ea6
--- /dev/null
+++ b/kDB/SMQuery/tests/TestEngine.h
@@ -0,0 +1,10 @@
+
+#include <QtTest/QtTest>
+
+class TestEngine : public QObject
+{
+  Q_OBJECT
+private slots:
+  void testQuery();
+};
+
diff --git a/kDBConfig.cmake.in b/kDBConfig.cmake.in
index 5c754833..ce09787c 100644
--- a/kDBConfig.cmake.in
+++ b/kDBConfig.cmake.in
@@ -59,6 +59,7 @@ macro(__kdb_add_nice_target_core_library CV KCF name)
 endmacro()
 
 __kdb_add_nice_target_core_library(TRUE __KDB_COMPONENTS_FINDABLE__ Repository)
+__kdb_add_nice_target_core_library(TRUE __KDB_COMPONENTS_FINDABLE__ SMQuery)
 
 macro(__kdb_add_nice_target_components CV KE KCF name)
   if(${CV})
-- 
GitLab