magpie
Loading...
Searching...
No Matches
RadixTree.hpp
1#pragma once
2
3#include "magpie/application/Methods.hpp"
4#include "magpie/routing/Compile.hpp"
5
6#include <variant>
7#include <vector>
8#include <memory>
9#include <string_view>
10
11
12namespace magpie::dsa {
13
14enum class MatchMode {
15 NoMatch,
16 SingleSegment,
17 // Currently unused, reserved for future {path} placeholder
18 CatchAll
19};
20
21template <typename Value>
22struct Node {
23 std::unordered_map<Method::HttpMethod, Value> value;
24
25 int weight;
26
27 std::vector<std::shared_ptr<Node<Value>>> childNodes;
28
29 virtual ~Node() = default;
30
31 virtual MatchMode match(const std::string_view& segment) = 0;
32 constexpr virtual bool operator==(const std::string_view& pathSegment) = 0;
33};
34
35template <typename Value>
36struct RootNode : public Node<Value> {
37 // this is unused, but needs to be overridden so we can exploit Node::otherNodes as standardised storage. Right now,
38 // it's just a vector, but if a supportive
39 MatchMode match(const std::string_view&) override { return MatchMode::SingleSegment; }
40 constexpr virtual bool operator==(const std::string_view&) override { return true; }
41};
42
43template <typename Value>
44struct StandardNode : public Node<Value> {
45 std::string segment;
46 StandardNode(const std::string& segment) : segment(segment) {
47 this->weight = 69000;
48 }
49
50 MatchMode match(const std::string_view& segment) override {
51 return segment == this->segment ? MatchMode::SingleSegment : MatchMode::NoMatch;
52 }
53 constexpr virtual bool operator==(const std::string_view& pathSegment) override {
54 return pathSegment == this->segment;
55 }
56};
57
58template <typename Value, typename NodeType>
59struct TemplateNode : public Node<Value> {
60 TemplateNode() {
61 if constexpr (std::is_same_v<NodeType, std::string_view>) {
62 this->weight = 1000;
63 } else if constexpr (std::is_same_v<NodeType, int64_t>) {
64 this->weight = 2000;
65 } else {
66 static_assert(false, "Invalid type");
67 }
68 }
69 virtual MatchMode match(const std::string_view& segment) override {
70 // Notes:
71 // - We do not care about integer/double overflow here. This is a specific kind of error that we don't want to
72 // handle while matching routes
73 // - No major parsing should be done here; if complex parsing is required to determine if a segment matches, it
74 // probably isn't a placeholder we want to support
75 if constexpr (std::is_same_v<NodeType, std::string_view>) {
76 return MatchMode::SingleSegment;
77 } else if constexpr (std::is_same_v<NodeType, int64_t>) {
78 for (size_t i = 0; i < segment.size(); ++i) {
79 auto c = segment.at(i);
80 if ((c < '0' || c > '9') && c != '+' && c != '-') {
81 return MatchMode::NoMatch;
82 }
83 }
84 return MatchMode::SingleSegment;
85 } else {
86 static_assert(false, "Invalid type");
87 }
88 }
89
90 constexpr virtual bool operator==(const std::string_view& pathSegment) override {
91 if constexpr (std::is_same_v<NodeType, std::string_view>) {
92 return pathSegment.starts_with("{string}");
93 } else if constexpr (std::is_same_v<NodeType, int64_t>) {
94 return pathSegment.starts_with("{int}");
95 } else {
96 static_assert(false, "Invalid type");
97 }
98 }
99};
100
101enum class FindError {
106 NoMatch,
111 IllegalMethod
112};
113
114// This isn't a radix tree right now, but that's the plan, and that's good enough for now
115template <typename Value>
116class RadixTree {
117private:
118 std::shared_ptr<RootNode<Value>> root;
119
120public:
121 RadixTree(): root(std::make_shared<RootNode<Value>>()) {
122
123 }
124
131 std::variant<FindError, Value> getRoute(const std::vector<std::string_view>& route, Method::HttpMethod method) const {
132 std::shared_ptr<Node<Value>> node = root;
133
134 for (auto& segment : route) {
135 bool hasMatch = false;
136 for (auto& childNode : node->childNodes) {
137 auto matchType = childNode->match(segment);
138 if (matchType == MatchMode::SingleSegment) {
139 node = childNode;
140 hasMatch = true;
141 break;
142 } else if (matchType == MatchMode::CatchAll) {
143 throw std::runtime_error("Not implemented");
144 }
145 }
146
147 if (!hasMatch) {
148 return FindError::NoMatch;
149 }
150 }
151 // Necessary to avoid incorrect IllegalMethod in sub-routes
152 // Given a server with the route /some/path, / is technically also defined with `value.size() == 0`, as it isn't
153 // a valid route.
154 // This check correctly handles those cases.
155 if (node->value.size() == 0) {
156 return FindError::NoMatch;
157 }
158 auto it = node->value.find(method);
159 if (it == node->value.end()) {
160 return FindError::IllegalMethod;
161 }
162 return it->second;
163 }
164
165 constexpr void pushRoute(
166 const Value& value,
167 Method::HttpMethod method,
168 const std::vector<std::string_view>& route
169 ) {
170 std::shared_ptr<Node<Value>> node = root;
171
172 for (auto& segment : route) {
173 bool hasRoute = false;
174 // This search is shit, but this code is complex and I want somewhere to start
175 //
176 // This checks if we have an existing node that matches the segment
177 for (auto& childNode : node->childNodes) {
178 if (*childNode == segment) {
179 node = childNode;
180 hasRoute = true;
181 break;
182 }
183 }
184
185 // If we don't have an existing node, we push a new one. This is necessary for all the intermediates to be
186 // populated.
187 if (!hasRoute) {
188 std::shared_ptr<Node<Value>> newNode;
189
190 // TODO: this could be constexpr, but that means adding a FixedString or ConstString split method too
191 if (segment.starts_with("{string}")) {
192 newNode = std::make_shared<TemplateNode<Value, routing::TypeInfo<"{string}">::type>>();
193 } else if (segment.starts_with("{int}")) {
194 newNode = std::make_shared<TemplateNode<Value, routing::TypeInfo<"{int}">::type>>();
195 } else {
196 if (segment.find('{') != std::string_view::npos) {
197 [[unlikely]]
198 throw std::runtime_error(
199 std::string(segment) + " is an invalid template that wasn't discarded. This should never happen"
200 );
201 }
202
203 newNode = std::make_shared<StandardNode<Value>>(std::string(segment));
204 }
205
206 node->childNodes.push_back(newNode);
207
208 std::sort(
209 node->childNodes.begin(),
210 node->childNodes.end(),
211 [](const auto& a, const auto& b) {
212 return a->weight > b->weight;
213 }
214 );
215
216 node = newNode;
217 }
218 }
219 // It would be nice if this could be caught at compile time, but not sure how many more constexpr data objects
220 // that would require
221 if (node->value.contains(method)) {
222 [[unlikely]]
223 throw std::runtime_error("Illegal multiple use of the same route with the same method");
224 }
225 node->value[method] = value;
226 }
227};
228
229}
std::variant< FindError, Value > getRoute(const std::vector< std::string_view > &route, Method::HttpMethod method) const
Definition RadixTree.hpp:131
Definition RadixTree.hpp:22
Definition RadixTree.hpp:36