diff --git a/docs/api/result.md b/docs/api/result.md index bda3e58..f500cfc 100644 --- a/docs/api/result.md +++ b/docs/api/result.md @@ -34,6 +34,9 @@ The public contract should support semantics equivalent to: template class [[nodiscard]] Result { public: + static Result success(T value); + static Result failure(Error error); + bool has_value() const noexcept; explicit operator bool() const noexcept; @@ -46,7 +49,28 @@ public: }; ``` -Construction/factory details may differ in implementation, but the success/failure semantics are frozen. +Construction details may differ in implementation, but the success/failure semantics are frozen. + +For normal `T != Error` usage, convenient direct construction from a success value or from `Error` may be provided. The explicit factories remain available when the desired state should be unambiguous. + +## Explicit Factories + +`success()` and `failure()` provide an unambiguous way to construct either state. + +```cpp +auto ok = Result::success(42); +auto failed = Result::failure(Error{ErrorCode::ReadTimeout}); +``` + +This is especially important for `Result`, where the success payload type and failure type are otherwise identical. + +For `Result`: + +- `Result::success(error_value)` creates a successful result whose value is an `Error` object, +- `Result::failure(error_value)` creates a failed result, +- direct construction from `Error` is treated as the failure form. + +This keeps both states representable without introducing `ErrorCode::None` or another sentinel value. ## `[[nodiscard]]` @@ -83,7 +107,7 @@ The v1 contract intentionally does not require `value()` to throw when called in Calling the wrong-state accessor is a programmer error and has a documented precondition. -This avoids imposing an exception-based access model on a library whose normal error handling is explicitly non-exception-based. +Implementations must not rely on a hidden `std::bad_variant_access` path while simultaneously declaring the accessor `noexcept`. A violated accessor precondition is outside normal runtime error handling and must not be modeled as a network/protocol failure. Recommended usage: @@ -115,6 +139,8 @@ Therefore: - moving a result transfers its active state according to `T` / `Error` move semantics, - copying is available only when the contained type permits it. +Move-only payloads must remain usable as successful result values. + ## Storage Representation The exact physical storage is intentionally not part of the public API contract. @@ -129,6 +155,8 @@ The implementation must be benchmarked/inspected before choosing a more complex C++17 makes `std::variant` a valid baseline candidate and avoids unnecessary custom lifetime machinery during initial development. +If the physical alternatives have identical types, state inspection and access must use the result discriminator rather than type-based lookup. + ## Allocation Policy `Result` itself must not require a separate heap allocation solely to store its success/error discriminator. @@ -147,7 +175,7 @@ Result operation(); The specialization must preserve the same state-inspection and `error()` semantics. -Exact implementation is deferred until a concrete internal/public operation needs it. +Exact implementation remains deferred until a concrete internal/public operation needs it. The primary template may reject `void` explicitly in the meantime. ## No `ErrorCode::None` Requirement diff --git a/docs/roadmap.md b/docs/roadmap.md index 83a8b48..f87b9ac 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -4,14 +4,14 @@ - Project: `cpp_request` - Target release: MVP v1.0 -- Roadmap status: Active planning baseline +- Roadmap status: Active implementation baseline - Language baseline: C++17 - Protocol scope: synchronous HTTP/1.1 over plaintext TCP - Last roadmap review: 2026-09-15 This document is the implementation-order source of truth for the v1 release. -The frozen functional and non-functional requirements remain authoritative for **what** v1 must provide. This roadmap defines **when** those requirements are expected to be implemented and which milestones must be complete before v1.0 can be released. +The frozen functional and non-functional requirements remain authoritative for **what** v1 must provide. This roadmap defines **when** those requirements are implemented and which milestone must be complete before v1.0 can ship. Version labels below are development milestones. A milestone does not require a public release tag unless the project explicitly decides to publish one. @@ -19,10 +19,10 @@ Version labels below are development milestones. A milestone does not require a ## Status Legend -- โœ… **Complete** โ€” merged into `main` and covered by the expected tests for that milestone. +- โœ… **Complete** โ€” merged into `main` and covered by the expected tests. - ๐Ÿšง **In progress** โ€” implementation exists in an open PR or active branch. - โณ **Planned** โ€” required for v1 but implementation has not started. -- ๐Ÿงช **Release gate** โ€” verification/hardening work required before v1.0. +- ๐Ÿงช **Release gate** โ€” verification/hardening required before v1.0. - โžก๏ธ **Post-v1** โ€” intentionally outside the frozen v1 scope. --- @@ -35,10 +35,10 @@ Version labels below are development milestones. A milestone does not require a | v0.2 | URL, platform sockets, DNS, TCP transport | โœ… Complete | | v0.3 | HTTP request data model and serialization | โœ… Complete | | v0.4 | HTTP response parsing and body framing | โœ… Complete | -| v0.5 | Public `Client` end-to-end execution path | ๐Ÿšง In progress โ€” PR #15 | -| v0.6 | HTTP/1.1 connection reuse / keep-alive | โณ Planned | -| v0.7 | Redirect handling | โณ Planned | -| v0.8 | Protocol/API correctness hardening | โณ Planned | +| v0.5 | Public `Client` end-to-end execution path | โœ… Complete | +| v0.6 | HTTP/1.1 connection reuse / keep-alive | โœ… Complete | +| v0.7 | Redirect handling | โœ… Complete | +| v0.8 | Protocol/API correctness hardening | ๐Ÿšง In progress | | v0.9 | Packaging, benchmarks, examples, documentation | โณ Planned | | v1.0 | Release hardening and acceptance gate | ๐Ÿงช Planned | @@ -52,12 +52,7 @@ Completed work: - v1 functional requirements frozen. - v1 non-functional requirements frozen. -- architecture layers defined: - - Public API - - HTTP - - Transport - - Platform - - OS +- architecture layers defined: Public API, HTTP, Transport, Platform, OS. - public API baseline defined. - lifetime and ownership contracts defined. - structured `Error` taxonomy defined. @@ -67,7 +62,7 @@ Completed work: - HTTPS/TLS explicitly excluded from v1. - synchronous-only v1 execution model frozen. -Historical implementation references: +Historical references: - PR #2 โ€” frozen v1 requirements - PR #3 โ€” architecture boundaries @@ -91,14 +86,13 @@ Completed work: - OS-backed DNS resolution. - timed TCP connection establishment. - endpoint fallback. -- timed `write_all()`. -- timed `read_some()`. +- timed `write_all()` and `read_some()`. - partial I/O handling. - connection-close/error normalization. - SIGPIPE-safe POSIX write behavior. - Windows/Linux/macOS CI coverage. -Historical implementation references: +Historical references: - PR #6 โ€” URL parser - PR #7 โ€” native socket RAII @@ -116,7 +110,7 @@ Goal: represent and serialize all v1 request methods without unnecessary payload Completed work: -- `Headers` owning ordered storage. +- owning ordered `Headers` storage. - case-insensitive header lookup. - duplicate request-header support. - `Request` with borrowed URL/body views. @@ -124,13 +118,12 @@ Completed work: - HTTP/1.1 request-line serialization. - automatic `Host` generation. - IPv6 host formatting. -- automatic `Content-Length` generation. -- caller-provided `Content-Length` validation. +- automatic and caller-validated `Content-Length`. - outgoing header syntax validation. - CR/LF request-injection rejection. - request-body zero-copy serialization boundary. -Historical implementation references: +Historical references: - PR #11 โ€” `Headers` and `Request` - PR #12 โ€” request serializer @@ -152,15 +145,14 @@ Completed work: - `Content-Length` body framing. - close-delimited body framing. - chunked transfer decoding. -- chunk extensions. -- trailer consumption/validation. +- chunk extensions and trailer consumption/validation. - premature EOF detection. - conflicting framing rejection. - `Connection: close` detection. - protocol-upgrade non-reuse semantics. - preservation of bytes belonging to a following response. -Historical implementation references: +Historical references: - PR #13 โ€” incremental response parser - PR #14 โ€” chunked transfer decoder @@ -169,178 +161,128 @@ Exit criteria: **complete**. --- -# v0.5 โ€” Public Client Core ๐Ÿšง +# v0.5 โ€” Public Client Core โœ… Goal: connect all merged layers into one usable synchronous HTTP request path. -Current implementation target: +Completed work: 1. serialize `Request`, 2. resolve host, 3. connect TCP, -4. write request head, -5. write borrowed request body, -6. read response bytes, -7. feed incremental parser, -8. return owning `Response`. +4. write request head and borrowed body, +5. read response bytes, +6. feed incremental parser, +7. return owning `Response`. -Required public API for this milestone: +Public API completed: - `Client::request(const Request&)` -- `Client::get()` -- `Client::head()` -- `Client::post()` -- `Client::put()` -- `Client::patch()` -- `Client::del()` +- `Client::get()` / `head()` / `post()` / `put()` / `patch()` / `del()` - equivalent one-shot free helpers - connect/read/write timeout setters -Verification target: - -- controlled loopback GET. -- POST body transmission. -- custom headers. -- Content-Length response. -- chunked response. -- close-delimited response. -- malformed response propagation. -- HTTPS rejection before networking. - -Current work: +Verification includes loopback GET, POST body transmission, custom headers, Content-Length, chunked, close-delimited, malformed response propagation, and HTTPS rejection before networking. -- PR #15 โ€” public `Client` core execution path. +Historical reference: -Important limitation for v0.5: +- PR #15 โ€” public `Client` core execution path -- each request may still establish a fresh TCP connection. -- redirect handling is not part of this milestone. - -Exit criteria: - -- PR #15 merged. -- all supported CI matrices pass. -- end-to-end loopback tests pass without public internet access. +Exit criteria: **complete**. --- -# v0.6 โ€” Connection Reuse / Keep-Alive โณ +# v0.6 โ€” Connection Reuse / Keep-Alive โœ… Goal: make `Client` genuinely stateful and satisfy HTTP/1.1 connection-reuse requirements. -Requirements covered: - -- REQ-CONN-002 -- REQ-CONN-003 -- REQ-PERF-004 -- REQ-API-001 reusable state -- REQ-TEST-002 reuse integration coverage - -Planned work: +Completed work: - retain one eligible TCP connection inside `Client`. -- define connection identity at minimum by effective host + port. -- reuse only when the previous response is fully consumed and parser marks the connection reusable. -- close retained connection when response includes `Connection: close`. -- never reuse close-delimited connections. -- never reuse protocol-upgraded connections. -- discard retained connection after timeout, transport error, protocol error, or partial write. -- reconnect automatically when the next request targets a different endpoint. -- handle stale peer-closed keep-alive connections safely. +- same-origin identity by effective host + port. +- reuse only after a fully consumed reusable response. +- close retained connection on `Connection: close`. +- never reuse close-delimited or protocol-upgraded connections. +- discard retained connection after transport/protocol failure. +- reconnect when the request origin changes. +- stale peer-closed keep-alive state is discarded without hidden automatic retry. - preserve parser pending-byte invariant. +- move-only `Client` resource ownership. -Required tests: +Verification includes one-socket sequential requests, forced reconnect scenarios, origin changes, and stale keep-alive behavior. -- two sequential requests over one accepted socket. -- `Connection: close` forces reconnect. -- close-delimited response forces reconnect. -- host/port change forces reconnect. -- failed connection is never retained. -- stale keep-alive socket does not corrupt a subsequent request. +Historical reference: -Exit criteria: +- PR #20 โ€” HTTP/1.1 connection reuse -- eligible sequential requests demonstrably avoid a second TCP handshake. -- no invalid/stale connection is reused. +Exit criteria: **complete**. --- -# v0.7 โ€” Redirect Handling โณ +# v0.7 โ€” Redirect Handling โœ… Goal: implement bounded automatic redirects without expanding beyond plaintext HTTP. -Requirements covered: +Completed work: -- REQ-REDIR-001 -- REQ-REDIR-002 -- REQ-REDIR-003 +- configurable redirect following. +- finite redirect limit. +- 301 / 302 / 303 / 307 / 308 handling. +- POST-to-GET policy for 301/302. +- non-HEAD-to-GET policy for 303. +- method/body preservation for 307/308. +- absolute, scheme-relative, absolute-path, relative-path, query-only, and fragment-aware `Location` resolution. +- explicit HTTPS/unsupported-scheme rejection. +- same-origin redirect reuse. +- cross-origin connection replacement. +- cross-origin stripping of `Host`, `Authorization`, `Proxy-Authorization`, and `Cookie`. +- `MissingRedirectLocation`, `RedirectLimitExceeded`, and `UnsupportedRedirectScheme` propagation. -Planned public configuration: +Verification includes loopback redirect chains, disabled following, finite limits, method/body policy, credential stripping, and cross-origin hops. -- `Client::set_follow_redirects(bool)` -- `Client::set_max_redirects(std::size_t)` +Historical reference: -Planned behavior: +- PR #21 โ€” bounded HTTP redirect handling -- finite default redirect limit. -- follow supported HTTP redirects when enabled. -- resolve relative `Location` values against the current URL. -- reject missing/invalid `Location` where redirect following is required. -- reject redirect targets requiring unsupported HTTPS/TLS. -- return `RedirectLimitExceeded` after the configured maximum. -- preserve method/body semantics according to the selected redirect status rules. +Exit criteria: **complete**. -Redirect-method policy must be explicitly documented and tested for: +--- -- 301 -- 302 -- 303 -- 307 -- 308 +# v0.8 โ€” Protocol and API Correctness Hardening ๐Ÿšง -Required tests: +Goal: close known correctness gaps before treating the API as release-candidate quality. -- absolute redirect. -- relative redirect. -- redirect chain. -- redirect loop/limit. -- redirect following disabled. -- redirect to HTTPS. -- redirect across host/port with correct connection replacement. +## 1. `Result` hardening โ€” ๐Ÿšง current work -Exit criteria: +Required work: -- all frozen redirect requirements have deterministic loopback tests. +- eliminate wrong-state type-based `std::get` behavior hidden behind `noexcept`. +- use explicit discriminant/index-based state access. +- keep accessor misuse as a documented programmer precondition violation rather than a network error. +- make `Result` capable of representing both success and failure unambiguously. +- preserve move-only payload support. +- keep `std::variant` as the baseline representation until measurement justifies custom storage. +- keep `Result` deferred until a concrete operation requires it. ---- - -# v0.8 โ€” Protocol and API Correctness Hardening โณ +Exit for this substep: -Goal: close known correctness gaps before treating the API as release-candidate quality. +- dedicated `Result` unit coverage passes on all CI matrices. +- `Result` success and failure are distinguishable. +- normal existing implicit error propagation remains source-compatible. -## URL and query correctness +## 2. URL and query correctness โ€” โณ next -REQ-HTTP-004 requires query-parameter behavior beyond merely preserving an already-encoded raw query. +REQ-HTTP-004 requires query behavior beyond merely preserving an already encoded raw query. Planned work: - define the v1 query-parameter API or explicitly refine the requirement if raw URL input is the intended contract. - percent-encode reserved characters where required. +- validate percent escapes. +- strengthen bracketed IPv6 literal validation. - add query/encoding tests. -- review percent-escape validation in URL parsing. -- strengthen bracketed IPv6 literal validation if necessary. - -## `Result` hardening - -Known implementation review items: -- eliminate wrong-state `std::get` behavior hidden behind `noexcept`. -- avoid termination hazards in release builds when preconditions are violated. -- make representation robust for edge cases such as `T == Error` if that instantiation is intended to be supported. -- benchmark `std::variant` representation before considering manual storage. -- decide whether `Result` is needed by the final v1 public/internal surface. - -## HTTP hardening +## 3. HTTP correctness review โ€” โณ Planned review: @@ -352,9 +294,9 @@ Planned review: - timeout boundary semantics. - EOF classification. -## Resource-bound review +## 4. Resource-bound review โ€” โณ -Because response bodies are memory-resident in v1, document or introduce practical limits where necessary for defensive behavior: +Because response bodies are memory-resident in v1, document or introduce practical defensive limits where appropriate: - response-head growth. - pathological chunk-size lines. @@ -363,10 +305,10 @@ Because response bodies are memory-resident in v1, document or introduce practic This milestone must not add streaming; it only hardens the frozen in-memory design. -Exit criteria: +v0.8 exit criteria: - no known correctness blocker remains for the frozen v1 scope. -- all accepted hardening decisions are reflected in docs/tests. +- accepted hardening decisions are reflected in docs and tests. --- @@ -376,12 +318,6 @@ Goal: make the library consumable and make performance claims measurable. ## Packaging -Requirements covered: - -- REQ-BUILD-001 -- REQ-BUILD-002 -- REQ-BUILD-003 - Planned work: - install public headers. @@ -393,10 +329,6 @@ Planned work: ## Benchmarks -Requirement covered: - -- REQ-PERF-005 - Required benchmark targets: - request serialization. @@ -409,13 +341,6 @@ Performance work should follow measurement; do not add complexity solely on intu ## Examples and user documentation -Requirements covered: - -- REQ-DOC-001 -- REQ-DOC-002 -- REQ-DOC-003 -- REQ-APIQ-004 - Planned work: - minimal GET example. @@ -452,22 +377,20 @@ Exit criteria: # v1.0 โ€” Release Hardening and Acceptance Gate ๐Ÿงช -Goal: verify the frozen requirements as a coherent release rather than as isolated merged features. +Goal: verify the frozen requirements as one coherent release. -## Required verification +Required verification: - Windows CI green. - Linux CI green. - macOS compatibility green where maintained. - C++17 build validated. -- required unit tests green. -- required loopback integration tests green. +- required unit and loopback integration tests green. - no required test depends on the public internet. - sanitizer configuration reviewed and exercised where supported. - deterministic socket cleanup verified on error paths. - connect/read/write timeout tests present. -- connection reuse test present. -- redirect tests present. +- connection reuse and redirect tests present. - install-tree consumer test present. - benchmark targets build and run. - public headers reviewed for accidental internal/platform leakage. @@ -477,27 +400,19 @@ Goal: verify the frozen requirements as a coherent release rather than as isolat v1.0 is ready only when all of the following are true: -1. every **MUST** functional requirement is implemented or an explicit requirements change has been accepted first, +1. every **MUST** functional requirement is implemented or an explicit requirements change is accepted first, 2. every **MUST** non-functional requirement has a concrete verification path, 3. no known correctness bug can corrupt an HTTP message boundary or reuse an invalid connection, 4. expected network/protocol failures remain representable through structured errors, 5. the installed library has zero third-party runtime dependency, -6. the public API and lifetime contracts are documented and match the implementation, +6. public API/lifetime contracts match implementation, 7. benchmarks exist for the performance claims the project intends to make. --- # Dependency Order From Current State -The implementation order after the current client-core work is: - ```text -Client core - โ†“ -Connection reuse / keep-alive - โ†“ -Redirect handling - โ†“ Protocol + API correctness hardening โ†“ Packaging + benchmarks + examples + docs @@ -507,11 +422,9 @@ Release acceptance / v1.0 Why this order: -- redirects depend on a correct request execution path and interact with connection ownership, -- connection reuse changes `Client` state/lifetime and should stabilize before redirects build on it, -- benchmarks are more meaningful after the core behavior is complete, -- packaging and public documentation should describe the API that will actually ship, -- release hardening should verify the final integrated system rather than unfinished intermediate layers. +- correctness decisions must stabilize before packaging and public documentation are finalized, +- benchmarks are more meaningful after behavior is stable, +- release hardening should verify the final integrated system rather than intermediate layers. --- diff --git a/include/cpp_request/result.hpp b/include/cpp_request/result.hpp index c88f635..b9f14d1 100644 --- a/include/cpp_request/result.hpp +++ b/include/cpp_request/result.hpp @@ -12,14 +12,31 @@ namespace cpp_request { template class [[nodiscard]] Result { public: - Result(T value) - : storage_(std::move(value)) {} + static_assert(!std::is_void_v, "Result is not implemented yet"); + + template < + typename U, + std::enable_if_t< + std::is_constructible_v + && !std::is_same_v, Error> + && !std::is_same_v, + int> = 0> + Result(U&& value) + : storage_(std::in_place_index<0>, std::forward(value)) {} Result(Error error) - : storage_(error) {} + : storage_(std::in_place_index<1>, error) {} + + [[nodiscard]] static Result success(T value) { + return Result{ValueTag{}, std::move(value)}; + } + + [[nodiscard]] static Result failure(Error error) { + return Result{ErrorTag{}, error}; + } [[nodiscard]] bool has_value() const noexcept { - return std::holds_alternative(storage_); + return storage_.index() == 0; } [[nodiscard]] explicit operator bool() const noexcept { @@ -27,31 +44,45 @@ class [[nodiscard]] Result { } T& value() & noexcept { - assert(has_value()); - return std::get(storage_); + auto* value = std::get_if<0>(&storage_); + assert(value != nullptr); + return *value; } const T& value() const& noexcept { - assert(has_value()); - return std::get(storage_); + const auto* value = std::get_if<0>(&storage_); + assert(value != nullptr); + return *value; } T&& value() && noexcept { - assert(has_value()); - return std::get(std::move(storage_)); + auto* value = std::get_if<0>(&storage_); + assert(value != nullptr); + return std::move(*value); } Error& error() & noexcept { - assert(!has_value()); - return std::get(storage_); + auto* error = std::get_if<1>(&storage_); + assert(error != nullptr); + return *error; } const Error& error() const& noexcept { - assert(!has_value()); - return std::get(storage_); + const auto* error = std::get_if<1>(&storage_); + assert(error != nullptr); + return *error; } private: + struct ValueTag final {}; + struct ErrorTag final {}; + + Result(ValueTag, T value) + : storage_(std::in_place_index<0>, std::move(value)) {} + + Result(ErrorTag, Error error) + : storage_(std::in_place_index<1>, error) {} + std::variant storage_; }; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index adc1827..306bad8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -2,6 +2,7 @@ include(${CMAKE_SOURCE_DIR}/cmake/packages/google-test.cmake) find_package(Threads REQUIRED) add_executable(cpp_request_tests + ${CMAKE_CURRENT_SOURCE_DIR}/result_test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/url_test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/headers_test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/request_test.cpp diff --git a/tests/result_test.cpp b/tests/result_test.cpp new file mode 100644 index 0000000..3968265 --- /dev/null +++ b/tests/result_test.cpp @@ -0,0 +1,94 @@ +#include + +#include +#include + +#include +#include +#include +#include + +namespace { + +using cpp_request::Error; +using cpp_request::ErrorCode; +using cpp_request::Result; + +static_assert(std::is_copy_constructible_v>); +static_assert(std::is_move_constructible_v>); +static_assert(!std::is_copy_constructible_v>>); +static_assert(std::is_move_constructible_v>>); + +TEST(ResultTest, StoresSuccessValue) { + Result result{42}; + + ASSERT_TRUE(result.has_value()); + ASSERT_TRUE(static_cast(result)); + EXPECT_EQ(result.value(), 42); +} + +TEST(ResultTest, StoresFailureError) { + Result result{Error{ErrorCode::ConnectFailed, 17}}; + + ASSERT_FALSE(result.has_value()); + ASSERT_FALSE(static_cast(result)); + EXPECT_EQ(result.error().code, ErrorCode::ConnectFailed); + EXPECT_EQ(result.error().native_code, 17); +} + +TEST(ResultTest, SupportsConvertibleSuccessInput) { + Result result{"hello"}; + + ASSERT_TRUE(result); + EXPECT_EQ(result.value(), "hello"); +} + +TEST(ResultTest, SupportsMoveOnlyPayload) { + Result> result{std::make_unique(7)}; + + ASSERT_TRUE(result); + auto value = std::move(result).value(); + ASSERT_NE(value, nullptr); + EXPECT_EQ(*value, 7); +} + +TEST(ResultTest, ProvidesMutableAndConstAccess) { + Result result{std::string{"before"}}; + result.value() = "after"; + + const Result& const_result = result; + EXPECT_EQ(const_result.value(), "after"); +} + +TEST(ResultTest, ExplicitFactoriesWorkForNormalPayloads) { + auto success = Result::success(11); + auto failure = Result::failure(Error{ErrorCode::ReadTimeout}); + + ASSERT_TRUE(success); + EXPECT_EQ(success.value(), 11); + ASSERT_FALSE(failure); + EXPECT_EQ(failure.error().code, ErrorCode::ReadTimeout); +} + +TEST(ResultTest, ResultOfErrorCanRepresentSuccessAndFailureDistinctly) { + auto success = Result::success(Error{ErrorCode::InvalidUrl, 3}); + auto failure = Result::failure(Error{ErrorCode::ResolveFailed, 9}); + + ASSERT_TRUE(success); + EXPECT_EQ(success.value().code, ErrorCode::InvalidUrl); + EXPECT_EQ(success.value().native_code, 3); + + ASSERT_FALSE(failure); + EXPECT_EQ(failure.error().code, ErrorCode::ResolveFailed); + EXPECT_EQ(failure.error().native_code, 9); +} + +TEST(ResultTest, DirectErrorConstructionRemainsFailureForResultOfError) { + Result result{Error{ErrorCode::Unknown, 99}}; + + ASSERT_FALSE(result); + EXPECT_EQ(result.error().code, ErrorCode::Unknown); + EXPECT_EQ(result.error().native_code, 99); +} + +} // namespace