diff --git a/simple_http.hpp b/simple_http.hpp index 86268b9..ec89b1b 100644 --- a/simple_http.hpp +++ b/simple_http.hpp @@ -91,7 +91,6 @@ using ErrorCallback = std::function; inline static ErrorCallback NoopErrorCallback = [](auto&){}; using Headers = std::unordered_map; -using PathSegments = std::vector; using QueryParameters = std::vector>; inline static const std::string WHITESPACE = "\n\t\f\v\r "; @@ -154,6 +153,33 @@ struct HttpResponse final { HttpResponseBody body; }; +struct PathSegments final { + PathSegments() = default; + PathSegments(PathSegments& path_segments) : value_(path_segments.value_) { } + PathSegments(PathSegments&& path_segments) : value_(path_segments.value_) { } + explicit PathSegments(std::vector value) : value_(std::move(value)) { } + PathSegments& operator=(const PathSegments& other) { + this->value_ = other.value_; + return *this; + } + + const std::vector& value() const { + return value_; + } + + std::string to_string() const { + std::stringstream ss; + + for (const auto &segment : value_) { + ss << "/" << segment; + } + + return ss.str(); + } +private: + std::vector value_; +}; + struct HttpUrl final { HttpUrl() = default; @@ -174,16 +200,13 @@ struct HttpUrl final { { } [[nodiscard]] - const Protcol & protocol() { + const Protcol & protocol() const { return protocol_; } [[nodiscard]] - std::string value() { - if (value_.empty()) { - value_ = to_string(); - } - return value_; + std::string value() const { + return value_.empty() ? to_string() : value_; } [[nodiscard]] @@ -210,6 +233,11 @@ struct HttpUrl final { return *this; } + [[nodiscard]] + const PathSegments& path_segments() const { + return path_segments_; + } + private: Protcol protocol_; Host host_; @@ -227,14 +255,9 @@ struct HttpUrl final { } [[nodiscard]] - std::string to_string() { + std::string to_string() const { std::stringstream ss; - ss << protocol_ << "://" << host_; - if (!path_segments_.empty()) { - for (const auto& segment : path_segments_) { - ss << "/" << segment; - } - } + ss << protocol_ << "://" << host_ << path_segments_.to_string(); if (!query_parameters_.empty()) { ss << "?" << query_parameters_[0].first << "=" << query_parameters_[0].second; @@ -476,7 +499,7 @@ struct Client final { } [[nodiscard]] - std::optional execute(HttpUrl url, + std::optional execute(const HttpUrl& url, const CurlHeaderCallback &curl_header_callback, const CurlSetupCallback &curl_setup_callback, const Predicate &successPredicate) { diff --git a/test/integration_tests.cpp b/test/integration_tests.cpp index df19c70..c4390bc 100644 --- a/test/integration_tests.cpp +++ b/test/integration_tests.cpp @@ -13,7 +13,7 @@ TEST_CASE("Integration Tests") SECTION("GET Request") { - auto maybe_response = client.get(url.with_path_segments({SimpleHttp::PathSegment{"get"}})); + auto maybe_response = client.get(url.with_path_segments(SimpleHttp::PathSegments{{SimpleHttp::PathSegment{"get"}}})); REQUIRE(maybe_response); @@ -25,7 +25,7 @@ TEST_CASE("Integration Tests") SECTION("Post request") { - auto maybe_response = client.post(url.with_path_segments({SimpleHttp::PathSegment{"post"}}), + auto maybe_response = client.post(url.with_path_segments(SimpleHttp::PathSegments{{SimpleHttp::PathSegment{"post"}}}), SimpleHttp::HttpRequestBody{R"({"name":"test"})"}, {{"Content-Type", "application/json"}}); @@ -39,7 +39,7 @@ TEST_CASE("Integration Tests") SECTION("Put request") { - auto maybe_response = client.put(url.with_path_segments({SimpleHttp::PathSegment{"put"}}), + auto maybe_response = client.put(url.with_path_segments(SimpleHttp::PathSegments{{SimpleHttp::PathSegment{"put"}}}), SimpleHttp::HttpRequestBody{R"({"update":"test"})"}, {{"Content-Type", "application/json"}}); @@ -53,7 +53,7 @@ TEST_CASE("Integration Tests") SECTION("Delete request") { - auto maybe_response = client.del(url.with_path_segments({SimpleHttp::PathSegment{"delete"}})); + auto maybe_response = client.del(url.with_path_segments(SimpleHttp::PathSegments{{SimpleHttp::PathSegment{"delete"}}})); REQUIRE(maybe_response); CHECK(maybe_response.value().status == SimpleHttp::OK); @@ -61,7 +61,7 @@ TEST_CASE("Integration Tests") SECTION("Head request") { - auto maybe_response = client.head(url.with_path_segments({SimpleHttp::PathSegment{"get"}})); + auto maybe_response = client.head(url.with_path_segments(SimpleHttp::PathSegments{{SimpleHttp::PathSegment{"get"}}})); REQUIRE(maybe_response); CHECK(maybe_response.value().status == SimpleHttp::OK); @@ -69,7 +69,7 @@ TEST_CASE("Integration Tests") SECTION("Options request") { - auto maybe_response = client.options(url.with_path_segments({SimpleHttp::PathSegment{"get"}})); + auto maybe_response = client.options(url.with_path_segments(SimpleHttp::PathSegments{{SimpleHttp::PathSegment{"get"}}})); REQUIRE(maybe_response); @@ -80,7 +80,7 @@ TEST_CASE("Integration Tests") SECTION("Trace request") { - auto maybe_response = client.trace(url.with_path_segments({SimpleHttp::PathSegment{"trace"}})); + auto maybe_response = client.trace(url.with_path_segments(SimpleHttp::PathSegments{{SimpleHttp::PathSegment{"trace"}}})); REQUIRE(maybe_response); CHECK(maybe_response.value().headers.value().at("Content-Type") == "message/http"); @@ -98,7 +98,7 @@ TEST_CASE("Integration Tests") SECTION("Post request that expects a 204 NO_CONTENT response") { - auto maybe_response = client.post(url.with_path_segments({SimpleHttp::PathSegment{"empty_post_response"}}), + auto maybe_response = client.post(url.with_path_segments(SimpleHttp::PathSegments{{SimpleHttp::PathSegment{"empty_post_response"}}}), SimpleHttp::HttpRequestBody{""}, SimpleHttp::eq(SimpleHttp::NO_CONTENT), {{"Content-Type", "application/json"}}); @@ -113,7 +113,7 @@ TEST_CASE("Integration Tests") SECTION("Get request that expects a 405 METHOD_NOT_ALLOWED response") { - auto maybe_response = client.get(url.with_path_segments({SimpleHttp::PathSegment{"empty_post_response"}}), + auto maybe_response = client.get(url.with_path_segments(SimpleHttp::PathSegments{{SimpleHttp::PathSegment{"empty_post_response"}}}), SimpleHttp::eq(SimpleHttp::METHOD_NOT_ALLOWED)); REQUIRE(maybe_response); @@ -122,7 +122,7 @@ TEST_CASE("Integration Tests") SECTION("Ranged response success predicate") { - auto maybe_response = client.get(url.with_path_segments({SimpleHttp::PathSegment{"get"}}), + auto maybe_response = client.get(url.with_path_segments(SimpleHttp::PathSegments{{SimpleHttp::PathSegment{"get"}}}), SimpleHttp::successful()); REQUIRE(maybe_response); @@ -134,7 +134,7 @@ TEST_CASE("Integration Tests") SECTION("Wrap Response") { - auto maybe_response = client.get(url.with_path_segments({SimpleHttp::PathSegment{"get"}}), + auto maybe_response = client.get(url.with_path_segments(SimpleHttp::PathSegments{{SimpleHttp::PathSegment{"get"}}}), SimpleHttp::successful()); REQUIRE(maybe_response); diff --git a/test/unit_tests.cpp b/test/unit_tests.cpp index 403a44b..728fe96 100644 --- a/test/unit_tests.cpp +++ b/test/unit_tests.cpp @@ -104,3 +104,69 @@ TEST_CASE("Trimming") { CHECK(SimpleHttp::trim(candidate) == "the \n thing \r works \v in \r\n ways"); } } + +TEST_CASE("PathSegments") { + SECTION("Path segments to_string no segments") + { + CHECK(SimpleHttp::PathSegments{}.to_string().empty()); + } + + SECTION("Path segments to_string one segment") + { + SimpleHttp::PathSegments segments{{SimpleHttp::PathSegment{"one"}}}; + CHECK(segments.to_string() == "/one"); + } + + SECTION("Path segments to_string multiple segment") + { + SimpleHttp::PathSegments segments{{ + SimpleHttp::PathSegment{"one"}, + SimpleHttp::PathSegment{"two"} + }}; + + CHECK(segments.to_string() == "/one/two"); + } +} + +TEST_CASE("HttpUrl") { + SECTION("path_segments parsed constructor, no segments") + { + SimpleHttp::HttpUrl url{"https://example.com"}; + CHECK(url.path_segments().value().empty()); + CHECK(url.path_segments().to_string().empty()); + } + + SECTION("path_segments parsed constructor, one segment") + { + SimpleHttp::HttpUrl url{"https://example.com/one"}; + // This exposes the lack of complete parsing when constructed with a string value + CHECK(url.path_segments().value().empty()); + CHECK(url.path_segments().to_string().empty()); + } + + SECTION("path_segments parsed constructor, one segment") + { + SimpleHttp::HttpUrl url{"https://example.com/one/two"}; + // This exposes the lack of complete parsing when constructed with a string value + CHECK(url.path_segments().value().empty()); + CHECK(url.path_segments().to_string().empty()); + } + + SECTION("Fully constructed, multiple segments") + { + SimpleHttp::HttpUrl url = SimpleHttp::HttpUrl() + .with_protocol(SimpleHttp::Protcol{"https"}) + .with_host(SimpleHttp::Host{"example.com"}) + .with_path_segments(SimpleHttp::PathSegments{{ + SimpleHttp::PathSegment{"one"}, + SimpleHttp::PathSegment{"two"} + }}); + + CHECK(url.path_segments().to_string() == "/one/two"); + } + + SECTION("value, when constructed with a string") + { + CHECK(SimpleHttp::HttpUrl{"http://example.com"}.value() == "http://example.com"); + } +} \ No newline at end of file