diff --git a/src/mqtt/topic_matcher.h b/src/mqtt/topic_matcher.h new file mode 100644 index 0000000..84c75ea --- /dev/null +++ b/src/mqtt/topic_matcher.h @@ -0,0 +1,442 @@ +///////////////////////////////////////////////////////////////////////////// +/// @file topic_matcher.h +/// Declaration of MQTT topic_matcher class +/// @date April 23, 2022 +/// @author Frank Pagliughi +///////////////////////////////////////////////////////////////////////////// + +/******************************************************************************* + * Copyright (c) 2022 Frank Pagliughi + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * Contributors: + * Frank Pagliughi - initial implementation and documentation + *******************************************************************************/ + +#ifndef __mqtt_topic_matcher_h +#define __mqtt_topic_matcher_h + +#include "mqtt/types.h" +#include "mqtt/topic.h" +#include +#include +#include +#include +#include + +// The make_unique<>() template functions from the original std proposal: +// https://isocpp.org/files/papers/N3656.txt + +#if __cplusplus < 201402L +namespace std { + template struct _Unique_if { + typedef unique_ptr _Single_object; + }; + + template struct _Unique_if { + typedef unique_ptr _Unknown_bound; + }; + + template struct _Unique_if { + typedef void _Known_bound; + }; + + template + typename _Unique_if::_Single_object + make_unique(Args&&... args) { + return unique_ptr(new T(std::forward(args)...)); + } + + template + typename _Unique_if::_Unknown_bound + make_unique(size_t n) { + typedef typename remove_extent::type U; + return unique_ptr(new U[n]()); + } + + template + typename _Unique_if::_Known_bound + make_unique(Args&&...) = delete; +} +#endif + +namespace mqtt { + +///////////////////////////////////////////////////////////////////////////// + +/** + * This can be used to get an iterator to all items that have a filter that + * matches a topic. To test against a single filter, see + * [`TopicFilter`](crate::TopicFilter). This collection is more commonly + * used when there are a nuber of filters and each needs to be associated + * with a particular action or piece of data. Note, though, that a single + * incoming topic could match against several items in the collection. For + * example, the topic: + * data/temperature/engine + * + * Could match against the filters: + * data/temperature/# data/+/engine + * + * Thus, the collection gives an iterator for the items matching a topic. + * + * A common use for this would be to store callbacks to proces incoming + * messages based on topics. + * + * This code was adapted from the Eclipse Python `MQTTMatcher` class: + * + + * + * which use a prefix tree (trie) to store the values. + * + * For example, if you had a `topic_mapper` and you inserted: + * insert({"some/random/topic", 42}) + * insert({"some/#", 99}) + * insert({"some/+/topic", 33}) + * + * The collection would be built like: + * "some" -> + * "random" -> + * "topic" -> <42> + * "#" -> <99> + * "+" -> + * "topic" -> <33> + */ +template +class topic_matcher +{ +public: + using key_type = string; + using mapped_type = T; + using value_type = std::pair; + using reference = value_type; + using const_reference = const value_type&; + +private: + /** + * The nodes of the collection. + */ + struct node + { + using ptr_t = std::unique_ptr; + using map_t = std::map; + + /** The value that matches the topic at this node, if any */ + std::unique_ptr content; + /** Child nodes mapped by the next field of the topic */ + map_t children; + + static ptr_t create() { + return std::make_unique(); + } + }; + using node_ptr = typename node::ptr_t; + using node_map = typename node::map_t; + + /** The root node of the collection */ + node_ptr root_; + +public: + class iterator { + /** + * Information about a node to search. + */ + struct search_node { + /** The current node being searched. null means end. */ + node* node_; + /** The fields of the topic still to be searched. */ + std::forward_list syms_; + + search_node() : node_(nullptr) {} + search_node(node* nd, const std::forward_list& sy) + : node_(nd), syms_(sy) {} + search_node(node* nd, std::forward_list&& sy) + : node_(nd), syms_(std::move(sy)) {} + }; + + /** The last-found value */ + value_type* pval_; + /** The current search node */ + search_node snode_; + /** The nodes still to be checked */ + std::vector nodes_; + + /** + * Move the next iterator to the next value, or to end(), if none + * left. + * + * This will keep recursing until it finds a mtching node that + * contains a value or it reaches the end. + */ + void next() { + pval_ = nullptr; + + // Can't move if we're already at the end + if (!snode_.node_) + return; + + if (snode_.syms_.empty()) { + pval_ = snode_.node_->content.get(); + } + else { + typename node_map::iterator child; + auto map_end = snode_.node_->children.end(); + auto sym = snode_.syms_.front(); + + if ((child = snode_.node_->children.find(sym)) != map_end) { + auto syms = snode_.syms_; + syms.pop_front(); + nodes_.push_back({child->second.get(), std::move(syms)}); + } + if ((child = snode_.node_->children.find("+")) != map_end) { + auto syms = snode_.syms_; + syms.pop_front(); + nodes_.push_back({child->second.get(), std::move(syms)}); + } + if ((child = snode_.node_->children.find("#")) != map_end) { + pval_ = child->second->content.get(); + } + } + + if (!nodes_.empty()) { + // TODO: List pop_front()? + snode_ = nodes_.back(); + nodes_.pop_back(); + if (!pval_) { + // Recurse + return this->next(); + } + } + else { + snode_ = search_node(); + } + } + + friend class topic_matcher; + + iterator() : pval_(nullptr) {} + iterator(value_type* pval) : pval_(pval) {} + iterator(node* root, const string& topic) : pval_(nullptr) { + auto v = topic::split(topic); + std::forward_list syms(v.begin(), v.end()); + snode_ = search_node(root, std::move(syms)); + next(); + } + + public: + /** + * Gets a reference to the current value. + * @return A reference to the current value. + */ + reference operator*() noexcept { + return *pval_; + } + /** + * Gets a const reference to the current value. + * @return A const reference to the current value. + */ + const_reference operator*() const noexcept { + return *pval_; + } + /** + * Get a pointer to the current value. + * @return A pointer to the current value. + */ + value_type* operator->() noexcept { + return pval_; + } + /** + * Get a const pointer to the current value. + * @return A const pointer to the current value. + */ + const value_type* operator->() const noexcept { + return pval_; + } + /** + * Postfix increment operator. + * @return An iterator pointing to the previous matching item. + */ + iterator operator++(int) noexcept { + auto tmp = *this; + this->next(); + return tmp; + } + /** + * Prefix increment operator. + * @return An iterator pointing to the next matching item. + */ + iterator& operator++() noexcept { + this->next(); + return *this; + } + /** + * Compares two iterators to see if they don't refer to the same + * node. + * + * @param other The other iterator to compare against this one. + * @return @em true if they don't match, @em false if they do + */ + bool operator!=(const iterator& other) const noexcept { + // TODO: Is this sufficient in the long run? + return pval_ != other.pval_ || snode_.node_ != other.snode_.node_; + } + }; + + /** + * A const iterator. + */ + class const_iterator : public iterator { + using base = iterator; + + friend class topic_matcher; + const_iterator(iterator it) : base(it) {} + + public: + /** + * Gets a const reference to the current value. + * @return A const reference to the current value. + */ + const_reference operator*() const noexcept { + return base::operator*(); + } + /** + * Get a const pointer to the current value. + * @return A const pointer to the current value. + */ + const value_type* operator->() const noexcept { + return base::operator->(); + } + }; + + /** + * Creates new, empty collection. + */ + topic_matcher() + : root_(node::create()) {} + /** + * Creates a new collection with a list of key/value pairs. + * + * This can be used to create a connection from a table of entries, as + * key/value pairs, like: + * + * topic_matcher matcher { + * { "#", -1 }, + * { "some/random/topic", 42 }, + * { "some/#", 99 } + * } + * + * @param lst The list of key/value pairs to populate the collection. + */ + topic_matcher(std::initializer_list lst) + : root_(node::create()) { + for (const auto& v : lst) { + insert(v); + } + } + /** + * Inserts a new key/value pair into the collection. + * @param val The value to place in the collection. + */ + void insert(value_type&& val) { + auto nd = root_.get(); + auto fields = topic::split(val.first); + + for (auto& field : fields) { + auto it = nd->children.find(field); + if (it == nd->children.end()) { + nd->children[field] = node::create(); + it = nd->children.find(field); + } + nd = it->second.get(); + } + nd->content = std::make_unique(std::move(val)); + } + /** + * Inserts a new value into the collection. + * @param key The topic/filter entry + * @param val The value to associate with that entry. + */ + void insert(const value_type& val) { + value_type v { val }; + this->insert(std::move(v)); + } + /** + * Gets a pointer to the value at the requested key. + * @param key The topic/filter entry to find. + * @return An iterator to the value if found, @em end() if not found. + */ + iterator find(const key_type& key) { + auto nd = root_.get(); + auto fields = topic::split(key); + + for (auto& field : fields) { + auto it = nd->children.find(field); + if (it == nd->children.end()) + return end(); + + nd = it->second.get(); + } + return iterator{ nd->content.get() }; + } + /** + * Gets a const pointer to the value at the requested key. + * @param key The topic/filter entry to find. + * @return A const pointer to the value if found, @em nullptr if not + * found. + */ + const_iterator find(const key_type& key) const { + return const_cast(this)->find(key); + } + /** + * Gets an iterator that can find the matches to the topic. + * @param topic The topic to search for matches. + * @return An iterator that can find the matches to the topic. + */ + iterator matches(const string& topic) { + return iterator(root_.get(), topic); + } + /** + * Gets a const iterator that can find the matches to the topic. + * @param topic The topic to search for matches. + * @return A const iterator that can find the matches to the topic. + */ + const_iterator matches(const string& topic) const { + return iterator(root_.get(), topic); + } + /** + * Gets an iterator for the end of the collection. + * + * This simply returns an empty/null iterator which we can use to signal + * the end of the collection. + * + * @return An empty/null iterator indicating the end of the collection. + */ + const_iterator end() const noexcept { + return iterator {}; + } + /** + * Gets an iterator for the end of the collection. + * + * This simply returns an empty/null iterator which we can use to signal + * the end of the collection. + * + * @return An empty/null iterator indicating the end of the collection. + */ + const_iterator cend() const noexcept { + return iterator {}; + } +}; + +///////////////////////////////////////////////////////////////////////////// +// end namespace mqtt +} + +#endif // __mqtt_topic_matcher_h + diff --git a/src/topic.cpp b/src/topic.cpp index 07d39fe..1428fab 100644 --- a/src/topic.cpp +++ b/src/topic.cpp @@ -28,24 +28,21 @@ namespace mqtt { // This is just a string split around '/' std::vector topic::split(const string& s) { - auto delim = '/'; std::vector v; if (s.empty()) return v; - using size_type = string::size_type; - const size_type npos = string::npos; - - size_type startPos = 0, pos; + const auto delim = '/'; + string::size_type startPos = 0, pos; do { pos = s.find(delim, startPos); - size_type n = (pos == npos) ? pos : (pos - startPos); + auto n = (pos == string::npos) ? pos : (pos - startPos); v.push_back(s.substr(startPos, n)); startPos = pos + 1; } - while (pos != npos); + while (pos != string::npos); return v; } diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt index 0e896a6..d03f522 100644 --- a/test/unit/CMakeLists.txt +++ b/test/unit/CMakeLists.txt @@ -43,6 +43,7 @@ add_executable(unit_tests unit_tests.cpp test_string_collection.cpp test_token.cpp test_topic.cpp + test_topic_matcher.cpp test_will_options.cpp ) diff --git a/test/unit/test_topic_matcher.cpp b/test/unit/test_topic_matcher.cpp new file mode 100644 index 0000000..037c094 --- /dev/null +++ b/test/unit/test_topic_matcher.cpp @@ -0,0 +1,64 @@ +// test_topic_matcher.cpp +// +// Unit tests for the topic_matcher class in the Paho MQTT C++ library. +// + +/******************************************************************************* + * Copyright (c) 2022 Frank Pagliughi + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + *******************************************************************************/ + +#define UNIT_TESTS + +#include "catch2/catch.hpp" +#include "mqtt/topic_matcher.h" + +#include + +using namespace mqtt; + +///////////////////////////////////////////////////////////////////////////// + +TEST_CASE("insert/get", "[topic_matcher]") +{ + topic_matcher matcher; + + matcher.insert({"some/random/topic", 42}); + + auto it = matcher.find("some/random/topic"); + + REQUIRE(it != matcher.end()); + REQUIRE(it->first == "some/random/topic"); + REQUIRE(it->second == 42); +} + +TEST_CASE("matcher matches", "[topic_matcher]") +{ + topic_matcher matcher { + { "some/random/topic", 42 }, + { "some/#", 99 }, + { "some/other/topic", 55 }, + { "some/+/topic", 33 } + }; + + auto it = matcher.matches("some/random/topic"); + + for ( ; it != matcher.end(); ++it) { + std::cout << "Matcher got: '" << it->first << "' -> " << it->second << std::endl; + bool ok = ( + (it->first == "some/random/topic" && it->second == 42) || + (it->first == "some/#" && it->second == 99) || + (it-> first == "some/+/topic" && it->second == 33) + ); + REQUIRE(ok); + } +}