summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--client/CMakeLists.txt2
-rw-r--r--client/clientapi.h387
-rw-r--r--client/test/CMakeLists.txt15
-rw-r--r--client/test/clientapitest.cpp54
4 files changed, 307 insertions, 151 deletions
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})
11target_link_libraries(${PROJECT_NAME} toynadicommon) 11target_link_libraries(${PROJECT_NAME} toynadicommon)
12qt5_use_modules(${PROJECT_NAME} Widgets Network) 12qt5_use_modules(${PROJECT_NAME} Widgets Network)
13install(TARGETS ${PROJECT_NAME} DESTINATION bin) 13install(TARGETS ${PROJECT_NAME} DESTINATION bin)
14
15add_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 @@
4#include <QSet> 4#include <QSet>
5#include <QSharedPointer> 5#include <QSharedPointer>
6#include <functional> 6#include <functional>
7#include "store/database.h"
8 7
9namespace async { 8namespace async {
10 //This should abstract if we execute from eventloop or in thread. 9 //This should abstract if we execute from eventloop or in thread.
@@ -18,9 +17,74 @@ namespace async {
18 timer->start(0); 17 timer->start(0);
19 }; 18 };
20 19
20 /**
21 * Query result set
22 *
23 * This should probably become part of a generic kasync library.
24 *
25 * Functional is nice because we don't have to store data in the emitter
26 * Non functional and storing may be the right thing because we want an in-memory representation of the set
27 * non-functional also allows us to batch move data across thread boundaries.
28 */
29
30 template<class T>
31 class ResultEmitter;
32
33 /*
34 * The promise side for the result provider
35 */
36 template<class T>
37 class ResultProvider {
38 public:
39 void add(const T &value)
40 {
41 //the handler will be called in the other thread, protect
42 mResultEmitter->addHandler(value);
43 }
44
45 void complete()
46 {
47 mResultEmitter->completeHandler();
48 }
49
50 QSharedPointer<ResultEmitter<T> > emitter()
51 {
52 mResultEmitter = QSharedPointer<ResultEmitter<T> >(new ResultEmitter<T>());
53 return mResultEmitter;
54 }
55
56 private:
57 QSharedPointer<ResultEmitter<T> > mResultEmitter;
58 };
59
60 /*
61 * The future side for the client.
62 *
63 * It does not directly hold the state.
64 */
65 template<class DomainType>
66 class ResultEmitter {
67 public:
68 void onAdded(const std::function<void(const DomainType&)> &handler)
69 {
70 addHandler = handler;
71 }
72 // void onRemoved(const std::function<void(const T&)> &handler);
73 void onComplete(const std::function<void(void)> &handler)
74 {
75 completeHandler = handler;
76 }
77
78 private:
79 friend class ResultProvider<DomainType>;
80 std::function<void(const DomainType&)> addHandler;
81 // std::function<void(const T&)> removeHandler;
82 std::function<void(void)> completeHandler;
83 };
84
21} 85}
22 86
23namespace ClientAPI { 87namespace Akonadi2 {
24 88
25/** 89/**
26 * Standardized Domain Types 90 * Standardized Domain Types
@@ -32,8 +96,10 @@ namespace ClientAPI {
32 * 96 *
33 * These types will be frequently modified (for every new feature that should be exposed to the any client) 97 * These types will be frequently modified (for every new feature that should be exposed to the any client)
34 */ 98 */
99namespace Domain {
35 100
36class AkonadiDomainType { 101class AkonadiDomainType {
102public:
37 /* 103 /*
38 * Each domain object needs to store the resource, identifier, revision triple so we can link back to the storage location. 104 * Each domain object needs to store the resource, identifier, revision triple so we can link back to the storage location.
39 */ 105 */
@@ -58,87 +124,30 @@ class Folder : public AkonadiDomainType {
58 124
59}; 125};
60 126
61/*
62 * Resource and domain object specific
63 * FIXME: should we hardcode the requirement that the domain adapter is a subclass for the domain object?
64 * * how do we allow copying of domain objects?
65 * ** dummy domain object that is a wrapper?
66 * ** domain adapter has an accessor for the domain object to hide subclassing
67 */
68class EventDomainAdapter : public Event {
69 // virtual void setFoo(const QString &value)
70 // {
71 // mBuffer.setFoo(value);
72 // }
73
74 // virtual QString foo() const
75 // {
76 // return mBuffer.foo();
77 // }
78
79 // MessageBuffer mBuffer;
80};
81
82
83
84/** 127/**
85 * Query result set 128 * All types need to be registered here an MUST return a different name.
86 * 129 *
87 * This should probably become part of a generic kasync library. 130 * Do not store these types to disk, they may change over time.
88 *
89 * Functional is nice because we don't have to store data in the emitter
90 * Non functional and storing may be the right thing because we want an in-memory representation of the set
91 * non-functional also allows us to batch move data across thread boundaries.
92 */
93
94template<class T>
95class ResultEmitter;
96
97/*
98 * The promise side for the result provider
99 */ 131 */
100template<class T>
101class ResultProvider {
102public:
103 void add(const T &value)
104 {
105 //the handler will be called in the other thread, protect
106 mResultEmitter->addHandler(value);
107 }
108
109 QSharedPointer<ResultEmitter<T> > emitter()
110 {
111 mResultEmitter = QSharedPointer<ResultEmitter<T> >(new ResultEmitter<T>());
112 return emitter;
113 }
114 132
115private: 133template<class DomainType>
116 QSharedPointer<ResultEmitter<T> > mResultEmitter; 134QString getTypeName();
117};
118
119/*
120 * The future side for the client.
121 *
122 * It does not directly hold the state.
123 */
124template<class T>
125class ResultEmitter {
126public:
127 void onAdded(const std::function<void(const T&)> &handler);
128 // void onRemoved(const std::function<void(const T&)> &handler);
129 135
130private: 136template<>
131 friend class SetSource; 137QString getTypeName<Event>()
132 std::function<void(const T&)> addHandler; 138{
133 // std::function<void(const T&)> removeHandler; 139 return "event";
134}; 140}
135 141
136// template<class T> 142template<>
137// class TreeSet : public Set<T> { 143QString getTypeName<Todo>()
138// 144{
139// }; 145 return "todo";
146}
140 147
148}
141 149
150using namespace async;
142 151
143/** 152/**
144 * A query that matches a set of objects 153 * A query that matches a set of objects
@@ -157,18 +166,22 @@ class Query
157{ 166{
158public: 167public:
159 //Resources to search 168 //Resources to search
160 QSet<QString> resources() const { return QSet<QString>(); } 169 QSet<QString> resources;
161}; 170};
162 171
163 172
164/** 173/**
165 * Interface for the store facade 174 * Interface for the store facade.
166 * 175 *
167 * All methods are synchronous. 176 * All methods are synchronous.
177 * Facades are stateful (they hold connections to resources and database).
178 *
179 * TODO: would it make sense to split the write, read and notification parts? (we could potentially save some connections)
168 */ 180 */
169template<class DomainType> 181template<class DomainType>
170class StoreFacade { 182class StoreFacade {
171public: 183public:
184 virtual ~StoreFacade(){};
172 virtual void create(const DomainType &domainObject) = 0; 185 virtual void create(const DomainType &domainObject) = 0;
173 virtual void modify(const DomainType &domainObject) = 0; 186 virtual void modify(const DomainType &domainObject) = 0;
174 virtual void remove(const DomainType &domainObject) = 0; 187 virtual void remove(const DomainType &domainObject) = 0;
@@ -177,94 +190,71 @@ public:
177 190
178 191
179/** 192/**
180 * Actual implementation of the store facade that is provided by the resource plugin. 193 * Facade factory that returns a store facade implementation, by loading a plugin and providing the relevant implementation.
181 *
182 * It knows the buffer type used by the resource as well as the actual store used.
183 * 194 *
184 * A resource must provide this facade for each domain type it knows. 195 * If we were to provide default implementations for certain capabilities. Here would be the place to do so.
185 * => is reimplemented a lot
186 * => we should have a base implementation
187 * 196 *
188 * This interface should be executed in a thread so we can synchronously retrieve data from the store. 197 * TODO: pluginmechansims for resources to provide their implementations.
189 * 198 * * We may want a way to recycle facades to avoid recreating socket connections all the time?
190 * TODO: perhaps we should also allow async access and leave the thread/non-thread decision up to the implementation?
191 */ 199 */
192template<typename DomainType>
193class StoreFacadeImpl : public StoreFacade<Event> {
194};
195 200
196template<> 201class FacadeFactory {
197class StoreFacadeImpl<Event> : public StoreFacade<Event> {
198public: 202public:
199 void create(const Event &domainObject) { 203 //FIXME: proper singleton implementation
200 //FIXME here we would need to cast to DomainAdapter 204 static FacadeFactory &instance()
201 //Do actual work 205 {
202 //transformFromDomainType(domainObject); 206 static FacadeFactory factory;
203 //Ideally we have an adapter 207 return factory;
204 //getAdater(domainObject).buffer();
205 //domainObject.key(); => The domain object needs to provide the id
206 //writeToDb();
207 } 208 }
208 209
209 void modify(const Event &domainObject) { 210 static QString key(const QString &resource, const QString &type)
210 //Do actual work 211 {
212 return resource + type;
211 } 213 }
212 214
213 void remove(const Event &domainObject) { 215 template<class DomainType, class Facade>
214 //Do actual work 216 void registerFacade(const QString &resource)
217 {
218 const QString typeName = Domain::getTypeName<DomainType>();
219 mFacadeRegistry.insert(key(resource, typeName), [](){ return new Facade; });
215 } 220 }
216 221
217 class EventBuffer { 222 /*
218 QString value; 223 * Allows the registrar to register a specific instance.
219 }; 224 *
220 225 * Primarily for testing.
221 static Event transformToDomainType(const EventBuffer &buffer) { 226 * The facade factory takes ovnership of the poniter and typically deletes the instance via shared pointer.
222 //We may want to avoid copies here 227 * Supplied factory functions should therefore always return a new pointer (i.e. via clone())
223 Event event; 228 *
224 // //Ideally we don't have to copy and can use an adaptor instead 229 * FIXME the factory function should really be returning QSharedPointer<void>, which doesn't work (std::shared_pointer<void> would though). That way i.e. a test could keep the object alive until it's done.
225 // return DomainAdaptor 230 */
226 return event; 231 template<class DomainType, class Facade>
227 }; 232 void registerFacade(const QString &resource, const std::function<void*(void)> &customFactoryFunction)
228 233 {
229 void load(const Query &query, const std::function<void(const Event &)> &resultCallback) { 234 const QString typeName = Domain::getTypeName<DomainType>();
230 //retrieve buffers from storage 235 mFacadeRegistry.insert(key(resource, typeName), customFactoryFunction);
231 QList<EventBuffer> queryresult;
232 for(const EventBuffer &buffer : queryresult) {
233 resultCallback(transformToDomainType(buffer));
234 }
235 } 236 }
236 237
237private:
238 //Dummy implementation
239 class ResourceImpl {};
240 ResourceImpl resource;
241 Database mDb;
242};
243
244/**
245 * Facade factory that returns a store facade implementation, by loading a plugin and providing the relevant implementation.
246 *
247 * If we were to provide default implementations for certain capabilities. Here would be the place to do so.
248 *
249 * TODO: pluginmechansims for resources to provide their implementations.
250 */
251class FacadeFactory {
252public:
253 template<class DomainType> 238 template<class DomainType>
254 static StoreFacade<DomainType> getFacade(const QString &resource) 239 QSharedPointer<StoreFacade<DomainType> > getFacade(const QString &resource)
255 { 240 {
256 //TODO errorhandling in case the resource doesn't support the domain type 241 const QString typeName = Domain::getTypeName<DomainType>();
257 if (resource == "dummyresource") { 242 auto factoryFunction = mFacadeRegistry.value(key(resource, typeName));
258 return StoreFacadeImpl<DomainType>(); 243 if (factoryFunction) {
244 return QSharedPointer<StoreFacade<DomainType> >(static_cast<StoreFacade<DomainType>* >(factoryFunction()));
259 } 245 }
260 return StoreFacadeImpl<DomainType>(); 246 qWarning() << "Failed to find facade for resource: " << resource << " and type: " << typeName;
247 return QSharedPointer<StoreFacade<DomainType> >();
261 } 248 }
249
250private:
251 QHash<QString, std::function<void*(void)> > mFacadeRegistry;
262}; 252};
263 253
264/** 254/**
265 * Store interface used in the client API 255 * Store interface used in the client API.
266 * 256 *
267 * TODO: For testing we need to be able to inject dummy StoreFacades. 257 * TODO: For testing we need to be able to inject dummy StoreFacades. Should we work with a store instance, or a singleton factory?
268 */ 258 */
269class Store { 259class Store {
270public: 260public:
@@ -282,10 +272,13 @@ public:
282 async::run([resultSet, query](){ 272 async::run([resultSet, query](){
283 // Query all resources and aggregate results 273 // Query all resources and aggregate results
284 // query tells us in which resources we're interested 274 // query tells us in which resources we're interested
285 for(const QString &resource : query.resources()) { 275 for(const QString &resource : query.resources) {
286 auto facade = FacadeFactory::getFacade<DomainType>(resource); 276 auto facade = FacadeFactory::instance().getFacade<DomainType>(resource);
287 facade.load(query, resultSet.add); 277 //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.
278 std::function<void(const DomainType &)> addCallback = std::bind(&ResultProvider<DomainType>::add, resultSet, std::placeholders::_1);
279 facade->load(query, addCallback);
288 } 280 }
281 resultSet->complete();
289 }); 282 });
290 return resultSet->emitter(); 283 return resultSet->emitter();
291 } 284 }
@@ -305,7 +298,7 @@ public:
305 template <class DomainType> 298 template <class DomainType>
306 static void create(const DomainType &domainObject, const QString &resourceIdentifier) { 299 static void create(const DomainType &domainObject, const QString &resourceIdentifier) {
307 //Potentially move to separate thread as well 300 //Potentially move to separate thread as well
308 auto facade = FacadeFactory::getFacade<DomainType>(resourceIdentifier); 301 auto facade = FacadeFactory::instance().getFacade<DomainType>(resourceIdentifier);
309 facade.create(domainObject); 302 facade.create(domainObject);
310 } 303 }
311 304
@@ -317,7 +310,7 @@ public:
317 template <class DomainType> 310 template <class DomainType>
318 static void modify(const DomainType &domainObject, const QString &resourceIdentifier) { 311 static void modify(const DomainType &domainObject, const QString &resourceIdentifier) {
319 //Potentially move to separate thread as well 312 //Potentially move to separate thread as well
320 auto facade = FacadeFactory::getFacade<DomainType>(resourceIdentifier); 313 auto facade = FacadeFactory::instance().getFacade<DomainType>(resourceIdentifier);
321 facade.modify(domainObject); 314 facade.modify(domainObject);
322 } 315 }
323 316
@@ -327,9 +320,101 @@ public:
327 template <class DomainType> 320 template <class DomainType>
328 static void remove(const DomainType &domainObject, const QString &resourceIdentifier) { 321 static void remove(const DomainType &domainObject, const QString &resourceIdentifier) {
329 //Potentially move to separate thread as well 322 //Potentially move to separate thread as well
330 auto facade = FacadeFactory::getFacade<DomainType>(resourceIdentifier); 323 auto facade = FacadeFactory::instance().getFacade<DomainType>(resourceIdentifier);
331 facade.remove(domainObject); 324 facade.remove(domainObject);
332 } 325 }
333}; 326};
334 327
335} 328}
329
330//Example implementations
331/*
332 * Resource and domain object specific
333 * FIXME: should we hardcode the requirement that the domain adapter is a subclass for the domain object?
334 * * how do we allow copying of domain objects?
335 * ** dummy domain object that is a wrapper?
336 * ** domain adapter has an accessor for the domain object to hide subclassing
337 */
338class EventDomainAdapter : public Akonadi2::Domain::Event {
339 // virtual void setFoo(const QString &value)
340 // {
341 // mBuffer.setFoo(value);
342 // }
343
344 // virtual QString foo() const
345 // {
346 // return mBuffer.foo();
347 // }
348
349 // MessageBuffer mBuffer;
350};
351
352
353/**
354 * Actual implementation of the store facade that is provided by the resource plugin.
355 *
356 * It knows the buffer type used by the resource as well as the actual store used.
357 *
358 * A resource must provide this facade for each domain type it knows.
359 * => is reimplemented a lot
360 * => we should have a base implementation
361 *
362 * This interface should be executed in a thread so we can synchronously retrieve data from the store.
363 *
364 * TODO: perhaps we should also allow async access and leave the thread/non-thread decision up to the implementation?
365 */
366template<typename DomainType>
367class StoreFacadeImpl : public Akonadi2::StoreFacade<Akonadi2::Domain::Event> {
368};
369
370template<>
371class StoreFacadeImpl<Akonadi2::Domain::Event> : public Akonadi2::StoreFacade<Akonadi2::Domain::Event> {
372public:
373 StoreFacadeImpl():StoreFacade() {};
374
375 void create(const Akonadi2::Domain::Event &domainObject) {
376 //FIXME here we would need to cast to DomainAdapter
377 //Do actual work
378 //transformFromDomainType(domainObject);
379 //Ideally we have an adapter
380 //getAdater(domainObject).buffer();
381 //domainObject.key(); => The domain object needs to provide the id
382 //writeToDb();
383 }
384
385 void modify(const Akonadi2::Domain::Event &domainObject) {
386 //Do actual work
387 }
388
389 void remove(const Akonadi2::Domain::Event &domainObject) {
390 //Do actual work
391 }
392
393 class EventBuffer {
394 QString value;
395 };
396
397 static Akonadi2::Domain::Event transformToDomainType(const EventBuffer &buffer) {
398 //We may want to avoid copies here
399 Akonadi2::Domain::Event event;
400 // //Ideally we don't have to copy and can use an adaptor instead
401 // return DomainAdaptor
402 return event;
403 };
404
405 void load(const Akonadi2::Query &query, const std::function<void(const Akonadi2::Domain::Event &)> &resultCallback) {
406 //retrieve buffers from storage
407 QList<EventBuffer> queryresult;
408 for(const EventBuffer &buffer : queryresult) {
409 resultCallback(transformToDomainType(buffer));
410 }
411 }
412
413private:
414 //Dummy implementation
415 class ResourceImpl {};
416 ResourceImpl resource;
417 class DatabaseImpl {};
418 DatabaseImpl mDb;
419};
420
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 @@
1set(CMAKE_AUTOMOC ON)
2include_directories(${CMAKE_CURRENT_BINARY_DIR})
3
4macro(auto_tests)
5 foreach(_testname ${ARGN})
6 add_executable(${_testname} ${_testname}.cpp ${store_SRCS})
7 qt5_use_modules(${_testname} Core Test)
8 target_link_libraries(${_testname} lmdb)
9 add_test(NAME ${_testname} COMMAND ${_testname})
10 endforeach(_testname)
11endmacro(auto_tests)
12
13auto_tests (
14 clientapitest
15)
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 @@
1#include <QtTest>
2#include <QDebug>
3#include <functional>
4
5#include "../clientapi.h"
6
7class DummyResourceFacade : public Akonadi2::StoreFacade<Akonadi2::Domain::Event>
8{
9public:
10 ~DummyResourceFacade(){};
11 virtual void create(const Akonadi2::Domain::Event &domainObject){};
12 virtual void modify(const Akonadi2::Domain::Event &domainObject){};
13 virtual void remove(const Akonadi2::Domain::Event &domainObject){};
14 virtual void load(const Akonadi2::Query &query, const std::function<void(const Akonadi2::Domain::Event &)> &resultCallback)
15 {
16 qDebug() << "load called";
17 for(const auto &result : results) {
18 resultCallback(result);
19 }
20 }
21
22 QList<Akonadi2::Domain::Event> results;
23};
24
25class ClientAPITest : public QObject
26{
27 Q_OBJECT
28private Q_SLOTS:
29
30 void testLoad()
31 {
32 DummyResourceFacade facade;
33 facade.results << Akonadi2::Domain::Event();
34
35 Akonadi2::FacadeFactory::instance().registerFacade<Akonadi2::Domain::Event, DummyResourceFacade>("dummyresource", [facade](){ return new DummyResourceFacade(facade); });
36
37 Akonadi2::Query query;
38 query.resources << "dummyresource";
39
40 auto result = Akonadi2::Store::load<Akonadi2::Domain::Event>(query);
41
42 QList<Akonadi2::Domain::Event> resultSet;
43 result->onAdded([&resultSet](const Akonadi2::Domain::Event &event){ resultSet << event; qDebug() << "result added";});
44
45 bool complete;
46 result->onComplete([&complete]{ complete = true; qDebug() << "complete";});
47
48 QTRY_VERIFY(complete);
49 }
50
51};
52
53QTEST_MAIN(ClientAPITest)
54#include "clientapitest.moc"