diff --git a/docs/guides/blueprints.md b/docs/guides/blueprints.md new file mode 100644 index 000000000..2a0637611 --- /dev/null +++ b/docs/guides/blueprints.md @@ -0,0 +1,43 @@ +!!!Warning + + This feature is currently only available on the "master" branch. + +Crow supports flask style blueprints.
+A blueprint is a limited app. It cannot handle networking. But it can handle routes.
+Blueprints allow developers to compartmentalize their Crow applications, making them a lot more modular.

+ +In order for a blueprint to work, it has to be registered with a Crow app before the app is run. This can be done using `#!cpp app.register_blueprint(blueprint);`.

+ +Blueprints let you do the following:

+ +### Define Routes +You can define routes in a blueprint, similarly to how `#!cpp CROW_ROUTE(app, "/xyz")` works, you can use `#!cpp CROW_BP_ROUTE(blueprint, "/xyz")` to define a blueprint route. + +### Define a Prefix +Blueprints can have a prefix assigned to them. This can be done when creating a new blueprint as in `#!cpp crow::blueprint bp("prefix");`. This prefix will be applied to all routes belonging to the blueprint. truning a route such as `/crow/rocks` into `/prefix/crow/rocks`. + +!!!Warning + + Unlike routes, blueprint prefixes should contain no slashes. + + +### Use a custom Static directory +Blueprints let you define a custom static directory (relative to your working directory). This can be done by initializing a blueprint as `#!cpp crow::blueprint bp("prefix", "custom_static");`. This does not have an effect on `#!cpp set_static_file_info()`, it's only for when you want direct access to a file. + +!!!note + + Currently changing which endpoint the blueprint uses isn't possible, so whatever you've set in `CROW_STATIC_ENDPOINT` (default is "static") will be used. Making your final route `/prefix/static/filename`. + + +### Use a custom Templates directory +Similar to static directories, You can set a custom templates directory (relative to your working directory). To do this you initialize the blueprint as `#!cpp crow::blueprint bp("prefix", "custom_static", "custom_templates");`. Any routes defined for the blueprint will use that directory when calling `#!cpp crow::mustache::load("filename.html")`. + +!!!note + + If you want to define a custom templates directory without defining a custom static directory, you can pass the static directory as an empty string. Making your constructor `#!cpp crow::blueprint bp("prefix", "", "custom_templates");`. + +### Define a custom Catchall route +You can define a custom catchall route for a blueprint by calling `#!cpp CROW_BP_CATCHALL_ROUTE(blueprint)`. This causes any requests with a URL starting with `/prefix` and no route found to call the blueprint's catchall route. if no catchall route is defined, Crow will default to either the parent blueprint or the app's catchall route. + +### Register other Blueprints +Blueprints can also register other blueprints. This is done through `#!cpp blueprint.register_blueprint(blueprint_2);`. The child blueprint's routes become `/prefix/prefix_2/abc/xyz`. diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index ded51c771..4841e37ff 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -77,6 +77,10 @@ else () target_compile_options(example_json_map PRIVATE "${compiler_options}") target_link_libraries(example_json_map PUBLIC ${REQUIRED_LIBRARIES}) + add_executable(example_blueprint example_blueprint.cpp) + target_compile_options(example_blueprint PRIVATE "${compiler_options}") + target_link_libraries(example_blueprint PUBLIC ${REQUIRED_LIBRARIES}) + add_custom_command(OUTPUT example_chat.html COMMAND ${CMAKE_COMMAND} -E copy ${PROJECT_SOURCE_DIR}/example_chat.html ${CMAKE_CURRENT_BINARY_DIR}/example_chat.html diff --git a/examples/example_blueprint.cpp b/examples/example_blueprint.cpp new file mode 100644 index 000000000..af007bd09 --- /dev/null +++ b/examples/example_blueprint.cpp @@ -0,0 +1,35 @@ +#define CROW_MAIN +#include "crow.h" + +int main() +{ + crow::SimpleApp app; + + crow::Blueprint bp("bp_prefix", "cstat", "ctemplate"); + + + crow::Blueprint sub_bp("bp2", "csstat", "cstemplate"); + + CROW_BP_ROUTE(sub_bp, "/") + ([]() { + return "Hello world!"; + }); + +/* CROW_BP_ROUTE(bp, "/templatt") + ([]() { + crow::mustache::context ctxdat; + ctxdat["messg"] = "fifty five!!"; + + auto page = crow::mustache::load("indks.html"); + + return page.render(ctxdat); + }); +*/ + CROW_BP_CATCHALL_ROUTE(sub_bp)([](){return "WRONG!!";}); + + + bp.register_blueprint(sub_bp); + app.register_blueprint(bp); + + app.loglevel(crow::LogLevel::DEBUG).port(18080).run(); +} diff --git a/include/crow/app.h b/include/crow/app.h index e0f8b14d4..2491ebc6a 100644 --- a/include/crow/app.h +++ b/include/crow/app.h @@ -25,10 +25,13 @@ #ifdef CROW_MSVC_WORKAROUND #define CROW_ROUTE(app, url) app.route_dynamic(url) +#define CROW_BP_ROUTE(blueprint, url) blueprint.new_rule_dynamic(url) #else #define CROW_ROUTE(app, url) app.route(url) +#define CROW_BP_ROUTE(blueprint, url) blueprint.new_rule_tagged(url) #endif #define CROW_CATCHALL_ROUTE(app) app.catchall_route() +#define CROW_BP_CATCHALL_ROUTE(blueprint) blueprint.catchall_rule() namespace crow { @@ -159,12 +162,18 @@ namespace crow /// crow::LogLevel::Warning (2)
/// crow::LogLevel::Error (3)
/// crow::LogLevel::Critical (4)
- self_t& loglevel(crow::LogLevel level) + self_t& loglevel(LogLevel level) { crow::logger::setLogLevel(level); return *this; } + self_t& register_blueprint(Blueprint& blueprint) + { + router_.register_blueprint(blueprint); + return *this; + } + ///Set a custom duration and function to run on every tick template self_t& tick(Duration d, Func f) { @@ -192,7 +201,34 @@ namespace crow ///Go through the rules, upgrade them if possible, and add them to the list of rules void validate() { - router_.validate(); + if (!validated_) + { + +#ifndef CROW_DISABLE_STATIC_DIR + route(CROW_STATIC_ENDPOINT) + ([](crow::response& res, std::string file_path_partial) + { + res.set_static_file_info(CROW_STATIC_DIRECTORY + file_path_partial); + res.end(); + }); + + for (auto& bp : router_.blueprints()) + { + if (!bp->static_dir().empty()) + { + bp->new_rule_tagged(CROW_STATIC_ENDPOINT) + ([bp](crow::response& res, std::string file_path_partial) + { + res.set_static_file_info(bp->static_dir() + '/' + file_path_partial); + res.end(); + }); + } + } +#endif + + router_.validate(); + validated_ = true; + } } ///Notify anything using `wait_for_server_start()` to proceed @@ -206,14 +242,7 @@ namespace crow ///Run the server void run() { -#ifndef CROW_DISABLE_STATIC_DIR - route(CROW_STATIC_ENDPOINT) - ([](crow::response& res, std::string file_path_partial) - { - res.set_static_file_info(CROW_STATIC_DIRECTORY + file_path_partial); - res.end(); - }); -#endif + validate(); #ifdef CROW_ENABLE_SSL @@ -361,6 +390,7 @@ namespace crow private: uint16_t port_ = 80; uint16_t concurrency_ = 1; + bool validated_ = false; std::string server_name_ = std::string("Crow/") + VERSION; std::string bindaddr_ = "0.0.0.0"; Router router_; diff --git a/include/crow/common.h b/include/crow/common.h index f38a3c767..2aa95a388 100644 --- a/include/crow/common.h +++ b/include/crow/common.h @@ -8,7 +8,7 @@ namespace crow { - enum class HTTPMethod + enum class HTTPMethod : char { #ifndef DELETE DELETE = 0, @@ -69,7 +69,7 @@ namespace crow return "invalid"; } - enum class ParamType + enum class ParamType : char { INT, UINT, diff --git a/include/crow/routing.h b/include/crow/routing.h index 6de9f90c2..64e1de09a 100644 --- a/include/crow/routing.h +++ b/include/crow/routing.h @@ -14,9 +14,16 @@ #include "crow/utility.h" #include "crow/logging.h" #include "crow/websocket.h" +#include "crow/mustache.h" namespace crow { + +#ifdef CROW_MAIN + uint16_t INVALID_BP_ID{0xFFFF}; +#else + extern uint16_t INVALID_BP_ID{0xFFFF}; +#endif /// A base class for all rules. /// Used to provide a common interface for code dealing with different types of rules. @@ -70,6 +77,9 @@ namespace crow } } + + std::string custom_templates_base; + const std::string& rule() { return rule_; } protected: @@ -81,6 +91,7 @@ namespace crow std::unique_ptr rule_to_upgrade_; friend class Router; + friend class Blueprint; template friend struct RuleParameterTraits; }; @@ -499,6 +510,10 @@ namespace crow void handle(const request& req, response& res, const routing_params& params) override { + if (!custom_templates_base.empty()) + mustache::set_base(custom_templates_base); + else if (mustache::detail::get_template_base_directory_ref() != "templates") + mustache::set_base("templates"); erased_handler_(req, res, params); } @@ -674,6 +689,11 @@ namespace crow void handle(const request& req, response& res, const routing_params& params) override { + if (!custom_templates_base.empty()) + mustache::set_base(custom_templates_base); + else if (mustache::detail::get_template_base_directory_ref() != "templates") + mustache::set_base("templates"); + detail::routing_handler_call_helper::call< detail::routing_handler_call_helper::call_params< decltype(handler_)>, @@ -694,13 +714,16 @@ namespace crow const int RULE_SPECIAL_REDIRECT_SLASH = 1; + /// A search tree. class Trie { public: struct Node { - unsigned rule_index{}; + uint16_t rule_index{}; + // Assign the index to the maximum 32 unsigned integer value by default so that any other number (specifically 0) is a valid BP id. + uint16_t blueprint_index{INVALID_BP_ID}; std::string key; ParamType param = ParamType::MAX; // MAX = No param. std::vector children; @@ -709,6 +732,7 @@ namespace crow { return !rule_index && + blueprint_index == INVALID_BP_ID && children.size() < 2 && param == ParamType::MAX && std::all_of(std::begin(children), std::end(children), [](Node* x){ return x->param == ParamType::MAX; }); @@ -736,6 +760,8 @@ namespace crow private: + + void optimizeNode(Node* node) { if (node->children.empty()) @@ -745,6 +771,7 @@ namespace crow Node* child_temp = node->children[0]; node->key = node->key + child_temp->key; node->rule_index = child_temp->rule_index; + node->blueprint_index = child_temp->blueprint_index; node->children = std::move(child_temp->children); delete(child_temp); optimizeNode(node); @@ -808,33 +835,44 @@ namespace crow optimize(); } - std::pair find(const std::string& req_url, const Node* node = nullptr, unsigned pos = 0, routing_params* params = nullptr) const + //Rule_index, Blueprint_index, routing_params + std::tuple, routing_params> find(const std::string& req_url, const Node* node = nullptr, unsigned pos = 0, routing_params* params = nullptr, std::vector* blueprints = nullptr) const { //start params as an empty struct routing_params empty; if (params == nullptr) params = ∅ + //same for blueprint vector + std::vector MT; + if (blueprints == nullptr) + blueprints = &MT; - unsigned found{}; //The rule index to be found + uint16_t found{}; //The rule index to be found + std::vector found_BP; //The Blueprint indices to be found routing_params match_params; //supposedly the final matched parameters //start from the head node if (node == nullptr) node = &head_; - //if the function was called on a node at the end of the string (the last recursion), return the nodes rule index, and whatever params were passed to the function - if (pos == req_url.size()) - return {node->rule_index, *params}; - - auto update_found = [&found, &match_params](std::pair& ret) + auto update_found = [&found, &found_BP, &match_params](std::tuple, routing_params>& ret) { - if (ret.first && (!found || found > ret.first)) + found_BP = std::move(std::get<1>(ret)); + if (std::get<0>(ret) && (!found || found > std::get<0>(ret))) { - found = ret.first; - match_params = std::move(ret.second); + found = std::get<0>(ret); + match_params = std::move(std::get<2>(ret)); } }; + //if the function was called on a node at the end of the string (the last recursion), return the nodes rule index, and whatever params were passed to the function + if (pos == req_url.size()) + { + found_BP = std::move(*blueprints); + return {node->rule_index, *blueprints, *params}; + } + + bool found_fragment = false; for(auto& child : node->children) { @@ -850,10 +888,13 @@ namespace crow long long int value = strtoll(req_url.data()+pos, &eptr, 10); if (errno != ERANGE && eptr != req_url.data()+pos) { + found_fragment = true; params->int_params.push_back(value); - auto ret = find(req_url, child, eptr - req_url.data(), params); + if (child->blueprint_index != INVALID_BP_ID) blueprints->push_back(child->blueprint_index); + auto ret = find(req_url, child, eptr - req_url.data(), params, blueprints); update_found(ret); params->int_params.pop_back(); + blueprints->pop_back(); } } } @@ -868,10 +909,13 @@ namespace crow unsigned long long int value = strtoull(req_url.data()+pos, &eptr, 10); if (errno != ERANGE && eptr != req_url.data()+pos) { + found_fragment = true; params->uint_params.push_back(value); - auto ret = find(req_url, child, eptr - req_url.data(), params); + if (child->blueprint_index != INVALID_BP_ID) blueprints->push_back(child->blueprint_index); + auto ret = find(req_url, child, eptr - req_url.data(), params, blueprints); update_found(ret); params->uint_params.pop_back(); + blueprints->pop_back(); } } } @@ -886,10 +930,13 @@ namespace crow double value = strtod(req_url.data()+pos, &eptr); if (errno != ERANGE && eptr != req_url.data()+pos) { + found_fragment = true; params->double_params.push_back(value); - auto ret = find(req_url, child, eptr - req_url.data(), params); + if (child->blueprint_index != INVALID_BP_ID) blueprints->push_back(child->blueprint_index); + auto ret = find(req_url, child, eptr - req_url.data(), params, blueprints); update_found(ret); params->double_params.pop_back(); + blueprints->pop_back(); } } } @@ -905,10 +952,13 @@ namespace crow if (epos != pos) { + found_fragment = true; params->string_params.push_back(req_url.substr(pos, epos-pos)); - auto ret = find(req_url, child, epos, params); + if (child->blueprint_index != INVALID_BP_ID) blueprints->push_back(child->blueprint_index); + auto ret = find(req_url, child, epos, params, blueprints); update_found(ret); params->string_params.pop_back(); + blueprints->pop_back(); } } @@ -918,10 +968,13 @@ namespace crow if (epos != pos) { + found_fragment = true; params->string_params.push_back(req_url.substr(pos, epos-pos)); - auto ret = find(req_url, child, epos, params); + if (child->blueprint_index != INVALID_BP_ID) blueprints->push_back(child->blueprint_index); + auto ret = find(req_url, child, epos, params, blueprints); update_found(ret); params->string_params.pop_back(); + blueprints->pop_back(); } } } @@ -931,18 +984,28 @@ namespace crow const std::string& fragment = child->key; if (req_url.compare(pos, fragment.size(), fragment) == 0) { - auto ret = find(req_url, child, pos + fragment.size(), params); + found_fragment = true; + if (child->blueprint_index != INVALID_BP_ID) blueprints->push_back(child->blueprint_index); + auto ret = find(req_url, child, pos + fragment.size(), params, blueprints); update_found(ret); + blueprints->pop_back(); } } } - return {found, match_params}; //Called after all the recursions have been done + + if (!found_fragment) + found_BP = std::move(*blueprints); + + return {found, found_BP, match_params}; //Called after all the recursions have been done } - void add(const std::string& url, unsigned rule_index) + //This functions assumes any blueprint info passed is valid + void add(const std::string& url, uint16_t rule_index, unsigned bp_prefix_length = 0, uint16_t blueprint_index = INVALID_BP_ID) { Node* idx = &head_; + bool has_blueprint = bp_prefix_length != 0 && blueprint_index != INVALID_BP_ID; + for(unsigned i = 0; i < url.size(); i ++) { char c = url[i]; @@ -1008,6 +1071,9 @@ namespace crow { auto new_node_idx = new_node(idx); new_node_idx->key = c; + //The assumption here is that you'd only need to add a blueprint index if the tree didn't have the BP prefix. + if (has_blueprint && i == bp_prefix_length) + new_node_idx->blueprint_index = blueprint_index; idx = new_node_idx; } } @@ -1026,7 +1092,7 @@ namespace crow size_t get_size(Node* node) { - unsigned size = 8; //rule_index and param + unsigned size = 5 ; //rule_index, blueprint_index, and param size += (node->key.size()); //each character in the key is 1 byte for (auto child: node->children) { @@ -1046,9 +1112,146 @@ namespace crow return children[children.size()-1]; } + Node head_; }; + /// A blueprint can be considered a smaller section of a Crow app, specifically where the router is conecerned. + /// + /// You can use blueprints to assign a common prefix to rules' prefix, set custom static and template folders, and set a custom catchall route. + /// You can also assign nest blueprints for maximum Compartmentalization. + class Blueprint + { + public: + + Blueprint(const std::string& prefix): + prefix_(prefix){}; + + Blueprint(const std::string& prefix, const std::string& static_dir): + prefix_(prefix), static_dir_(static_dir){}; + + Blueprint(const std::string& prefix, const std::string& static_dir, const std::string& templates_dir): + prefix_(prefix), static_dir_(static_dir), templates_dir_(templates_dir){}; + +/* + Blueprint(Blueprint& other) + { + prefix_ = std::move(other.prefix_); + all_rules_ = std::move(other.all_rules_); + } + + Blueprint(const Blueprint& other) + { + prefix_ = other.prefix_; + all_rules_ = other.all_rules_; + } +*/ + Blueprint(Blueprint&& value) + { + *this = std::move(value); + } + + Blueprint& operator = (const Blueprint& value) = delete; + + Blueprint& operator = (Blueprint&& value) noexcept + { + prefix_ = std::move(value.prefix_); + all_rules_ = std::move(value.all_rules_); + catchall_rule_ = std::move(value.catchall_rule_); + return *this; + } + + bool operator == (const Blueprint& value) + { + return value.prefix() == prefix_; + } + + bool operator != (const Blueprint& value) + { + return value.prefix() != prefix_; + } + + std::string prefix() const + { + return prefix_; + } + + std::string static_dir() const + { + return static_dir_; + } + + DynamicRule& new_rule_dynamic(std::string&& rule) + { + std::string new_rule = std::move(rule); + new_rule = '/' + prefix_ + new_rule; + auto ruleObject = new DynamicRule(new_rule); + ruleObject->custom_templates_base = templates_dir_; + all_rules_.emplace_back(ruleObject); + + return *ruleObject; + } + + template + typename black_magic::arguments::type::template rebind& new_rule_tagged(std::string&& rule) + { + std::string new_rule = std::move(rule); + new_rule = '/' + prefix_ + new_rule; + using RuleT = typename black_magic::arguments::type::template rebind; + + auto ruleObject = new RuleT(new_rule); + ruleObject->custom_templates_base = templates_dir_; + all_rules_.emplace_back(ruleObject); + + return *ruleObject; + } + + void register_blueprint(Blueprint& blueprint) + { + if (blueprints_.empty() || std::find(blueprints_.begin(), blueprints_.end(), &blueprint) == blueprints_.end()) + { + apply_blueprint(blueprint); + blueprints_.emplace_back(&blueprint); + } + else + throw std::runtime_error("blueprint \"" + blueprint.prefix_ + "\" already exists in blueprint \"" + prefix_ + '\"'); + } + + + CatchallRule& catchall_rule() + { + return catchall_rule_; + } + + private: + + void apply_blueprint(Blueprint& blueprint) + { + + blueprint.prefix_ = prefix_ + '/' + blueprint.prefix_; + blueprint.static_dir_ = static_dir_ + '/' + blueprint.static_dir_; + blueprint.templates_dir_ = templates_dir_ + '/' + blueprint.templates_dir_; + for (auto& rule : blueprint.all_rules_) + { + std::string new_rule = '/' + prefix_ + rule->rule_; + rule->rule_ = new_rule; + } + for (Blueprint* bp_child : blueprint.blueprints_) + { + Blueprint& bp_ref = *bp_child; + apply_blueprint(bp_ref); + } + } + + std::string prefix_; + std::string static_dir_; + std::string templates_dir_; + std::vector> all_rules_; + CatchallRule catchall_rule_; + std::vector blueprints_; + + friend class Router; + }; /// Handles matching requests to existing rules and upgrade requests. class Router @@ -1082,7 +1285,7 @@ namespace crow return catchall_rule_; } - void internal_add_rule_object(const std::string& rule, BaseRule* ruleObject) + void internal_add_rule_object(const std::string& rule, BaseRule* ruleObject, const uint16_t& BP_index, std::vector& blueprints) { bool has_trailing_slash = false; std::string rule_without_trailing_slash; @@ -1096,20 +1299,85 @@ namespace crow ruleObject->foreach_method([&](int method) { per_methods_[method].rules.emplace_back(ruleObject); - per_methods_[method].trie.add(rule, per_methods_[method].rules.size() - 1); + per_methods_[method].trie.add(rule, per_methods_[method].rules.size() - 1, BP_index != INVALID_BP_ID ? blueprints[BP_index]->prefix().length() : 0, BP_index); // directory case: // request to '/about' url matches '/about/' rule if (has_trailing_slash) { - per_methods_[method].trie.add(rule_without_trailing_slash, RULE_SPECIAL_REDIRECT_SLASH); + per_methods_[method].trie.add(rule_without_trailing_slash, RULE_SPECIAL_REDIRECT_SLASH, BP_index != INVALID_BP_ID ? blueprints_[BP_index]->prefix().length() : 0, BP_index); } }); } + void register_blueprint(Blueprint& blueprint) + { + if (std::find(blueprints_.begin(), blueprints_.end(), &blueprint) == blueprints_.end()) + { + blueprints_.emplace_back(&blueprint); + } + else + throw std::runtime_error("blueprint \"" + blueprint.prefix_ + "\" already exists in router"); + } + + void get_recursive_child_methods(Blueprint* blueprint, std::vector& methods) + { + //we only need to deal with children if the blueprint has absolutely no methods (meaning its index won't be added to the trie) + if (blueprint->static_dir_.empty() && blueprint->all_rules_.empty()) + { + for(Blueprint* bp : blueprint->blueprints_) + { + get_recursive_child_methods(bp, methods); + } + } + else if (!blueprint->static_dir_.empty()) + methods.emplace_back(HTTPMethod::GET); + for (auto& rule: blueprint->all_rules_) + { + rule->foreach_method([&methods](unsigned method){ + HTTPMethod method_final = static_cast(method); + if (std::find(methods.begin(), methods.end(), method_final) == methods.end()) + methods.emplace_back(method_final); + }); + } + } + + void validate_bp(std::vector blueprints) + { + for (unsigned i = 0; i < blueprints.size(); i++) + { + Blueprint* blueprint = blueprints[i]; + if (blueprint->static_dir_ == "" && blueprint->all_rules_.empty()) + { + std::vector methods; + get_recursive_child_methods(blueprint, methods); + for (HTTPMethod x : methods) + { + int i = static_cast(x); + per_methods_[i].trie.add(blueprint->prefix(), 0, blueprint->prefix().length(), i); + } + } + for (auto& rule: blueprint->all_rules_) + { + if (rule) + { + auto upgraded = rule->upgrade(); + if (upgraded) + rule = std::move(upgraded); + rule->validate(); + internal_add_rule_object(rule->rule(), rule.get(), i, blueprints); + } + } + validate_bp(blueprint->blueprints_); + } + } + void validate() { + //Take all the routes from the registered blueprints and add them to `all_rules_` to be processed. + validate_bp(blueprints_); + for(auto& rule:all_rules_) { if (rule) @@ -1118,7 +1386,7 @@ namespace crow if (upgraded) rule = std::move(upgraded); rule->validate(); - internal_add_rule_object(rule->rule(), rule.get()); + internal_add_rule_object(rule->rule(), rule.get(), INVALID_BP_ID, blueprints_); } } for(auto& per_method:per_methods_) @@ -1136,13 +1404,13 @@ namespace crow auto& per_method = per_methods_[static_cast(req.method)]; auto& rules = per_method.rules; - unsigned rule_index = per_method.trie.find(req.url).first; + unsigned rule_index = std::get<0>(per_method.trie.find(req.url)); if (!rule_index) { for (auto& per_method: per_methods_) { - if (per_method.trie.find(req.url).first) + if (std::get<0>(per_method.trie.find(req.url))) { CROW_LOG_DEBUG << "Cannot match method " << req.url << " " << method_name(req.method); res = response(405); @@ -1201,6 +1469,51 @@ namespace crow } } + void get_found_bp(std::vector& bp_i, std::vector& blueprints, std::vector& found_bps, uint16_t index = 0) + { + // This statement makes 3 assertions: + // 1. The index is above 0. + // 2. The index does not lie outside the given blueprint list. + // 3. The next blueprint we're adding has a prefix that starts the same as the already added blueprint + a slash (the rest is irrelevant). + // + // This is done to prevent a blueprint that has a prefix of "bp_prefix2" to be assumed as a child of one that has "bp_prefix". + // + // If any of the assertions is untrue, we delete the last item added, and continue using the blueprint list of the blueprint found before, the topmost being the router's list + auto verify_prefix = [&bp_i, &index, &blueprints, &found_bps]() + { + return index > 0 && + bp_i[index] < blueprints.size() && + blueprints[bp_i[index]]->prefix().substr(0,found_bps[index-1]->prefix().length()+1) + .compare(std::string(found_bps[index-1]->prefix()+'/')) == 0; + }; + if (index < bp_i.size()) + { + + if (verify_prefix()) + { + found_bps.push_back(blueprints[bp_i[index]]); + get_found_bp(bp_i, found_bps.back()->blueprints_, found_bps, ++index); + } + else + { + if (!found_bps.empty()) + found_bps.pop_back(); + + if (found_bps.empty()) + { + found_bps.push_back(blueprints_[bp_i[index]]); + get_found_bp(bp_i, found_bps.back()->blueprints_, found_bps, ++index); + } + else + { + Blueprint* last_element = found_bps.back(); + found_bps.push_back(last_element->blueprints_[bp_i[index]]); + get_found_bp(bp_i, found_bps.back()->blueprints_, found_bps, ++index); + } + } + } + } + void handle(const request& req, response& res) { HTTPMethod method_actual = req.method; @@ -1235,7 +1548,7 @@ namespace crow { for(int i = 0; i < static_cast(HTTPMethod::InternalMethodCount); i ++) { - if (per_methods_[i].trie.find(req.url).first) + if (std::get<0>(per_methods_[i].trie.find(req.url))) { allow += method_name(static_cast(i)) + ", "; } @@ -1265,13 +1578,13 @@ namespace crow auto found = trie.find(req.url); - unsigned rule_index = found.first; + unsigned rule_index = std::get<0>(found); if (!rule_index) { for (auto& per_method: per_methods_) { - if (per_method.trie.find(req.url).first) + if (std::get<0>(per_method.trie.find(req.url))) { CROW_LOG_DEBUG << "Cannot match method " << req.url << " " << method_name(method_actual); res = response(405); @@ -1280,15 +1593,32 @@ namespace crow } } - if (catchall_rule_.has_handler()) + std::vector bps_found; + get_found_bp(std::get<1>(found), blueprints_, bps_found); + bool no_bp_catchall = true; + for (int i = bps_found.size()-1; i > 0; i--) { - CROW_LOG_DEBUG << "Cannot match rules " << req.url << ". Redirecting to Catchall rule"; - catchall_rule_.handler_(req, res); + std::vector bpi = std::get<1>(found); + if (bps_found[i]->catchall_rule().has_handler()) + { + no_bp_catchall = false; + CROW_LOG_DEBUG << "Cannot match rules " << req.url << ". Redirecting to Blueprint \"" << bps_found[i]->prefix() << "\" Catchall rule"; + bps_found[i]->catchall_rule().handler_(req, res); + break; + } } - else + if (no_bp_catchall) { - CROW_LOG_DEBUG << "Cannot match rules " << req.url; - res = response(404); + if (catchall_rule_.has_handler()) + { + CROW_LOG_DEBUG << "Cannot match rules " << req.url << ". Redirecting to global Catchall rule"; + catchall_rule_.handler_(req, res); + } + else + { + CROW_LOG_DEBUG << "Cannot match rules " << req.url; + res = response(404); + } } res.end(); return; @@ -1320,7 +1650,7 @@ namespace crow // any uncaught exceptions become 500s try { - rules[rule_index]->handle(req, res, found.second); + rules[rule_index]->handle(req, res, std::get<2>(found)); } catch(std::exception& e) { @@ -1347,6 +1677,11 @@ namespace crow } } + std::vector& blueprints() + { + return blueprints_; + } + private: CatchallRule catchall_rule_; @@ -1360,6 +1695,7 @@ namespace crow }; std::array(HTTPMethod::InternalMethodCount)> per_methods_; std::vector> all_rules_; + std::vector blueprints_; }; } diff --git a/mkdocs.yml b/mkdocs.yml index d3507a839..adac457e4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,9 +1,9 @@ site_name: Crow # Repository -repo_name: crowcpp/crow -repo_url: https://github.com/crowcpp/crow -site_url: https://crowcpp.github.io/crow +repo_name: CrowCpp/Crow +repo_url: https://github.com/CrowCpp/Crow +site_url: https://crowcpp.org edit_uri: "" theme: @@ -41,6 +41,7 @@ nav: - Middleware: guides/middleware.md - SSL: guides/ssl.md - Static Files: guides/static.md + - Blueprints: guides/blueprints.md - Compression: guides/compression.md - Websockets: guides/websockets.md - Writing Tests: guides/testing.md diff --git a/tests/img/cat.jpg b/tests/img/cat.jpg index 2cd578ec1..b5fcec3bd 100644 Binary files a/tests/img/cat.jpg and b/tests/img/cat.jpg differ diff --git a/tests/unittest.cpp b/tests/unittest.cpp index 70d9fab58..22d2e038d 100644 --- a/tests/unittest.cpp +++ b/tests/unittest.cpp @@ -1418,14 +1418,15 @@ TEST_CASE("stream_response") SimpleApp app; + + std::string keyword_ = "hello"; + std::string key_response; + for (unsigned int i = 0; i<250000; i++) + key_response += keyword_; + CROW_ROUTE(app, "/test") - ([](const crow::request&, crow::response& res) + ([&key_response](const crow::request&, crow::response& res) { - std::string keyword_ = "hello"; - std::string key_response; - for (unsigned int i = 0; i<250000; i++) - key_response += keyword_; - res.body = key_response; res.end(); }); @@ -1433,7 +1434,7 @@ TEST_CASE("stream_response") app.validate(); //running the test on a separate thread to allow the client to sleep - std::thread runTest([&app](){ + std::thread runTest([&app, &key_response](){ auto _ = async(launch::async, [&] { app.bindaddr(LOCALHOST_ADDRESS).port(45451).run(); }); @@ -1447,9 +1448,6 @@ TEST_CASE("stream_response") { asio::streambuf b; - std::string keyword_ = "hello"; - std::string key_response; - asio::ip::tcp::socket c(is); c.connect(asio::ip::tcp::endpoint( asio::ip::address::from_string(LOCALHOST_ADDRESS), 45451)); @@ -1459,10 +1457,6 @@ TEST_CASE("stream_response") static char buf[2048]; c.receive(asio::buffer(buf, 2048)); - //creating the string to compare against - for (unsigned int i = 0; i<250000; i++) - key_response += keyword_; - //"hello" is 5 bytes, (5*250000)/16384 = 76.2939 for (unsigned int i = 0; i<76; i++) { @@ -1887,3 +1881,115 @@ TEST_CASE("catchall") CHECK(404 == res.code); } } + +TEST_CASE("blueprint") +{ + SimpleApp app; + crow::Blueprint bp("bp_prefix", "cstat", "ctemplate"); + crow::Blueprint bp_not_sub("bp_prefix_second"); + crow::Blueprint sub_bp("bp2", "csstat", "cstemplate"); + crow::Blueprint sub_sub_bp("bp3"); + + CROW_BP_ROUTE(sub_bp, "/hello") + ([]() { + return "Hello world!"; + }); + + CROW_BP_ROUTE(bp_not_sub, "/hello") + ([]() { + return "Hello world!"; + }); + + CROW_BP_ROUTE(sub_sub_bp, "/hi") + ([]() { + return "Hi world!"; + }); + + CROW_BP_CATCHALL_ROUTE(sub_bp)([](){return response(200, "WRONG!!");}); + + app. register_blueprint(bp); + app. register_blueprint(bp_not_sub); + bp. register_blueprint(sub_bp); + sub_bp.register_blueprint(sub_sub_bp); + + app.validate(); + + { + request req; + response res; + + req.url = "/bp_prefix/bp2/hello"; + + app.handle(req, res); + + CHECK("Hello world!" == res.body); + } + + { + request req; + response res; + + req.url = "/bp_prefix_second/hello"; + + app.handle(req, res); + + CHECK("Hello world!" == res.body); + } + + { + request req; + response res; + + req.url = "/bp_prefix/bp2/bp3/hi"; + + app.handle(req, res); + + CHECK("Hi world!" == res.body); + } + + { + request req; + response res; + + req.url = "/bp_prefix/nonexistent"; + + app.handle(req, res); + + CHECK(404 == res.code); + } + + { + request req; + response res; + + req.url = "/bp_prefix_second/nonexistent"; + + app.handle(req, res); + + CHECK(404 == res.code); + } + + { + request req; + response res; + + req.url = "/bp_prefix/bp2/nonexistent"; + + app.handle(req, res); + + CHECK(200 == res.code); + CHECK("WRONG!!" == res.body); + } + + { + request req; + response res; + + req.url = "/bp_prefix/bp2/bp3/nonexistent"; + + app.handle(req, res); + + CHECK(200 == res.code); + CHECK("WRONG!!" == res.body); + } +}