Line data Source code
1 : #include "route.hpp"
2 : #include "../response.hpp"
3 : #include <regex>
4 : #include "../../../util/logger.hpp"
5 :
6 : namespace thinger::http {
7 :
8 6925 : route::route(const std::string& pattern)
9 6925 : : pattern_(pattern)
10 : {
11 : // Convert route pattern to regex
12 : // Support two syntaxes:
13 : // 1. :param_name - matches any non-slash characters
14 : // 2. :param_name(regex) - matches the specified regex pattern
15 :
16 6925 : std::string regex_pattern = pattern;
17 :
18 : // First, handle parameters with custom regex: :param(regex)
19 6925 : std::regex custom_param_regex(":([a-zA-Z_][a-zA-Z0-9_]*)\\(([^)]+)\\)");
20 6925 : std::smatch match;
21 6925 : std::string temp = pattern;
22 :
23 : // Find all :param(regex) patterns
24 6981 : while (std::regex_search(temp, match, custom_param_regex)) {
25 56 : parameters_.push_back(match[1]);
26 56 : temp = match.suffix();
27 : }
28 :
29 : // Then, handle simple parameters: :param
30 6925 : std::regex simple_param_regex(":([a-zA-Z_][a-zA-Z0-9_]*)(?![\\(])");
31 6925 : temp = pattern;
32 8461 : while (std::regex_search(temp, match, simple_param_regex)) {
33 : // Only add if not already added (avoid duplicates with custom regex params)
34 1536 : std::string param_name = match[1];
35 1536 : if (std::find(parameters_.begin(), parameters_.end(), param_name) == parameters_.end()) {
36 1536 : parameters_.push_back(param_name);
37 : }
38 1536 : temp = match.suffix();
39 1536 : }
40 :
41 : // Escape special regex characters in the pattern (but not in our parameter patterns)
42 6925 : std::string escaped = pattern;
43 :
44 : // First, temporarily replace our parameter patterns to protect them
45 6925 : escaped = std::regex_replace(escaped, custom_param_regex, "__CUSTOM_PARAM_$1__");
46 6925 : escaped = std::regex_replace(escaped, simple_param_regex, "__SIMPLE_PARAM_$1__");
47 :
48 : // Escape special characters
49 6925 : escaped = std::regex_replace(escaped, std::regex("([.^$*+?{}\\[\\]\\\\|])"), "\\\\$1");
50 :
51 : // Now replace parameters with their regex groups
52 : // Custom parameters: restore the custom regex
53 6925 : temp = pattern;
54 6925 : std::string result = escaped;
55 6981 : while (std::regex_search(temp, match, custom_param_regex)) {
56 56 : std::string param_name = match[1];
57 56 : std::string param_regex = match[2];
58 56 : std::string placeholder = "__CUSTOM_PARAM_" + param_name + "__";
59 56 : result = std::regex_replace(result, std::regex(placeholder), "(" + param_regex + ")");
60 56 : temp = match.suffix();
61 56 : }
62 :
63 : // Simple parameters: use default regex
64 6925 : result = std::regex_replace(result, std::regex("__SIMPLE_PARAM_([a-zA-Z_][a-zA-Z0-9_]*)__"), "([^/]+)");
65 :
66 : // Add anchors
67 6925 : regex_pattern = "^" + result + "$";
68 :
69 6925 : regex_ = std::regex(regex_pattern);
70 6925 : }
71 :
72 354 : route& route::operator=(route_callback_response_only callback) {
73 354 : callback_ = std::move(callback);
74 354 : return *this;
75 : }
76 :
77 344 : route& route::operator=(route_callback_json_response callback) {
78 344 : callback_ = std::move(callback);
79 344 : return *this;
80 : }
81 :
82 6086 : route& route::operator=(route_callback_request_response callback) {
83 6086 : callback_ = std::move(callback);
84 6086 : return *this;
85 : }
86 :
87 79 : route& route::operator=(route_callback_request_json_response callback) {
88 79 : callback_ = std::move(callback);
89 79 : return *this;
90 : }
91 :
92 20 : route& route::operator=(route_callback_awaitable callback) {
93 20 : callback_ = std::move(callback);
94 20 : deferred_body_ = true; // auto-enable deferred body for awaitable callbacks
95 20 : return *this;
96 : }
97 :
98 3 : route& route::deferred_body(bool enabled) {
99 3 : deferred_body_ = enabled;
100 3 : return *this;
101 : }
102 :
103 3 : route& route::auth(auth_level level) {
104 3 : auth_level_ = level;
105 3 : return *this;
106 : }
107 :
108 3 : route& route::description(const std::string& desc) {
109 3 : description_ = desc;
110 3 : return *this;
111 : }
112 :
113 2748 : bool route::matches(const std::string& path, std::smatch& matches) const {
114 2748 : return std::regex_match(path, matches, regex_);
115 : }
116 :
117 1019 : void route::handle_request(request& req, response& res) const {
118 : // Handle response-only callback
119 1019 : if (std::holds_alternative<route_callback_response_only>(callback_)) {
120 132 : std::get<route_callback_response_only>(callback_)(res);
121 : }
122 : // Handle JSON + response callback (json is parsed from request body)
123 887 : else if (std::holds_alternative<route_callback_json_response>(callback_)) {
124 61 : auto http_req = req.get_http_request();
125 61 : if (!http_req->get_body().empty()) {
126 53 : auto json = nlohmann::json::parse(http_req->get_body(), nullptr, false);
127 53 : if (json.is_discarded()) {
128 6 : res.error(http_response::status::bad_request, "Invalid JSON");
129 : } else {
130 : #ifdef THINGER_HTTP_VALIJSON_ENABLED
131 50 : if (!validate_json(json, res)) return;
132 : #endif
133 29 : std::get<route_callback_json_response>(callback_)(json, res);
134 : }
135 53 : } else {
136 8 : nlohmann::json empty_json;
137 : #ifdef THINGER_HTTP_VALIJSON_ENABLED
138 8 : if (!validate_json(empty_json, res)) return;
139 : #endif
140 5 : std::get<route_callback_json_response>(callback_)(empty_json, res);
141 8 : }
142 61 : }
143 : // Handle request + response callback
144 826 : else if (std::holds_alternative<route_callback_request_response>(callback_)) {
145 799 : std::get<route_callback_request_response>(callback_)(req, res);
146 : }
147 : // Handle request + JSON + response callback (json is parsed from request body)
148 27 : else if (std::holds_alternative<route_callback_request_json_response>(callback_)) {
149 24 : auto http_req = req.get_http_request();
150 24 : if (!http_req->get_body().empty()) {
151 18 : auto json = nlohmann::json::parse(http_req->get_body(), nullptr, false);
152 18 : if (json.is_discarded()) {
153 12 : res.error(http_response::status::bad_request, "Invalid JSON");
154 : } else {
155 : #ifdef THINGER_HTTP_VALIJSON_ENABLED
156 12 : if (!validate_json(json, res)) return;
157 : #endif
158 9 : std::get<route_callback_request_json_response>(callback_)(req, json, res);
159 : }
160 18 : } else {
161 6 : nlohmann::json empty_json;
162 : #ifdef THINGER_HTTP_VALIJSON_ENABLED
163 6 : if (!validate_json(empty_json, res)) return;
164 : #endif
165 6 : std::get<route_callback_request_json_response>(callback_)(req, empty_json, res);
166 6 : }
167 24 : }
168 : // Awaitable callback — cannot be called synchronously
169 3 : else if (std::holds_alternative<route_callback_awaitable>(callback_)) {
170 6 : res.error(http_response::status::internal_server_error,
171 : "Awaitable route handler invoked synchronously; use handle_request_coro() instead");
172 : }
173 : }
174 :
175 10 : thinger::awaitable<void> route::handle_request_coro(request& req, response& res) const {
176 : if (std::holds_alternative<route_callback_awaitable>(callback_)) {
177 : co_await std::get<route_callback_awaitable>(callback_)(req, res);
178 : } else {
179 : handle_request(req, res);
180 : }
181 20 : }
182 :
183 0 : void route::parse_parameters() {
184 : // Parameters are now parsed in the constructor
185 0 : }
186 :
187 324 : route& route::schema(const nlohmann::json& json_schema) {
188 : #ifdef THINGER_HTTP_VALIJSON_ENABLED
189 324 : json_schema_ = json_schema;
190 324 : schema_ = std::make_shared<valijson::Schema>();
191 324 : valijson::SchemaParser parser;
192 324 : valijson::adapters::NlohmannJsonAdapter adapter(json_schema_);
193 : try {
194 324 : parser.populateSchema(adapter, *schema_);
195 0 : } catch (const std::exception& e) {
196 0 : LOG_ERROR("Failed to parse JSON Schema: {}", e.what());
197 0 : schema_.reset();
198 0 : }
199 : #else
200 : LOG_WARNING("JSON Schema validation requested but Valijson is not enabled");
201 : #endif
202 324 : return *this;
203 324 : }
204 :
205 : #ifdef THINGER_HTTP_VALIJSON_ENABLED
206 76 : bool route::validate_json(const nlohmann::json& json, response& res) const {
207 76 : if (!schema_) return true;
208 :
209 48 : valijson::Validator validator;
210 48 : valijson::adapters::NlohmannJsonAdapter adapter(json);
211 48 : valijson::ValidationResults results;
212 :
213 48 : if (validator.validate(*schema_, adapter, &results)) {
214 21 : return true;
215 : }
216 :
217 : // Build error response with first validation error
218 27 : valijson::ValidationResults::Error error;
219 243 : nlohmann::json error_response = {{"error", {{"message", "Schema validation failed"}}}};
220 27 : if (results.popError(error)) {
221 27 : error_response["error"]["message"] = error.description;
222 27 : nlohmann::json context = nlohmann::json::array();
223 72 : for (const auto& c : error.context) {
224 45 : context.push_back(c);
225 : }
226 27 : if (!context.empty()) {
227 27 : error_response["error"]["context"] = std::move(context);
228 : }
229 27 : }
230 :
231 27 : res.json(error_response, http_response::status::bad_request);
232 27 : return false;
233 291 : }
234 : #endif
235 :
236 : } // namespace thinger::http
|