Prechádzať zdrojové kódy

DK-4 implement http::Client::post

Bence Balint 3 rokov pred
rodič
commit
7f537e04d6

+ 41 - 17
include/kbf/http/common.h

@@ -4,12 +4,11 @@
 #include <string>
 #include <vector>
 #include <map>
+#include <memory>
 
 #include <esp_http_server.h>
+#include <nlohmann_json/json.hpp>
 
-using std::string;
-using std::vector;
-using std::map;
 
 /**
  * @brief HTTP functions
@@ -42,22 +41,29 @@ namespace kbf::http {
          * @note Only the headers listed in acceptedHeaders will be parsed automatically for incoming
          * requests. For parsing custom headers, use readHeader().
          */
-        map<string, string> headers;
+        std::map<std::string, std::string> headers;
 
         /** @brief body */
-        string body;
+        std::string body;
 
         /**
          * @brief POST data
          */
-        map<string, string> postData;
+        std::map<std::string, std::string> postData;
+
+        /**
+         * @brief Parses the body as a JSON object.
+         *
+         * @return json object or nullptr if the body is not a valid JSON object
+         */
+        [[nodiscard]] nlohmann::json json() const;
 
     protected:
         HTTPObject() = default;
 
-        explicit HTTPObject(string body);
+        explicit HTTPObject(std::string body);
 
-        static void parseToMap(map<string, string> &target, const string &buffer);
+        static void parseToMap(std::map<std::string, std::string> &target, const std::string &buffer);
     };
 
     /**
@@ -75,7 +81,7 @@ namespace kbf::http {
         /**
          * @brief Vector of headers automatically parsed for incoming requests.
          */
-        static inline const vector<string> acceptedHeaders = { // NOLINT(cert-err58-cpp)
+        static inline const std::vector<std::string> acceptedHeaders = {
                 "Host",
                 "User-Agent",
                 "Content-Type",
@@ -90,18 +96,18 @@ namespace kbf::http {
          * @warning Can only be used on incoming requests.
          *
          * @param header
-         * @return header string
+         * @return header std::string
          */
-        string readHeader(const string &header);
+        std::string readHeader(const std::string &header);
 
         /** @brief HTTP method */
         Method method;
 
         /** @brief request URI */
-        string uri;
+        std::string uri;
 
         /** @brief query parameters */
-        map<string, string> query{};
+        std::map<std::string, std::string> query{};
 
 
     private:
@@ -118,22 +124,40 @@ namespace kbf::http {
     class Response : public HTTPObject {
     public:
         /**
-         * @brief Default constructor.
+         * @brief Creates a Response from a char array.
+         *
+         * @param body response body; default is ""
+         * @param status HTTP status code; default is 200
+         * @param contentType value of Content-Type header; default is "text/html"
+         */
+        explicit Response(const char *body = "", int status = 200, std::string contentType = "text/html");
+
+        /**
+         * @brief Creates a Response from a string.
          *
          * @param body response body; default is ""
          * @param status HTTP status code; default is 200
          * @param contentType value of Content-Type header; default is "text/html"
          */
-        explicit Response(string body = "", int status = 200, string contentType = "text/html");
+        explicit Response(std::string body, int status = 200, std::string contentType = "text/html");
+
+        /**
+         * @brief Creates a Response from a JSON object.
+         *
+         * @param json response body
+         * @param status HTTP status code; default is 200
+         * @param contentType value of Content-Type header; default is "application/json"
+         */
+        explicit Response(const nlohmann::json &json, int status = 200, std::string contentType = "application/json");
 
         /** @brief HTTP status code */
         int status; // TODO enumerate me
 
         /** @brief HTTP status text */
-        string statusText = "OK";
+        std::string statusText = "OK";
 
         /** @brief value of Content-Type header */
-        string contentType;
+        std::string contentType;
     };
 }
 

+ 29 - 3
src/http/common.cpp

@@ -9,6 +9,11 @@
 #include "kbf/macros.h"
 
 using namespace kbf;
+using std::string;
+using std::map;
+using std::vector;
+using std::shared_ptr;
+using std::make_shared;
 
 void http::HTTPObject::parseToMap(map<string, string> &target, const string &buffer) {
     auto queryString = string(buffer);
@@ -92,10 +97,31 @@ void http::Request::readBody() {
     }
 }
 
-http::Response::Response(string body, int status, string contentType) : HTTPObject(std::move(body)),
-                                                                             status(status),
-                                                                             contentType(std::move(contentType)) {
+http::Response::Response(string body, int status, string contentType) :
+        HTTPObject(std::move(body)),
+        status(status),
+        contentType(std::move(contentType)) {
+}
+
+http::Response::Response(const char *body, int status, string contentType) :
+        HTTPObject(body),
+        status(status),
+        contentType(std::move(contentType)) {
+}
+
+http::Response::Response(const nlohmann::json &json, int status, string contentType) :
+        HTTPObject(json.dump()),
+        status(status),
+        contentType(std::move(contentType)) {
 }
 
 http::HTTPObject::HTTPObject(string body) : body(std::move(body)) {
 }
+
+nlohmann::json http::HTTPObject::json() const {
+    ESP_LOGV(TAG, "parsing JSON: %s", body.c_str());
+    if (!nlohmann::json::accept(body)) {
+        return nullptr;
+    }
+    return nlohmann::json::parse(body);
+}

+ 50 - 10
test/test_http.cpp

@@ -15,7 +15,7 @@ using nlohmann::json;
 
 std::atomic<bool> asyncFinished = {false};
 
-TEST_CASE("HTTP server <--> client / GET, POST, 404, 405", "[kbf_http]") {
+TEST_CASE("HTTP GET, POST, 404, 405", "[kbf_http]") {
     wifi::start();
     auto server = http::Server();
     TEST_ASSERT_FALSE(server.isRunning())
@@ -38,7 +38,8 @@ TEST_CASE("HTTP server <--> client / GET, POST, 404, 405", "[kbf_http]") {
         } else if (request.method == http::POST) {
             TEST_ASSERT_EQUAL_STRING("application/json", request.headers.at("Content-Type").c_str());
             auto requestJson = json::parse(request.body);
-            return http::Response(requestJson["foo"]);
+            string response = requestJson["foo"];
+            return http::Response(response);
         } else {
             TEST_FAIL();
             return http::Response("fail"); // unreachable but the compiler moans otherwise
@@ -75,7 +76,7 @@ TEST_CASE("HTTP server <--> client / GET, POST, 404, 405", "[kbf_http]") {
     wifi::stop();
 }
 
-TEST_CASE("HTTP server <--> client / custom headers", "[kbf_http]") {
+TEST_CASE("HTTP custom headers", "[kbf_http]") {
     wifi::start();
     auto server = http::Server();
 
@@ -86,14 +87,14 @@ TEST_CASE("HTTP server <--> client / custom headers", "[kbf_http]") {
         }
         return response;
     }};
-    server.route({http::GET, "/content-type-test", handleContentTypeTest});
+    server.route({http::GET, "/content-type-test", handleContentTypeTest, nullptr});
 
     http::Response (*handleHeaderTest)(const http::Request &, void *) = {[](const http::Request &request, void *) {
         auto response = http::Response("OK");
         response.headers["X-Secret"] = request.query.at("secret");
         return response;
     }};
-    server.route({http::GET, "/custom-header-test", handleHeaderTest});
+    server.route({http::GET, "/custom-header-test", handleHeaderTest, nullptr});
 
     server.start();
     auto client = http::Client();
@@ -114,7 +115,7 @@ TEST_CASE("HTTP server <--> client / custom headers", "[kbf_http]") {
     wifi::stop();
 }
 
-TEST_CASE("HTTP server <--> client / async", "[kbf_http]") {
+TEST_CASE("HTTP async", "[notimplemented]") {
     wifi::start();
 
     http::Response (*handleRequest)(const http::Request &, void *) = {[](const http::Request &request, void *) {
@@ -123,7 +124,7 @@ TEST_CASE("HTTP server <--> client / async", "[kbf_http]") {
         return response;
     }};
     auto server = http::Server()
-            .route({http::GET, "/", handleRequest})
+            .route({http::GET, "/", handleRequest, nullptr})
             .start();
 
     auto client = http::Client(true);
@@ -133,7 +134,7 @@ TEST_CASE("HTTP server <--> client / async", "[kbf_http]") {
     }};
 
     auto response = client.get("http://localhost/");
-    TEST_ASSERT_NULL(response);
+    TEST_ASSERT_NULL(response)
     TEST_ASSERT_EQUAL(false, asyncFinished);
     kbf::sleep(200);
     TEST_ASSERT_EQUAL(true, asyncFinished);
@@ -142,10 +143,49 @@ TEST_CASE("HTTP server <--> client / async", "[kbf_http]") {
     wifi::stop();
 }
 
-TEST_CASE("HTTP server <--> client / CORS", "[kbf_http]") {
+TEST_CASE("HTTP JSON request / response", "[kbf_http]") {
+    wifi::start();
+
+    static const string testKey   = "key";
+    static const string testValue = "value";
+
+    auto server = http::Server();
+    http::Response (*handleJson)(const http::Request &, void *) = {[](const http::Request &request, void *) {
+        TEST_ASSERT_EQUAL(http::POST, request.method);
+        TEST_ASSERT_EQUAL_STRING("application/json", request.headers.at("Content-Type").c_str());
+        auto requestJson = request.json();
+        TEST_ASSERT_NOT_NULL(requestJson)
+        auto responseJson = json({{testKey, requestJson.find(testKey)->get<string>()}});
+        return http::Response(responseJson);
+    }};
+    http::Response (*handleNotJson)(const http::Request &, void *) = {[](const http::Request &request, void *) {
+        auto requestJson = request.json();
+        TEST_ASSERT_NULL(requestJson)
+        return http::Response("OK");
+    }};
+    server.route({http::POST, "/json", handleJson, nullptr});
+    server.route({http::POST, "/not-json", handleNotJson, nullptr});
+    server.start();
+
+    auto client = http::Client();
+    auto response = client.post("http://localhost/json", {{testKey, testValue}});
+    TEST_ASSERT_EQUAL_STRING("application/json", response->headers.at("Content-Type").c_str());
+    auto responseJson = response->json();
+    TEST_ASSERT_NOT_NULL(responseJson)
+    TEST_ASSERT_EQUAL_STRING(testValue.c_str(), responseJson.find(testKey)->get<string>().c_str());
+
+    // TODO enable after implementing support for posting data types other than JSON
+//    response = client.post("http://localhost/not-json", "");
+//    TEST_ASSERT_NULL(response->json());
+
+    server.stop();
+    wifi::stop();
+}
+
+TEST_CASE("HTTP CORS", "[notimplemented]") {
     TEST_FAIL_MESSAGE("not yet implemented");
 }
 
-TEST_CASE("HTTP server <--> client / SPIFFS static route", "[kbf_http]") {
+TEST_CASE("HTTP SPIFFS static route", "[notimplemented]") {
     TEST_FAIL_MESSAGE("not yet implemented");
 }

+ 8 - 0
test/test_json.cpp

@@ -1,7 +1,11 @@
+#include <string>
+
 #include <nlohmann_json/json.hpp>
 
 #include <unity.h>
 
+using std::string;
+
 TEST_CASE("nlohmann JSON", "[lib][json]") {
     nlohmann::json json;
     json["foo"]               = 123;
@@ -28,4 +32,8 @@ TEST_CASE("nlohmann JSON", "[lib][json]") {
             "{\"answer\":{\"everything\":42},\"happy\":true,\"list\":[1,0,2],\"name\":\"Niels\",\"nothing\":null,\"object\":{\"currency\":\"USD\",\"value\":42.99},\"pi\":3.141}",
             noway.dump().c_str()
     );
+
+    string str = R"({"key":"value"})";
+    auto parsed = nlohmann::json::parse(str);
+    TEST_ASSERT_EQUAL_STRING("value", parsed.find("key")->get<string>().c_str());
 }

+ 1 - 0
test_app/.idea/misc.xml

@@ -4,6 +4,7 @@
   <component name="CidrRootsConfiguration">
     <libraryRoots>
       <file path="$USER_HOME$/esp/esp-idf" />
+      <file path="$PROJECT_DIR$/../lib" />
     </libraryRoots>
   </component>
 </project>

+ 1 - 1
test_app/main/main_test.c

@@ -16,7 +16,7 @@ void app_main(void) {
     UNITY_END();
 
     TaskHandle_t handle;
-    xTaskCreate(menu_task, "unity_menu", 4096, NULL, 5, &handle);
+    xTaskCreate(menu_task, "unity_menu", 8192, NULL, 5, &handle);
 }
 
 static void menu_task(void *arg) {