From af38dcdf3a836a1b94064a617acac387a2de6539 Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Mon, 1 Dec 2014 10:36:54 +0100 Subject: ClientAPI is starting to work and has a first test. --- client/CMakeLists.txt | 2 + client/clientapi.h | 387 ++++++++++++++++++++++++++---------------- client/test/CMakeLists.txt | 15 ++ client/test/clientapitest.cpp | 54 ++++++ 4 files changed, 307 insertions(+), 151 deletions(-) create mode 100644 client/test/CMakeLists.txt create mode 100644 client/test/clientapitest.cpp (limited to 'client') diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 859f707..20b00a5 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -11,3 +11,5 @@ add_executable(${PROJECT_NAME} ${toynadiclient_SRCS}) target_link_libraries(${PROJECT_NAME} toynadicommon) qt5_use_modules(${PROJECT_NAME} Widgets Network) install(TARGETS ${PROJECT_NAME} DESTINATION bin) + +add_subdirectory(test) diff --git a/client/clientapi.h b/client/clientapi.h index 4e98d05..4ec90e2 100644 --- a/client/clientapi.h +++ b/client/clientapi.h @@ -4,7 +4,6 @@ #include #include #include -#include "store/database.h" namespace async { //This should abstract if we execute from eventloop or in thread. @@ -18,9 +17,74 @@ namespace async { timer->start(0); }; + /** + * Query result set + * + * This should probably become part of a generic kasync library. + * + * Functional is nice because we don't have to store data in the emitter + * Non functional and storing may be the right thing because we want an in-memory representation of the set + * non-functional also allows us to batch move data across thread boundaries. + */ + + template + class ResultEmitter; + + /* + * The promise side for the result provider + */ + template + class ResultProvider { + public: + void add(const T &value) + { + //the handler will be called in the other thread, protect + mResultEmitter->addHandler(value); + } + + void complete() + { + mResultEmitter->completeHandler(); + } + + QSharedPointer > emitter() + { + mResultEmitter = QSharedPointer >(new ResultEmitter()); + return mResultEmitter; + } + + private: + QSharedPointer > mResultEmitter; + }; + + /* + * The future side for the client. + * + * It does not directly hold the state. + */ + template + class ResultEmitter { + public: + void onAdded(const std::function &handler) + { + addHandler = handler; + } + // void onRemoved(const std::function &handler); + void onComplete(const std::function &handler) + { + completeHandler = handler; + } + + private: + friend class ResultProvider; + std::function addHandler; + // std::function removeHandler; + std::function completeHandler; + }; + } -namespace ClientAPI { +namespace Akonadi2 { /** * Standardized Domain Types @@ -32,8 +96,10 @@ namespace ClientAPI { * * These types will be frequently modified (for every new feature that should be exposed to the any client) */ +namespace Domain { class AkonadiDomainType { +public: /* * Each domain object needs to store the resource, identifier, revision triple so we can link back to the storage location. */ @@ -58,87 +124,30 @@ class Folder : public AkonadiDomainType { }; -/* - * Resource and domain object specific - * FIXME: should we hardcode the requirement that the domain adapter is a subclass for the domain object? - * * how do we allow copying of domain objects? - * ** dummy domain object that is a wrapper? - * ** domain adapter has an accessor for the domain object to hide subclassing - */ -class EventDomainAdapter : public Event { - // virtual void setFoo(const QString &value) - // { - // mBuffer.setFoo(value); - // } - - // virtual QString foo() const - // { - // return mBuffer.foo(); - // } - - // MessageBuffer mBuffer; -}; - - - /** - * Query result set - * - * This should probably become part of a generic kasync library. - * - * Functional is nice because we don't have to store data in the emitter - * Non functional and storing may be the right thing because we want an in-memory representation of the set - * non-functional also allows us to batch move data across thread boundaries. - */ - -template -class ResultEmitter; - -/* - * The promise side for the result provider + * All types need to be registered here an MUST return a different name. + * + * Do not store these types to disk, they may change over time. */ -template -class ResultProvider { -public: - void add(const T &value) - { - //the handler will be called in the other thread, protect - mResultEmitter->addHandler(value); - } - - QSharedPointer > emitter() - { - mResultEmitter = QSharedPointer >(new ResultEmitter()); - return emitter; - } -private: - QSharedPointer > mResultEmitter; -}; - -/* - * The future side for the client. - * - * It does not directly hold the state. - */ -template -class ResultEmitter { -public: - void onAdded(const std::function &handler); - // void onRemoved(const std::function &handler); +template +QString getTypeName(); -private: - friend class SetSource; - std::function addHandler; - // std::function removeHandler; -}; +template<> +QString getTypeName() +{ + return "event"; +} -// template -// class TreeSet : public Set { -// -// }; +template<> +QString getTypeName() +{ + return "todo"; +} +} +using namespace async; /** * A query that matches a set of objects @@ -157,18 +166,22 @@ class Query { public: //Resources to search - QSet resources() const { return QSet(); } + QSet resources; }; /** - * Interface for the store facade + * Interface for the store facade. * * All methods are synchronous. + * Facades are stateful (they hold connections to resources and database). + * + * TODO: would it make sense to split the write, read and notification parts? (we could potentially save some connections) */ template class StoreFacade { public: + virtual ~StoreFacade(){}; virtual void create(const DomainType &domainObject) = 0; virtual void modify(const DomainType &domainObject) = 0; virtual void remove(const DomainType &domainObject) = 0; @@ -177,94 +190,71 @@ public: /** - * Actual implementation of the store facade that is provided by the resource plugin. - * - * It knows the buffer type used by the resource as well as the actual store used. + * Facade factory that returns a store facade implementation, by loading a plugin and providing the relevant implementation. * - * A resource must provide this facade for each domain type it knows. - * => is reimplemented a lot - * => we should have a base implementation + * If we were to provide default implementations for certain capabilities. Here would be the place to do so. * - * This interface should be executed in a thread so we can synchronously retrieve data from the store. - * - * TODO: perhaps we should also allow async access and leave the thread/non-thread decision up to the implementation? + * TODO: pluginmechansims for resources to provide their implementations. + * * We may want a way to recycle facades to avoid recreating socket connections all the time? */ -template -class StoreFacadeImpl : public StoreFacade { -}; -template<> -class StoreFacadeImpl : public StoreFacade { +class FacadeFactory { public: - void create(const Event &domainObject) { - //FIXME here we would need to cast to DomainAdapter - //Do actual work - //transformFromDomainType(domainObject); - //Ideally we have an adapter - //getAdater(domainObject).buffer(); - //domainObject.key(); => The domain object needs to provide the id - //writeToDb(); + //FIXME: proper singleton implementation + static FacadeFactory &instance() + { + static FacadeFactory factory; + return factory; } - void modify(const Event &domainObject) { - //Do actual work + static QString key(const QString &resource, const QString &type) + { + return resource + type; } - void remove(const Event &domainObject) { - //Do actual work + template + void registerFacade(const QString &resource) + { + const QString typeName = Domain::getTypeName(); + mFacadeRegistry.insert(key(resource, typeName), [](){ return new Facade; }); } - class EventBuffer { - QString value; - }; - - static Event transformToDomainType(const EventBuffer &buffer) { - //We may want to avoid copies here - Event event; - // //Ideally we don't have to copy and can use an adaptor instead - // return DomainAdaptor - return event; - }; - - void load(const Query &query, const std::function &resultCallback) { - //retrieve buffers from storage - QList queryresult; - for(const EventBuffer &buffer : queryresult) { - resultCallback(transformToDomainType(buffer)); - } + /* + * Allows the registrar to register a specific instance. + * + * Primarily for testing. + * The facade factory takes ovnership of the poniter and typically deletes the instance via shared pointer. + * Supplied factory functions should therefore always return a new pointer (i.e. via clone()) + * + * FIXME the factory function should really be returning QSharedPointer, which doesn't work (std::shared_pointer would though). That way i.e. a test could keep the object alive until it's done. + */ + template + void registerFacade(const QString &resource, const std::function &customFactoryFunction) + { + const QString typeName = Domain::getTypeName(); + mFacadeRegistry.insert(key(resource, typeName), customFactoryFunction); } -private: - //Dummy implementation - class ResourceImpl {}; - ResourceImpl resource; - Database mDb; -}; - -/** - * Facade factory that returns a store facade implementation, by loading a plugin and providing the relevant implementation. - * - * If we were to provide default implementations for certain capabilities. Here would be the place to do so. - * - * TODO: pluginmechansims for resources to provide their implementations. - */ -class FacadeFactory { -public: template - static StoreFacade getFacade(const QString &resource) + QSharedPointer > getFacade(const QString &resource) { - //TODO errorhandling in case the resource doesn't support the domain type - if (resource == "dummyresource") { - return StoreFacadeImpl(); + const QString typeName = Domain::getTypeName(); + auto factoryFunction = mFacadeRegistry.value(key(resource, typeName)); + if (factoryFunction) { + return QSharedPointer >(static_cast* >(factoryFunction())); } - return StoreFacadeImpl(); + qWarning() << "Failed to find facade for resource: " << resource << " and type: " << typeName; + return QSharedPointer >(); } + +private: + QHash > mFacadeRegistry; }; /** - * Store interface used in the client API + * Store interface used in the client API. * - * TODO: For testing we need to be able to inject dummy StoreFacades. + * TODO: For testing we need to be able to inject dummy StoreFacades. Should we work with a store instance, or a singleton factory? */ class Store { public: @@ -282,10 +272,13 @@ public: async::run([resultSet, query](){ // Query all resources and aggregate results // query tells us in which resources we're interested - for(const QString &resource : query.resources()) { - auto facade = FacadeFactory::getFacade(resource); - facade.load(query, resultSet.add); + for(const QString &resource : query.resources) { + auto facade = FacadeFactory::instance().getFacade(resource); + //We have to bind an instance to the function callback. Since we use a shared pointer this keeps the result provider instance (and thus also the emitter) alive. + std::function addCallback = std::bind(&ResultProvider::add, resultSet, std::placeholders::_1); + facade->load(query, addCallback); } + resultSet->complete(); }); return resultSet->emitter(); } @@ -305,7 +298,7 @@ public: template static void create(const DomainType &domainObject, const QString &resourceIdentifier) { //Potentially move to separate thread as well - auto facade = FacadeFactory::getFacade(resourceIdentifier); + auto facade = FacadeFactory::instance().getFacade(resourceIdentifier); facade.create(domainObject); } @@ -317,7 +310,7 @@ public: template static void modify(const DomainType &domainObject, const QString &resourceIdentifier) { //Potentially move to separate thread as well - auto facade = FacadeFactory::getFacade(resourceIdentifier); + auto facade = FacadeFactory::instance().getFacade(resourceIdentifier); facade.modify(domainObject); } @@ -327,9 +320,101 @@ public: template static void remove(const DomainType &domainObject, const QString &resourceIdentifier) { //Potentially move to separate thread as well - auto facade = FacadeFactory::getFacade(resourceIdentifier); + auto facade = FacadeFactory::instance().getFacade(resourceIdentifier); facade.remove(domainObject); } }; } + +//Example implementations +/* + * Resource and domain object specific + * FIXME: should we hardcode the requirement that the domain adapter is a subclass for the domain object? + * * how do we allow copying of domain objects? + * ** dummy domain object that is a wrapper? + * ** domain adapter has an accessor for the domain object to hide subclassing + */ +class EventDomainAdapter : public Akonadi2::Domain::Event { + // virtual void setFoo(const QString &value) + // { + // mBuffer.setFoo(value); + // } + + // virtual QString foo() const + // { + // return mBuffer.foo(); + // } + + // MessageBuffer mBuffer; +}; + + +/** + * Actual implementation of the store facade that is provided by the resource plugin. + * + * It knows the buffer type used by the resource as well as the actual store used. + * + * A resource must provide this facade for each domain type it knows. + * => is reimplemented a lot + * => we should have a base implementation + * + * This interface should be executed in a thread so we can synchronously retrieve data from the store. + * + * TODO: perhaps we should also allow async access and leave the thread/non-thread decision up to the implementation? + */ +template +class StoreFacadeImpl : public Akonadi2::StoreFacade { +}; + +template<> +class StoreFacadeImpl : public Akonadi2::StoreFacade { +public: + StoreFacadeImpl():StoreFacade() {}; + + void create(const Akonadi2::Domain::Event &domainObject) { + //FIXME here we would need to cast to DomainAdapter + //Do actual work + //transformFromDomainType(domainObject); + //Ideally we have an adapter + //getAdater(domainObject).buffer(); + //domainObject.key(); => The domain object needs to provide the id + //writeToDb(); + } + + void modify(const Akonadi2::Domain::Event &domainObject) { + //Do actual work + } + + void remove(const Akonadi2::Domain::Event &domainObject) { + //Do actual work + } + + class EventBuffer { + QString value; + }; + + static Akonadi2::Domain::Event transformToDomainType(const EventBuffer &buffer) { + //We may want to avoid copies here + Akonadi2::Domain::Event event; + // //Ideally we don't have to copy and can use an adaptor instead + // return DomainAdaptor + return event; + }; + + void load(const Akonadi2::Query &query, const std::function &resultCallback) { + //retrieve buffers from storage + QList queryresult; + for(const EventBuffer &buffer : queryresult) { + resultCallback(transformToDomainType(buffer)); + } + } + +private: + //Dummy implementation + class ResourceImpl {}; + ResourceImpl resource; + class DatabaseImpl {}; + DatabaseImpl mDb; +}; + diff --git a/client/test/CMakeLists.txt b/client/test/CMakeLists.txt new file mode 100644 index 0000000..6453fc5 --- /dev/null +++ b/client/test/CMakeLists.txt @@ -0,0 +1,15 @@ +set(CMAKE_AUTOMOC ON) +include_directories(${CMAKE_CURRENT_BINARY_DIR}) + +macro(auto_tests) + foreach(_testname ${ARGN}) + add_executable(${_testname} ${_testname}.cpp ${store_SRCS}) + qt5_use_modules(${_testname} Core Test) + target_link_libraries(${_testname} lmdb) + add_test(NAME ${_testname} COMMAND ${_testname}) + endforeach(_testname) +endmacro(auto_tests) + +auto_tests ( + clientapitest +) diff --git a/client/test/clientapitest.cpp b/client/test/clientapitest.cpp new file mode 100644 index 0000000..8d8f552 --- /dev/null +++ b/client/test/clientapitest.cpp @@ -0,0 +1,54 @@ +#include +#include +#include + +#include "../clientapi.h" + +class DummyResourceFacade : public Akonadi2::StoreFacade +{ +public: + ~DummyResourceFacade(){}; + virtual void create(const Akonadi2::Domain::Event &domainObject){}; + virtual void modify(const Akonadi2::Domain::Event &domainObject){}; + virtual void remove(const Akonadi2::Domain::Event &domainObject){}; + virtual void load(const Akonadi2::Query &query, const std::function &resultCallback) + { + qDebug() << "load called"; + for(const auto &result : results) { + resultCallback(result); + } + } + + QList results; +}; + +class ClientAPITest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + + void testLoad() + { + DummyResourceFacade facade; + facade.results << Akonadi2::Domain::Event(); + + Akonadi2::FacadeFactory::instance().registerFacade("dummyresource", [facade](){ return new DummyResourceFacade(facade); }); + + Akonadi2::Query query; + query.resources << "dummyresource"; + + auto result = Akonadi2::Store::load(query); + + QList resultSet; + result->onAdded([&resultSet](const Akonadi2::Domain::Event &event){ resultSet << event; qDebug() << "result added";}); + + bool complete; + result->onComplete([&complete]{ complete = true; qDebug() << "complete";}); + + QTRY_VERIFY(complete); + } + +}; + +QTEST_MAIN(ClientAPITest) +#include "clientapitest.moc" -- cgit v1.2.3