Browse Source

Complete implemetation of gRPC client API

- Add grpc quick plugin
- Implement error handling support for gRPC calls
- Make GrpcStatus available in QML
- Add GrpcSubscription QML type to handle server and bidirectional
  streaming methods
- Update CMake rules for quick plugins
- Add static gRPC quick plugin support
- Update and implement tests

Fixes: #69
Alexey Edelev 4 years ago
parent
commit
6e23ab6ec1

+ 1 - 0
CMakeLists.txt

@@ -47,6 +47,7 @@ endif()
 set(gtest_force_shared_crt OFF)
 set(GENERATOR_TARGET qtprotobufgen)
 set(PROTOBUF_QUICK_PLUGIN_NAME protobufquickplugin)
+set(GRPC_QUICK_PLUGIN_NAME grpcquickplugin)
 
 if(UNIX)
     if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")

+ 14 - 0
src/generator/clientdeclarationprinter.cpp

@@ -93,4 +93,18 @@ void ClientDeclarationPrinter::printClientMethodsDeclaration()
         mPrinter->Print("\n");
     }
     Outdent();
+
+    printPrivateBlock();
+    Indent();
+    for (int i = 0; i < mDescriptor->method_count(); i++) {
+        const MethodDescriptor *method = mDescriptor->method(i);
+        std::map<std::string, std::string> parameters = common::produceMethodMap(method, mName);
+        if (method->server_streaming()) {
+            if (GeneratorOptions::instance().hasQml()) {
+                mPrinter->Print(parameters, Templates::ClientMethodServerStreamQmlDeclarationTemplate);
+            }
+        }
+    }
+    Outdent();
+    mPrinter->Print("\n");
 }

+ 3 - 0
src/generator/clientdefinitionprinter.cpp

@@ -50,6 +50,9 @@ void ClientDefinitionPrinter::printMethods()
         if (method->server_streaming()) {
             mPrinter->Print(parameters, Templates::ClientMethodServerStreamDefinitionTemplate);
             mPrinter->Print(parameters, Templates::ClientMethodServerStream2DefinitionTemplate);
+            if (GeneratorOptions::instance().hasQml()) {
+                mPrinter->Print(parameters, Templates::ClientMethodServerStreamQmlDefinitionTemplate);
+            }
         } else {
             mPrinter->Print(parameters, Templates::ClientMethodDefinitionSyncTemplate);
             mPrinter->Print(parameters, Templates::ClientMethodDefinitionAsyncTemplate);

+ 12 - 4
src/generator/templates.cpp

@@ -286,7 +286,7 @@ const char *Templates::QObjectMacro = "Q_OBJECT";
 const char *Templates::ClientMethodDeclarationSyncTemplate = "QtProtobuf::QGrpcStatus $method_name$(const $param_type$ &$param_name$, const QPointer<$return_type$> &$return_name$);\n";
 const char *Templates::ClientMethodDeclarationAsyncTemplate = "QtProtobuf::QGrpcAsyncReply *$method_name$(const $param_type$ &$param_name$);\n";
 const char *Templates::ClientMethodDeclarationAsync2Template = "Q_INVOKABLE void $method_name$(const $param_type$ &$param_name$, const QObject *context, const std::function<void(QtProtobuf::QGrpcAsyncReply *)> &callback);\n";
-const char *Templates::ClientMethodDeclarationQmlTemplate = "Q_INVOKABLE void $method_name$($param_type$ *$param_name$, const QJSValue &callback);";
+const char *Templates::ClientMethodDeclarationQmlTemplate = "Q_INVOKABLE void $method_name$($param_type$ *$param_name$, const QJSValue &callback, const QJSValue &errorCallback);\n";
 
 const char *Templates::ServerMethodDeclarationTemplate = "Q_INVOKABLE virtual $return_type$ $method_name$(const $param_type$ &$param_name$) = 0;\n";
 
@@ -310,7 +310,7 @@ const char *Templates::ClientMethodDefinitionAsync2Template = "\nvoid $classname
                                                               "    });\n"
                                                               "}\n";
 
-const char *Templates::ClientMethodDefinitionQmlTemplate = "\nvoid $classname$::$method_name$($param_type$ *$param_name$, const QJSValue &callback)\n"
+const char *Templates::ClientMethodDefinitionQmlTemplate = "\nvoid $classname$::$method_name$($param_type$ *$param_name$, const QJSValue &callback, const QJSValue &errorCallback)\n"
                                                            "{\n"
                                                            "    if (!callback.isCallable()) {\n"
                                                            "        qProtoWarning() << \"Unable to call $classname$::$method_name$, callback is not callable\";\n"
@@ -326,10 +326,12 @@ const char *Templates::ClientMethodDefinitionQmlTemplate = "\nvoid $classname$::
                                                            "        return;\n"
                                                            "    }\n\n"
                                                            "    QtProtobuf::QGrpcAsyncReply *reply = call(\"$method_name$\", *$param_name$);\n"
-                                                           "    QObject::connect(reply, &QtProtobuf::QGrpcAsyncReply::finished, jsEngine, [this, reply, callback, jsEngine]() {\n"
+                                                           "    reply->subscribe(jsEngine, [this, reply, callback, jsEngine]() {\n"
                                                            "        auto result = new $param_type$(reply->read<$param_type$>());\n"
-                                                           "        qmlEngine(this)->setObjectOwnership(result,  QQmlEngine::JavaScriptOwnership);\n"
+                                                           "        qmlEngine(this)->setObjectOwnership(result, QQmlEngine::JavaScriptOwnership);\n"
                                                            "        QJSValue(callback).call(QJSValueList{jsEngine->toScriptValue(result)});\n"
+                                                           "    }, [errorCallback, jsEngine](const QGrpcStatus &status) {\n"
+                                                           "        QJSValue(errorCallback).call(QJSValueList{jsEngine->toScriptValue(status)});\n"
                                                            "    });\n"
                                                            "}\n";
 const char *Templates::RegisterSerializersTemplate = "qRegisterProtobufType<$classname$>();\n";
@@ -344,6 +346,8 @@ const char *Templates::QmlRegisterEnumTypeTemplate = "qmlRegisterUncreatableType
 const char *Templates::ClientMethodSignalDeclarationTemplate = "Q_SIGNAL void $method_name$Updated(const $return_type$ &);\n";
 const char *Templates::ClientMethodServerStreamDeclarationTemplate = "QtProtobuf::QGrpcSubscription *subscribe$method_name_upper$Updates(const $param_type$ &$param_name$);\n";
 const char *Templates::ClientMethodServerStream2DeclarationTemplate = "QtProtobuf::QGrpcSubscription *subscribe$method_name_upper$Updates(const $param_type$ &$param_name$, const QPointer<$return_type$> &$return_name$);\n";
+const char *Templates::ClientMethodServerStreamQmlDeclarationTemplate = "Q_INVOKABLE QtProtobuf::QGrpcSubscription *qmlSubscribe$method_name_upper$Updates_p($param_type$ *$param_name$, $return_type$ *$return_name$);\n";
+
 const char *Templates::ClientMethodServerStreamDefinitionTemplate = "QtProtobuf::QGrpcSubscription *$classname$::subscribe$method_name_upper$Updates(const $param_type$ &$param_name$)\n"
                                                                     "{\n"
                                                                     "    return subscribe(\"$method_name$\", $param_name$);\n"
@@ -352,6 +356,10 @@ const char *Templates::ClientMethodServerStream2DefinitionTemplate = "QtProtobuf
                                                                      "{\n"
                                                                      "    return subscribe(\"$method_name$\", $param_name$, $return_name$);\n"
                                                                      "}\n";
+const char *Templates::ClientMethodServerStreamQmlDefinitionTemplate = "QtProtobuf::QGrpcSubscription *$classname$::qmlSubscribe$method_name_upper$Updates_p($param_type$ *$param_name$, $return_type$ *$return_name$)\n"
+                                                                       "{\n"
+                                                                       "    return subscribe(\"$method_name$\", *$param_name$, QPointer<$return_type$>($return_name$));\n"
+                                                                       "}\n";
 
 const char *Templates::ListSuffix = "Repeated";
 

+ 2 - 0
src/generator/templates.h

@@ -196,8 +196,10 @@ public:
     static const char *ClientMethodSignalDeclarationTemplate;
     static const char *ClientMethodServerStreamDeclarationTemplate;
     static const char *ClientMethodServerStream2DeclarationTemplate;
+    static const char *ClientMethodServerStreamQmlDeclarationTemplate;
     static const char *ClientMethodServerStreamDefinitionTemplate;
     static const char *ClientMethodServerStream2DefinitionTemplate;
+    static const char *ClientMethodServerStreamQmlDefinitionTemplate;
 
     static const char *ListSuffix;
     static const char *ProtoFileSuffix;

+ 2 - 0
src/grpc/CMakeLists.txt

@@ -101,4 +101,6 @@ endif()
 configure_file("${CMAKE_CURRENT_SOURCE_DIR}/qt_lib_grpc.pri.in" "${QT_PROTOBUF_BINARY_DIR}/qt_lib_grpc.pri" @ONLY)
 install(FILES "${QT_PROTOBUF_BINARY_DIR}/qt_lib_grpc.pri" DESTINATION "${QT_HOST_DATA}/mkspecs/modules" COMPONENT dev)
 
+add_subdirectory("quick")
+
 add_coverage_target(TARGET ${TARGET})

+ 4 - 0
src/grpc/QtGrpcConfig.cmake.in

@@ -8,5 +8,9 @@ if(NOT TARGET @TARGET@ AND NOT @TARGET@_BINARY_DIR)
     include("${CMAKE_CURRENT_LIST_DIR}/@TARGET_EXPORT@.cmake")
 endif()
 
+if(QT_PROTOBUF_STATIC AND NOT TARGET @GRPC_QUICK_PLUGIN_NAME@ AND NOT @GRPC_QUICK_PLUGIN_NAME@_BINARY_DIR)
+    include("${CMAKE_CURRENT_LIST_DIR}/@GRPC_QUICK_PLUGIN_NAME@Targets.cmake")
+endif()
+
 @PACKAGE_INIT@
 

+ 1 - 1
src/grpc/qgrpcasyncoperationbase_p.h

@@ -91,7 +91,7 @@ signals:
      * \brief The signal is emitted when error happend in channel or during serialization
      * \param[out] status received from gRPC channel
      */
-    void error(const QGrpcStatus &status);
+    void error(const QtProtobuf::QGrpcStatus &status);
 
 protected:
     //! \private

+ 9 - 0
src/grpc/qgrpcstatus.h

@@ -26,6 +26,8 @@
 #pragma once //QGrpcStatus
 
 #include <QString>
+#include <QMetaType>
+#include <qobjectdefs.h>
 
 #include "qtgrpcglobal.h"
 #include <memory>
@@ -41,6 +43,9 @@ class QGrpcStatusPrivate;
  *        This class combines QGrpcStatus::StatusCode and message returned from channel or QGrpc framework.
  */
 class Q_GRPC_EXPORT QGrpcStatus final {
+    Q_GADGET
+    Q_PROPERTY(StatusCode code READ code CONSTANT)
+    Q_PROPERTY(QString message READ message CONSTANT)
 public:
     /*!
      * \enum StatusCode
@@ -68,6 +73,8 @@ public:
         DataLoss = 15,          //!< Unrecoverable data loss or corruption
     };
 
+    Q_ENUM(StatusCode)
+
     QGrpcStatus(StatusCode code = StatusCode::Ok, const QString &message = QString());
     ~QGrpcStatus();
 
@@ -98,3 +105,5 @@ private:
 
 bool operator ==(QtProtobuf::QGrpcStatus::StatusCode code, const QtProtobuf::QGrpcStatus &status);
 bool operator !=(QtProtobuf::QGrpcStatus::StatusCode code, const QtProtobuf::QGrpcStatus &status);
+
+Q_DECLARE_METATYPE(QtProtobuf::QGrpcStatus)

+ 2 - 2
src/grpc/qgrpcsubscription.h

@@ -73,10 +73,10 @@ public:
      */
     void handler(const QByteArray& data) {
         setData(data);
-        updated();
         for (auto handler : m_handlers) {
             handler(data);
         }
+        updated();
     }
 
     bool operator ==(const QGrpcSubscription &other) const {
@@ -86,7 +86,7 @@ public:
 
 signals:
     /*!
-     * \brief The signal is emitted when subscription is finished by user
+     * \brief The signal is emitted when subscription received updated value from server
      */
     void updated();
 

+ 12 - 0
src/grpc/qtgrpcglobal.h

@@ -50,3 +50,15 @@
         Q_DISABLE_COPY(Class) \
         Q_DISABLE_MOVE(Class)
 #endif
+
+#ifndef Q_GRPC_IMPORT_QUICK_PLUGIN
+    #ifdef QT_PROTOBUF_STATIC
+        #include <QtPlugin>
+        #include <QQmlExtensionPlugin>
+        #define Q_GRPC_IMPORT_QUICK_PLUGIN() \
+            Q_IMPORT_PLUGIN(QtGrpcQuickPlugin) \
+            qobject_cast<QQmlExtensionPlugin*>(qt_static_plugin_QtGrpcQuickPlugin().instance())->registerTypes("QtGrpc");
+    #else
+        #define Q_GRPC_IMPORT_QUICK_PLUGIN()
+    #endif //QT_PROTOBUF_STATIC
+#endif //Q_PROTOBUF_IMPORT_QUICK_PLUGIN

+ 70 - 0
src/grpc/quick/CMakeLists.txt

@@ -0,0 +1,70 @@
+set(TARGET ${GRPC_QUICK_PLUGIN_NAME})
+
+set(TARGET_EXPORT ${TARGET}Targets)
+set(TARGET_INCLUDE_DIR ${CMAKE_INSTALL_INCLUDEDIR}/${TARGET})
+set(TARGET_CMAKE_DIR ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME})
+
+find_package(Qt5 COMPONENTS Core Qml REQUIRED)
+
+set(CMAKE_AUTOMOC ON)
+set(CMAKE_AUTOMOC_MOC_OPTIONS -Muri=QtGrpc)
+set(CMAKE_AUTORCC ON)
+
+include(${QT_PROTOBUF_CMAKE_DIR}/QtProtobufCommon.cmake)
+
+extract_qt_variable(QT_INSTALL_QML)
+
+set(TARGET_IMPORTS_DIR ${QT_INSTALL_QML}/QtGrpc)
+
+file(GLOB SOURCES
+    qquickgrpcsubscription.cpp
+    qtgrpcquickplugin.cpp)
+
+file(GLOB HEADERS
+    qtgrpcquickplugin.h
+    qquickgrpcsubscription_p.h
+    qtgrpcquick_global.h)
+
+if(QT_PROTOBUF_STATIC)
+    if(WIN32)
+        message(WARNING "Static version of QtProtobuf is not fully tested on Win32 platforms")
+    endif()
+    add_library(${TARGET} STATIC ${SOURCES})
+    target_compile_definitions(${TARGET} PRIVATE QT_PROTOBUF_STATIC QT_STATICPLUGIN PUBLIC QT_GRPC_QUICK_PLUGIN_NAME="QtGrpcQuickPlugin")
+    set(QT_PROTOBUF_EXTRA_COMPILE_DIFINITIONS QT_PROTOBUF_STATIC)
+else()
+    add_library(${TARGET} SHARED ${SOURCES})
+endif()
+
+target_link_libraries(${TARGET} PRIVATE Qt5::Core Qt5::Qml ${QT_PROTOBUF_PROJECT}::QtGrpc)
+set_target_properties(${TARGET} PROPERTIES
+    LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/QtGrpc"
+    RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/QtGrpc"
+    RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_CURRENT_BINARY_DIR}/QtGrpc"
+    RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_CURRENT_BINARY_DIR}/QtGrpc")
+target_compile_definitions(${TARGET} PRIVATE QT_GRPC_QUICK_LIB)
+
+if(QT_PROTOBUF_STATIC)
+    add_library(${QT_PROTOBUF_PROJECT}::${TARGET} ALIAS ${TARGET})
+    install(TARGETS ${TARGET} COMPONENT lib
+        EXPORT ${TARGET_EXPORT} COMPONENT dev
+        ARCHIVE DESTINATION "${TARGET_IMPORTS_DIR}" COMPONENT lib
+        RUNTIME DESTINATION "${TARGET_IMPORTS_DIR}" COMPONENT lib
+        LIBRARY DESTINATION "${TARGET_IMPORTS_DIR}" COMPONENT lib)
+    install(EXPORT ${TARGET_EXPORT} NAMESPACE ${QT_PROTOBUF_PROJECT}:: FILE ${TARGET_EXPORT}.cmake DESTINATION ${TARGET_CMAKE_DIR} COMPONENT dev)
+else()
+    install(TARGETS ${TARGET} COMPONENT lib
+        ARCHIVE DESTINATION "${TARGET_IMPORTS_DIR}" COMPONENT lib
+        RUNTIME DESTINATION "${TARGET_IMPORTS_DIR}" COMPONENT lib
+        LIBRARY DESTINATION "${TARGET_IMPORTS_DIR}" COMPONENT lib)
+endif()
+
+add_custom_command(TARGET ${TARGET}
+    COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/qmldir $<TARGET_FILE_DIR:${TARGET}>/qmldir
+    COMMENT "Copying qmldir to binary directory")
+
+install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/qmldir DESTINATION "${TARGET_IMPORTS_DIR}" COMPONENT lib)
+
+if(QT_PROTOBUF_STATIC)
+    export(TARGETS ${TARGET} NAMESPACE ${QT_PROTOBUF_PROJECT}:: FILE ${TARGET_EXPORT}.cmake)
+endif()

+ 4 - 0
src/grpc/quick/qmldir

@@ -0,0 +1,4 @@
+module QtGrpc
+plugin grpcquickplugin
+classname QtGrpcQuickPlugin
+

+ 157 - 0
src/grpc/quick/qquickgrpcsubscription.cpp

@@ -0,0 +1,157 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 Alexey Edelev <semlanik@gmail.com>
+ *
+ * This file is part of qtprotobuf project https://git.semlanik.org/semlanik/qtprotobuf
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this
+ * software and associated documentation files (the "Software"), to deal in the Software
+ * without restriction, including without limitation the rights to use, copy, modify,
+ * merge, publish, distribute, sublicense, and/or sell copies of the Software, and
+ * to permit persons to whom the Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies
+ * or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+ * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+ * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+
+#include "qquickgrpcsubscription_p.h"
+
+#include <QGrpcSubscription>
+#include <QJSEngine>
+
+using namespace QtProtobuf;
+
+QQuickGrpcSubscription::QQuickGrpcSubscription(QObject *parent) : QObject(parent)
+  , m_enabled(false)
+  , m_subscription(nullptr)
+  , m_returnValue(nullptr)
+{
+
+}
+
+void QQuickGrpcSubscription::updateSubscription()
+{
+    if (m_subscription != nullptr) {
+        m_subscription->cancel();
+        m_subscription = nullptr;
+
+        m_returnValue->deleteLater(); //TODO: probably need to take care about return value cleanup other way. It's just reminder about weak memory management.
+        m_returnValue = nullptr;
+    }
+
+    if (m_client.isNull() || m_method.isEmpty() || !m_enabled || m_argument.isNull()) {
+        return;
+    }
+
+    if (!subscribe()) {
+        setEnabled(false);
+    }
+}
+
+bool QQuickGrpcSubscription::subscribe()
+{
+    QString uppercaseMethodName = m_method;
+    uppercaseMethodName.replace(0, 1, m_method[0].toUpper());
+    const QMetaObject *metaObject = m_client->metaObject();
+    QMetaMethod method;
+    for (int i = 0; i < metaObject->methodCount(); i++) {
+        if (QString("qmlSubscribe%1Updates_p").arg(uppercaseMethodName) == metaObject->method(i).name()) {
+            method = metaObject->method(i);
+            break;
+        }
+    }
+
+    QString errorString;
+    if (!method.isValid()) {
+        errorString = m_method + "is not either server or bidirectional stream.";
+        qProtoWarning() << errorString;
+        error({QGrpcStatus::Unimplemented, errorString});
+        return false;
+    }
+
+    if (method.parameterCount() < 2) {
+        errorString = QString("Unable to call ") + method.name() + ". Invalid arguments set.";
+        qProtoWarning() << errorString;
+        error({QGrpcStatus::InvalidArgument, errorString});
+        return false;
+    }
+
+    QMetaType argumentPointerMetaType(method.parameterType(0));
+    if (argumentPointerMetaType.metaObject() != m_argument->metaObject()) {
+        errorString = QString("Unable to call ") + method.name() + ". Argument type mismatch: '" + method.parameterTypes().at(0) + "' expected, '" + m_argument->metaObject()->className() + "' provided";
+        qProtoWarning() << errorString;
+        error({QGrpcStatus::InvalidArgument, errorString});
+        return false;
+    }
+
+    QMetaType argumentMetaType(QMetaType::type(m_argument->metaObject()->className()));
+    if (!argumentMetaType.isValid()) {
+        errorString = QString("Argument of type '") + m_argument->metaObject()->className() + "' is not registred in metatype system";
+        qProtoWarning() << errorString;
+        error({QGrpcStatus::InvalidArgument, errorString});
+        return false;
+    }
+
+    QObject *argument = reinterpret_cast<QObject*>(argumentMetaType.create(m_argument));
+    if (argument == nullptr) {
+        errorString = "Unable to create argument copy. Unknown metatype system error";
+        qProtoWarning() << errorString;
+        error({QGrpcStatus::InvalidArgument, errorString});
+        return false;
+    }
+    argument->deleteLater(); //TODO: probably need to take care about temporary argument value cleanup other way. It's just reminder about weak memory management.
+
+    QMetaType returnPointerType(method.parameterType(1));
+    if (!returnPointerType.isValid()) {
+        errorString = QString("Return type argument of type '") + method.parameterTypes().at(1) + "' is not registred in metatype system";
+        qProtoWarning() << errorString;
+        error({QGrpcStatus::InvalidArgument, errorString});
+        return false;
+    }
+
+    QMetaType returnMetaType(QMetaType::type(returnPointerType.metaObject()->className()));
+    if (!returnMetaType.isValid()) {
+        errorString = QString("Unable to allocate return value. '") + returnPointerType.metaObject()->className() + "' is not registred in metatype system";
+        qProtoWarning() << errorString;
+        error({QGrpcStatus::InvalidArgument, errorString});
+        return false;
+    }
+
+    m_returnValue = reinterpret_cast<QObject*>(returnMetaType.create());
+
+    if (m_returnValue == nullptr) {
+        errorString = "Unable to allocate return value. Unknown metatype system error";
+        qProtoWarning() << errorString;
+        error({QGrpcStatus::Unknown, errorString});
+        return false;
+    }
+
+    bool ok = method.invoke(m_client, Qt::DirectConnection,
+                                      QGenericReturnArgument("QtProtobuf::QGrpcSubscription*", static_cast<void *>(&m_subscription)),
+                                      QGenericArgument(method.parameterTypes().at(0).data(), static_cast<const void *>(&argument)),
+                                      QGenericArgument(method.parameterTypes().at(1).data(), static_cast<const void *>(&m_returnValue)));
+    if (!ok) {
+        errorString = QString("Unable to call ") + m_method + "invalidate subscription.";
+        qProtoWarning() << errorString;
+        error({QGrpcStatus::Unknown, errorString});
+        return false;
+    }
+
+    connect(m_subscription, &QGrpcSubscription::updated, this, [this](){
+        updated(qjsEngine(this)->toScriptValue(m_returnValue));
+    });
+
+    connect(m_subscription, &QGrpcSubscription::error, this, &QQuickGrpcSubscription::error);
+
+    return true;
+}

+ 126 - 0
src/grpc/quick/qquickgrpcsubscription_p.h

@@ -0,0 +1,126 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 Alexey Edelev <semlanik@gmail.com>
+ *
+ * This file is part of qtprotobuf project https://git.semlanik.org/semlanik/qtprotobuf
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this
+ * software and associated documentation files (the "Software"), to deal in the Software
+ * without restriction, including without limitation the rights to use, copy, modify,
+ * merge, publish, distribute, sublicense, and/or sell copies of the Software, and
+ * to permit persons to whom the Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies
+ * or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+ * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+ * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#pragma once
+
+#include <QObject>
+#include <QAbstractGrpcClient>
+#include <QGrpcStatus>
+
+class QJSValue;
+
+namespace QtProtobuf {
+
+class QGrpcSubscription;
+
+class QQuickGrpcSubscription : public QObject
+{
+    Q_OBJECT
+    Q_PROPERTY(QAbstractGrpcClient *client READ client WRITE setClient NOTIFY clientChanged)
+    Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged)
+    Q_PROPERTY(QString method READ method WRITE setMethod NOTIFY methodChanged)
+    Q_PROPERTY(QObject *argument READ argument WRITE setArgument NOTIFY argumentChanged)
+
+public:
+    QQuickGrpcSubscription(QObject *parent = nullptr);
+    ~QQuickGrpcSubscription() = default;
+
+    QAbstractGrpcClient *client() const {
+        return m_client;
+    }
+
+    bool enabled() const {
+        return m_enabled;
+    }
+
+    QString method() const {
+        return m_method;
+    }
+
+    QObject *argument() const {
+        return m_argument;
+    }
+
+    void setClient(QAbstractGrpcClient *client) {
+        if (m_client == client) {
+            return;
+        }
+
+        m_client = client;
+        emit clientChanged();
+        updateSubscription();
+    }
+
+    void setEnabled(bool enabled) {
+        if (m_enabled == enabled) {
+            return;
+        }
+
+        m_enabled = enabled;
+        emit enabledChanged();
+        updateSubscription();
+    }
+
+    void setMethod(QString method) {
+        if (m_method == method) {
+            return;
+        }
+
+        m_method = method;
+        emit methodChanged();
+        updateSubscription();
+    }
+
+    void setArgument(QObject *argument) {
+        if (m_argument == argument) {
+            return;
+        }
+
+        m_argument = argument;
+        emit argumentChanged();
+        updateSubscription();
+    }
+
+signals:
+    void updated(const QJSValue &value);
+    void error(const QtProtobuf::QGrpcStatus &status);
+
+    void clientChanged();
+    void methodChanged();
+    void enabledChanged();
+    void argumentChanged();
+
+private:
+    void updateSubscription();
+    bool subscribe();
+    QPointer<QAbstractGrpcClient> m_client;
+    bool m_enabled;
+    QString m_method;
+    QPointer<QObject> m_argument;
+    QGrpcSubscription *m_subscription;
+    QObject *m_returnValue;
+};
+
+}

+ 34 - 0
src/grpc/quick/qtgrpcquick_global.h

@@ -0,0 +1,34 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 Alexey Edelev <semlanik@gmail.com>
+ *
+ * This file is part of QtProtobuf project https://git.semlanik.org/semlanik/qtprotobuf
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this
+ * software and associated documentation files (the "Software"), to deal in the Software
+ * without restriction, including without limitation the rights to use, copy, modify,
+ * merge, publish, distribute, sublicense, and/or sell copies of the Software, and
+ * to permit persons to whom the Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies
+ * or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+ * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+ * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#pragma once
+
+#include <QtCore/QtGlobal>
+
+#ifdef QT_GRPC_QUICK_LIB
+    #define QT_GRPC_QUICK_SHARED_EXPORT Q_DECL_EXPORT
+#else
+    #define QT_GRPC_QUICK_SHARED_EXPORT Q_DECL_IMPORT
+#endif

+ 43 - 0
src/grpc/quick/qtgrpcquickplugin.cpp

@@ -0,0 +1,43 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 Alexey Edelev <semlanik@gmail.com>
+ *
+ * This file is part of qtprotobuf project https://git.semlanik.org/semlanik/qtprotobuf
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this
+ * software and associated documentation files (the "Software"), to deal in the Software
+ * without restriction, including without limitation the rights to use, copy, modify,
+ * merge, publish, distribute, sublicense, and/or sell copies of the Software, and
+ * to permit persons to whom the Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies
+ * or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+ * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+ * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "qtgrpcquickplugin.h"
+
+#include "qquickgrpcsubscription_p.h"
+
+#include <QDebug>
+#include <QQmlEngine>
+#include <QGrpcStatus>
+
+using namespace QtProtobuf;
+
+void QtGrpcQuickPlugin::registerTypes(const char *uri)
+{
+    // @uri QtGrpc
+    Q_ASSERT(uri == QLatin1String("QtGrpc"));
+
+    qmlRegisterUncreatableType<QtProtobuf::QGrpcStatus>("QtGrpc", QT_PROTOBUF_VERSION_MAJOR, QT_PROTOBUF_VERSION_MINOR, "GrpcStatus", "GrpcStatus only could be recevied from gRPC calls");
+    qmlRegisterType<QQuickGrpcSubscription>("QtGrpc", QT_PROTOBUF_VERSION_MAJOR, QT_PROTOBUF_VERSION_MINOR, "GrpcSubscription");
+}

+ 43 - 0
src/grpc/quick/qtgrpcquickplugin.h

@@ -0,0 +1,43 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 Alexey Edelev <semlanik@gmail.com>
+ *
+ * This file is part of QtProtobuf project https://git.semlanik.org/semlanik/qtprotobuf
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this
+ * software and associated documentation files (the "Software"), to deal in the Software
+ * without restriction, including without limitation the rights to use, copy, modify,
+ * merge, publish, distribute, sublicense, and/or sell copies of the Software, and
+ * to permit persons to whom the Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies
+ * or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+ * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+ * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#pragma once
+
+#include <QQmlExtensionPlugin>
+#include "qtgrpcquick_global.h"
+
+/*!
+ * \private
+ * \brief The QtGrpcQuickPlugin class
+ */
+class QT_GRPC_QUICK_SHARED_EXPORT QtGrpcQuickPlugin : public QQmlExtensionPlugin
+{
+    Q_OBJECT
+    Q_PLUGIN_METADATA(IID QQmlExtensionInterface_iid)
+
+public:
+    ~QtGrpcQuickPlugin() = default;
+    void registerTypes(const char *) override;
+};

+ 0 - 1
src/protobuf/qtprotobufglobal.h

@@ -88,4 +88,3 @@
         #define Q_PROTOBUF_IMPORT_QUICK_PLUGIN()
     #endif //QT_PROTOBUF_STATIC
 #endif //Q_PROTOBUF_IMPORT_QUICK_PLUGIN
-

+ 0 - 1
src/protobuf/quick/CMakeLists.txt

@@ -71,7 +71,6 @@ else()
         LIBRARY DESTINATION "${TARGET_IMPORTS_DIR}" COMPONENT lib)
 endif()
 
-
 add_custom_command(TARGET ${TARGET}
     COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/qmldir $<TARGET_FILE_DIR:${TARGET}>/qmldir
     COMMENT "Copying qmldir to binary directory")

+ 8 - 3
tests/test_grpc_qml/CMakeLists.txt

@@ -16,7 +16,7 @@ add_executable(${TARGET} ${MOC_SOURCES} ${SOURCES} ${QML_FILES})
 target_link_libraries(${TARGET} PRIVATE Qt5::Qml Qt5::Quick Qt5::Test Qt5::QuickTest QtProtobufProject::QtGrpc)
 
 if(QT_PROTOBUF_STATIC)
-    target_link_libraries(${TARGET} PRIVATE protobufquickplugin)
+    target_link_libraries(${TARGET} PRIVATE ${PROTOBUF_QUICK_PLUGIN_NAME} ${GRPC_QUICK_PLUGIN_NAME})
 endif()
 
 qtprotobuf_link_target(${TARGET} qtgrpc_test_qtprotobuf_gen)
@@ -35,5 +35,10 @@ add_test(NAME ${TARGET}
 add_target_qml(TARGET ${TARGET} QML_FILES ${QML_FILES})
 add_target_windeployqt(TARGET ${TARGET} QML_DIR ${CMAKE_CURRENT_SOURCE_DIR}/qml)
 
-set_tests_properties(${TARGET} PROPERTIES
-    ENVIRONMENT QML2_IMPORT_PATH=$<TARGET_FILE_DIR:protobufquickplugin>/..)
+if(WIN32)
+    set_tests_properties(${TARGET} PROPERTIES
+        ENVIRONMENT QML2_IMPORT_PATH=$<TARGET_FILE_DIR:${PROTOBUF_QUICK_PLUGIN_NAME}>/..\;$<TARGET_FILE_DIR:${GRPC_QUICK_PLUGIN_NAME}>/..)
+else()
+    set_tests_properties(${TARGET} PROPERTIES
+        ENVIRONMENT QML2_IMPORT_PATH=$<TARGET_FILE_DIR:${PROTOBUF_QUICK_PLUGIN_NAME}>/..:$<TARGET_FILE_DIR:${GRPC_QUICK_PLUGIN_NAME}>/..)
+endif()

+ 111 - 2
tests/test_grpc_qml/qml/tst_grpc.qml

@@ -27,6 +27,7 @@ import QtQuick 2.12
 import QtTest 1.0
 
 import QtProtobuf 0.3
+import QtGrpc 0.3
 import qtprotobufnamespace.tests 1.0
 
 TestCase {
@@ -36,12 +37,120 @@ TestCase {
         testFieldString: "Test string"
     }
 
+    GrpcSubscription {
+        id: serverStreamSubscription
+        property bool ok: true
+        property int updateCount: 0
+
+        enabled: false
+        client: TestServiceClient
+        method: "testMethodServerStream"
+        argument: stringMsg
+        onUpdated: {
+            ++updateCount;
+            ok = ok && value.testFieldString === "Test string" + updateCount
+        }
+        onError: {
+            console.log("Subscription error: " + status.code + " " + status.message)
+            ok = false;
+        }
+    }
+
+    GrpcSubscription {
+        id: serverStreamCancelSubscription
+        property bool ok: true
+        property int updateCount: 0
+
+        enabled: false
+        client: TestServiceClient
+        method: "testMethodServerStream"
+        argument: stringMsg
+        onUpdated: {
+            ++updateCount;
+            ok = ok && value.testFieldString === "Test string" + updateCount
+            if (updateCount === 3) {
+                serverStreamCancelSubscription.enabled = false;
+            }
+        }
+        onError: {
+            console.log("Subscription error: " + status.code + " " + status.message)
+            ok = false;
+        }
+    }
+
+    GrpcSubscription {
+        id: serverStreamInvalidSubscription
+        property bool ok: false
+        enabled: false
+        client: TestServiceClient
+        method: "testMethodServerStreamNotExist"
+        argument: stringMsg
+        onUpdated: {
+            ok = false;
+        }
+        onError: {
+            ok = status.code === GrpcStatus.Unimplemented;
+        }
+    }
+
     function test_stringEchoTest() {
         var called = false;
+        var errorCalled = false;
+        TestServiceClient.testMethod(stringMsg, function(result) {
+            called = result.testFieldString === stringMsg.testFieldString;
+        }, function(status) {
+            errorCalled = true
+        })
+        wait(300)
+        compare(called && !errorCalled, true, "testMethod was not called proper way")
+    }
+
+    function test_statusTest() {
+        var called = false;
+        var errorCalled = false;
+        TestServiceClient.testMethodStatusMessage(stringMsg, function(result) {
+            called = true;
+        }, function(status) {
+            errorCalled = status.code === GrpcStatus.Unimplemented && status.message === stringMsg.testFieldString;
+        })
+        wait(300)
+        compare(!called && errorCalled, true, "testMethodStatusMessage was not called proper way")
+    }
+
+    function test_stringEchoAsyncTest() {
+        var called = false;
+        var errorCalled = false;
+        stringMsg.testFieldString = "sleep";
         TestServiceClient.testMethod(stringMsg, function(result) {
             called = result.testFieldString === stringMsg.testFieldString;
+        }, function(status) {
+            errorCalled = true
         })
-        wait(500)
-        compare(called, true, "testMethod was not called proper way")
+        wait(300)
+        compare(!called && !errorCalled, true, "testMethod was not called proper way")
+        wait(1000)
+        compare(called && !errorCalled, true, "testMethod was not called proper way")
+        stringMsg.testFieldString = "Test string";
+    }
+
+    function test_serverStreamSubscription() {
+        serverStreamSubscription.enabled = true;
+        wait(20000);
+        compare(serverStreamSubscription.ok, true, "Subscription data failed")
+        compare(serverStreamSubscription.updateCount, 4, "Subscription failed, update was not called right amount times")
+    }
+
+    function test_serverStreamCancelSubscription() {
+        serverStreamCancelSubscription.enabled = true;
+        wait(20000);
+        compare(serverStreamCancelSubscription.ok, true, "Subscription data failed")
+        compare(serverStreamCancelSubscription.updateCount, 3, "Subscription failed, update was not called right amount times")
+    }
+
+    function test_serverStreamInvalidSubscription() {
+        serverStreamInvalidSubscription.enabled = true;
+        wait(500);
+        compare(serverStreamInvalidSubscription.ok, true, "Subscription data failed")
+        compare(serverStreamInvalidSubscription.enabled, false, "Subscription data failed")
     }
 }

+ 1 - 0
tests/test_grpc_qml/test.h

@@ -44,6 +44,7 @@ public:
     TestSetup() {
         QtProtobuf::qRegisterProtobufTypes();
         Q_PROTOBUF_IMPORT_QUICK_PLUGIN()
+        Q_GRPC_IMPORT_QUICK_PLUGIN()
         qmlRegisterSingletonType<TestServiceClient>("qtprotobufnamespace.tests", 1, 0, "TestServiceClient", [](QQmlEngine *engine, QJSEngine *) -> QObject *{
             static TestServiceClient clientInstance;
             clientInstance.attachChannel(std::make_shared<QGrpcHttp2Channel>(m_echoServerAddress, QGrpcInsecureChannelCredentials() | QGrpcInsecureCallCredentials()));

+ 2 - 2
tests/test_qml/CMakeLists.txt

@@ -20,7 +20,7 @@ if(TARGET QtProtobufProject::QtProtobufWellKnownTypes)
 endif()
 
 if(QT_PROTOBUF_STATIC)
-    target_link_libraries(${TARGET} PRIVATE protobufquickplugin)
+    target_link_libraries(${TARGET} PRIVATE ${PROTOBUF_QUICK_PLUGIN_NAME})
 endif()
 
 qtprotobuf_link_target(${TARGET} qtprotobuf_test_qtprotobuf_gen)
@@ -31,4 +31,4 @@ add_target_windeployqt(TARGET ${TARGET} QML_DIR ${CMAKE_CURRENT_SOURCE_DIR}/qml)
 add_test(NAME ${TARGET} COMMAND ${TARGET})
 
 set_tests_properties(${TARGET} PROPERTIES
-    ENVIRONMENT QML2_IMPORT_PATH=$<TARGET_FILE_DIR:protobufquickplugin>/..)
+    ENVIRONMENT QML2_IMPORT_PATH=$<TARGET_FILE_DIR:${PROTOBUF_QUICK_PLUGIN_NAME}>/..)