Skip to content

Commit abfc822

Browse files
committedJun 25, 2023
Add library with basic API
0 parents  commit abfc822

9 files changed

+480
-0
lines changed
 

‎.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
build/
2+
.idea/

‎CMakeLists.txt

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
cmake_minimum_required(VERSION 3.11)
2+
3+
project(tinyevents)
4+
5+
set(CMAKE_CXX_STANDARD 17)
6+
add_library(tinyevents INTERFACE include/tinyevents/tinyevents.hpp)
7+
target_include_directories(tinyevents INTERFACE include)

‎LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (C) 2023 KyrietS.
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in
13+
all copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
THE SOFTWARE.

‎README.md

+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# TinyEvents - A Simple Event-Dispatcher System for C++
2+
3+
*TinyEvents* is a simple header-only library for C++ that provides a basic, yet powerfull, event-dispatcher system. It is designed to be easy to use and to have minimal dependencies. It is written in C++17 and has no dependencies other than the standard library.
4+
5+
In *TinyEvents* any type can be used as an event. The events are dispatched to listeners that are registered for a specific event type. Asynchronous (deferred) dispatching using a queue is also supported.
6+
7+
## Basic Usage
8+
9+
```cpp
10+
#include <tinyevents/tinyevents.hpp>
11+
12+
struct MyEvent {
13+
int value;
14+
};
15+
16+
int maint() {
17+
tinyevents::Dispatcher dispatcher;
18+
19+
dispatcher.listen<MyEvent>([](const auto& event) {
20+
std::cout << "Received MyEvent: " << event.value << std::endl;
21+
});
22+
23+
dispatcher.queue(MyEvent{77});
24+
dispatcher.dispatch(MyEvent{42}); // Prints "Received MyEvent: 42"
25+
dispatcher.process(); // Prints "Received MyEvent: 77"
26+
27+
dispatcher.dispatch(123); // No listener for this event, so nothing happens
28+
29+
return 0;
30+
}
31+
```
32+
33+
## Getting Started
34+
35+
### Manual Installation
36+
Just copy the `include/tinyevents` folder to your project and add the `include` folder to your include path. The library is header-only, so no compilation is needed.
37+
38+
### CMake
39+
If you use CMake, you can use the `FetchContent` module to download the library and add it to your project. For example:
40+
41+
```cmake
42+
include(FetchContent)
43+
FetchContent_Declare(
44+
tinyevents
45+
GIT_REPOSITORY https://github.com/KyrietS/tinyevents.git
46+
GIT_TAG master
47+
)
48+
FetchContent_MakeAvailable(tinyevents)
49+
target_link_libraries(${TARGET} PRIVATE tinyevents)
50+
```
51+
52+
Or you can just clone this repository manually and add it as a subdirectory to your CMake project.
53+
```cmake
54+
add_subdirectory(tinyevents)
55+
target_link_libraries(${TARGET} PRIVATE tinyevents)
56+
```
57+
58+
Don't forget to change the `${TARGET}` to the name of your target.
59+
60+
## Documentation
61+
62+
### Listening to Events
63+
64+
Register a listener for a specific event type. The listener will be called when an event of the specified type is dispatched.
65+
66+
```cpp
67+
template<typename Event>
68+
void listen(const std::function<void(const Event&)>& listener)
69+
```
70+
71+
##### Parameters
72+
* listener - A callable object that will be called when an event of type `Event` is dispatched. The object must be copyable.
73+
74+
##### Example
75+
```cpp
76+
Dispatcher dispatcher;
77+
dispatcher.listen<int>([](const int& n){ /* ... */ });
78+
```
79+
80+
### Dispatching Events
81+
82+
Immediately dispatch an event to all the listeners that are registered for the event type.
83+
84+
```cpp
85+
template<typename Event>
86+
void send(const Event& event)
87+
```
88+
##### Parameters
89+
* event - The event to dispatch.
90+
91+
##### Example
92+
```cpp
93+
Dispatcher dispatcher;
94+
dispatcher.send(2137);
95+
```
96+
97+
### Queueing Events
98+
99+
Add an event to the queue. The event will be dispatched once `process()` is called.
100+
101+
```cpp
102+
template<typename Event>
103+
void queue(const Event& event)
104+
```
105+
##### Parameters
106+
* event - The event to queue. The event will be copied and stored in the queue.
107+
108+
##### Example
109+
```cpp
110+
Dispatcher dispatcher;
111+
dispatcher.queue(2137);
112+
```
113+
114+
### Processing the Queue
115+
116+
Processing the queue will dispatch all the events that were queued using `queue()`. The events will be dispatched in the order they were queued.
117+
118+
```cpp
119+
void process()
120+
```
121+
122+
## Tests
123+
124+
Tests are written using Google Test. The library is fetched automatically by CMake during the configuration step of the tests.
125+
126+
In order to run the tests with CMake, you can use the following commands:
127+
```
128+
cd tests
129+
cmake -B build
130+
cd build
131+
cmake --build .
132+
ctest
133+
```
134+
135+
## License
136+
Copyright © 2023 KyrietS\
137+
Use of this software is granted under the terms of the MIT License.
138+
139+
See the [LICENSE](LICENSE) file for more details.

‎include/tinyevents/tinyevents.hpp

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#pragma once
2+
3+
#include <functional>
4+
#include <list>
5+
#include <map>
6+
#include <typeindex>
7+
8+
namespace tinyevents
9+
{
10+
class Dispatcher {
11+
public:
12+
Dispatcher() = default;
13+
Dispatcher(Dispatcher &&) noexcept = default;
14+
Dispatcher(const Dispatcher &) = delete;
15+
Dispatcher &operator=(Dispatcher &&) noexcept = default;
16+
17+
template<typename T>
18+
void listen(const std::function<void(const T &)> &listener) {
19+
listenersByType[std::type_index(typeid(T))].push_back([listener](const auto &msg) {
20+
const T *concreteMessage = static_cast<const T *>(msg);
21+
listener(*concreteMessage);
22+
});
23+
}
24+
25+
template<typename T>
26+
void send(const T &msg) {
27+
const auto &listeners = listenersByType.find(std::type_index(typeid(T)));
28+
if (listeners != listenersByType.end()) {
29+
for (auto &listener: listeners->second) {
30+
listener(&msg);
31+
}
32+
}
33+
}
34+
35+
template<typename T>
36+
void queue(const T &msg) {
37+
queuedDispatches.push_back([msg](Dispatcher& dispatcher) {
38+
dispatcher.send(msg);
39+
});
40+
}
41+
42+
void process() {
43+
for (auto &queuedDispatch: queuedDispatches) {
44+
queuedDispatch(*this);
45+
}
46+
queuedDispatches.clear();
47+
}
48+
49+
private:
50+
using Listeners = std::list<std::function<void(const void *)>>;
51+
std::map<std::type_index, Listeners> listenersByType;
52+
std::list<std::function<void(Dispatcher&)>> queuedDispatches;
53+
};
54+
}// namespace tinyevents

‎tests/CMakeLists.txt

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
cmake_minimum_required(VERSION 3.11)
2+
include(FetchContent)
3+
4+
FetchContent_Declare(
5+
googletest
6+
GIT_REPOSITORY https://github.com/google/googletest.git
7+
GIT_TAG release-1.12.1
8+
)
9+
10+
set(TEST_TARGET tinyevents_tests)
11+
12+
project(${TEST_TARGET})
13+
set(CMAKE_CXX_STANDARD 17)
14+
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
15+
set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT ${TEST_TARGET})
16+
17+
add_executable(${TEST_TARGET}
18+
TestEventDispatch.cpp
19+
TestEventDispatchQueue.cpp
20+
TestEventDispatcherMove.cpp
21+
)
22+
23+
# tinyevents
24+
add_subdirectory(../ tinyevents)
25+
target_link_libraries(${TEST_TARGET} PRIVATE tinyevents)
26+
27+
# GoogleTest
28+
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
29+
FetchContent_MakeAvailable(googletest)
30+
target_link_libraries(${TEST_TARGET} PRIVATE gtest gtest_main gmock)
31+
32+
enable_testing()
33+
include(GoogleTest)
34+
gtest_discover_tests(${TEST_TARGET})

‎tests/TestEventDispatch.cpp

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
#include <gmock/gmock.h>
2+
#include <gtest/gtest.h>
3+
#include <tinyevents/tinyevents.hpp>
4+
5+
using namespace tinyevents;
6+
using namespace testing;
7+
8+
struct DispatcherTest : public Test {
9+
Dispatcher dispatcher{};
10+
};
11+
12+
TEST_F(DispatcherTest, MessagesIgnoredWhenNoListeners) {
13+
dispatcher.send(123);
14+
dispatcher.send(123.0f);
15+
dispatcher.send("abc");
16+
17+
struct CustomType { };
18+
dispatcher.send(CustomType{});
19+
}
20+
21+
TEST_F(DispatcherTest, MessagesAreDispatchedToSingleListeners) {
22+
dispatcher.listen<int>([](const auto &msg) {
23+
EXPECT_EQ(msg, 123);
24+
});
25+
dispatcher.send(123);
26+
27+
int n = 123;
28+
dispatcher.listen<int>([&n](const auto &msg) {
29+
EXPECT_EQ(msg, n);
30+
});
31+
dispatcher.send(n);
32+
}
33+
34+
TEST_F(DispatcherTest, TheSameListenerCanBeAddedMultipleTimes) {
35+
MockFunction<void(const int &)> intCallback;
36+
EXPECT_CALL(intCallback, Call(123)).Times(2);
37+
38+
dispatcher.listen(intCallback.AsStdFunction());
39+
dispatcher.listen(intCallback.AsStdFunction());
40+
41+
dispatcher.send(123);
42+
}
43+
44+
TEST_F(DispatcherTest, DifferentListenersOfSameTypeAreCalled) {
45+
StrictMock<MockFunction<void(const int &)>> intCallback1;
46+
StrictMock<MockFunction<void(const int &)>> intCallback2;
47+
EXPECT_CALL(intCallback1, Call(123)).Times(1);
48+
EXPECT_CALL(intCallback2, Call(123)).Times(1);
49+
50+
dispatcher.listen(intCallback1.AsStdFunction());
51+
dispatcher.listen(intCallback2.AsStdFunction());
52+
53+
dispatcher.send(123);
54+
}
55+
56+
TEST_F(DispatcherTest, DifferentListenersOfDifferentTypesAreCalled) {
57+
StrictMock<MockFunction<void(const int &)>> intCallback;
58+
StrictMock<MockFunction<void(const float &)>> floatCallback;
59+
60+
61+
dispatcher.listen(intCallback.AsStdFunction());
62+
dispatcher.listen(floatCallback.AsStdFunction());
63+
64+
EXPECT_CALL(intCallback, Call(1)).Times(1);
65+
dispatcher.send(1);
66+
67+
EXPECT_CALL(floatCallback, Call(2.0f)).Times(1);
68+
dispatcher.send(2.0f);
69+
}
70+
71+
72+
TEST_F(DispatcherTest, ListenerWithCustomEmptyType) {
73+
struct EmptyType { };
74+
75+
StrictMock<MockFunction<void(const EmptyType &)>> emptyTypeCallback;
76+
dispatcher.listen(emptyTypeCallback.AsStdFunction());
77+
78+
EXPECT_CALL(emptyTypeCallback, Call(A<const EmptyType &>())).Times(1);
79+
dispatcher.send(EmptyType{});
80+
81+
EXPECT_CALL(emptyTypeCallback, Call(A<const EmptyType &>())).Times(1);
82+
dispatcher.send(EmptyType{});
83+
}
84+
85+
TEST_F(DispatcherTest, ListenerWithCustomType) {
86+
struct CustomType {
87+
int n;
88+
};
89+
90+
StrictMock<MockFunction<void(const CustomType &)>> customTypeCallback;
91+
dispatcher.listen(customTypeCallback.AsStdFunction());
92+
93+
EXPECT_CALL(customTypeCallback, Call(Field(&CustomType::n, 111))).Times(1);
94+
dispatcher.send(CustomType{ 111 });
95+
96+
EXPECT_CALL(customTypeCallback, Call(Field(&CustomType::n, 222))).Times(1);
97+
dispatcher.send(CustomType{ 222 });
98+
99+
dispatcher.send(333);
100+
}
101+
102+
TEST_F(DispatcherTest, ParentMessageTypeListenerShouldNotBeCalledForChildMessage) {
103+
struct Parent { };
104+
struct Child : Parent { };
105+
106+
StrictMock<MockFunction<void(const Parent &)>> parentCallback;
107+
StrictMock<MockFunction<void(const Child &)>> childCallback;
108+
dispatcher.listen(parentCallback.AsStdFunction());
109+
dispatcher.listen(childCallback.AsStdFunction());
110+
111+
EXPECT_CALL(childCallback, Call(A<const Child &>())).Times(1);
112+
dispatcher.send(Child{});
113+
}

‎tests/TestEventDispatchQueue.cpp

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#include <gmock/gmock.h>
2+
#include <gtest/gtest.h>
3+
#include <tinyevents/tinyevents.hpp>
4+
5+
using namespace tinyevents;
6+
using namespace testing;
7+
8+
struct DispatcherQueueTest : public testing::Test {
9+
Dispatcher dispatcher{};
10+
};
11+
12+
TEST_F(DispatcherQueueTest, MessagesQueuedAndThenIgnoredWhenNoListeners) {
13+
dispatcher.queue(123);
14+
dispatcher.queue(123.0f);
15+
dispatcher.queue("abc");
16+
17+
struct CustomType { };
18+
dispatcher.queue(CustomType{});
19+
20+
dispatcher.process();
21+
}
22+
23+
TEST_F(DispatcherQueueTest, ShouldNotSendAnythingWhenNoMessagesQueued) {
24+
StrictMock<MockFunction<void(const int &)>> intCallback;
25+
26+
dispatcher.listen(intCallback.AsStdFunction());
27+
dispatcher.process();
28+
}
29+
30+
TEST_F(DispatcherQueueTest, ShouldSendMessageQueued) {
31+
StrictMock<MockFunction<void(const int &)>> intCallback;
32+
33+
dispatcher.listen(intCallback.AsStdFunction());
34+
dispatcher.queue(123);
35+
dispatcher.queue(123);
36+
dispatcher.queue(123);
37+
38+
EXPECT_CALL(intCallback, Call(123)).Times(3);
39+
dispatcher.process();
40+
}
41+
42+
TEST_F(DispatcherQueueTest, ShouldSendQueuedMessagesOfDifferentTypes) {
43+
StrictMock<MockFunction<void(const int &)>> intCallback;
44+
45+
dispatcher.listen(intCallback.AsStdFunction());
46+
dispatcher.queue(123);
47+
dispatcher.queue(123.0f);
48+
dispatcher.queue("123");
49+
50+
EXPECT_CALL(intCallback, Call(123)).Times(1);
51+
dispatcher.process();
52+
}
53+
54+
TEST_F(DispatcherQueueTest, MessageQeuedBeforeListenerAddedAndThenSent) {
55+
StrictMock<MockFunction<void(const int &)>> intCallback;
56+
57+
dispatcher.queue(123);
58+
dispatcher.listen(intCallback.AsStdFunction());
59+
60+
EXPECT_CALL(intCallback, Call(123)).Times(1);
61+
dispatcher.process();
62+
}

‎tests/TestEventDispatcherMove.cpp

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#include <gmock/gmock.h>
2+
#include <gtest/gtest.h>
3+
#include <tinyevents/tinyevents.hpp>
4+
5+
using namespace tinyevents;
6+
using namespace testing;
7+
8+
9+
TEST(TestDispatcherCopyAndMove, VerifyDispatcherPredicates) {
10+
// Check if Dispatcher is movable
11+
EXPECT_TRUE(std::is_move_constructible_v<Dispatcher>);
12+
EXPECT_TRUE(std::is_move_assignable_v<Dispatcher>);
13+
14+
// Check if Dispatcher is NOT copyable
15+
EXPECT_FALSE(std::is_copy_constructible_v<Dispatcher>);
16+
EXPECT_FALSE(std::is_copy_assignable_v<Dispatcher>);
17+
}
18+
19+
TEST(TestDispatcherCopyAndMove, WhenDispatcherIsMovedThenListenersAreMoved) {
20+
Dispatcher movedDispatcher;
21+
StrictMock<MockFunction<void(const int &)>> intCallback;
22+
23+
movedDispatcher.listen(intCallback.AsStdFunction());
24+
Dispatcher dispatcher = std::move(movedDispatcher);
25+
26+
// Should not call the listener
27+
EXPECT_CALL(intCallback, Call(222)).Times(0);
28+
movedDispatcher.send(222); // NOLINT(*-use-after-move)
29+
30+
EXPECT_CALL(intCallback, Call(111)).Times(1);
31+
dispatcher.send(111);
32+
}
33+
34+
TEST(TestDispatcherCopyAndMove, WhenDispatcherIsMovedThenQueuedMessagesAreMoved) {
35+
Dispatcher movedDispatcher;
36+
StrictMock<MockFunction<void(const int &)>> intCallback;
37+
38+
movedDispatcher.listen(intCallback.AsStdFunction());
39+
movedDispatcher.queue(111);
40+
Dispatcher dispatcher = std::move(movedDispatcher);
41+
42+
// Should not call the listener
43+
EXPECT_CALL(intCallback, Call(222)).Times(0);
44+
movedDispatcher.process(); // NOLINT(*-use-after-move)
45+
46+
EXPECT_CALL(intCallback, Call(111)).Times(1);
47+
dispatcher.process();
48+
}

0 commit comments

Comments
 (0)
Please sign in to comment.