diff --git a/.github/workflows/website-build.yml b/.github/workflows/website-build.yml index 49bd13fff..50315bdd5 100644 --- a/.github/workflows/website-build.yml +++ b/.github/workflows/website-build.yml @@ -30,6 +30,7 @@ jobs: -DSOURCEMETA_CORE_JSONSCHEMA:BOOL=OFF -DSOURCEMETA_CORE_JSONPOINTER:BOOL=OFF -DSOURCEMETA_CORE_YAML:BOOL=OFF + -DSOURCEMETA_CORE_URLPATTERN:BOOL=OFF -DSOURCEMETA_CORE_EXTENSION_ALTERSCHEMA:BOOL=OFF -DSOURCEMETA_CORE_EXTENSION_EDITORSCHEMA:BOOL=OFF -DSOURCEMETA_CORE_EXTENSION_SCHEMACONFIG:BOOL=OFF diff --git a/.github/workflows/website-deploy.yml b/.github/workflows/website-deploy.yml index 14ac0be15..cad19d195 100644 --- a/.github/workflows/website-deploy.yml +++ b/.github/workflows/website-deploy.yml @@ -40,6 +40,7 @@ jobs: -DSOURCEMETA_CORE_JSONSCHEMA:BOOL=OFF -DSOURCEMETA_CORE_JSONPOINTER:BOOL=OFF -DSOURCEMETA_CORE_YAML:BOOL=OFF + -DSOURCEMETA_CORE_URLPATTERN:BOOL=OFF -DSOURCEMETA_CORE_EXTENSION_ALTERSCHEMA:BOOL=OFF -DSOURCEMETA_CORE_EXTENSION_EDITORSCHEMA:BOOL=OFF -DSOURCEMETA_CORE_EXTENSION_SCHEMACONFIG:BOOL=OFF diff --git a/CMakeLists.txt b/CMakeLists.txt index fc8d88c18..9537a6aea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,6 +18,7 @@ option(SOURCEMETA_CORE_JSONSCHEMA "Build the Sourcemeta Core JSON Schema library option(SOURCEMETA_CORE_JSONPOINTER "Build the Sourcemeta Core JSON Pointer library" ON) option(SOURCEMETA_CORE_JSONL "Build the Sourcemeta Core JSONL library" ON) option(SOURCEMETA_CORE_YAML "Build the Sourcemeta Core YAML library" ON) +option(SOURCEMETA_CORE_URLPATTERN "Build the Sourcemeta Core URL Pattern library" ON) option(SOURCEMETA_CORE_EXTENSION_ALTERSCHEMA "Build the Sourcemeta Core AlterSchema library" ON) option(SOURCEMETA_CORE_EXTENSION_EDITORSCHEMA "Build the Sourcemeta Core EditorSchema library" ON) option(SOURCEMETA_CORE_EXTENSION_SCHEMACONFIG "Build the Sourcemeta Core SchemaConfig library" ON) @@ -121,6 +122,10 @@ if(SOURCEMETA_CORE_YAML) add_subdirectory(src/core/yaml) endif() +if(SOURCEMETA_CORE_URLPATTERN) + add_subdirectory(src/core/urlpattern) +endif() + if(SOURCEMETA_CORE_EXTENSION_ALTERSCHEMA) add_subdirectory(src/extension/alterschema) endif() @@ -232,6 +237,10 @@ if(SOURCEMETA_CORE_TESTS) add_subdirectory(test/yaml) endif() + if(SOURCEMETA_CORE_URLPATTERN) + add_subdirectory(test/urlpattern) + endif() + if(SOURCEMETA_CORE_EXTENSION_ALTERSCHEMA) add_subdirectory(test/alterschema) endif() diff --git a/DEPENDENCIES b/DEPENDENCIES index 7cdc84b0e..f2ab55470 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -15,3 +15,4 @@ yaml https://github.com/yaml/libyaml 0.2.5 pcre2 https://github.com/PCRE2Project/pcre2 pcre2-10.47 googletest https://github.com/google/googletest a7f443b80b105f940225332ed3c31f2790092f47 googlebenchmark https://github.com/google/benchmark 378fe693a1ef51500db21b11ff05a8018c5f0e55 +wpt https://github.com/web-platform-tests/wpt 71154f49e6cdda82c43e11bb74cfe3ad7b3f9368 diff --git a/config.cmake.in b/config.cmake.in index 2db4b0aaa..bf05319d9 100644 --- a/config.cmake.in +++ b/config.cmake.in @@ -19,6 +19,7 @@ if(NOT SOURCEMETA_CORE_COMPONENTS) list(APPEND SOURCEMETA_CORE_COMPONENTS jsonpointer) list(APPEND SOURCEMETA_CORE_COMPONENTS jsonschema) list(APPEND SOURCEMETA_CORE_COMPONENTS yaml) + list(APPEND SOURCEMETA_CORE_COMPONENTS urlpattern) list(APPEND SOURCEMETA_CORE_COMPONENTS alterschema) list(APPEND SOURCEMETA_CORE_COMPONENTS editorschema) list(APPEND SOURCEMETA_CORE_COMPONENTS schemaconfig) @@ -87,6 +88,11 @@ foreach(component ${SOURCEMETA_CORE_COMPONENTS}) find_dependency(yaml CONFIG) include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_io.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_yaml.cmake") + elseif(component STREQUAL "urlpattern") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_json.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_regex.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_punycode.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_urlpattern.cmake") elseif(component STREQUAL "alterschema") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_uri.cmake") find_dependency(mpdecimal CONFIG) diff --git a/patches/wpt/0001-Remove-invalid-surrogate-pairs-tests.patch b/patches/wpt/0001-Remove-invalid-surrogate-pairs-tests.patch new file mode 100644 index 000000000..a3135ccfd --- /dev/null +++ b/patches/wpt/0001-Remove-invalid-surrogate-pairs-tests.patch @@ -0,0 +1,44 @@ +From ec3c41d5eb68cf24e86ef67b1067fc43df859ab0 Mon Sep 17 00:00:00 2001 +From: Juan Cruz Viotti +Date: Fri, 28 Nov 2025 15:42:16 -0400 +Subject: [PATCH] Remove invalid surrogate pairs tests + +Signed-off-by: Juan Cruz Viotti +--- + urlpattern/resources/urlpatterntestdata.json | 20 -------------------- + 1 file changed, 20 deletions(-) + +diff --git a/urlpattern/resources/urlpatterntestdata.json b/urlpattern/resources/urlpatterntestdata.json +index 4b1b9ee5f6..ae10c412ad 100644 +--- a/urlpattern/resources/urlpatterntestdata.json ++++ b/urlpattern/resources/urlpatterntestdata.json +@@ -1136,26 +1136,6 @@ + "pathname": { "input": "/", "groups": {}} + } + }, +- { +- "pattern": ["http://\uD83D \uDEB2"], +- "expected_obj": "error" +- }, +- { +- "pattern": [{"hostname":"\uD83D \uDEB2"}], +- "expected_obj": "error" +- }, +- { +- "pattern": [{"pathname":"\uD83D \uDEB2"}], +- "inputs": [], +- "expected_obj": { +- "pathname": "%EF%BF%BD%20%EF%BF%BD" +- }, +- "expected_match": null +- }, +- { +- "pattern": [{"pathname":":\uD83D \uDEB2"}], +- "expected_obj": "error" +- }, + { + "pattern": [{"pathname":":a\uDB40\uDD00b"}], + "inputs": [], +-- +2.52.0 + diff --git a/src/core/urlpattern/CMakeLists.txt b/src/core/urlpattern/CMakeLists.txt new file mode 100644 index 000000000..3bd756c45 --- /dev/null +++ b/src/core/urlpattern/CMakeLists.txt @@ -0,0 +1,17 @@ +sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME urlpattern + PRIVATE_HEADERS error.h part.h component.h + SOURCES urlpattern.cc urlpattern_part.cc urlpattern_component.cc) + +if(SOURCEMETA_CORE_INSTALL) + sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME urlpattern) +endif() + +target_link_libraries(sourcemeta_core_urlpattern PUBLIC + sourcemeta::core::json) +target_link_libraries(sourcemeta_core_urlpattern PUBLIC + sourcemeta::core::regex) +target_link_libraries(sourcemeta_core_urlpattern PRIVATE + sourcemeta::core::punycode) + +# TODO: Find a way to get rid of this? +target_link_libraries(sourcemeta_core_urlpattern PRIVATE PCRE2::pcre2) diff --git a/src/core/urlpattern/include/sourcemeta/core/urlpattern.h b/src/core/urlpattern/include/sourcemeta/core/urlpattern.h new file mode 100644 index 000000000..a2a4b35e5 --- /dev/null +++ b/src/core/urlpattern/include/sourcemeta/core/urlpattern.h @@ -0,0 +1,86 @@ +#ifndef SOURCEMETA_CORE_URLPATTERN_H_ +#define SOURCEMETA_CORE_URLPATTERN_H_ + +#ifndef SOURCEMETA_CORE_URLPATTERN_EXPORT +#include +#endif + +// NOLINTBEGIN(misc-include-cleaner) +#include +#include +#include +// NOLINTEND(misc-include-cleaner) + +#include + +#include // std::strong_ordering +#include // std::optional +#include // std::string +#include // std::string_view + +/// @defgroup urlpattern URL Pattern +/// @brief A WHATWG URL Pattern implementation. +/// +/// This functionality is included as follows: +/// +/// ```cpp +/// #include +/// ``` + +namespace sourcemeta::core { + +/// @ingroup urlpattern +struct URLPatternResult { + std::optional protocol; + std::optional username; + std::optional password; + std::optional hostname; + std::optional port; + std::optional pathname; + std::optional search; + std::optional hash; +}; + +/// @ingroup urlpattern +struct SOURCEMETA_CORE_URLPATTERN_EXPORT URLPatternInput { + std::string protocol; + std::string username; + std::string password; + std::string hostname; + std::string port; + std::string pathname; + std::string search; + std::string hash; + + [[nodiscard]] static auto parse(const JSON &input) + -> std::optional; + [[nodiscard]] static auto parse(const std::string_view input) + -> std::optional; +}; + +/// @ingroup urlpattern +/// See https://urlpattern.spec.whatwg.org/#url-pattern-struct +struct SOURCEMETA_CORE_URLPATTERN_EXPORT URLPattern { + URLPatternProtocol protocol; + URLPatternUsername username; + URLPatternPassword password; + URLPatternHostname hostname; + URLPatternPort port; + URLPatternPathname pathname; + URLPatternSearch search; + URLPatternHash hash; + + auto operator==(const URLPattern &other) const -> bool = default; + auto operator<=>(const URLPattern &other) const -> std::strong_ordering; + + [[nodiscard]] static auto parse(const std::string_view input) -> URLPattern; + [[nodiscard]] static auto parse(const JSON &input) + -> std::optional; + + [[nodiscard]] auto match(const URLPatternInput &input) const + -> URLPatternResult; +}; + +} // namespace sourcemeta::core + +#endif diff --git a/src/core/urlpattern/include/sourcemeta/core/urlpattern_component.h b/src/core/urlpattern/include/sourcemeta/core/urlpattern_component.h new file mode 100644 index 000000000..93b920b9c --- /dev/null +++ b/src/core/urlpattern/include/sourcemeta/core/urlpattern_component.h @@ -0,0 +1,188 @@ +#ifndef SOURCEMETA_CORE_URLPATTERN_COMPONENT_H_ +#define SOURCEMETA_CORE_URLPATTERN_COMPONENT_H_ + +#ifndef SOURCEMETA_CORE_URLPATTERN_EXPORT +#include +#endif + +#include + +#include // std::strong_ordering +#include // std::optional +#include // std::string +#include // std::string_view +#include // std::unordered_map +#include // std::vector + +namespace sourcemeta::core { + +/// @ingroup urlpattern +struct SOURCEMETA_CORE_URLPATTERN_EXPORT URLPatternComponentResult { + [[nodiscard]] auto size() const noexcept -> std::size_t; + [[nodiscard]] auto at(const std::size_t index) const noexcept + -> std::string_view; + [[nodiscard]] auto at(const std::string_view name) const noexcept + -> std::optional; + + auto insert(const std::string_view value) -> void; + auto insert(const std::string_view name, const std::string_view value) + -> void; + auto insert(const std::string_view name, const std::size_t index) -> void; + +private: + std::vector positions; + std::unordered_map names; +}; + +/// @ingroup urlpattern +struct SOURCEMETA_CORE_URLPATTERN_EXPORT URLPatternProtocol { + URLPatternProtocol() : value{URLPatternPartAsterisk{}} {} + URLPatternProtocol(const char *input); + URLPatternProtocol(const URLPatternProtocol &) = default; + URLPatternProtocol(URLPatternProtocol &&) noexcept = default; + auto operator=(const URLPatternProtocol &) -> URLPatternProtocol & = default; + auto operator=(URLPatternProtocol &&) noexcept + -> URLPatternProtocol & = default; + + auto operator==(const URLPatternProtocol &other) const -> bool = default; + auto operator<=>(const URLPatternProtocol &other) const + -> std::strong_ordering; + + URLPatternPart value; + [[nodiscard]] auto match(const std::string_view input) const + -> std::optional; +}; + +/// @ingroup urlpattern +struct SOURCEMETA_CORE_URLPATTERN_EXPORT URLPatternUsername { + URLPatternUsername() : value{URLPatternPartAsterisk{}} {} + URLPatternUsername(const char *input); + URLPatternUsername(const URLPatternUsername &) = default; + URLPatternUsername(URLPatternUsername &&) noexcept = default; + auto operator=(const URLPatternUsername &) -> URLPatternUsername & = default; + auto operator=(URLPatternUsername &&) noexcept + -> URLPatternUsername & = default; + + auto operator==(const URLPatternUsername &other) const -> bool = default; + auto operator<=>(const URLPatternUsername &other) const + -> std::strong_ordering; + + URLPatternPart value; + [[nodiscard]] auto match(const std::string_view input) const + -> std::optional; +}; + +/// @ingroup urlpattern +struct SOURCEMETA_CORE_URLPATTERN_EXPORT URLPatternPassword { + URLPatternPassword() : value{URLPatternPartAsterisk{}} {} + URLPatternPassword(const char *input); + URLPatternPassword(const URLPatternPassword &) = default; + URLPatternPassword(URLPatternPassword &&) noexcept = default; + auto operator=(const URLPatternPassword &) -> URLPatternPassword & = default; + auto operator=(URLPatternPassword &&) noexcept + -> URLPatternPassword & = default; + + auto operator==(const URLPatternPassword &other) const -> bool = default; + auto operator<=>(const URLPatternPassword &other) const + -> std::strong_ordering; + + URLPatternPart value; + [[nodiscard]] auto match(const std::string_view input) const + -> std::optional; +}; + +/// @ingroup urlpattern +struct SOURCEMETA_CORE_URLPATTERN_EXPORT URLPatternHostname { + URLPatternHostname() : value{{URLPatternPartAsterisk{}}} {} + URLPatternHostname(const char *input); + URLPatternHostname(const URLPatternHostname &) = default; + URLPatternHostname(URLPatternHostname &&) noexcept = default; + auto operator=(const URLPatternHostname &) -> URLPatternHostname & = default; + auto operator=(URLPatternHostname &&) noexcept + -> URLPatternHostname & = default; + + auto operator==(const URLPatternHostname &other) const -> bool = default; + auto operator<=>(const URLPatternHostname &other) const + -> std::strong_ordering; + + std::vector value; + [[nodiscard]] auto match(const std::string_view input) const + -> std::optional; +}; + +/// @ingroup urlpattern +struct SOURCEMETA_CORE_URLPATTERN_EXPORT URLPatternPort { + URLPatternPort() : value{URLPatternPartAsterisk{}} {} + URLPatternPort(const char *input); + URLPatternPort(const URLPatternPort &) = default; + URLPatternPort(URLPatternPort &&) noexcept = default; + auto operator=(const URLPatternPort &) -> URLPatternPort & = default; + auto operator=(URLPatternPort &&) noexcept -> URLPatternPort & = default; + + auto operator==(const URLPatternPort &other) const -> bool = default; + auto operator<=>(const URLPatternPort &other) const -> std::strong_ordering; + + URLPatternPart value; + [[nodiscard]] auto match(const std::string_view input) const + -> std::optional; +}; + +/// @ingroup urlpattern +struct SOURCEMETA_CORE_URLPATTERN_EXPORT URLPatternPathname { + URLPatternPathname() : value{{URLPatternPartAsterisk{}}} {} + URLPatternPathname(const char *input); + URLPatternPathname(const URLPatternPathname &) = default; + URLPatternPathname(URLPatternPathname &&) noexcept = default; + auto operator=(const URLPatternPathname &) -> URLPatternPathname & = default; + auto operator=(URLPatternPathname &&) noexcept + -> URLPatternPathname & = default; + + auto operator==(const URLPatternPathname &other) const -> bool = default; + auto operator<=>(const URLPatternPathname &other) const + -> std::strong_ordering; + + std::vector value; + // TODO: Find a way to get rid of this + bool is_bare_pattern{false}; + [[nodiscard]] auto match(const std::string_view input) const + -> std::optional; +}; + +/// @ingroup urlpattern +struct SOURCEMETA_CORE_URLPATTERN_EXPORT URLPatternSearch { + URLPatternSearch() : value{URLPatternPartAsterisk{}} {} + URLPatternSearch(const char *input); + URLPatternSearch(const URLPatternSearch &) = default; + URLPatternSearch(URLPatternSearch &&) noexcept = default; + auto operator=(const URLPatternSearch &) -> URLPatternSearch & = default; + auto operator=(URLPatternSearch &&) noexcept -> URLPatternSearch & = default; + + auto operator==(const URLPatternSearch &other) const -> bool = default; + auto operator<=>(const URLPatternSearch &other) const -> std::strong_ordering; + + // URL Pattern treats search queries as opaque strings + URLPatternPart value; + [[nodiscard]] auto match(const std::string_view input) const + -> std::optional; +}; + +/// @ingroup urlpattern +struct SOURCEMETA_CORE_URLPATTERN_EXPORT URLPatternHash { + URLPatternHash() : value{URLPatternPartAsterisk{}} {} + URLPatternHash(const char *input); + URLPatternHash(const URLPatternHash &) = default; + URLPatternHash(URLPatternHash &&) noexcept = default; + auto operator=(const URLPatternHash &) -> URLPatternHash & = default; + auto operator=(URLPatternHash &&) noexcept -> URLPatternHash & = default; + + auto operator==(const URLPatternHash &other) const -> bool = default; + auto operator<=>(const URLPatternHash &other) const -> std::strong_ordering; + + URLPatternPart value; + [[nodiscard]] auto match(const std::string_view input) const + -> std::optional; +}; + +} // namespace sourcemeta::core + +#endif diff --git a/src/core/urlpattern/include/sourcemeta/core/urlpattern_error.h b/src/core/urlpattern/include/sourcemeta/core/urlpattern_error.h new file mode 100644 index 000000000..b2a7393ce --- /dev/null +++ b/src/core/urlpattern/include/sourcemeta/core/urlpattern_error.h @@ -0,0 +1,46 @@ +#ifndef SOURCEMETA_CORE_URLPATTERN_ERROR_H_ +#define SOURCEMETA_CORE_URLPATTERN_ERROR_H_ + +#ifndef SOURCEMETA_CORE_URLPATTERN_EXPORT +#include +#endif + +#include // std::uint64_t +#include // std::exception + +namespace sourcemeta::core { + +// Exporting symbols that depends on the standard C++ library is considered +// safe. +// https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-2-c4275?view=msvc-170&redirectedfrom=MSDN +#if defined(_MSC_VER) +#pragma warning(disable : 4251 4275) +#endif + +/// @ingroup urlpattern +/// An error that represents a URL Pattern failure +class SOURCEMETA_CORE_URLPATTERN_EXPORT URLPatternParseError + : public std::exception { +public: + URLPatternParseError(const std::uint64_t column) : column_{column} {} + + [[nodiscard]] auto what() const noexcept -> const char * override { + return "The input is not a valid URL Pattern"; + } + + /// Get the column number of the error + [[nodiscard]] auto column() const noexcept -> std::uint64_t { + return column_; + } + +private: + std::uint64_t column_; +}; + +#if defined(_MSC_VER) +#pragma warning(default : 4251 4275) +#endif + +} // namespace sourcemeta::core + +#endif diff --git a/src/core/urlpattern/include/sourcemeta/core/urlpattern_part.h b/src/core/urlpattern/include/sourcemeta/core/urlpattern_part.h new file mode 100644 index 000000000..f53713d84 --- /dev/null +++ b/src/core/urlpattern/include/sourcemeta/core/urlpattern_part.h @@ -0,0 +1,250 @@ +#ifndef SOURCEMETA_CORE_URLPATTERN_PART_H_ +#define SOURCEMETA_CORE_URLPATTERN_PART_H_ + +#ifndef SOURCEMETA_CORE_URLPATTERN_EXPORT +#include +#endif + +#include + +#include // std::string +#include // std::string_view +#include // std::variant +#include // std::vector + +namespace sourcemeta::core { + +// See https://urlpattern.spec.whatwg.org/#tokens + +/// @ingroup urlpattern +/// The token represents a string of the form "()". The +/// regular expression is required to consist of only ASCII code points. +struct SOURCEMETA_CORE_URLPATTERN_EXPORT URLPatternPartRegExp { + Regex value; + std::string original_pattern; + auto operator==(const URLPatternPartRegExp &) const -> bool = default; + [[nodiscard]] auto matches(const std::string_view segment) const noexcept + -> bool; +}; + +/// @ingroup urlpattern +/// The token represents a string of the form "()?". +struct SOURCEMETA_CORE_URLPATTERN_EXPORT URLPatternPartRegExpOptional { + Regex value; + std::string original_pattern; + auto operator==(const URLPatternPartRegExpOptional &) const -> bool = default; + [[nodiscard]] auto matches(const std::string_view segment) const noexcept + -> bool; +}; + +/// @ingroup urlpattern +/// The token represents a string of the form "()+". +struct SOURCEMETA_CORE_URLPATTERN_EXPORT URLPatternPartRegExpMultiple { + Regex value; + std::string original_pattern; + auto operator==(const URLPatternPartRegExpMultiple &) const -> bool = default; + [[nodiscard]] auto matches(const std::string_view segment) const noexcept + -> bool; +}; + +/// @ingroup urlpattern +/// The token represents a string of the form "()*". +struct SOURCEMETA_CORE_URLPATTERN_EXPORT URLPatternPartRegExpAsterisk { + Regex value; + std::string original_pattern; + auto operator==(const URLPatternPartRegExpAsterisk &) const -> bool = default; + [[nodiscard]] auto matches(const std::string_view segment) const noexcept + -> bool; +}; + +/// @ingroup urlpattern +/// The token represents a string of the form ":". The name value is +/// restricted to code points that are consistent with JavaScript identifiers. +struct SOURCEMETA_CORE_URLPATTERN_EXPORT URLPatternPartName { + std::string value; + auto operator==(const URLPatternPartName &) const -> bool = default; + [[nodiscard]] auto matches(const std::string_view segment) const noexcept + -> bool; +}; + +/// @ingroup urlpattern +/// The token represents a string of the form ":()". +struct SOURCEMETA_CORE_URLPATTERN_EXPORT URLPatternPartNameRegExp { + std::string value; + Regex modifier; + std::string original_pattern; + auto operator==(const URLPatternPartNameRegExp &) const -> bool = default; + [[nodiscard]] auto matches(const std::string_view segment) const noexcept + -> bool; +}; + +/// @ingroup urlpattern +/// The token represents a string of the form ":?". +struct SOURCEMETA_CORE_URLPATTERN_EXPORT URLPatternPartNameOptional { + std::string value; + auto operator==(const URLPatternPartNameOptional &) const -> bool = default; + [[nodiscard]] auto matches(const std::string_view segment) const noexcept + -> bool; +}; + +/// @ingroup urlpattern +/// The token represents a string of the form ":+". +struct SOURCEMETA_CORE_URLPATTERN_EXPORT URLPatternPartNameMultiple { + std::string value; + auto operator==(const URLPatternPartNameMultiple &) const -> bool = default; + [[nodiscard]] auto matches(const std::string_view segment) const noexcept + -> bool; +}; + +/// @ingroup urlpattern +/// The token represents a string of the form ":*". +struct SOURCEMETA_CORE_URLPATTERN_EXPORT URLPatternPartNameAsterisk { + std::string value; + auto operator==(const URLPatternPartNameAsterisk &) const -> bool = default; + [[nodiscard]] auto matches(const std::string_view segment) const noexcept + -> bool; +}; + +/// @ingroup urlpattern +/// The token represents a valid pattern code point without any special +/// syntactical meaning. +struct SOURCEMETA_CORE_URLPATTERN_EXPORT URLPatternPartChar { + std::string value; + auto operator==(const URLPatternPartChar &) const -> bool = default; + [[nodiscard]] auto matches(const std::string_view segment) const noexcept + -> bool; +}; + +/// @ingroup urlpattern +/// The token represents a U+002A (*) code point that is a wildcard matching +/// group +struct SOURCEMETA_CORE_URLPATTERN_EXPORT URLPatternPartAsterisk { + auto operator==(const URLPatternPartAsterisk &) const -> bool = default; + [[nodiscard]] auto matches(const std::string_view segment) const noexcept + -> bool; +}; + +/// @ingroup urlpattern +/// The token represents a string of the form "*?". +struct SOURCEMETA_CORE_URLPATTERN_EXPORT URLPatternPartAsteriskOptional { + auto operator==(const URLPatternPartAsteriskOptional &) const + -> bool = default; + [[nodiscard]] auto matches(const std::string_view segment) const noexcept + -> bool; +}; + +/// @ingroup urlpattern +/// The token represents a string of the form "*+". +struct SOURCEMETA_CORE_URLPATTERN_EXPORT URLPatternPartAsteriskMultiple { + auto operator==(const URLPatternPartAsteriskMultiple &) const + -> bool = default; + [[nodiscard]] auto matches(const std::string_view segment) const noexcept + -> bool; +}; + +/// @ingroup urlpattern +/// The token represents a string of the form "**". +struct SOURCEMETA_CORE_URLPATTERN_EXPORT URLPatternPartAsteriskAsterisk { + auto operator==(const URLPatternPartAsteriskAsterisk &) const + -> bool = default; + [[nodiscard]] auto matches(const std::string_view segment) const noexcept + -> bool; +}; + +/// @ingroup urlpattern +using URLPatternPartNonGroup = std::vector>; + +/// @ingroup urlpattern +/// The token represents a string of the form "{}". +struct SOURCEMETA_CORE_URLPATTERN_EXPORT URLPatternPartGroup { + URLPatternPartNonGroup value; + // Whether the group content starts with a segment delimiter (e.g. {/:bar}) + bool has_inner_segment_prefix = false; + // Whether the group content ends with a segment delimiter (e.g. {bar/}) + bool has_inner_segment_suffix = false; + auto operator==(const URLPatternPartGroup &) const -> bool = default; + [[nodiscard]] auto matches(const std::string_view segment) const noexcept + -> bool; +}; + +/// @ingroup urlpattern +/// The token represents a string of the form "{}?". +struct SOURCEMETA_CORE_URLPATTERN_EXPORT URLPatternPartGroupOptional { + URLPatternPartNonGroup value; + // Whether the group content starts with a segment delimiter (e.g. {/:bar}?) + bool has_inner_segment_prefix = false; + // Whether the group content ends with a segment delimiter (e.g. {bar/}?) + bool has_inner_segment_suffix = false; + auto operator==(const URLPatternPartGroupOptional &) const -> bool = default; + [[nodiscard]] auto matches(const std::string_view segment) const noexcept + -> bool; +}; + +/// @ingroup urlpattern +/// The token represents a string of the form "{}+". +struct SOURCEMETA_CORE_URLPATTERN_EXPORT URLPatternPartGroupMultiple { + URLPatternPartNonGroup value; + // Whether the group content starts with a segment delimiter (e.g. {/:bar}+) + bool has_inner_segment_prefix = false; + // Whether the group content ends with a segment delimiter (e.g. {bar/}+) + bool has_inner_segment_suffix = false; + auto operator==(const URLPatternPartGroupMultiple &) const -> bool = default; + [[nodiscard]] auto matches(const std::string_view segment) const noexcept + -> bool; +}; + +/// @ingroup urlpattern +/// The token represents a string of the form "{}*". +struct SOURCEMETA_CORE_URLPATTERN_EXPORT URLPatternPartGroupAsterisk { + URLPatternPartNonGroup value; + // Whether the group content starts with a segment delimiter (e.g. {/:bar}*) + bool has_inner_segment_prefix = false; + // Whether the group content ends with a segment delimiter (e.g. {bar/}*) + bool has_inner_segment_suffix = false; + auto operator==(const URLPatternPartGroupAsterisk &) const -> bool = default; + [[nodiscard]] auto matches(const std::string_view segment) const noexcept + -> bool; +}; + +/// @ingroup urlpattern +/// This token represents a single segment that consist of multiple tokens, such +/// as "foo-:bar{baz}+" +struct SOURCEMETA_CORE_URLPATTERN_EXPORT URLPatternPartComplexSegment { + std::vector> + value; + auto operator==(const URLPatternPartComplexSegment &) const -> bool = default; + [[nodiscard]] auto matches(const std::string_view segment) const noexcept + -> bool; +}; + +/// @ingroup urlpattern +using URLPatternPart = + std::variant; + +} // namespace sourcemeta::core + +#endif diff --git a/src/core/urlpattern/urlpattern.cc b/src/core/urlpattern/urlpattern.cc new file mode 100644 index 000000000..f11b3f7f7 --- /dev/null +++ b/src/core/urlpattern/urlpattern.cc @@ -0,0 +1,852 @@ +#include + +#include + +#include // std::tolower +#include // std::uint8_t +#include // std::optional +#include // std::vector + +namespace sourcemeta::core { + +namespace { + +auto hex_digit_value(const char character) -> int { + if (character >= '0' && character <= '9') { + return character - '0'; + } + if (character >= 'A' && character <= 'F') { + return character - 'A' + 10; + } + if (character >= 'a' && character <= 'f') { + return character - 'a' + 10; + } + return -1; +} + +auto is_uppercase_hex(const char character) -> bool { + return (character >= '0' && character <= '9') || + (character >= 'A' && character <= 'F'); +} + +auto strip_tab_newline(const std::string &input) -> std::string { + std::string result; + result.reserve(input.size()); + for (const auto character : input) { + if (character != '\t' && character != '\n' && character != '\r') { + result += character; + } + } + return result; +} + +auto is_ascii(const std::string &input) -> bool { + for (const auto character : input) { + if (static_cast(character) > 127) { + return false; + } + } + return true; +} + +auto to_lowercase(const std::string &input) -> std::string { + std::string result; + result.reserve(input.size()); + for (const auto character : input) { + result += + static_cast(std::tolower(static_cast(character))); + } + return result; +} + +auto hostname_to_ascii(const std::string &input) -> std::string { + std::string result; + std::string label; + + const auto process_label = [&result, &label]() { + if (label.empty()) { + return; + } + + if (!result.empty()) { + result += '.'; + } + + if (is_ascii(label)) { + result += to_lowercase(label); + } else { + result += "xn--"; + try { + result += utf8_to_punycode(label); + } catch (const PunycodeError &) { + result += label; + } + } + label.clear(); + }; + + for (const auto character : input) { + if (character == '.') { + process_label(); + } else { + label += character; + } + } + process_label(); + + return result; +} + +auto has_pattern_syntax(const std::string &input) -> bool { + bool after_backslash = false; + for (const auto character : input) { + if (after_backslash) { + after_backslash = false; + continue; + } + if (character == '\\') { + after_backslash = true; + continue; + } + if (character == ':' || character == '*' || character == '{' || + character == '}' || character == '(' || character == ')' || + character == '?') { + return true; + } + } + return false; +} + +auto canonicalize_hostname_pattern(const std::string &input) -> std::string { + auto stripped = strip_tab_newline(input); + + std::string truncated; + for (const auto character : stripped) { + if (character == '#' || character == '/') { + break; + } + truncated += character; + } + + if (!has_pattern_syntax(truncated)) { + return hostname_to_ascii(truncated); + } + + stripped = truncated; + + std::string result; + bool in_name = false; + bool after_backslash = false; + for (const auto character : stripped) { + if (character == '#' || character == '/') { + break; + } + + if (after_backslash) { + result += static_cast( + std::tolower(static_cast(character))); + after_backslash = false; + continue; + } + + if (character == '\\') { + result += character; + after_backslash = true; + continue; + } + + if (character == ':') { + result += character; + in_name = true; + continue; + } + + if (in_name) { + const auto byte = static_cast(character); + if (std::isalnum(byte) || character == '_' || character == '$' || + byte >= 0x80) { + result += character; + continue; + } + in_name = false; + } + + if (character == '*' || character == '{' || character == '}' || + character == '(' || character == ')' || character == '?') { + result += character; + } else { + result += static_cast( + std::tolower(static_cast(character))); + } + } + return result; +} + +auto canonicalize_port(const std::string &input) -> std::optional { + auto stripped = strip_tab_newline(input); + std::string result; + for (const auto character : stripped) { + if (character >= '0' && character <= '9') { + result += character; + } else { + break; + } + } + // If the stripped input starts with a non-digit (excluding case of empty + // stripped input becoming empty result), the port is invalid + if (!stripped.empty() && result.empty()) { + return std::nullopt; + } + return result; +} + +auto percent_decode(const std::string &input) -> std::string { + std::string result; + result.reserve(input.size()); + + for (std::string::size_type index = 0; index < input.size(); index += 1) { + if (input[index] == '%' && index + 2 < input.size()) { + // Only decode uppercase percent-encoding (canonical form) + if (is_uppercase_hex(input[index + 1]) && + is_uppercase_hex(input[index + 2])) { + const auto high = hex_digit_value(input[index + 1]); + const auto low = hex_digit_value(input[index + 2]); + const auto byte = static_cast((high << 4) | low); + result += static_cast(byte); + index += 2; + continue; + } + } + result += input[index]; + } + + return result; +} + +auto normalize_pathname_input(const std::string &pathname) -> std::string { + if (pathname.empty()) { + return pathname; + } + + const auto starts_with_slash = pathname[0] == '/'; + + // Only normalize absolute paths (starting with /) + // Bare inputs like "./foo" should not be normalized + if (!starts_with_slash) { + return pathname; + } + + // Only normalize . and .. segments for absolute input pathnames + std::vector segments; + std::string::size_type position{1}; + const auto ends_with_slash = + pathname.size() > 1 && pathname[pathname.size() - 1] == '/'; + + while (position < pathname.size()) { + auto segment_end = position; + while (segment_end < pathname.size() && pathname[segment_end] != '/') { + segment_end += 1; + } + + const std::string_view segment{pathname.data() + position, + segment_end - position}; + + if (segment == "..") { + if (!segments.empty()) { + segments.pop_back(); + } + } else if (segment != "." && !segment.empty()) { + segments.emplace_back(segment); + } + + position = segment_end + 1; + } + + std::string result; + result += '/'; + for (std::size_t index = 0; index < segments.size(); index += 1) { + result += segments[index]; + if (index + 1 < segments.size()) { + result += '/'; + } + } + + if (ends_with_slash && (result.empty() || result[result.size() - 1] != '/')) { + result += '/'; + } + + return result; +} + +auto normalize_pathname_pattern(const std::string &pathname) -> std::string { + if (pathname.empty()) { + return pathname; + } + + const auto starts_with_slash = pathname[0] == '/'; + + // Only normalize absolute patterns (starting with /) + // Bare patterns like "./foo" or "../foo" should not be normalized + if (!starts_with_slash) { + return pathname; + } + + // Normalize .. segments while preserving empty segments (from //) + std::vector segments; + std::string::size_type position{1}; + const auto ends_with_slash = + pathname.size() > 1 && pathname[pathname.size() - 1] == '/'; + + while (position < pathname.size()) { + auto segment_end = position; + while (segment_end < pathname.size() && pathname[segment_end] != '/') { + segment_end += 1; + } + + const std::string_view segment{pathname.data() + position, + segment_end - position}; + + if (segment == "..") { + if (!segments.empty() && segments.back() != "..") { + segments.pop_back(); + } else { + segments.emplace_back(segment); + } + } else if (segment == ".") { + // Skip single dot segments + } else { + // Keep all other segments including empty ones + segments.emplace_back(segment); + } + + position = segment_end + 1; + } + + std::string result; + result += '/'; + for (std::size_t index = 0; index < segments.size(); index += 1) { + result += segments[index]; + if (index + 1 < segments.size()) { + result += '/'; + } + } + + if (ends_with_slash && (result.empty() || result[result.size() - 1] != '/')) { + result += '/'; + } + + return result; +} +} // namespace + +auto URLPatternInput::parse(const JSON &input) + -> std::optional { + if (!input.is_object()) { + return std::nullopt; + } + + URLPatternInput result; + + if (input.defines("protocol")) { + const auto &value{input.at("protocol")}; + if (!value.is_string()) { + return std::nullopt; + } + const auto protocol_str = value.to_string(); + if (!is_ascii(protocol_str)) { + return std::nullopt; + } + result.protocol = protocol_str; + } + + if (input.defines("username")) { + const auto &value{input.at("username")}; + if (!value.is_string()) { + return std::nullopt; + } + result.username = value.to_string(); + } + + if (input.defines("password")) { + const auto &value{input.at("password")}; + if (!value.is_string()) { + return std::nullopt; + } + result.password = value.to_string(); + } + + if (input.defines("hostname")) { + const auto &value{input.at("hostname")}; + if (!value.is_string()) { + return std::nullopt; + } + result.hostname = hostname_to_ascii(value.to_string()); + } + + if (input.defines("port")) { + const auto &value{input.at("port")}; + if (!value.is_string()) { + return std::nullopt; + } + const auto canonicalized = canonicalize_port(value.to_string()); + if (!canonicalized.has_value()) { + return std::nullopt; + } + result.port = canonicalized.value(); + } + + if (input.defines("pathname")) { + const auto &value{input.at("pathname")}; + if (!value.is_string()) { + return std::nullopt; + } + const auto decoded = percent_decode(value.to_string()); + result.pathname = normalize_pathname_input(decoded); + } + + if (input.defines("search")) { + const auto &value{input.at("search")}; + if (!value.is_string()) { + return std::nullopt; + } + result.search = value.to_string(); + } + + if (input.defines("hash")) { + const auto &value{input.at("hash")}; + if (!value.is_string()) { + return std::nullopt; + } + result.hash = value.to_string(); + } + + return result; +} + +auto URLPatternInput::parse(const std::string_view input) + -> std::optional { + if (input.empty()) { + return std::nullopt; + } + + URLPatternInput result; + const std::string_view remaining{input}; + std::string::size_type position{0}; + + const auto protocol_end = remaining.find("://"); + const auto colon_pos = remaining.find(':'); + + if (protocol_end == std::string_view::npos) { + if (colon_pos == std::string_view::npos) { + return std::nullopt; + } + + const auto protocol_str = remaining.substr(0, colon_pos); + if (protocol_str.empty() || !is_ascii(std::string{protocol_str})) { + return std::nullopt; + } + result.protocol = std::string{protocol_str}; + + const auto path_str = remaining.substr(colon_pos + 1); + if (!path_str.empty()) { + const auto decoded = percent_decode(std::string{path_str}); + result.pathname = normalize_pathname_input(decoded); + } + + return result; + } + + const auto protocol_str = remaining.substr(0, protocol_end); + if (protocol_str.empty() || !is_ascii(std::string{protocol_str})) { + return std::nullopt; + } + result.protocol = std::string{protocol_str}; + position = protocol_end + 3; + + if (position >= remaining.size()) { + return result; + } + + const auto path_start = remaining.find('/', position); + const auto query_start = remaining.find('?', position); + const auto hash_start = remaining.find('#', position); + + auto authority_end = path_start; + if (query_start != std::string_view::npos && + (authority_end == std::string_view::npos || + query_start < authority_end)) { + authority_end = query_start; + } + if (hash_start != std::string_view::npos && + (authority_end == std::string_view::npos || hash_start < authority_end)) { + authority_end = hash_start; + } + + if (authority_end == std::string_view::npos) { + authority_end = remaining.size(); + } + + const auto authority = remaining.substr(position, authority_end - position); + if (!authority.empty()) { + auto userinfo_end = authority.find('@'); + std::string::size_type host_start = 0; + + if (userinfo_end != std::string_view::npos) { + const auto userinfo = authority.substr(0, userinfo_end); + const auto password_sep = userinfo.find(':'); + + if (password_sep != std::string_view::npos) { + result.username = std::string{userinfo.substr(0, password_sep)}; + result.password = std::string{userinfo.substr(password_sep + 1)}; + } else { + result.username = std::string{userinfo}; + } + + host_start = userinfo_end + 1; + } + + const auto host_part = authority.substr(host_start); + if (!host_part.empty()) { + auto port_sep = host_part.rfind(':'); + + if (host_part[0] == '[') { + const auto bracket_end = host_part.find(']'); + if (bracket_end != std::string_view::npos) { + if (port_sep != std::string_view::npos && port_sep < bracket_end) { + port_sep = std::string_view::npos; + } + } + } + + if (port_sep != std::string_view::npos) { + const auto hostname_str = host_part.substr(0, port_sep); + result.hostname = hostname_to_ascii(std::string{hostname_str}); + const auto port_str = host_part.substr(port_sep + 1); + const auto canonicalized = canonicalize_port(std::string{port_str}); + if (canonicalized.has_value()) { + result.port = canonicalized.value(); + } + } else { + result.hostname = hostname_to_ascii(std::string{host_part}); + } + } + } + + position = authority_end; + + if (path_start != std::string_view::npos && path_start == position) { + auto path_end = query_start; + if (hash_start != std::string_view::npos && + (path_end == std::string_view::npos || hash_start < path_end)) { + path_end = hash_start; + } + if (path_end == std::string_view::npos) { + path_end = remaining.size(); + } + + const auto path_str = remaining.substr(path_start, path_end - path_start); + const auto decoded = percent_decode(std::string{path_str}); + result.pathname = normalize_pathname_input(decoded); + + position = path_end; + } + + if (query_start != std::string_view::npos && query_start == position) { + auto query_end = hash_start; + if (query_end == std::string_view::npos) { + query_end = remaining.size(); + } + + result.search = + std::string{remaining.substr(query_start + 1, query_end - query_start - 1)}; + + position = query_end; + } + + if (hash_start != std::string_view::npos && hash_start == position) { + result.hash = std::string{remaining.substr(hash_start + 1)}; + } + + return result; +} + +auto URLPattern::parse(const std::string_view input) -> URLPattern { + URLPattern pattern; + + if (input.empty()) { + return pattern; + } + + const std::string_view remaining{input}; + std::string::size_type position{0}; + + if (remaining[0] == '/') { + pattern.pathname = URLPatternPathname{std::string{remaining}.c_str()}; + return pattern; + } + + const auto protocol_end = remaining.find("://"); + if (protocol_end != std::string_view::npos) { + const auto protocol_str = remaining.substr(0, protocol_end); + if (!protocol_str.empty()) { + pattern.protocol = URLPatternProtocol{std::string{protocol_str}.c_str()}; + } + position = protocol_end + 3; + } + + if (position >= remaining.size()) { + return pattern; + } + + const auto path_start = remaining.find('/', position); + const auto query_start = remaining.find('?', position); + const auto hash_start = remaining.find('#', position); + + auto authority_end = path_start; + if (query_start != std::string_view::npos && + (authority_end == std::string_view::npos || + query_start < authority_end)) { + authority_end = query_start; + } + if (hash_start != std::string_view::npos && + (authority_end == std::string_view::npos || hash_start < authority_end)) { + authority_end = hash_start; + } + + if (authority_end == std::string_view::npos) { + authority_end = remaining.size(); + } + + const auto authority = remaining.substr(position, authority_end - position); + if (!authority.empty()) { + auto userinfo_end = authority.find('@'); + std::string::size_type host_start = 0; + + if (userinfo_end != std::string_view::npos) { + const auto userinfo = authority.substr(0, userinfo_end); + const auto password_sep = userinfo.find(':'); + + if (password_sep != std::string_view::npos) { + const auto username_str = userinfo.substr(0, password_sep); + if (!username_str.empty()) { + pattern.username = + URLPatternUsername{std::string{username_str}.c_str()}; + } + const auto password_str = userinfo.substr(password_sep + 1); + if (!password_str.empty()) { + pattern.password = + URLPatternPassword{std::string{password_str}.c_str()}; + } + } else if (!userinfo.empty()) { + pattern.username = URLPatternUsername{std::string{userinfo}.c_str()}; + } + + host_start = userinfo_end + 1; + } + + const auto host_part = authority.substr(host_start); + if (!host_part.empty()) { + const auto port_sep = host_part.rfind(':'); + + if (port_sep != std::string_view::npos) { + const auto hostname_str = host_part.substr(0, port_sep); + if (!hostname_str.empty()) { + pattern.hostname = + URLPatternHostname{std::string{hostname_str}.c_str()}; + } + const auto port_str = host_part.substr(port_sep + 1); + if (!port_str.empty()) { + pattern.port = URLPatternPort{std::string{port_str}.c_str()}; + } + } else { + pattern.hostname = URLPatternHostname{std::string{host_part}.c_str()}; + } + } + } + + position = authority_end; + + if (path_start != std::string_view::npos && path_start == position) { + auto path_end = query_start; + if (hash_start != std::string_view::npos && + (path_end == std::string_view::npos || hash_start < path_end)) { + path_end = hash_start; + } + if (path_end == std::string_view::npos) { + path_end = remaining.size(); + } + + const auto path_str = remaining.substr(path_start, path_end - path_start); + if (!path_str.empty()) { + pattern.pathname = URLPatternPathname{std::string{path_str}.c_str()}; + } + + position = path_end; + } + + if (query_start != std::string_view::npos && query_start == position) { + auto query_end = hash_start; + if (query_end == std::string_view::npos) { + query_end = remaining.size(); + } + + const auto query_str = + remaining.substr(query_start + 1, query_end - query_start - 1); + if (!query_str.empty()) { + pattern.search = URLPatternSearch{std::string{query_str}.c_str()}; + } + + position = query_end; + } + + if (hash_start != std::string_view::npos && hash_start == position) { + const auto hash_str = remaining.substr(hash_start + 1); + if (!hash_str.empty()) { + pattern.hash = URLPatternHash{std::string{hash_str}.c_str()}; + } + } + + return pattern; +} + +auto URLPattern::parse(const JSON &input) -> std::optional { + if (!input.is_object()) { + return std::nullopt; + } + + URLPattern pattern; + + try { + if (input.defines("protocol")) { + const auto &value{input.at("protocol")}; + if (!value.is_string()) { + return std::nullopt; + } + pattern.protocol = URLPatternProtocol{value.to_string().c_str()}; + } + + if (input.defines("username")) { + const auto &value{input.at("username")}; + if (!value.is_string()) { + return std::nullopt; + } + const auto decoded = percent_decode(value.to_string()); + pattern.username = URLPatternUsername{decoded.c_str()}; + } + + if (input.defines("password")) { + const auto &value{input.at("password")}; + if (!value.is_string()) { + return std::nullopt; + } + const auto decoded = percent_decode(value.to_string()); + pattern.password = URLPatternPassword{decoded.c_str()}; + } + + if (input.defines("hostname")) { + const auto &value{input.at("hostname")}; + if (!value.is_string()) { + return std::nullopt; + } + const auto canonicalized = + canonicalize_hostname_pattern(value.to_string()); + pattern.hostname = URLPatternHostname{canonicalized.c_str()}; + } + + if (input.defines("port")) { + const auto &value{input.at("port")}; + if (!value.is_string()) { + return std::nullopt; + } + pattern.port = URLPatternPort{value.to_string().c_str()}; + } + + if (input.defines("pathname")) { + const auto &value{input.at("pathname")}; + if (!value.is_string()) { + return std::nullopt; + } + const auto decoded = percent_decode(value.to_string()); + const auto normalized = normalize_pathname_pattern(decoded); + pattern.pathname = URLPatternPathname{normalized.c_str()}; + } + + if (input.defines("search")) { + const auto &value{input.at("search")}; + if (!value.is_string()) { + return std::nullopt; + } + const auto decoded = percent_decode(value.to_string()); + pattern.search = URLPatternSearch{decoded.c_str()}; + } + + if (input.defines("hash")) { + const auto &value{input.at("hash")}; + if (!value.is_string()) { + return std::nullopt; + } + const auto decoded = percent_decode(value.to_string()); + pattern.hash = URLPatternHash{decoded.c_str()}; + } + } catch (const URLPatternParseError &) { + return std::nullopt; + } + + return pattern; +} + +auto URLPattern::match(const URLPatternInput &input) const -> URLPatternResult { + URLPatternResult result; + result.protocol = this->protocol.match(input.protocol); + result.username = this->username.match(input.username); + result.password = this->password.match(input.password); + result.hostname = this->hostname.match(input.hostname); + result.port = this->port.match(input.port); + result.pathname = this->pathname.match(input.pathname); + result.search = this->search.match(input.search); + result.hash = this->hash.match(input.hash); + + if (!result.protocol.has_value() || !result.username.has_value() || + !result.password.has_value() || !result.hostname.has_value() || + !result.port.has_value() || !result.pathname.has_value() || + !result.search.has_value() || !result.hash.has_value()) { + return URLPatternResult{}; + } + + return result; +} + +auto URLPattern::operator<=>(const URLPattern &other) const + -> std::strong_ordering { + if (auto cmp = this->protocol <=> other.protocol; + cmp != std::strong_ordering::equal) { + return cmp; + } + if (auto cmp = this->username <=> other.username; + cmp != std::strong_ordering::equal) { + return cmp; + } + if (auto cmp = this->password <=> other.password; + cmp != std::strong_ordering::equal) { + return cmp; + } + if (auto cmp = this->hostname <=> other.hostname; + cmp != std::strong_ordering::equal) { + return cmp; + } + if (auto cmp = this->port <=> other.port; + cmp != std::strong_ordering::equal) { + return cmp; + } + if (auto cmp = this->pathname <=> other.pathname; + cmp != std::strong_ordering::equal) { + return cmp; + } + if (auto cmp = this->search <=> other.search; + cmp != std::strong_ordering::equal) { + return cmp; + } + return this->hash <=> other.hash; +} + +} // namespace sourcemeta::core diff --git a/src/core/urlpattern/urlpattern_component.cc b/src/core/urlpattern/urlpattern_component.cc new file mode 100644 index 000000000..87a9bf1a8 --- /dev/null +++ b/src/core/urlpattern/urlpattern_component.cc @@ -0,0 +1,3564 @@ +#include + +#include +#include + +#include // pcre2_compile, pcre2_match, etc. + +#include // std::min +#include // assert +#include // std::isalpha, std::isdigit, std::isalnum +#include // std::uint8_t +#include // std::string::size_type +#include // std::unordered_map +#include // std::visit, std::holds_alternative, std::get +#include // std::vector + +namespace { + +auto is_valid_name_start(const char character) -> bool { + const auto byte = static_cast(character); + return std::isalpha(byte) || character == '_' || character == '$' || + byte >= 0x80; +} + +auto is_valid_name_char(const char character) -> bool { + const auto byte = static_cast(character); + return std::isalnum(byte) || character == '_' || character == '$' || + byte >= 0x80; +} + +auto escape_regex_metachar(const char character) -> std::string { + switch (character) { + case '\\': + case '^': + case '$': + case '.': + case '|': + case '?': + case '*': + case '+': + case '(': + case ')': + case '[': + case ']': + case '{': + case '}': + return std::string{"\\"} + character; + default: + // NOLINTNEXTLINE(modernize-return-braced-init-list) + return std::string(1, character); + } +} + +auto escape_regex_string(const std::string &input) -> std::string { + std::string result; + for (const auto character : input) { + result += escape_regex_metachar(character); + } + return result; +} + +// Forward declarations for group token regex building +auto build_token_regex(const sourcemeta::core::URLPatternPart &token, + std::vector &group_names) -> std::string; + +auto build_group_inner_regex( + const sourcemeta::core::URLPatternPartNonGroup &tokens, + std::vector &group_names) -> std::string { + std::string result; + for (const auto &inner_token : tokens) { + if (std::holds_alternative( + inner_token)) { + const auto &char_token = + std::get(inner_token); + result += escape_regex_string(char_token.value); + } else if (std::holds_alternative( + inner_token)) { + const auto ®ex_token = + std::get(inner_token); + result += "(" + regex_token.original_pattern + ")"; + group_names.emplace_back(""); + } else if (std::holds_alternative( + inner_token)) { + const auto &name_token = + std::get(inner_token); + result += "(.+?)"; + group_names.emplace_back(name_token.value); + } else if (std::holds_alternative< + sourcemeta::core::URLPatternPartNameRegExp>(inner_token)) { + const auto &name_regex_token = + std::get(inner_token); + result += "(" + name_regex_token.original_pattern + ")"; + group_names.emplace_back(name_regex_token.value); + } else if (std::holds_alternative( + inner_token)) { + result += "(.*?)"; + group_names.emplace_back(""); + } + } + return result; +} + +auto build_token_regex(const sourcemeta::core::URLPatternPart &token, + std::vector &group_names) -> std::string { + if (std::holds_alternative(token)) { + const auto &char_token = + std::get(token); + return escape_regex_string(char_token.value); + } + + if (std::holds_alternative(token)) { + const auto ®ex_token = + std::get(token); + group_names.emplace_back(""); + return "(" + regex_token.original_pattern + ")"; + } + + if (std::holds_alternative( + token)) { + const auto ®ex_token = + std::get(token); + group_names.emplace_back(""); + return "(" + regex_token.original_pattern + ")?"; + } + + if (std::holds_alternative( + token)) { + const auto ®ex_token = + std::get(token); + group_names.emplace_back(""); + return "(" + regex_token.original_pattern + ")+"; + } + + if (std::holds_alternative( + token)) { + const auto ®ex_token = + std::get(token); + group_names.emplace_back(""); + return "(" + regex_token.original_pattern + ")*"; + } + + if (std::holds_alternative(token)) { + group_names.emplace_back(""); + return "(.*)"; + } + + if (std::holds_alternative( + token)) { + group_names.emplace_back(""); + return "(.*)?"; + } + + if (std::holds_alternative( + token)) { + group_names.emplace_back(""); + return "(.*)+"; + } + + if (std::holds_alternative( + token)) { + group_names.emplace_back(""); + return "(.*)*"; + } + + if (std::holds_alternative(token)) { + const auto &name_token = + std::get(token); + group_names.emplace_back(name_token.value); + // Non-greedy to allow subsequent patterns to match + return "(.+?)"; + } + + if (std::holds_alternative( + token)) { + const auto &name_regex_token = + std::get(token); + group_names.emplace_back(name_regex_token.value); + return "(" + name_regex_token.original_pattern + ")"; + } + + if (std::holds_alternative( + token)) { + const auto &name_token = + std::get(token); + group_names.emplace_back(name_token.value); + // The ? modifier means "optional" (zero or more chars) + return "(.*)"; + } + + if (std::holds_alternative( + token)) { + const auto &name_token = + std::get(token); + group_names.emplace_back(name_token.value); + // The + modifier means "one or more characters", not "repeat the capture" + return "(.+)"; + } + + if (std::holds_alternative( + token)) { + const auto &name_token = + std::get(token); + group_names.emplace_back(name_token.value); + // The * modifier means "zero or more characters", not "repeat the capture" + return "(.*)"; + } + + if (std::holds_alternative(token)) { + const auto &group_token = + std::get(token); + return build_group_inner_regex(group_token.value, group_names); + } + + if (std::holds_alternative( + token)) { + const auto &group_token = + std::get(token); + return "(?:" + build_group_inner_regex(group_token.value, group_names) + + ")?"; + } + + if (std::holds_alternative( + token)) { + const auto &group_token = + std::get(token); + return "(?:" + build_group_inner_regex(group_token.value, group_names) + + ")+"; + } + + if (std::holds_alternative( + token)) { + const auto &group_token = + std::get(token); + return "(?:" + build_group_inner_regex(group_token.value, group_names) + + ")*"; + } + + if (std::holds_alternative( + token)) { + const auto &segment_token = + std::get(token); + std::string result; + for (const auto &inner_token : segment_token.value) { + if (std::holds_alternative( + inner_token)) { + const auto &char_token = + std::get(inner_token); + result += escape_regex_string(char_token.value); + } else if (std::holds_alternative( + inner_token)) { + const auto &name_token = + std::get(inner_token); + group_names.emplace_back(name_token.value); + // Non-greedy, at least one char, to allow subsequent patterns to match + result += "(.+?)"; + } else if (std::holds_alternative< + sourcemeta::core::URLPatternPartNameRegExp>(inner_token)) { + const auto &name_regex_token = + std::get(inner_token); + group_names.emplace_back(name_regex_token.value); + result += "(" + name_regex_token.original_pattern + ")"; + } else if (std::holds_alternative< + sourcemeta::core::URLPatternPartAsterisk>(inner_token)) { + group_names.emplace_back(""); + // Greedy match for asterisk in complex segment + result += "([^/]*)"; + } else if (std::holds_alternative( + inner_token)) { + const auto ®ex_token = + std::get(inner_token); + group_names.emplace_back(""); + result += "(" + regex_token.original_pattern + ")"; + } else if (std::holds_alternative< + sourcemeta::core::URLPatternPartRegExpOptional>( + inner_token)) { + const auto ®ex_token = + std::get( + inner_token); + group_names.emplace_back(""); + result += "(" + regex_token.original_pattern + ")?"; + } else if (std::holds_alternative< + sourcemeta::core::URLPatternPartRegExpMultiple>( + inner_token)) { + const auto ®ex_token = + std::get( + inner_token); + group_names.emplace_back(""); + result += "(" + regex_token.original_pattern + ")+"; + } else if (std::holds_alternative< + sourcemeta::core::URLPatternPartRegExpAsterisk>( + inner_token)) { + const auto ®ex_token = + std::get( + inner_token); + group_names.emplace_back(""); + result += "(" + regex_token.original_pattern + ")*"; + } else if (std::holds_alternative< + sourcemeta::core::URLPatternPartNameOptional>( + inner_token)) { + const auto &name_token = + std::get(inner_token); + group_names.emplace_back(name_token.value); + result += "([^/]*)?"; + } else if (std::holds_alternative< + sourcemeta::core::URLPatternPartNameMultiple>( + inner_token)) { + const auto &name_token = + std::get(inner_token); + group_names.emplace_back(name_token.value); + result += "([^/]*)+"; + } else if (std::holds_alternative< + sourcemeta::core::URLPatternPartNameAsterisk>( + inner_token)) { + const auto &name_token = + std::get(inner_token); + group_names.emplace_back(name_token.value); + result += "([^/]*)*"; + } else if (std::holds_alternative< + sourcemeta::core::URLPatternPartAsteriskOptional>( + inner_token)) { + group_names.emplace_back(""); + // Use (?:([^/]+))? so group is truly unset when nothing matches + result += "(?:([^/]+))?"; + } else if (std::holds_alternative< + sourcemeta::core::URLPatternPartAsteriskMultiple>( + inner_token)) { + group_names.emplace_back(""); + result += "([^/]*)+"; + } else if (std::holds_alternative< + sourcemeta::core::URLPatternPartAsteriskAsterisk>( + inner_token)) { + group_names.emplace_back(""); + result += "([^/]*)*"; + } else if (std::holds_alternative( + inner_token)) { + const auto &group_token = + std::get(inner_token); + result += build_group_inner_regex(group_token.value, group_names); + } else if (std::holds_alternative< + sourcemeta::core::URLPatternPartGroupOptional>( + inner_token)) { + const auto &group_token = + std::get( + inner_token); + result += + "(?:" + build_group_inner_regex(group_token.value, group_names) + + ")?"; + } else if (std::holds_alternative< + sourcemeta::core::URLPatternPartGroupMultiple>( + inner_token)) { + const auto &group_token = + std::get( + inner_token); + result += + "(?:" + build_group_inner_regex(group_token.value, group_names) + + ")+"; + } else if (std::holds_alternative< + sourcemeta::core::URLPatternPartGroupAsterisk>( + inner_token)) { + const auto &group_token = + std::get( + inner_token); + result += + "(?:" + build_group_inner_regex(group_token.value, group_names) + + ")*"; + } + } + return result; + } + + return ""; +} + +auto match_bare_pattern_with_captures( + const std::vector &tokens, + const std::string_view input) + -> std::optional { + std::vector group_names; + std::string regex_pattern = "^"; + for (std::size_t index = 0; index < tokens.size(); index += 1) { + if (index > 0) { + // Add segment delimiter between tokens + regex_pattern += "/"; + } + regex_pattern += build_token_regex(tokens[index], group_names); + } + regex_pattern += "$"; + + int error_code{0}; + PCRE2_SIZE error_offset{0}; + pcre2_code *regex_raw{pcre2_compile( + reinterpret_cast(regex_pattern.c_str()), regex_pattern.size(), + PCRE2_UTF, &error_code, &error_offset, nullptr)}; + + if (regex_raw == nullptr) { + return std::nullopt; + } + + pcre2_match_data *match_data{ + pcre2_match_data_create_from_pattern(regex_raw, nullptr)}; + + const auto match_result{ + pcre2_match(regex_raw, reinterpret_cast(input.data()), + input.size(), 0, 0, match_data, nullptr)}; + + if (match_result < 0) { + pcre2_match_data_free(match_data); + pcre2_code_free(regex_raw); + return std::nullopt; + } + + sourcemeta::core::URLPatternComponentResult result; + const PCRE2_SIZE *ovector = pcre2_get_ovector_pointer(match_data); + + const auto match_count = static_cast(match_result); + std::size_t unnamed_index{0}; + for (std::size_t index = 1; + index < match_count && index <= group_names.size(); index += 1) { + const PCRE2_SIZE start = ovector[2 * index]; + const PCRE2_SIZE end = ovector[2 * index + 1]; + + if (start == PCRE2_UNSET || end == PCRE2_UNSET) { + continue; + } + + const std::string_view captured{input.data() + start, end - start}; + const auto &name = group_names[index - 1]; + if (name.empty()) { + result.insert(captured); + result.insert(std::to_string(unnamed_index), result.size() - 1); + unnamed_index += 1; + } else { + result.insert(name, captured); + } + } + + pcre2_match_data_free(match_data); + pcre2_code_free(regex_raw); + return result; +} + +auto parse_single_token(const char *input) -> sourcemeta::core::URLPatternPart { + const std::string_view input_view{input}; + + enum class State : std::uint8_t { + ReadingChar, + ReadingName, + ReadingRegExp, + AfterBackslash, + ReadingGroup + }; + + auto position = std::string::size_type{0}; + auto current_state = State::ReadingChar; + std::string token_buffer; + std::string name_for_regex; + sourcemeta::core::URLPatternPartGroup current_group; + auto inside_group = false; + + while (position < input_view.size()) { + const auto current = input_view[position]; + + if (current_state == State::ReadingChar) { + if (current == ':') { + if (!token_buffer.empty()) { + return sourcemeta::core::URLPatternPartChar{token_buffer}; + } + current_state = State::ReadingName; + position += 1; + } else if (current == '*') { + if (!token_buffer.empty()) { + return sourcemeta::core::URLPatternPartChar{token_buffer}; + } + const auto is_asterisk_asterisk = + position + 1 < input_view.size() && input_view[position + 1] == '*'; + const auto is_optional = + position + 1 < input_view.size() && input_view[position + 1] == '?'; + const auto is_multiple = + position + 1 < input_view.size() && input_view[position + 1] == '+'; + if (is_asterisk_asterisk) { + if (position + 2 != input_view.size()) { + throw sourcemeta::core::URLPatternParseError{position + 2}; + } + return sourcemeta::core::URLPatternPartAsteriskAsterisk{}; + } else if (is_optional) { + if (position + 2 != input_view.size()) { + throw sourcemeta::core::URLPatternParseError{position + 2}; + } + return sourcemeta::core::URLPatternPartAsteriskOptional{}; + } else if (is_multiple) { + if (position + 2 != input_view.size()) { + throw sourcemeta::core::URLPatternParseError{position + 2}; + } + return sourcemeta::core::URLPatternPartAsteriskMultiple{}; + } else { + if (position + 1 != input_view.size()) { + throw sourcemeta::core::URLPatternParseError{position + 1}; + } + return sourcemeta::core::URLPatternPartAsterisk{}; + } + } else if (current == '(') { + if (!token_buffer.empty()) { + return sourcemeta::core::URLPatternPartChar{token_buffer}; + } + current_state = State::ReadingRegExp; + position += 1; + } else if (current == '{') { + if (!token_buffer.empty()) { + return sourcemeta::core::URLPatternPartChar{token_buffer}; + } + current_group = sourcemeta::core::URLPatternPartGroup{}; + current_state = State::ReadingGroup; + inside_group = true; + position += 1; + } else if (current == '\\') { + current_state = State::AfterBackslash; + position += 1; + } else { + token_buffer += current; + position += 1; + } + } else if (current_state == State::ReadingName) { + if (token_buffer.empty()) { + if (!is_valid_name_start(current)) { + throw sourcemeta::core::URLPatternParseError{position}; + } + token_buffer += current; + position += 1; + } else { + if (current == '}' && inside_group) { + current_group.value.emplace_back( + sourcemeta::core::URLPatternPartName{token_buffer}); + token_buffer.clear(); + const auto is_optional = position + 1 < input_view.size() && + input_view[position + 1] == '?'; + const auto is_multiple = position + 1 < input_view.size() && + input_view[position + 1] == '+'; + const auto is_asterisk = position + 1 < input_view.size() && + input_view[position + 1] == '*'; + if (is_optional) { + if (position + 2 != input_view.size()) { + throw sourcemeta::core::URLPatternParseError{position + 2}; + } + return sourcemeta::core::URLPatternPartGroupOptional{ + .value = current_group.value}; + } else if (is_multiple) { + if (position + 2 != input_view.size()) { + throw sourcemeta::core::URLPatternParseError{position + 2}; + } + return sourcemeta::core::URLPatternPartGroupMultiple{ + .value = current_group.value}; + } else if (is_asterisk) { + if (position + 2 != input_view.size()) { + throw sourcemeta::core::URLPatternParseError{position + 2}; + } + return sourcemeta::core::URLPatternPartGroupAsterisk{ + .value = current_group.value}; + } else { + if (position + 1 != input_view.size()) { + throw sourcemeta::core::URLPatternParseError{position + 1}; + } + return current_group; + } + } else if (current == '(') { + name_for_regex = token_buffer; + token_buffer.clear(); + current_state = State::ReadingRegExp; + position += 1; + } else if (current == '?') { + if (position + 1 != input_view.size()) { + throw sourcemeta::core::URLPatternParseError{position + 1}; + } + return sourcemeta::core::URLPatternPartNameOptional{token_buffer}; + } else if (current == '+') { + if (position + 1 != input_view.size()) { + throw sourcemeta::core::URLPatternParseError{position + 1}; + } + return sourcemeta::core::URLPatternPartNameMultiple{token_buffer}; + } else if (current == '*') { + if (position + 1 != input_view.size()) { + throw sourcemeta::core::URLPatternParseError{position + 1}; + } + return sourcemeta::core::URLPatternPartNameAsterisk{token_buffer}; + } else if (is_valid_name_char(current)) { + token_buffer += current; + position += 1; + } else { + throw sourcemeta::core::URLPatternParseError{position}; + } + } + } else if (current_state == State::ReadingRegExp) { + if (current == '\\') { + token_buffer += current; + current_state = State::AfterBackslash; + position += 1; + } else if (current == ')') { + const auto regex_opt{ + sourcemeta::core::to_regex("^" + token_buffer + "$")}; + if (!regex_opt.has_value()) { + throw sourcemeta::core::URLPatternParseError{position}; + } + const auto ®ex{regex_opt.value()}; + const std::string original_pattern{token_buffer}; + token_buffer.clear(); + const auto is_optional = + position + 1 < input_view.size() && input_view[position + 1] == '?'; + const auto is_multiple = + position + 1 < input_view.size() && input_view[position + 1] == '+'; + const auto is_asterisk = + position + 1 < input_view.size() && input_view[position + 1] == '*'; + if (!name_for_regex.empty()) { + if (is_optional || is_multiple || is_asterisk) { + throw sourcemeta::core::URLPatternParseError{position + 1}; + } + if (position + 1 != input_view.size()) { + throw sourcemeta::core::URLPatternParseError{position + 1}; + } + return sourcemeta::core::URLPatternPartNameRegExp{ + .value = name_for_regex, + .modifier = regex, + .original_pattern = original_pattern}; + } else if (inside_group) { + if (is_optional) { + current_group.value.emplace_back( + sourcemeta::core::URLPatternPartRegExpOptional{ + .value = regex, .original_pattern = original_pattern}); + } else if (is_multiple) { + current_group.value.emplace_back( + sourcemeta::core::URLPatternPartRegExpMultiple{ + .value = regex, .original_pattern = original_pattern}); + } else if (is_asterisk) { + current_group.value.emplace_back( + sourcemeta::core::URLPatternPartRegExpAsterisk{ + .value = regex, .original_pattern = original_pattern}); + } else { + current_group.value.emplace_back( + sourcemeta::core::URLPatternPartRegExp{ + .value = regex, .original_pattern = original_pattern}); + } + if (is_optional || is_multiple || is_asterisk) { + position += 2; + } else { + position += 1; + } + current_state = State::ReadingGroup; + } else { + if (is_optional) { + if (position + 2 != input_view.size()) { + throw sourcemeta::core::URLPatternParseError{position + 2}; + } + return sourcemeta::core::URLPatternPartRegExpOptional{ + .value = regex, .original_pattern = original_pattern}; + } else if (is_multiple) { + if (position + 2 != input_view.size()) { + throw sourcemeta::core::URLPatternParseError{position + 2}; + } + return sourcemeta::core::URLPatternPartRegExpMultiple{ + .value = regex, .original_pattern = original_pattern}; + } else if (is_asterisk) { + if (position + 2 != input_view.size()) { + throw sourcemeta::core::URLPatternParseError{position + 2}; + } + return sourcemeta::core::URLPatternPartRegExpAsterisk{ + .value = regex, .original_pattern = original_pattern}; + } else { + if (position + 1 != input_view.size()) { + throw sourcemeta::core::URLPatternParseError{position + 1}; + } + return sourcemeta::core::URLPatternPartRegExp{ + .value = regex, .original_pattern = original_pattern}; + } + } + } else { + token_buffer += current; + position += 1; + } + } else if (current_state == State::AfterBackslash) { + token_buffer += current; + current_state = inside_group ? State::ReadingGroup : State::ReadingRegExp; + position += 1; + } else if (current_state == State::ReadingGroup) { + if (current == ':') { + if (!token_buffer.empty()) { + current_group.value.emplace_back( + sourcemeta::core::URLPatternPartChar{token_buffer}); + token_buffer.clear(); + } + current_state = State::ReadingName; + position += 1; + } else if (current == '*') { + if (!token_buffer.empty()) { + current_group.value.emplace_back( + sourcemeta::core::URLPatternPartChar{token_buffer}); + token_buffer.clear(); + } + const auto is_asterisk_asterisk = + position + 1 < input_view.size() && input_view[position + 1] == '*'; + const auto is_optional = + position + 1 < input_view.size() && input_view[position + 1] == '?'; + const auto is_multiple = + position + 1 < input_view.size() && input_view[position + 1] == '+'; + if (is_asterisk_asterisk) { + current_group.value.emplace_back( + sourcemeta::core::URLPatternPartAsteriskAsterisk{}); + position += 2; + } else if (is_optional) { + current_group.value.emplace_back( + sourcemeta::core::URLPatternPartAsteriskOptional{}); + position += 2; + } else if (is_multiple) { + current_group.value.emplace_back( + sourcemeta::core::URLPatternPartAsteriskMultiple{}); + position += 2; + } else { + current_group.value.emplace_back( + sourcemeta::core::URLPatternPartAsterisk{}); + position += 1; + } + } else if (current == '(') { + if (!token_buffer.empty()) { + current_group.value.emplace_back( + sourcemeta::core::URLPatternPartChar{token_buffer}); + token_buffer.clear(); + } + current_state = State::ReadingRegExp; + position += 1; + } else if (current == '}') { + if (!token_buffer.empty()) { + current_group.value.emplace_back( + sourcemeta::core::URLPatternPartChar{token_buffer}); + token_buffer.clear(); + } + const auto is_optional = + position + 1 < input_view.size() && input_view[position + 1] == '?'; + const auto is_multiple = + position + 1 < input_view.size() && input_view[position + 1] == '+'; + const auto is_asterisk = + position + 1 < input_view.size() && input_view[position + 1] == '*'; + if (is_optional) { + if (position + 2 != input_view.size()) { + throw sourcemeta::core::URLPatternParseError{position + 2}; + } + return sourcemeta::core::URLPatternPartGroupOptional{ + .value = current_group.value}; + } else if (is_multiple) { + if (position + 2 != input_view.size()) { + throw sourcemeta::core::URLPatternParseError{position + 2}; + } + return sourcemeta::core::URLPatternPartGroupMultiple{ + .value = current_group.value}; + } else if (is_asterisk) { + if (position + 2 != input_view.size()) { + throw sourcemeta::core::URLPatternParseError{position + 2}; + } + return sourcemeta::core::URLPatternPartGroupAsterisk{ + .value = current_group.value}; + } else { + if (position + 1 != input_view.size()) { + throw sourcemeta::core::URLPatternParseError{position + 1}; + } + return current_group; + } + } else if (current == '\\') { + current_state = State::AfterBackslash; + position += 1; + } else { + token_buffer += current; + position += 1; + } + } + } + + if (current_state == State::ReadingChar) { + return sourcemeta::core::URLPatternPartChar{token_buffer}; + } else if (current_state == State::ReadingName) { + if (!token_buffer.empty()) { + return sourcemeta::core::URLPatternPartName{token_buffer}; + } + throw sourcemeta::core::URLPatternParseError{position}; + } else if (current_state == State::ReadingRegExp || + current_state == State::ReadingGroup) { + throw sourcemeta::core::URLPatternParseError{position}; + } + + return sourcemeta::core::URLPatternPartChar{""}; +} + +} // namespace + +namespace sourcemeta::core { + +auto URLPatternComponentResult::size() const noexcept -> std::size_t { + return this->positions.size(); +} + +auto URLPatternComponentResult::at(const std::size_t index) const noexcept + -> std::string_view { + assert(index < this->positions.size()); + return this->positions.at(index); +} + +auto URLPatternComponentResult::at(const std::string_view name) const noexcept + -> std::optional { + const auto iterator{this->names.find(std::string{name})}; + if (iterator == this->names.end()) { + return std::nullopt; + } + return this->at(iterator->second); +} + +auto URLPatternComponentResult::insert(const std::string_view value) -> void { + this->positions.push_back(value); +} + +auto URLPatternComponentResult::insert(const std::string_view name, + const std::string_view value) -> void { + this->insert(value); + this->names.emplace(std::string{name}, this->positions.size() - 1); +} + +auto URLPatternComponentResult::insert(const std::string_view name, + const std::size_t index) -> void { + this->names.emplace(std::string{name}, index); +} + +namespace { + +auto match_single_part(const URLPatternPart &value, + const std::string_view input) + -> std::optional { + URLPatternComponentResult result; + + const auto token_matches = std::visit( + [&input](const auto &token_value) -> bool { + return token_value.matches(input); + }, + value); + + if (!token_matches) { + return std::nullopt; + } + + if (std::holds_alternative(value) || + std::holds_alternative(value) || + std::holds_alternative(value) || + std::holds_alternative(value) || + std::holds_alternative(value)) { + return result; + } + + if (std::holds_alternative(value)) { + const auto &name_token = std::get(value); + result.insert(name_token.value, input); + } else if (std::holds_alternative(value)) { + const auto &name_regex_token = std::get(value); + result.insert(name_regex_token.value, input); + } else if (std::holds_alternative(value)) { + const auto &name_optional_token = + std::get(value); + result.insert(name_optional_token.value, input); + } else if (std::holds_alternative(value)) { + const auto &name_multiple_token = + std::get(value); + result.insert(name_multiple_token.value, input); + } else if (std::holds_alternative(value)) { + const auto &name_asterisk_token = + std::get(value); + result.insert(name_asterisk_token.value, input); + } else { + result.insert(input); + } + + return result; +} + +} // namespace + +namespace { + +// WHATWG URLPattern comparison: part type indices +// See https://urlpattern.spec.whatwg.org/#compare-component-part-lists +// Lower index = more specific = sorts first +enum class PartType : std::uint8_t { + FixedText = 0, // Literal characters + Regexp = 1, // Custom regex pattern + SegmentWildcard = 2, // Named parameter like :id + FullWildcard = 3 // Asterisk wildcard * +}; + +// WHATWG URLPattern comparison: modifier indices +// Lower index = more specific = sorts first +enum class ModifierType : std::uint8_t { + None = 0, // Required, single match + Optional = 1, // ? modifier + OneOrMore = 2, // + modifier + ZeroOrMore = 3 // * modifier +}; + +auto get_part_type(const URLPatternPart &part) -> PartType; +auto get_modifier_type(const URLPatternPart &part) -> ModifierType; +auto get_fixed_text_value(const URLPatternPart &part) -> std::string_view; +auto get_regexp_pattern(const URLPatternPart &part) -> std::string_view; + +template constexpr auto get_nongroup_part_type_impl() -> PartType { + if constexpr (std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v) { + return PartType::Regexp; + } else if constexpr (std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v) { + return PartType::SegmentWildcard; + } else if constexpr (std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v) { + return PartType::FullWildcard; + } else { + return PartType::FixedText; + } +} + +auto get_nongroup_part_type(const auto &part) -> PartType { + using T = std::decay_t; + return get_nongroup_part_type_impl(); +} + +auto get_nongroup_modifier_type(const auto &part) -> ModifierType { + using T = std::decay_t; + if constexpr (std::is_same_v || + std::is_same_v || + std::is_same_v) { + return ModifierType::Optional; + } else if constexpr (std::is_same_v || + std::is_same_v || + std::is_same_v) { + return ModifierType::OneOrMore; + } else if constexpr (std::is_same_v || + std::is_same_v || + std::is_same_v) { + return ModifierType::ZeroOrMore; + } else { + return ModifierType::None; + } +} + +auto get_group_part_type(const URLPatternPartNonGroup &contents) -> PartType { + auto result = PartType::FixedText; + for (const auto &item : contents) { + const auto item_type = std::visit( + [](const auto &p) { return get_nongroup_part_type(p); }, item); + if (static_cast(item_type) > static_cast(result)) { + result = item_type; + } + } + return result; +} + +auto get_part_type(const URLPatternPart &part) -> PartType { + return std::visit( + [](const auto &p) -> PartType { + using T = std::decay_t; + if constexpr (std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v) { + return get_group_part_type(p.value); + } else if constexpr (std::is_same_v) { + auto result = PartType::FixedText; + for (const auto &item : p.value) { + const auto item_type = std::visit( + [](const auto &inner) -> PartType { + using InnerT = std::decay_t; + if constexpr (std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v) { + return get_group_part_type(inner.value); + } else { + return get_nongroup_part_type(inner); + } + }, + item); + if (static_cast(item_type) > static_cast(result)) { + result = item_type; + } + } + return result; + } else { + return get_nongroup_part_type(p); + } + }, + part); +} + +auto get_modifier_type(const URLPatternPart &part) -> ModifierType { + return std::visit( + [](const auto &p) -> ModifierType { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return ModifierType::Optional; + } else if constexpr (std::is_same_v) { + return ModifierType::OneOrMore; + } else if constexpr (std::is_same_v) { + return ModifierType::ZeroOrMore; + } else if constexpr (std::is_same_v || + std::is_same_v) { + return ModifierType::None; + } else { + return get_nongroup_modifier_type(p); + } + }, + part); +} + +auto get_fixed_text_value(const URLPatternPart &part) -> std::string_view { + return std::visit( + [](const auto &p) -> std::string_view { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return p.value; + } else if constexpr (std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v) { + if (p.value.size() == 1) { + if (const auto *char_part = + std::get_if(&p.value[0])) { + return char_part->value; + } + } + return {}; + } else { + return {}; + } + }, + part); +} + +auto get_regexp_pattern(const URLPatternPart &part) -> std::string_view { + return std::visit( + [](const auto &p) -> std::string_view { + using T = std::decay_t; + if constexpr (std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v) { + return p.original_pattern; + } else { + return {}; + } + }, + part); +} + +auto compare_urlpattern_parts(const URLPatternPart &left, + const URLPatternPart &right) + -> std::strong_ordering; +auto is_group_type(const URLPatternPart &part) -> bool; +auto get_inner_segment_prefix(const URLPatternPart &part) -> bool; +auto get_inner_segment_suffix(const URLPatternPart &part) -> bool; + +auto compare_urlpattern_parts_vector(const std::vector &left, + const std::vector &right) + -> std::strong_ordering { + const auto min_size = std::min(left.size(), right.size()); + + for (std::size_t index{0}; index < min_size; index++) { + const auto comparison{compare_urlpattern_parts(left[index], right[index])}; + if (comparison != std::strong_ordering::equal) { + return comparison; + } + } + + if (left.size() != right.size()) { + return left.size() <=> right.size(); + } + + return std::strong_ordering::equal; +} + +auto is_group_type(const URLPatternPart &part) -> bool { + return std::holds_alternative(part) || + std::holds_alternative(part) || + std::holds_alternative(part) || + std::holds_alternative(part); +} + +auto get_inner_segment_prefix(const URLPatternPart &part) -> bool { + return std::visit( + [](const auto &p) -> bool { + using T = std::decay_t; + if constexpr (std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v) { + return p.has_inner_segment_prefix; + } else { + return false; + } + }, + part); +} + +auto get_inner_segment_suffix(const URLPatternPart &part) -> bool { + return std::visit( + [](const auto &p) -> bool { + using T = std::decay_t; + if constexpr (std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v) { + return p.has_inner_segment_suffix; + } else { + return false; + } + }, + part); +} + +auto compare_urlpattern_parts(const URLPatternPart &left, + const URLPatternPart &right) + -> std::strong_ordering { + const auto left_type = get_part_type(left); + const auto right_type = get_part_type(right); + + if (left_type != right_type) { + return static_cast(right_type) <=> static_cast(left_type); + } + + if (left_type == PartType::FixedText) { + const auto left_value = get_fixed_text_value(left); + const auto right_value = get_fixed_text_value(right); + if (left_value != right_value) { + return left_value <=> right_value; + } + } + + if (left_type == PartType::Regexp) { + const auto left_pattern = get_regexp_pattern(left); + const auto right_pattern = get_regexp_pattern(right); + if (left_pattern.size() != right_pattern.size()) { + return left_pattern.size() <=> right_pattern.size(); + } + } + + if (left_type == PartType::SegmentWildcard) { + const auto left_is_group = is_group_type(left); + const auto right_is_group = is_group_type(right); + + if (left_is_group != right_is_group) { + auto group_is_more_specific = [](const URLPatternPart &part) -> bool { + const auto has_prefix = get_inner_segment_prefix(part); + const auto has_suffix = get_inner_segment_suffix(part); + return !has_prefix || has_suffix; + }; + + if (left_is_group && group_is_more_specific(left)) { + return std::strong_ordering::greater; + } + if (right_is_group && group_is_more_specific(right)) { + return std::strong_ordering::less; + } + } + } + + const auto left_modifier = get_modifier_type(left); + const auto right_modifier = get_modifier_type(right); + + auto modifier_sort_value = [](ModifierType mod) -> int { + switch (mod) { + case ModifierType::None: + return 0; + case ModifierType::OneOrMore: + return 1; + case ModifierType::Optional: + return 2; + case ModifierType::ZeroOrMore: + return 3; + } + return 0; + }; + + return modifier_sort_value(right_modifier) <=> + modifier_sort_value(left_modifier); +} + +} // namespace + +auto URLPatternProtocol::match(const std::string_view input) const + -> std::optional { + return match_single_part(this->value, input); +} + +auto URLPatternUsername::match(const std::string_view input) const + -> std::optional { + return match_single_part(this->value, input); +} + +auto URLPatternPassword::match(const std::string_view input) const + -> std::optional { + return match_single_part(this->value, input); +} + +auto URLPatternPort::match(const std::string_view input) const + -> std::optional { + return match_single_part(this->value, input); +} + +auto URLPatternSearch::match(const std::string_view input) const + -> std::optional { + return match_single_part(this->value, input); +} + +auto URLPatternHash::match(const std::string_view input) const + -> std::optional { + return match_single_part(this->value, input); +} + +auto URLPatternHostname::match(const std::string_view input) const + -> std::optional { + URLPatternComponentResult result; + auto input_position = std::string::size_type{0}; + auto pattern_position = std::size_t{0}; + auto just_consumed_delimiter = true; + + while (pattern_position < this->value.size()) { + const auto &token = this->value[pattern_position]; + const auto is_optional = + std::holds_alternative(token) || + std::holds_alternative(token) || + std::holds_alternative(token) || + std::holds_alternative(token); + const auto is_multiple = + std::holds_alternative(token); + const auto is_regex_multiple = + std::holds_alternative(token); + const auto is_regex_asterisk = + std::holds_alternative(token); + const auto is_asterisk_multiple = + std::holds_alternative(token); + const auto is_asterisk_asterisk = + std::holds_alternative(token); + const auto is_name_asterisk = + std::holds_alternative(token); + const auto is_group = std::holds_alternative(token); + const auto is_group_optional = + std::holds_alternative(token); + const auto is_group_multiple = + std::holds_alternative(token); + const auto is_group_asterisk = + std::holds_alternative(token); + const auto is_complex_segment = + std::holds_alternative(token); + const auto is_asterisk = + std::holds_alternative(token); + + // When the pattern is just a single asterisk (default wildcard), + // it should consume all remaining input + if (is_asterisk && this->value.size() == 1) { + if (input_position >= input.size()) { + pattern_position += 1; + break; + } + const auto remaining_host = input.substr(input_position); + if (!remaining_host.empty()) { + result.insert(remaining_host); + } + input_position = input.size(); + pattern_position += 1; + break; + } + + if (is_asterisk_asterisk) { + if (input_position >= input.size()) { + pattern_position += 1; + break; + } + const auto remaining_host = input.substr(input_position); + if (!remaining_host.empty()) { + result.insert(remaining_host); + } + input_position = input.size(); + pattern_position += 1; + break; + } + + if (is_regex_asterisk) { + if (input_position >= input.size()) { + pattern_position += 1; + break; + } + const auto remaining_host = input.substr(input_position); + if (!remaining_host.empty()) { + const auto ®ex_asterisk_token = + std::get(token); + if (!regex_asterisk_token.matches(remaining_host)) { + return std::nullopt; + } + result.insert(remaining_host); + } + input_position = input.size(); + pattern_position += 1; + break; + } + + if (is_asterisk_multiple) { + if (input_position >= input.size()) { + return std::nullopt; + } + const auto remaining_host = input.substr(input_position); + if (remaining_host.empty()) { + return std::nullopt; + } + result.insert(remaining_host); + input_position = input.size(); + pattern_position += 1; + break; + } + + if (is_regex_multiple) { + if (input_position >= input.size()) { + return std::nullopt; + } + const auto remaining_host = input.substr(input_position); + if (remaining_host.empty()) { + return std::nullopt; + } + const auto ®ex_multiple_token = + std::get(token); + if (!regex_multiple_token.matches(remaining_host)) { + return std::nullopt; + } + result.insert(remaining_host); + input_position = input.size(); + pattern_position += 1; + break; + } + + if (is_multiple) { + if (input_position >= input.size()) { + return std::nullopt; + } + const auto remaining_host = input.substr(input_position); + if (remaining_host.empty()) { + return std::nullopt; + } + const auto &multiple_token = std::get(token); + result.insert(multiple_token.value, remaining_host); + input_position = input.size(); + pattern_position += 1; + break; + } + + if (is_name_asterisk) { + if (input_position >= input.size()) { + pattern_position += 1; + break; + } + const auto remaining_host = input.substr(input_position); + if (!remaining_host.empty()) { + const auto &asterisk_token = + std::get(token); + result.insert(asterisk_token.value, remaining_host); + } + input_position = input.size(); + pattern_position += 1; + break; + } + + if (is_group || is_group_optional || is_group_multiple || + is_group_asterisk) { + const auto &group_tokens = + [&token, is_group, is_group_optional, + is_group_multiple]() -> const URLPatternPartNonGroup & { + if (is_group) { + return std::get(token).value; + } else if (is_group_optional) { + return std::get(token).value; + } else if (is_group_multiple) { + return std::get(token).value; + } else { + return std::get(token).value; + } + }(); + + auto group_match_count = std::size_t{0}; + auto current_input_position = input_position; + + while (true) { + + auto group_input_position = current_input_position; + auto group_token_index = std::size_t{0}; + auto group_result_positions = std::vector{}; + auto group_result_names = + std::unordered_map{}; + auto group_matched = true; + + while (group_token_index < group_tokens.size()) { + if (group_input_position >= input.size()) { + group_matched = false; + break; + } + + auto group_segment_start = group_input_position; + auto temp_group_position = group_input_position; + while (temp_group_position < input.size() && + input[temp_group_position] != '.') { + temp_group_position += 1; + } + const auto group_segment = input.substr( + group_segment_start, temp_group_position - group_segment_start); + + const auto group_token_matches = std::visit( + [&group_segment](const auto &group_token_value) -> bool { + return group_token_value.matches(group_segment); + }, + group_tokens[group_token_index]); + + if (!group_token_matches) { + group_matched = false; + break; + } + + const auto needs_capture = std::visit( + [](const auto &group_token_value) -> bool { + using T = std::decay_t; + return std::is_same_v || + std::is_same_v || + std::is_same_v; + }, + group_tokens[group_token_index]); + + if (needs_capture && !group_segment.empty()) { + const auto &group_tok = group_tokens[group_token_index]; + const auto &token_name = [&group_tok]() -> const std::string & { + if (std::holds_alternative(group_tok)) { + return std::get(group_tok).value; + } else if (std::holds_alternative( + group_tok)) { + return std::get(group_tok).value; + } else { + return std::get(group_tok).value; + } + }(); + const auto position_index = + result.size() + group_result_positions.size(); + group_result_positions.emplace_back(group_segment); + group_result_names.emplace(token_name, position_index); + } + + group_input_position = temp_group_position; + if (group_input_position < input.size() && + input[group_input_position] == '.') { + group_input_position += 1; + } + group_token_index += 1; + } + + if (group_matched && group_token_index == group_tokens.size()) { + for (const auto &pos : group_result_positions) { + result.insert(pos); + } + for (const auto &[name, index] : group_result_names) { + result.insert(name, index); + } + current_input_position = group_input_position; + group_match_count += 1; + + if (is_group || is_group_optional) { + break; + } + } else { + break; + } + } + + if (is_group_multiple && group_match_count == 0) { + return std::nullopt; + } + if (is_group && group_match_count == 0) { + return std::nullopt; + } + input_position = current_input_position; + pattern_position += 1; + just_consumed_delimiter = + (input_position > 0 && input_position <= input.size() && + input[input_position - 1] == '.'); + continue; + } + + if (input_position >= input.size() && !just_consumed_delimiter) { + if (is_optional) { + pattern_position += 1; + continue; + } + return std::nullopt; + } + + just_consumed_delimiter = false; + + auto segment_start = input_position; + auto temp_input_position = input_position; + while (temp_input_position < input.size() && + input[temp_input_position] != '.') { + temp_input_position += 1; + } + const auto segment = + input.substr(segment_start, temp_input_position - segment_start); + + if (is_optional && segment.empty()) { + pattern_position += 1; + continue; + } + + if (is_optional && pattern_position + 1 < this->value.size()) { + const auto &next_token = this->value[pattern_position + 1]; + if (std::holds_alternative(next_token)) { + const auto &char_token = std::get(next_token); + if (char_token.value == segment) { + pattern_position += 1; + continue; + } + } + } + + input_position = temp_input_position; + + if (is_complex_segment) { + const auto &complex_token = std::get(token); + + auto segment_position = std::string_view::size_type{0}; + auto complex_token_index = std::size_t{0}; + + while (complex_token_index < complex_token.value.size()) { + const auto &sub_token = complex_token.value[complex_token_index]; + + const auto is_sub_char = + std::holds_alternative(sub_token); + const auto is_sub_name = + std::holds_alternative(sub_token); + const auto is_sub_asterisk = + std::holds_alternative(sub_token); + const auto is_sub_group = + std::holds_alternative(sub_token) || + std::holds_alternative(sub_token) || + std::holds_alternative(sub_token) || + std::holds_alternative(sub_token); + + if (is_sub_char) { + const auto &char_token = std::get(sub_token); + if (segment_position + char_token.value.size() > segment.size() || + segment.substr(segment_position, char_token.value.size()) != + char_token.value) { + return std::nullopt; + } + segment_position += char_token.value.size(); + } else if (is_sub_asterisk) { + auto asterisk_end = segment_position; + if (complex_token_index + 1 < complex_token.value.size()) { + const auto &next_token = + complex_token.value[complex_token_index + 1]; + if (std::holds_alternative(next_token)) { + const auto &next_char = std::get(next_token); + const auto found_pos = + segment.find(next_char.value, segment_position); + if (found_pos == std::string_view::npos) { + return std::nullopt; + } + asterisk_end = found_pos; + } else { + asterisk_end = segment.size(); + } + } else { + asterisk_end = segment.size(); + } + + const auto captured_value = + segment.substr(segment_position, asterisk_end - segment_position); + result.insert(captured_value); + segment_position = asterisk_end; + } else if (is_sub_name) { + const auto &name_token = std::get(sub_token); + + auto name_end = segment_position; + if (complex_token_index + 1 < complex_token.value.size()) { + const auto &next_token = + complex_token.value[complex_token_index + 1]; + if (std::holds_alternative(next_token)) { + const auto &next_char = std::get(next_token); + const auto found_pos = + segment.find(next_char.value, segment_position); + if (found_pos == std::string_view::npos) { + return std::nullopt; + } + name_end = found_pos; + } else if (std::holds_alternative( + next_token) || + std::holds_alternative( + next_token) || + std::holds_alternative( + next_token) || + std::holds_alternative( + next_token)) { + const auto is_next_group_optional = + std::holds_alternative( + next_token); + const auto is_next_group_multiple = + std::holds_alternative( + next_token); + const auto is_next_group_asterisk = + std::holds_alternative( + next_token); + + const auto &next_group_tokens = + [&next_token, is_next_group_optional, is_next_group_multiple, + is_next_group_asterisk]() -> const URLPatternPartNonGroup & { + if (is_next_group_optional) { + return std::get(next_token) + .value; + } else if (is_next_group_multiple) { + return std::get(next_token) + .value; + } else if (is_next_group_asterisk) { + return std::get(next_token) + .value; + } else { + return std::get(next_token).value; + } + }(); + + if (!next_group_tokens.empty() && + std::holds_alternative( + next_group_tokens[0])) { + const auto &group_first_char = + std::get(next_group_tokens[0]); + const auto found_pos = + segment.find(group_first_char.value, segment_position); + if (found_pos == std::string_view::npos) { + return std::nullopt; + } + name_end = found_pos; + } else { + name_end = segment.size(); + } + } else { + name_end = segment.size(); + } + } else { + name_end = segment.size(); + } + + const auto captured_value = + segment.substr(segment_position, name_end - segment_position); + + if (captured_value.empty()) { + return std::nullopt; + } + + result.insert(name_token.value, captured_value); + segment_position = name_end; + } else if (is_sub_group) { + const auto is_sub_group_optional = + std::holds_alternative(sub_token); + const auto is_sub_group_multiple = + std::holds_alternative(sub_token); + const auto is_sub_group_asterisk = + std::holds_alternative(sub_token); + + const auto &group_tokens = + [&sub_token, is_sub_group_optional, is_sub_group_multiple, + is_sub_group_asterisk]() -> const URLPatternPartNonGroup & { + if (is_sub_group_optional) { + return std::get(sub_token).value; + } else if (is_sub_group_multiple) { + return std::get(sub_token).value; + } else if (is_sub_group_asterisk) { + return std::get(sub_token).value; + } else { + return std::get(sub_token).value; + } + }(); + + auto group_match_count = std::size_t{0}; + + while (true) { + auto group_segment_position = segment_position; + auto group_matched = true; + + for (const auto &group_token : group_tokens) { + const auto is_group_char = + std::holds_alternative(group_token); + const auto is_group_name = + std::holds_alternative(group_token); + + if (is_group_char) { + const auto &char_token = + std::get(group_token); + if (group_segment_position + char_token.value.size() > + segment.size() || + segment.substr(group_segment_position, + char_token.value.size()) != + char_token.value) { + group_matched = false; + break; + } + group_segment_position += char_token.value.size(); + } else if (is_group_name) { + const auto &name_token = + std::get(group_token); + auto name_end = group_segment_position; + + if (complex_token_index + 1 < complex_token.value.size()) { + const auto &next_token = + complex_token.value[complex_token_index + 1]; + if (std::holds_alternative(next_token)) { + const auto &next_char = + std::get(next_token); + const auto found_pos = + segment.find(next_char.value, group_segment_position); + if (found_pos == std::string_view::npos) { + group_matched = false; + break; + } + name_end = found_pos; + } else { + name_end = segment.size(); + } + } else { + name_end = segment.size(); + } + + const auto captured_value = segment.substr( + group_segment_position, name_end - group_segment_position); + if (captured_value.empty()) { + group_matched = false; + break; + } + + result.insert(name_token.value, captured_value); + group_segment_position = name_end; + } + } + + if (group_matched) { + segment_position = group_segment_position; + group_match_count += 1; + if (!is_sub_group_multiple && !is_sub_group_asterisk) { + break; + } + } else { + break; + } + } + + if (is_sub_group_multiple && group_match_count == 0) { + return std::nullopt; + } + if (!is_sub_group_optional && !is_sub_group_multiple && + !is_sub_group_asterisk && group_match_count == 0) { + return std::nullopt; + } + } + + complex_token_index += 1; + } + + if (segment_position != segment.size()) { + return std::nullopt; + } + } else { + const auto token_matches = std::visit( + [&segment](const auto &token_value) -> bool { + return token_value.matches(segment); + }, + token); + + if (!token_matches) { + return std::nullopt; + } + + if (!std::holds_alternative(token)) { + if (std::holds_alternative(token)) { + const auto &name_token = std::get(token); + result.insert(name_token.value, segment); + } else if (std::holds_alternative(token)) { + const auto &name_regex_token = + std::get(token); + result.insert(name_regex_token.value, segment); + } else if (std::holds_alternative(token)) { + const auto &name_optional_token = + std::get(token); + result.insert(name_optional_token.value, segment); + } else { + result.insert(segment); + } + } + } + + if (input_position < input.size()) { + input_position += 1; + just_consumed_delimiter = true; + } + pattern_position += 1; + } + + if (input_position < input.size()) { + return std::nullopt; + } + + return result; +} + +auto URLPatternPathname::match(const std::string_view input) const + -> std::optional { + // An empty input matches only if the pattern is a single asterisk (wildcard) + if (input.empty()) { + if (this->value.size() == 1 && + std::holds_alternative(this->value[0])) { + return URLPatternComponentResult{}; + } + return std::nullopt; + } + + if (input[0] != '/') { + if (!this->is_bare_pattern) { + return std::nullopt; + } + + return match_bare_pattern_with_captures(this->value, input); + } + + if (this->is_bare_pattern) { + return std::nullopt; + } + + URLPatternComponentResult result; + auto input_position = std::string::size_type{1}; + auto pattern_position = std::size_t{0}; + auto just_consumed_delimiter = true; + + while (pattern_position < this->value.size()) { + const auto &token = this->value[pattern_position]; + const auto is_optional = + std::holds_alternative(token) || + std::holds_alternative(token) || + std::holds_alternative(token) || + std::holds_alternative(token); + const auto is_multiple = + std::holds_alternative(token); + const auto is_regex_multiple = + std::holds_alternative(token); + const auto is_regex_asterisk = + std::holds_alternative(token); + const auto is_asterisk_multiple = + std::holds_alternative(token); + const auto is_asterisk_asterisk = + std::holds_alternative(token); + const auto is_name_asterisk = + std::holds_alternative(token); + const auto is_group = std::holds_alternative(token); + const auto is_group_optional = + std::holds_alternative(token); + const auto is_group_multiple = + std::holds_alternative(token); + const auto is_group_asterisk = + std::holds_alternative(token); + const auto is_complex_segment = + std::holds_alternative(token); + + if (is_asterisk_asterisk && pattern_position + 1 == this->value.size()) { + const auto remaining_path = input.substr(input_position); + const bool should_capture = + !remaining_path.empty() || + (just_consumed_delimiter && pattern_position > 0); + if (should_capture) { + result.insert(remaining_path); + input_position = input.size(); + } + pattern_position += 1; + break; + } + + if (is_regex_asterisk && pattern_position + 1 == this->value.size()) { + const auto remaining_path = input.substr(input_position); + const bool should_capture = + !remaining_path.empty() || + (just_consumed_delimiter && pattern_position > 0); + if (should_capture) { + const auto ®ex_asterisk_token = + std::get(token); + if (!regex_asterisk_token.matches(remaining_path)) { + return std::nullopt; + } + result.insert(remaining_path); + input_position = input.size(); + } + pattern_position += 1; + break; + } + + if (is_asterisk_multiple && pattern_position + 1 == this->value.size()) { + const auto remaining_path = input.substr(input_position); + const bool should_capture = + !remaining_path.empty() || + (just_consumed_delimiter && pattern_position > 0); + if (!should_capture) { + return std::nullopt; + } + result.insert(remaining_path); + input_position = input.size(); + pattern_position += 1; + break; + } + + if (is_regex_multiple && pattern_position + 1 == this->value.size()) { + const auto remaining_path = input.substr(input_position); + const bool should_capture = + !remaining_path.empty() || + (just_consumed_delimiter && pattern_position > 0); + if (!should_capture) { + return std::nullopt; + } + const auto ®ex_multiple_token = + std::get(token); + if (!regex_multiple_token.matches(remaining_path)) { + return std::nullopt; + } + result.insert(remaining_path); + input_position = input.size(); + pattern_position += 1; + break; + } + + if (is_multiple) { + if (input_position >= input.size()) { + return std::nullopt; + } + const auto remaining_path = input.substr(input_position); + if (remaining_path.empty()) { + return std::nullopt; + } + const auto &multiple_token = std::get(token); + result.insert(multiple_token.value, remaining_path); + input_position = input.size(); + pattern_position += 1; + break; + } + + if (is_name_asterisk) { + if (input_position >= input.size()) { + if (just_consumed_delimiter && pattern_position > 0) { + return std::nullopt; + } + pattern_position += 1; + break; + } + const auto remaining_path = input.substr(input_position); + if (!remaining_path.empty()) { + const auto &asterisk_token = + std::get(token); + result.insert(asterisk_token.value, remaining_path); + } + input_position = input.size(); + pattern_position += 1; + break; + } + + const auto is_plain_regex = + std::holds_alternative(token); + const auto is_plain_asterisk = + std::holds_alternative(token); + const auto is_optional_regex = + std::holds_alternative(token); + const auto is_optional_asterisk = + std::holds_alternative(token); + const auto is_named_regex = + std::holds_alternative(token); + + if (is_plain_regex && pattern_position + 1 == this->value.size()) { + if (input_position >= input.size() && !just_consumed_delimiter) { + return std::nullopt; + } + const auto remaining_path = input.substr(input_position); + const auto ®ex_token = std::get(token); + if (!regex_token.matches(remaining_path)) { + return std::nullopt; + } + result.insert(remaining_path); + input_position = input.size(); + pattern_position += 1; + break; + } + + if (is_named_regex && pattern_position + 1 == this->value.size()) { + if (input_position >= input.size() && !just_consumed_delimiter) { + return std::nullopt; + } + const auto remaining_path = input.substr(input_position); + const auto ®ex_token = std::get(token); + if (!regex_token.matches(remaining_path)) { + return std::nullopt; + } + result.insert(regex_token.value, remaining_path); + input_position = input.size(); + pattern_position += 1; + break; + } + + if (is_plain_asterisk && pattern_position + 1 == this->value.size()) { + if (input_position >= input.size() && !just_consumed_delimiter) { + return std::nullopt; + } + const auto remaining_path = input.substr(input_position); + result.insert(remaining_path); + input_position = input.size(); + pattern_position += 1; + break; + } + + if (is_optional_regex && pattern_position + 1 == this->value.size()) { + const auto remaining_path = input.substr(input_position); + const bool should_capture = + !remaining_path.empty() || + (just_consumed_delimiter && pattern_position > 0); + if (should_capture) { + const auto ®ex_token = std::get(token); + if (!regex_token.matches(remaining_path)) { + return std::nullopt; + } + result.insert(remaining_path); + input_position = input.size(); + } + pattern_position += 1; + break; + } + + if (is_optional_asterisk && pattern_position + 1 == this->value.size()) { + const auto remaining_path = input.substr(input_position); + const bool should_capture = + !remaining_path.empty() || + (just_consumed_delimiter && pattern_position > 0); + if (should_capture) { + result.insert(remaining_path); + input_position = input.size(); + } + pattern_position += 1; + break; + } + + if (is_group || is_group_optional || is_group_multiple || + is_group_asterisk) { + const auto &group_tokens = + [&token, is_group, is_group_optional, + is_group_multiple]() -> const URLPatternPartNonGroup & { + if (is_group) { + return std::get(token).value; + } else if (is_group_optional) { + return std::get(token).value; + } else if (is_group_multiple) { + return std::get(token).value; + } else { + return std::get(token).value; + } + }(); + + const auto group_has_prefix = get_inner_segment_prefix(token); + + auto group_match_count = std::size_t{0}; + auto current_input_position = input_position; + + while (true) { + auto group_input_position = current_input_position; + + if (group_has_prefix) { + if (group_input_position >= input.size() || + input[group_input_position] != '/') { + break; + } + group_input_position += 1; + } + + auto group_token_index = std::size_t{0}; + auto group_result_positions = std::vector{}; + auto group_result_names = + std::unordered_map{}; + auto group_matched = true; + + while (group_token_index < group_tokens.size()) { + if (group_input_position >= input.size()) { + group_matched = false; + break; + } + + auto group_segment_start = group_input_position; + auto temp_group_position = group_input_position; + while (temp_group_position < input.size() && + input[temp_group_position] != '/') { + temp_group_position += 1; + } + const auto group_segment = input.substr( + group_segment_start, temp_group_position - group_segment_start); + + const auto group_token_matches = std::visit( + [&group_segment](const auto &group_token_value) -> bool { + return group_token_value.matches(group_segment); + }, + group_tokens[group_token_index]); + + if (!group_token_matches) { + group_matched = false; + break; + } + + const auto needs_capture = std::visit( + [](const auto &group_token_value) -> bool { + using T = std::decay_t; + return std::is_same_v || + std::is_same_v || + std::is_same_v; + }, + group_tokens[group_token_index]); + + if (needs_capture && !group_segment.empty()) { + const auto &group_tok = group_tokens[group_token_index]; + const auto &token_name = [&group_tok]() -> const std::string & { + if (std::holds_alternative(group_tok)) { + return std::get(group_tok).value; + } else if (std::holds_alternative( + group_tok)) { + return std::get(group_tok).value; + } else { + return std::get(group_tok).value; + } + }(); + const auto position_index = + result.size() + group_result_positions.size(); + group_result_positions.emplace_back(group_segment); + group_result_names.emplace(token_name, position_index); + } + + group_input_position = temp_group_position; + if (group_input_position < input.size() && + input[group_input_position] == '/') { + group_input_position += 1; + } + group_token_index += 1; + } + + if (group_matched && group_token_index == group_tokens.size()) { + for (const auto &pos : group_result_positions) { + result.insert(pos); + } + for (const auto &[name, index] : group_result_names) { + result.insert(name, index); + } + if (group_has_prefix && group_input_position > 0 && + group_input_position <= input.size() && + input[group_input_position - 1] == '/') { + current_input_position = group_input_position - 1; + } else { + current_input_position = group_input_position; + } + group_match_count += 1; + + if (is_group || is_group_optional) { + break; + } + } else { + break; + } + } + + if (is_group_multiple && group_match_count == 0) { + return std::nullopt; + } + if (is_group && group_match_count == 0) { + return std::nullopt; + } + input_position = current_input_position; + pattern_position += 1; + just_consumed_delimiter = + (input_position > 0 && input_position <= input.size() && + input[input_position - 1] == '/'); + continue; + } + + if (input_position >= input.size() && !just_consumed_delimiter) { + if (is_optional) { + pattern_position += 1; + continue; + } + return std::nullopt; + } + + if (input_position >= input.size() && just_consumed_delimiter && + pattern_position > 0) { + const auto is_name_based = + std::holds_alternative(token) || + std::holds_alternative(token) || + std::holds_alternative(token) || + std::holds_alternative(token) || + std::holds_alternative(token); + if (is_name_based) { + return std::nullopt; + } + } + + just_consumed_delimiter = false; + + auto segment_start = input_position; + auto temp_input_position = input_position; + while (temp_input_position < input.size() && + input[temp_input_position] != '/') { + temp_input_position += 1; + } + const auto segment = + input.substr(segment_start, temp_input_position - segment_start); + + if (is_optional && segment.empty()) { + pattern_position += 1; + continue; + } + + if (is_optional && pattern_position + 1 < this->value.size()) { + const auto &next_token = this->value[pattern_position + 1]; + if (std::holds_alternative(next_token)) { + const auto &char_token = std::get(next_token); + if (char_token.value == segment) { + pattern_position += 1; + continue; + } + } + } + + input_position = temp_input_position; + + if (is_complex_segment) { + const auto &complex_token = std::get(token); + + auto segment_position = std::string_view::size_type{0}; + auto complex_token_index = std::size_t{0}; + + while (complex_token_index < complex_token.value.size()) { + const auto &sub_token = complex_token.value[complex_token_index]; + + const auto is_sub_char = + std::holds_alternative(sub_token); + const auto is_sub_name = + std::holds_alternative(sub_token); + const auto is_sub_group = + std::holds_alternative(sub_token) || + std::holds_alternative(sub_token) || + std::holds_alternative(sub_token) || + std::holds_alternative(sub_token); + + if (is_sub_char) { + const auto &char_token = std::get(sub_token); + if (segment_position + char_token.value.size() > segment.size() || + segment.substr(segment_position, char_token.value.size()) != + char_token.value) { + return std::nullopt; + } + segment_position += char_token.value.size(); + } else if (is_sub_name) { + const auto &name_token = std::get(sub_token); + + auto name_end = segment_position; + if (complex_token_index + 1 < complex_token.value.size()) { + const auto &next_token = + complex_token.value[complex_token_index + 1]; + if (std::holds_alternative(next_token)) { + const auto &next_char = std::get(next_token); + const auto found_pos = + segment.find(next_char.value, segment_position); + if (found_pos == std::string_view::npos) { + return std::nullopt; + } + name_end = found_pos; + } else if (std::holds_alternative( + next_token) || + std::holds_alternative( + next_token) || + std::holds_alternative( + next_token) || + std::holds_alternative( + next_token)) { + const auto is_next_group_optional = + std::holds_alternative( + next_token); + const auto is_next_group_multiple = + std::holds_alternative( + next_token); + const auto is_next_group_asterisk = + std::holds_alternative( + next_token); + + const auto &next_group_tokens = + [&next_token, is_next_group_optional, is_next_group_multiple, + is_next_group_asterisk]() -> const URLPatternPartNonGroup & { + if (is_next_group_optional) { + return std::get(next_token) + .value; + } else if (is_next_group_multiple) { + return std::get(next_token) + .value; + } else if (is_next_group_asterisk) { + return std::get(next_token) + .value; + } else { + return std::get(next_token).value; + } + }(); + + if (!next_group_tokens.empty() && + std::holds_alternative( + next_group_tokens[0])) { + const auto &group_first_char = + std::get(next_group_tokens[0]); + const auto found_pos = + segment.find(group_first_char.value, segment_position); + if (found_pos == std::string_view::npos) { + return std::nullopt; + } + name_end = found_pos; + } else { + name_end = segment.size(); + } + } else { + name_end = segment.size(); + } + } else { + name_end = segment.size(); + } + + const auto captured_value = + segment.substr(segment_position, name_end - segment_position); + + if (captured_value.empty()) { + return std::nullopt; + } + + result.insert(name_token.value, captured_value); + segment_position = name_end; + } else if (is_sub_group) { + const auto is_sub_group_optional = + std::holds_alternative(sub_token); + const auto is_sub_group_multiple = + std::holds_alternative(sub_token); + const auto is_sub_group_asterisk = + std::holds_alternative(sub_token); + + const auto &group_tokens = + [&sub_token, is_sub_group_optional, is_sub_group_multiple, + is_sub_group_asterisk]() -> const URLPatternPartNonGroup & { + if (is_sub_group_optional) { + return std::get(sub_token).value; + } else if (is_sub_group_multiple) { + return std::get(sub_token).value; + } else if (is_sub_group_asterisk) { + return std::get(sub_token).value; + } else { + return std::get(sub_token).value; + } + }(); + + auto group_match_count = std::size_t{0}; + + while (true) { + auto group_segment_position = segment_position; + auto group_matched = true; + + for (const auto &group_token : group_tokens) { + const auto is_group_char = + std::holds_alternative(group_token); + const auto is_group_name = + std::holds_alternative(group_token); + + if (is_group_char) { + const auto &char_token = + std::get(group_token); + if (group_segment_position + char_token.value.size() > + segment.size() || + segment.substr(group_segment_position, + char_token.value.size()) != + char_token.value) { + group_matched = false; + break; + } + group_segment_position += char_token.value.size(); + } else if (is_group_name) { + const auto &name_token = + std::get(group_token); + auto name_end = group_segment_position; + + if (complex_token_index + 1 < complex_token.value.size()) { + const auto &next_token = + complex_token.value[complex_token_index + 1]; + if (std::holds_alternative(next_token)) { + const auto &next_char = + std::get(next_token); + const auto found_pos = + segment.find(next_char.value, group_segment_position); + if (found_pos == std::string_view::npos) { + group_matched = false; + break; + } + name_end = found_pos; + } else { + name_end = segment.size(); + } + } else { + name_end = segment.size(); + } + + const auto captured_value = segment.substr( + group_segment_position, name_end - group_segment_position); + if (captured_value.empty()) { + group_matched = false; + break; + } + + result.insert(name_token.value, captured_value); + group_segment_position = name_end; + } + } + + if (group_matched) { + segment_position = group_segment_position; + group_match_count += 1; + if (!is_sub_group_multiple && !is_sub_group_asterisk) { + break; + } + } else { + break; + } + } + + if (is_sub_group_multiple && group_match_count == 0) { + return std::nullopt; + } + if (!is_sub_group_optional && !is_sub_group_multiple && + !is_sub_group_asterisk && group_match_count == 0) { + return std::nullopt; + } + } + + complex_token_index += 1; + } + + if (segment_position != segment.size()) { + return std::nullopt; + } + } else { + const auto token_matches = std::visit( + [&segment](const auto &token_value) -> bool { + return token_value.matches(segment); + }, + token); + + if (!token_matches) { + return std::nullopt; + } + + if (!std::holds_alternative(token)) { + if (std::holds_alternative(token)) { + const auto &name_token = std::get(token); + result.insert(name_token.value, segment); + } else if (std::holds_alternative(token)) { + const auto &name_regex_token = + std::get(token); + result.insert(name_regex_token.value, segment); + } else if (std::holds_alternative(token)) { + const auto &name_optional_token = + std::get(token); + result.insert(name_optional_token.value, segment); + } else { + result.insert(segment); + } + } + } + + if (input_position < input.size() && + pattern_position + 1 < this->value.size()) { + const auto &next_token = this->value[pattern_position + 1]; + const auto next_has_prefix = get_inner_segment_prefix(next_token); + if (!next_has_prefix) { + input_position += 1; + just_consumed_delimiter = true; + } + } + pattern_position += 1; + } + + if (input_position < input.size()) { + return std::nullopt; + } + + return result; +} + +URLPatternProtocol::URLPatternProtocol(const char *input) + : value{parse_single_token(input)} {} + +URLPatternUsername::URLPatternUsername(const char *input) + : value{parse_single_token(input)} {} + +URLPatternPassword::URLPatternPassword(const char *input) + : value{parse_single_token(input)} {} + +URLPatternPort::URLPatternPort(const char *input) + : value{parse_single_token(input)} {} + +URLPatternSearch::URLPatternSearch(const char *input) + : value{parse_single_token(input)} {} + +URLPatternHash::URLPatternHash(const char *input) + : value{parse_single_token(input)} {} + +URLPatternPathname::URLPatternPathname(const char *input) { + const std::string_view input_view{input}; + if (input_view.empty()) { + throw URLPatternParseError{0}; + } + + const auto starts_with_slash = input_view[0] == '/'; + if (!starts_with_slash) { + this->is_bare_pattern = true; + } + + enum class State : std::uint8_t { + ReadingChar, + ReadingName, + ReadingRegExp, + AfterBackslash, + ReadingGroup + }; + + auto position = + starts_with_slash ? std::string::size_type{1} : std::string::size_type{0}; + auto current_state = State::ReadingChar; + std::string token_buffer; + std::string name_for_regex; + auto after_segment_delimiter = false; + URLPatternPartGroup current_group; + auto group_after_segment_delimiter = false; + auto group_has_delimiter = false; + auto group_inner_segment_prefix = false; + auto group_inner_segment_suffix = false; + auto inside_group = false; + std::vector> + segment_tokens; + + const auto flush_segment_tokens = [&]() -> void { + if (!token_buffer.empty()) { + segment_tokens.emplace_back(URLPatternPartChar{token_buffer}); + token_buffer.clear(); + } + if (segment_tokens.size() == 0 && after_segment_delimiter) { + this->value.emplace_back(URLPatternPartChar{""}); + } else if (segment_tokens.size() == 1) { + std::visit([&](const auto &token) { this->value.emplace_back(token); }, + segment_tokens[0]); + } else if (segment_tokens.size() > 1) { + this->value.emplace_back(URLPatternPartComplexSegment{segment_tokens}); + } + segment_tokens.clear(); + }; + + while (position < input_view.size()) { + const auto current = input_view[position]; + + if (current_state == State::ReadingChar) { + if (current == '/') { + flush_segment_tokens(); + after_segment_delimiter = true; + position += 1; + } else if (current == ':') { + if (!token_buffer.empty()) { + segment_tokens.emplace_back(URLPatternPartChar{token_buffer}); + token_buffer.clear(); + } + current_state = State::ReadingName; + after_segment_delimiter = false; + position += 1; + } else if (current == '*') { + if (!token_buffer.empty()) { + segment_tokens.emplace_back(URLPatternPartChar{token_buffer}); + token_buffer.clear(); + } + const auto is_asterisk_asterisk = + position + 1 < input_view.size() && input_view[position + 1] == '*'; + const auto is_optional = + position + 1 < input_view.size() && input_view[position + 1] == '?'; + const auto is_multiple = + position + 1 < input_view.size() && input_view[position + 1] == '+'; + if (is_asterisk_asterisk) { + segment_tokens.emplace_back(URLPatternPartAsteriskAsterisk{}); + position += 2; + } else if (is_optional) { + segment_tokens.emplace_back(URLPatternPartAsteriskOptional{}); + position += 2; + } else if (is_multiple) { + segment_tokens.emplace_back(URLPatternPartAsteriskMultiple{}); + position += 2; + } else { + segment_tokens.emplace_back(URLPatternPartAsterisk{}); + position += 1; + } + after_segment_delimiter = false; + } else if (current == '(') { + if (!token_buffer.empty()) { + segment_tokens.emplace_back(URLPatternPartChar{token_buffer}); + token_buffer.clear(); + } + current_state = State::ReadingRegExp; + after_segment_delimiter = false; + position += 1; + } else if (current == '{') { + if (!token_buffer.empty()) { + segment_tokens.emplace_back(URLPatternPartChar{token_buffer}); + token_buffer.clear(); + } + current_group = URLPatternPartGroup{}; + current_state = State::ReadingGroup; + group_after_segment_delimiter = false; + group_has_delimiter = false; + group_inner_segment_prefix = false; + group_inner_segment_suffix = false; + after_segment_delimiter = false; + inside_group = true; + position += 1; + } else if (current == '\\') { + current_state = State::AfterBackslash; + after_segment_delimiter = false; + position += 1; + } else { + token_buffer += current; + after_segment_delimiter = false; + position += 1; + } + } else if (current_state == State::ReadingName) { + if (token_buffer.empty()) { + if (!is_valid_name_start(current)) { + throw URLPatternParseError{position}; + } + token_buffer += current; + position += 1; + } else { + if (current == '/') { + if (inside_group) { + current_group.value.emplace_back(URLPatternPartName{token_buffer}); + token_buffer.clear(); + current_state = State::ReadingGroup; + group_after_segment_delimiter = true; + position += 1; + } else { + segment_tokens.emplace_back(URLPatternPartName{token_buffer}); + token_buffer.clear(); + current_state = State::ReadingChar; + after_segment_delimiter = false; + } + } else if (current == '}' && inside_group) { + current_group.value.emplace_back(URLPatternPartName{token_buffer}); + token_buffer.clear(); + const auto is_optional = position + 1 < input_view.size() && + input_view[position + 1] == '?'; + const auto is_multiple = position + 1 < input_view.size() && + input_view[position + 1] == '+'; + const auto is_asterisk = position + 1 < input_view.size() && + input_view[position + 1] == '*'; + const auto has_delimiter = group_has_delimiter; + group_inner_segment_suffix = group_after_segment_delimiter; + if (has_delimiter) { + flush_segment_tokens(); + } + if (is_optional) { + segment_tokens.emplace_back(URLPatternPartGroupOptional{ + .value = current_group.value, + .has_inner_segment_prefix = group_inner_segment_prefix, + .has_inner_segment_suffix = group_inner_segment_suffix}); + position += 2; + } else if (is_multiple) { + segment_tokens.emplace_back(URLPatternPartGroupMultiple{ + .value = current_group.value, + .has_inner_segment_prefix = group_inner_segment_prefix, + .has_inner_segment_suffix = group_inner_segment_suffix}); + position += 2; + } else if (is_asterisk) { + segment_tokens.emplace_back(URLPatternPartGroupAsterisk{ + .value = current_group.value, + .has_inner_segment_prefix = group_inner_segment_prefix, + .has_inner_segment_suffix = group_inner_segment_suffix}); + position += 2; + } else { + current_group.has_inner_segment_prefix = group_inner_segment_prefix; + current_group.has_inner_segment_suffix = group_inner_segment_suffix; + segment_tokens.emplace_back(current_group); + position += 1; + } + if (has_delimiter) { + flush_segment_tokens(); + } + current_state = State::ReadingChar; + after_segment_delimiter = false; + inside_group = false; + group_after_segment_delimiter = false; + group_has_delimiter = false; + } else if (current == '(') { + name_for_regex = token_buffer; + token_buffer.clear(); + current_state = State::ReadingRegExp; + after_segment_delimiter = false; + position += 1; + } else if (current == '*') { + if (inside_group) { + current_group.value.emplace_back( + URLPatternPartNameAsterisk{token_buffer}); + token_buffer.clear(); + current_state = State::ReadingGroup; + group_after_segment_delimiter = false; + } else { + segment_tokens.emplace_back( + URLPatternPartNameAsterisk{token_buffer}); + token_buffer.clear(); + current_state = State::ReadingChar; + after_segment_delimiter = false; + } + position += 1; + } else if (current == '?') { + if (inside_group) { + current_group.value.emplace_back( + URLPatternPartNameOptional{token_buffer}); + token_buffer.clear(); + current_state = State::ReadingGroup; + group_after_segment_delimiter = false; + } else { + segment_tokens.emplace_back( + URLPatternPartNameOptional{token_buffer}); + token_buffer.clear(); + current_state = State::ReadingChar; + after_segment_delimiter = false; + } + position += 1; + } else if (current == '+') { + if (inside_group) { + current_group.value.emplace_back( + URLPatternPartNameMultiple{token_buffer}); + token_buffer.clear(); + current_state = State::ReadingGroup; + group_after_segment_delimiter = false; + } else { + segment_tokens.emplace_back( + URLPatternPartNameMultiple{token_buffer}); + token_buffer.clear(); + current_state = State::ReadingChar; + after_segment_delimiter = false; + } + position += 1; + } else if (is_valid_name_char(current)) { + token_buffer += current; + position += 1; + } else if (current == '\\') { + // End name and process escape sequence + if (inside_group) { + current_group.value.emplace_back(URLPatternPartName{token_buffer}); + token_buffer.clear(); + current_state = State::AfterBackslash; + group_after_segment_delimiter = false; + } else { + segment_tokens.emplace_back(URLPatternPartName{token_buffer}); + token_buffer.clear(); + current_state = State::AfterBackslash; + after_segment_delimiter = false; + } + position += 1; + } else { + if (inside_group) { + throw URLPatternParseError{position}; + } else { + const auto can_start_new_token = current == '.' || current == ':' || + current == '{' || current == '(' || + current == '*'; + if (can_start_new_token) { + segment_tokens.emplace_back(URLPatternPartName{token_buffer}); + token_buffer.clear(); + current_state = State::ReadingChar; + after_segment_delimiter = false; + } else { + throw URLPatternParseError{position}; + } + } + } + } + } else if (current_state == State::ReadingRegExp) { + if (current == ')') { + if (token_buffer.empty()) { + throw URLPatternParseError{position}; + } + const auto regex = to_regex(token_buffer); + if (!regex.has_value()) { + throw URLPatternParseError{position}; + } + const auto is_optional = + position + 1 < input_view.size() && input_view[position + 1] == '?'; + const auto is_multiple = + position + 1 < input_view.size() && input_view[position + 1] == '+'; + const auto is_asterisk = + position + 1 < input_view.size() && input_view[position + 1] == '*'; + if (name_for_regex.empty()) { + if (inside_group) { + if (is_optional) { + current_group.value.emplace_back(URLPatternPartRegExpOptional{ + .value = regex.value(), .original_pattern = token_buffer}); + } else if (is_multiple) { + current_group.value.emplace_back(URLPatternPartRegExpMultiple{ + .value = regex.value(), .original_pattern = token_buffer}); + } else if (is_asterisk) { + current_group.value.emplace_back(URLPatternPartRegExpAsterisk{ + .value = regex.value(), .original_pattern = token_buffer}); + } else { + current_group.value.emplace_back(URLPatternPartRegExp{ + .value = regex.value(), .original_pattern = token_buffer}); + } + } else { + if (is_optional) { + segment_tokens.emplace_back(URLPatternPartRegExpOptional{ + .value = regex.value(), .original_pattern = token_buffer}); + } else if (is_multiple) { + segment_tokens.emplace_back(URLPatternPartRegExpMultiple{ + .value = regex.value(), .original_pattern = token_buffer}); + } else if (is_asterisk) { + segment_tokens.emplace_back(URLPatternPartRegExpAsterisk{ + .value = regex.value(), .original_pattern = token_buffer}); + } else { + segment_tokens.emplace_back(URLPatternPartRegExp{ + .value = regex.value(), .original_pattern = token_buffer}); + } + } + } else { + if (inside_group) { + current_group.value.emplace_back( + URLPatternPartNameRegExp{.value = name_for_regex, + .modifier = regex.value(), + .original_pattern = token_buffer}); + } else { + segment_tokens.emplace_back( + URLPatternPartNameRegExp{.value = name_for_regex, + .modifier = regex.value(), + .original_pattern = token_buffer}); + } + name_for_regex.clear(); + } + token_buffer.clear(); + current_state = inside_group ? State::ReadingGroup : State::ReadingChar; + if (inside_group) { + group_after_segment_delimiter = false; + } else { + after_segment_delimiter = false; + } + position += (is_optional || is_multiple || is_asterisk) ? 2 : 1; + } else { + token_buffer += current; + position += 1; + } + } else if (current_state == State::AfterBackslash) { + if (position >= input_view.size()) { + throw URLPatternParseError{position - 1}; + } + token_buffer += current; + current_state = inside_group ? State::ReadingGroup : State::ReadingChar; + position += 1; + } else if (current_state == State::ReadingGroup) { + if (current == '}') { + if (!token_buffer.empty()) { + current_group.value.emplace_back(URLPatternPartChar{token_buffer}); + token_buffer.clear(); + } else if (group_after_segment_delimiter) { + current_group.value.emplace_back(URLPatternPartChar{""}); + } + const auto is_optional = + position + 1 < input_view.size() && input_view[position + 1] == '?'; + const auto is_multiple = + position + 1 < input_view.size() && input_view[position + 1] == '+'; + const auto is_asterisk = + position + 1 < input_view.size() && input_view[position + 1] == '*'; + const auto has_delimiter = group_has_delimiter; + group_inner_segment_suffix = group_after_segment_delimiter; + if (has_delimiter) { + flush_segment_tokens(); + } + if (is_optional) { + segment_tokens.emplace_back(URLPatternPartGroupOptional{ + .value = current_group.value, + .has_inner_segment_prefix = group_inner_segment_prefix, + .has_inner_segment_suffix = group_inner_segment_suffix}); + position += 2; + } else if (is_multiple) { + segment_tokens.emplace_back(URLPatternPartGroupMultiple{ + .value = current_group.value, + .has_inner_segment_prefix = group_inner_segment_prefix, + .has_inner_segment_suffix = group_inner_segment_suffix}); + position += 2; + } else if (is_asterisk) { + segment_tokens.emplace_back(URLPatternPartGroupAsterisk{ + .value = current_group.value, + .has_inner_segment_prefix = group_inner_segment_prefix, + .has_inner_segment_suffix = group_inner_segment_suffix}); + position += 2; + } else { + current_group.has_inner_segment_prefix = group_inner_segment_prefix; + current_group.has_inner_segment_suffix = group_inner_segment_suffix; + segment_tokens.emplace_back(current_group); + position += 1; + } + if (has_delimiter) { + flush_segment_tokens(); + } + current_state = State::ReadingChar; + after_segment_delimiter = false; + inside_group = false; + group_after_segment_delimiter = false; + group_has_delimiter = false; + } else if (current == '/') { + if (current_group.value.empty() && token_buffer.empty()) { + group_inner_segment_prefix = true; + } + if (!token_buffer.empty()) { + current_group.value.emplace_back(URLPatternPartChar{token_buffer}); + token_buffer.clear(); + } else if (group_after_segment_delimiter) { + current_group.value.emplace_back(URLPatternPartChar{""}); + } + group_after_segment_delimiter = true; + group_has_delimiter = true; + position += 1; + } else if (current == ':') { + if (!token_buffer.empty()) { + current_group.value.emplace_back(URLPatternPartChar{token_buffer}); + token_buffer.clear(); + } + current_state = State::ReadingName; + group_after_segment_delimiter = false; + position += 1; + } else if (current == '*') { + if (!token_buffer.empty()) { + current_group.value.emplace_back(URLPatternPartChar{token_buffer}); + token_buffer.clear(); + } + const auto is_asterisk_asterisk = + position + 1 < input_view.size() && input_view[position + 1] == '*'; + const auto is_optional = + position + 1 < input_view.size() && input_view[position + 1] == '?'; + const auto is_multiple = + position + 1 < input_view.size() && input_view[position + 1] == '+'; + if (is_asterisk_asterisk) { + current_group.value.emplace_back(URLPatternPartAsteriskAsterisk{}); + position += 2; + } else if (is_optional) { + current_group.value.emplace_back(URLPatternPartAsteriskOptional{}); + position += 2; + } else if (is_multiple) { + current_group.value.emplace_back(URLPatternPartAsteriskMultiple{}); + position += 2; + } else { + current_group.value.emplace_back(URLPatternPartAsterisk{}); + position += 1; + } + group_after_segment_delimiter = false; + } else if (current == '(') { + if (!token_buffer.empty()) { + current_group.value.emplace_back(URLPatternPartChar{token_buffer}); + token_buffer.clear(); + } + current_state = State::ReadingRegExp; + group_after_segment_delimiter = false; + position += 1; + } else if (current == '\\') { + current_state = State::AfterBackslash; + group_after_segment_delimiter = false; + position += 1; + } else { + token_buffer += current; + group_after_segment_delimiter = false; + position += 1; + } + } + } + + if (current_state == State::AfterBackslash) { + throw URLPatternParseError{position - 1}; + } + + if (current_state == State::ReadingRegExp) { + throw URLPatternParseError{position}; + } + + if (current_state == State::ReadingName && token_buffer.empty()) { + throw URLPatternParseError{position}; + } + + if (!token_buffer.empty()) { + if (current_state == State::ReadingName) { + segment_tokens.emplace_back(URLPatternPartName{token_buffer}); + token_buffer.clear(); + } + } + + flush_segment_tokens(); +} + +URLPatternHostname::URLPatternHostname(const char *input) { + const std::string_view input_view{input}; + + enum class State : std::uint8_t { + ReadingChar, + ReadingName, + ReadingRegExp, + AfterBackslash, + ReadingGroup + }; + + auto position = std::string::size_type{0}; + auto current_state = State::ReadingChar; + std::string token_buffer; + std::string name_for_regex; + auto after_segment_delimiter = true; + URLPatternPartGroup current_group; + auto group_after_segment_delimiter = false; + auto group_has_delimiter = false; + auto group_inner_segment_prefix = false; + auto group_inner_segment_suffix = false; + auto inside_group = false; + std::vector> + segment_tokens; + + const auto flush_segment_tokens = [&]() -> void { + if (!token_buffer.empty()) { + segment_tokens.emplace_back(URLPatternPartChar{token_buffer}); + token_buffer.clear(); + } + if (segment_tokens.size() == 0 && after_segment_delimiter) { + this->value.emplace_back(URLPatternPartChar{""}); + } else if (segment_tokens.size() == 1) { + std::visit([&](const auto &token) { this->value.emplace_back(token); }, + segment_tokens[0]); + } else if (segment_tokens.size() > 1) { + this->value.emplace_back(URLPatternPartComplexSegment{segment_tokens}); + } + segment_tokens.clear(); + }; + + while (position < input_view.size()) { + const auto current = input_view[position]; + + if (current_state == State::ReadingChar) { + if (current == '.') { + flush_segment_tokens(); + after_segment_delimiter = true; + position += 1; + } else if (current == ':') { + if (!token_buffer.empty()) { + segment_tokens.emplace_back(URLPatternPartChar{token_buffer}); + token_buffer.clear(); + } + current_state = State::ReadingName; + after_segment_delimiter = false; + position += 1; + } else if (current == '*') { + if (!token_buffer.empty()) { + segment_tokens.emplace_back(URLPatternPartChar{token_buffer}); + token_buffer.clear(); + } + const auto is_asterisk_asterisk = + position + 1 < input_view.size() && input_view[position + 1] == '*'; + const auto is_optional = + position + 1 < input_view.size() && input_view[position + 1] == '?'; + const auto is_multiple = + position + 1 < input_view.size() && input_view[position + 1] == '+'; + if (is_asterisk_asterisk) { + segment_tokens.emplace_back(URLPatternPartAsteriskAsterisk{}); + position += 2; + } else if (is_optional) { + segment_tokens.emplace_back(URLPatternPartAsteriskOptional{}); + position += 2; + } else if (is_multiple) { + segment_tokens.emplace_back(URLPatternPartAsteriskMultiple{}); + position += 2; + } else { + segment_tokens.emplace_back(URLPatternPartAsterisk{}); + position += 1; + } + after_segment_delimiter = false; + } else if (current == '(') { + if (!token_buffer.empty()) { + segment_tokens.emplace_back(URLPatternPartChar{token_buffer}); + token_buffer.clear(); + } + current_state = State::ReadingRegExp; + after_segment_delimiter = false; + position += 1; + } else if (current == '{') { + if (!token_buffer.empty()) { + segment_tokens.emplace_back(URLPatternPartChar{token_buffer}); + token_buffer.clear(); + } + current_group = URLPatternPartGroup{}; + current_state = State::ReadingGroup; + group_after_segment_delimiter = false; + group_has_delimiter = false; + group_inner_segment_prefix = false; + group_inner_segment_suffix = false; + after_segment_delimiter = false; + inside_group = true; + position += 1; + } else if (current == '\\') { + current_state = State::AfterBackslash; + after_segment_delimiter = false; + position += 1; + } else { + token_buffer += current; + after_segment_delimiter = false; + position += 1; + } + } else if (current_state == State::ReadingName) { + if (token_buffer.empty()) { + if (!is_valid_name_start(current)) { + throw URLPatternParseError{position}; + } + token_buffer += current; + position += 1; + } else { + if (current == '.') { + if (inside_group) { + current_group.value.emplace_back(URLPatternPartName{token_buffer}); + token_buffer.clear(); + current_state = State::ReadingGroup; + group_after_segment_delimiter = true; + position += 1; + } else { + segment_tokens.emplace_back(URLPatternPartName{token_buffer}); + token_buffer.clear(); + current_state = State::ReadingChar; + after_segment_delimiter = false; + } + } else if (current == '}' && inside_group) { + current_group.value.emplace_back(URLPatternPartName{token_buffer}); + token_buffer.clear(); + const auto is_optional = position + 1 < input_view.size() && + input_view[position + 1] == '?'; + const auto is_multiple = position + 1 < input_view.size() && + input_view[position + 1] == '+'; + const auto is_asterisk = position + 1 < input_view.size() && + input_view[position + 1] == '*'; + const auto has_delimiter = group_has_delimiter; + group_inner_segment_suffix = group_after_segment_delimiter; + if (has_delimiter) { + flush_segment_tokens(); + } + if (is_optional) { + segment_tokens.emplace_back(URLPatternPartGroupOptional{ + .value = current_group.value, + .has_inner_segment_prefix = group_inner_segment_prefix, + .has_inner_segment_suffix = group_inner_segment_suffix}); + position += 2; + } else if (is_multiple) { + segment_tokens.emplace_back(URLPatternPartGroupMultiple{ + .value = current_group.value, + .has_inner_segment_prefix = group_inner_segment_prefix, + .has_inner_segment_suffix = group_inner_segment_suffix}); + position += 2; + } else if (is_asterisk) { + segment_tokens.emplace_back(URLPatternPartGroupAsterisk{ + .value = current_group.value, + .has_inner_segment_prefix = group_inner_segment_prefix, + .has_inner_segment_suffix = group_inner_segment_suffix}); + position += 2; + } else { + current_group.has_inner_segment_prefix = group_inner_segment_prefix; + current_group.has_inner_segment_suffix = group_inner_segment_suffix; + segment_tokens.emplace_back(current_group); + position += 1; + } + if (has_delimiter) { + flush_segment_tokens(); + } + current_state = State::ReadingChar; + after_segment_delimiter = false; + inside_group = false; + group_after_segment_delimiter = false; + group_has_delimiter = false; + } else if (current == '(') { + name_for_regex = token_buffer; + token_buffer.clear(); + current_state = State::ReadingRegExp; + after_segment_delimiter = false; + position += 1; + } else if (current == '*') { + if (inside_group) { + current_group.value.emplace_back( + URLPatternPartNameAsterisk{token_buffer}); + token_buffer.clear(); + current_state = State::ReadingGroup; + group_after_segment_delimiter = false; + } else { + segment_tokens.emplace_back( + URLPatternPartNameAsterisk{token_buffer}); + token_buffer.clear(); + current_state = State::ReadingChar; + after_segment_delimiter = false; + } + position += 1; + } else if (current == '?') { + if (inside_group) { + current_group.value.emplace_back( + URLPatternPartNameOptional{token_buffer}); + token_buffer.clear(); + current_state = State::ReadingGroup; + group_after_segment_delimiter = false; + } else { + segment_tokens.emplace_back( + URLPatternPartNameOptional{token_buffer}); + token_buffer.clear(); + current_state = State::ReadingChar; + after_segment_delimiter = false; + } + position += 1; + } else if (current == '+') { + if (inside_group) { + current_group.value.emplace_back( + URLPatternPartNameMultiple{token_buffer}); + token_buffer.clear(); + current_state = State::ReadingGroup; + group_after_segment_delimiter = false; + } else { + segment_tokens.emplace_back( + URLPatternPartNameMultiple{token_buffer}); + token_buffer.clear(); + current_state = State::ReadingChar; + after_segment_delimiter = false; + } + position += 1; + } else if (is_valid_name_char(current)) { + token_buffer += current; + position += 1; + } else { + const auto can_end_name = current == ':' || current == '{' || + current == '(' || current == '*' || + current == '[' || current == ']' || + current == '}' || current == '\\'; + if (inside_group) { + if (can_end_name) { + current_group.value.emplace_back( + URLPatternPartName{token_buffer}); + token_buffer.clear(); + current_state = State::ReadingGroup; + group_after_segment_delimiter = false; + } else { + throw URLPatternParseError{position}; + } + } else { + if (can_end_name) { + segment_tokens.emplace_back(URLPatternPartName{token_buffer}); + token_buffer.clear(); + current_state = State::ReadingChar; + after_segment_delimiter = false; + } else { + throw URLPatternParseError{position}; + } + } + } + } + } else if (current_state == State::ReadingRegExp) { + if (current == ')') { + if (token_buffer.empty()) { + throw URLPatternParseError{position}; + } + const auto regex = to_regex(token_buffer); + if (!regex.has_value()) { + throw URLPatternParseError{position}; + } + const auto is_optional = + position + 1 < input_view.size() && input_view[position + 1] == '?'; + const auto is_multiple = + position + 1 < input_view.size() && input_view[position + 1] == '+'; + const auto is_asterisk = + position + 1 < input_view.size() && input_view[position + 1] == '*'; + if (name_for_regex.empty()) { + if (inside_group) { + if (is_optional) { + current_group.value.emplace_back(URLPatternPartRegExpOptional{ + .value = regex.value(), .original_pattern = token_buffer}); + } else if (is_multiple) { + current_group.value.emplace_back(URLPatternPartRegExpMultiple{ + .value = regex.value(), .original_pattern = token_buffer}); + } else if (is_asterisk) { + current_group.value.emplace_back(URLPatternPartRegExpAsterisk{ + .value = regex.value(), .original_pattern = token_buffer}); + } else { + current_group.value.emplace_back(URLPatternPartRegExp{ + .value = regex.value(), .original_pattern = token_buffer}); + } + } else { + if (is_optional) { + segment_tokens.emplace_back(URLPatternPartRegExpOptional{ + .value = regex.value(), .original_pattern = token_buffer}); + } else if (is_multiple) { + segment_tokens.emplace_back(URLPatternPartRegExpMultiple{ + .value = regex.value(), .original_pattern = token_buffer}); + } else if (is_asterisk) { + segment_tokens.emplace_back(URLPatternPartRegExpAsterisk{ + .value = regex.value(), .original_pattern = token_buffer}); + } else { + segment_tokens.emplace_back(URLPatternPartRegExp{ + .value = regex.value(), .original_pattern = token_buffer}); + } + } + } else { + if (inside_group) { + current_group.value.emplace_back( + URLPatternPartNameRegExp{.value = name_for_regex, + .modifier = regex.value(), + .original_pattern = token_buffer}); + } else { + segment_tokens.emplace_back( + URLPatternPartNameRegExp{.value = name_for_regex, + .modifier = regex.value(), + .original_pattern = token_buffer}); + } + name_for_regex.clear(); + } + token_buffer.clear(); + current_state = inside_group ? State::ReadingGroup : State::ReadingChar; + if (inside_group) { + group_after_segment_delimiter = false; + } else { + after_segment_delimiter = false; + } + position += (is_optional || is_multiple || is_asterisk) ? 2 : 1; + } else { + token_buffer += current; + position += 1; + } + } else if (current_state == State::AfterBackslash) { + if (position >= input_view.size()) { + throw URLPatternParseError{position - 1}; + } + token_buffer += current; + current_state = inside_group ? State::ReadingGroup : State::ReadingChar; + position += 1; + } else if (current_state == State::ReadingGroup) { + if (current == '}') { + if (!token_buffer.empty()) { + current_group.value.emplace_back(URLPatternPartChar{token_buffer}); + token_buffer.clear(); + } else if (group_after_segment_delimiter) { + current_group.value.emplace_back(URLPatternPartChar{""}); + } + const auto is_optional = + position + 1 < input_view.size() && input_view[position + 1] == '?'; + const auto is_multiple = + position + 1 < input_view.size() && input_view[position + 1] == '+'; + const auto is_asterisk = + position + 1 < input_view.size() && input_view[position + 1] == '*'; + const auto has_delimiter = group_has_delimiter; + group_inner_segment_suffix = group_after_segment_delimiter; + if (has_delimiter) { + flush_segment_tokens(); + } + if (is_optional) { + segment_tokens.emplace_back(URLPatternPartGroupOptional{ + .value = current_group.value, + .has_inner_segment_prefix = group_inner_segment_prefix, + .has_inner_segment_suffix = group_inner_segment_suffix}); + position += 2; + } else if (is_multiple) { + segment_tokens.emplace_back(URLPatternPartGroupMultiple{ + .value = current_group.value, + .has_inner_segment_prefix = group_inner_segment_prefix, + .has_inner_segment_suffix = group_inner_segment_suffix}); + position += 2; + } else if (is_asterisk) { + segment_tokens.emplace_back(URLPatternPartGroupAsterisk{ + .value = current_group.value, + .has_inner_segment_prefix = group_inner_segment_prefix, + .has_inner_segment_suffix = group_inner_segment_suffix}); + position += 2; + } else { + current_group.has_inner_segment_prefix = group_inner_segment_prefix; + current_group.has_inner_segment_suffix = group_inner_segment_suffix; + segment_tokens.emplace_back(current_group); + position += 1; + } + if (has_delimiter) { + flush_segment_tokens(); + } + current_state = State::ReadingChar; + after_segment_delimiter = false; + inside_group = false; + group_after_segment_delimiter = false; + group_has_delimiter = false; + } else if (current == '.') { + if (current_group.value.empty() && token_buffer.empty()) { + group_inner_segment_prefix = true; + } + if (!token_buffer.empty()) { + current_group.value.emplace_back(URLPatternPartChar{token_buffer}); + token_buffer.clear(); + } else if (group_after_segment_delimiter) { + current_group.value.emplace_back(URLPatternPartChar{""}); + } + group_after_segment_delimiter = true; + group_has_delimiter = true; + position += 1; + } else if (current == ':') { + if (!token_buffer.empty()) { + current_group.value.emplace_back(URLPatternPartChar{token_buffer}); + token_buffer.clear(); + } + current_state = State::ReadingName; + group_after_segment_delimiter = false; + position += 1; + } else if (current == '*') { + if (!token_buffer.empty()) { + current_group.value.emplace_back(URLPatternPartChar{token_buffer}); + token_buffer.clear(); + } + const auto is_asterisk_asterisk = + position + 1 < input_view.size() && input_view[position + 1] == '*'; + const auto is_optional = + position + 1 < input_view.size() && input_view[position + 1] == '?'; + const auto is_multiple = + position + 1 < input_view.size() && input_view[position + 1] == '+'; + if (is_asterisk_asterisk) { + current_group.value.emplace_back(URLPatternPartAsteriskAsterisk{}); + position += 2; + } else if (is_optional) { + current_group.value.emplace_back(URLPatternPartAsteriskOptional{}); + position += 2; + } else if (is_multiple) { + current_group.value.emplace_back(URLPatternPartAsteriskMultiple{}); + position += 2; + } else { + current_group.value.emplace_back(URLPatternPartAsterisk{}); + position += 1; + } + group_after_segment_delimiter = false; + } else if (current == '(') { + if (!token_buffer.empty()) { + current_group.value.emplace_back(URLPatternPartChar{token_buffer}); + token_buffer.clear(); + } + current_state = State::ReadingRegExp; + group_after_segment_delimiter = false; + position += 1; + } else if (current == '\\') { + current_state = State::AfterBackslash; + group_after_segment_delimiter = false; + position += 1; + } else { + token_buffer += current; + group_after_segment_delimiter = false; + position += 1; + } + } + } + + if (current_state == State::AfterBackslash) { + throw URLPatternParseError{position - 1}; + } + + if (current_state == State::ReadingRegExp) { + throw URLPatternParseError{position}; + } + + if (current_state == State::ReadingName && token_buffer.empty()) { + throw URLPatternParseError{position}; + } + + if (!token_buffer.empty()) { + if (current_state == State::ReadingName) { + segment_tokens.emplace_back(URLPatternPartName{token_buffer}); + token_buffer.clear(); + } + } + + flush_segment_tokens(); +} + +auto URLPatternProtocol::operator<=>(const URLPatternProtocol &other) const + -> std::strong_ordering { + return compare_urlpattern_parts(this->value, other.value); +} + +auto URLPatternUsername::operator<=>(const URLPatternUsername &other) const + -> std::strong_ordering { + return compare_urlpattern_parts(this->value, other.value); +} + +auto URLPatternPassword::operator<=>(const URLPatternPassword &other) const + -> std::strong_ordering { + return compare_urlpattern_parts(this->value, other.value); +} + +auto URLPatternHostname::operator<=>(const URLPatternHostname &other) const + -> std::strong_ordering { + std::vector left_reversed(this->value.rbegin(), + this->value.rend()); + std::vector right_reversed(other.value.rbegin(), + other.value.rend()); + return compare_urlpattern_parts_vector(left_reversed, right_reversed); +} + +auto URLPatternPort::operator<=>(const URLPatternPort &other) const + -> std::strong_ordering { + return compare_urlpattern_parts(this->value, other.value); +} + +auto URLPatternPathname::operator<=>(const URLPatternPathname &other) const + -> std::strong_ordering { + const auto comparison{ + compare_urlpattern_parts_vector(this->value, other.value)}; + if (comparison != std::strong_ordering::equal) { + return comparison; + } + return this->is_bare_pattern <=> other.is_bare_pattern; +} + +auto URLPatternSearch::operator<=>(const URLPatternSearch &other) const + -> std::strong_ordering { + return compare_urlpattern_parts(this->value, other.value); +} + +auto URLPatternHash::operator<=>(const URLPatternHash &other) const + -> std::strong_ordering { + return compare_urlpattern_parts(this->value, other.value); +} + +} // namespace sourcemeta::core diff --git a/src/core/urlpattern/urlpattern_part.cc b/src/core/urlpattern/urlpattern_part.cc new file mode 100644 index 000000000..1a68996c3 --- /dev/null +++ b/src/core/urlpattern/urlpattern_part.cc @@ -0,0 +1,117 @@ +#include + +namespace sourcemeta::core { + +auto URLPatternPartRegExp::matches( + const std::string_view segment) const noexcept -> bool { + // TODO: Avoid this std::string conversion by updating + // sourcemeta::core::matches to accept std::string_view + return sourcemeta::core::matches(this->value, std::string{segment}); +} + +auto URLPatternPartRegExpOptional::matches( + const std::string_view segment) const noexcept -> bool { + // TODO: Avoid this std::string conversion by updating + // sourcemeta::core::matches to accept std::string_view + return sourcemeta::core::matches(this->value, std::string{segment}); +} + +auto URLPatternPartRegExpMultiple::matches( + const std::string_view segment) const noexcept -> bool { + // TODO: Avoid this std::string conversion by updating + // sourcemeta::core::matches to accept std::string_view + return sourcemeta::core::matches(this->value, std::string{segment}); +} + +auto URLPatternPartRegExpAsterisk::matches( + const std::string_view segment) const noexcept -> bool { + // TODO: Avoid this std::string conversion by updating + // sourcemeta::core::matches to accept std::string_view + return sourcemeta::core::matches(this->value, std::string{segment}); +} + +auto URLPatternPartChar::matches(const std::string_view segment) const noexcept + -> bool { + return segment == this->value; +} + +auto URLPatternPartName::matches(const std::string_view) const noexcept + -> bool { + return true; +} + +auto URLPatternPartNameRegExp::matches( + const std::string_view segment) const noexcept -> bool { + // TODO: Avoid this std::string conversion by updating + // sourcemeta::core::matches to accept std::string_view + return sourcemeta::core::matches(this->modifier, std::string{segment}); +} + +auto URLPatternPartNameOptional::matches(const std::string_view) const noexcept + -> bool { + return true; +} + +auto URLPatternPartNameMultiple::matches(const std::string_view) const noexcept + -> bool { + return true; +} + +auto URLPatternPartNameAsterisk::matches(const std::string_view) const noexcept + -> bool { + return true; +} + +auto URLPatternPartAsterisk::matches(const std::string_view) const noexcept + -> bool { + return true; +} + +auto URLPatternPartAsteriskOptional::matches( + const std::string_view) const noexcept -> bool { + return true; +} + +auto URLPatternPartAsteriskMultiple::matches( + const std::string_view) const noexcept -> bool { + return true; +} + +auto URLPatternPartAsteriskAsterisk::matches( + const std::string_view) const noexcept -> bool { + return true; +} + +// NOLINTNEXTLINE(bugprone-exception-escape) +auto URLPatternPartGroup::matches(const std::string_view segment) const noexcept + -> bool { + if (this->value.size() == 1 && + std::holds_alternative(this->value.front())) { + const auto &char_token = std::get(this->value.front()); + return char_token.value == segment; + } + + return true; +} + +auto URLPatternPartGroupOptional::matches(const std::string_view) const noexcept + -> bool { + return true; +} + +auto URLPatternPartGroupMultiple::matches(const std::string_view) const noexcept + -> bool { + return true; +} + +auto URLPatternPartGroupAsterisk::matches(const std::string_view) const noexcept + -> bool { + return true; +} + +auto URLPatternPartComplexSegment::matches( + const std::string_view) const noexcept -> bool { + return true; +} + +} // namespace sourcemeta::core diff --git a/test/packaging/find_package/CMakeLists.txt b/test/packaging/find_package/CMakeLists.txt index a23d5bb31..60a4ce564 100644 --- a/test/packaging/find_package/CMakeLists.txt +++ b/test/packaging/find_package/CMakeLists.txt @@ -13,6 +13,7 @@ target_link_libraries(core_hello PRIVATE sourcemeta::core::time) target_link_libraries(core_hello PRIVATE sourcemeta::core::uuid) target_link_libraries(core_hello PRIVATE sourcemeta::core::md5) target_link_libraries(core_hello PRIVATE sourcemeta::core::uri) +target_link_libraries(core_hello PRIVATE sourcemeta::core::urlpattern) target_link_libraries(core_hello PRIVATE sourcemeta::core::json) target_link_libraries(core_hello PRIVATE sourcemeta::core::jsonschema) target_link_libraries(core_hello PRIVATE sourcemeta::core::jsonpointer) diff --git a/test/packaging/find_package/hello.cc b/test/packaging/find_package/hello.cc index 4e31351a4..221e6f3e1 100644 --- a/test/packaging/find_package/hello.cc +++ b/test/packaging/find_package/hello.cc @@ -14,6 +14,7 @@ #include #include #include +#include #include #include diff --git a/test/urlpattern/CMakeLists.txt b/test/urlpattern/CMakeLists.txt new file mode 100644 index 000000000..5f5a97482 --- /dev/null +++ b/test/urlpattern/CMakeLists.txt @@ -0,0 +1,40 @@ +sourcemeta_googletest(NAMESPACE sourcemeta PROJECT core NAME urlpattern + SOURCES + urlpattern_test_utils.h + urlpattern_parse_test.cc + urlpattern_parse_string_test.cc + urlpattern_parse_json_test.cc + urlpattern_parse_input_json_test.cc + urlpattern_match_test.cc + urlpattern_match_pathname_test.cc + urlpattern_parse_pathname_test.cc + urlpattern_parse_hostname_test.cc + urlpattern_match_hostname_test.cc + urlpattern_parse_protocol_test.cc + urlpattern_parse_username_test.cc + urlpattern_parse_password_test.cc + urlpattern_parse_port_test.cc + urlpattern_parse_search_test.cc + urlpattern_parse_hash_test.cc + urlpattern_match_protocol_test.cc + urlpattern_match_username_test.cc + urlpattern_match_password_test.cc + urlpattern_match_port_test.cc + urlpattern_match_search_test.cc + urlpattern_match_hash_test.cc + urlpattern_compare_test.cc) + +target_link_libraries(sourcemeta_core_urlpattern_unit + PRIVATE sourcemeta::core::urlpattern) + +# WPT URL Pattern Test Suite +sourcemeta_googletest(NAMESPACE sourcemeta PROJECT core NAME urlpattern_wpt + SOURCES urlpattern_wpt.cc) +target_compile_definitions(sourcemeta_core_urlpattern_wpt_unit + PRIVATE WPT_URLPATTERN_PATH="${PROJECT_SOURCE_DIR}/vendor/wpt/urlpattern") +target_link_libraries(sourcemeta_core_urlpattern_wpt_unit + PRIVATE sourcemeta::core::urlpattern) +target_link_libraries(sourcemeta_core_urlpattern_wpt_unit + PRIVATE sourcemeta::core::json) +target_link_libraries(sourcemeta_core_urlpattern_wpt_unit + PRIVATE sourcemeta::core::io) diff --git a/test/urlpattern/urlpattern_compare_test.cc b/test/urlpattern/urlpattern_compare_test.cc new file mode 100644 index 000000000..fbed099ea --- /dev/null +++ b/test/urlpattern/urlpattern_compare_test.cc @@ -0,0 +1,414 @@ +#include + +#include + +TEST(URLPattern_compare, protocol_equality_same_literal) { + const sourcemeta::core::URLPatternProtocol protocol1{"https"}; + const sourcemeta::core::URLPatternProtocol protocol2{"https"}; + EXPECT_TRUE(protocol1 == protocol2); + EXPECT_FALSE(protocol1 != protocol2); +} + +TEST(URLPattern_compare, protocol_equality_different_literal) { + const sourcemeta::core::URLPatternProtocol protocol1{"https"}; + const sourcemeta::core::URLPatternProtocol protocol2{"http"}; + EXPECT_FALSE(protocol1 == protocol2); + EXPECT_TRUE(protocol1 != protocol2); +} + +TEST(URLPattern_compare, protocol_equality_asterisk) { + const sourcemeta::core::URLPatternProtocol protocol1{"*"}; + const sourcemeta::core::URLPatternProtocol protocol2{"*"}; + EXPECT_TRUE(protocol1 == protocol2); + EXPECT_FALSE(protocol1 != protocol2); +} + +TEST(URLPattern_compare, protocol_less_than) { + const sourcemeta::core::URLPatternProtocol protocol1{"http"}; + const sourcemeta::core::URLPatternProtocol protocol2{"https"}; + EXPECT_TRUE(protocol1 < protocol2); + EXPECT_FALSE(protocol2 < protocol1); + EXPECT_TRUE(protocol1 <= protocol2); + EXPECT_FALSE(protocol2 <= protocol1); +} + +TEST(URLPattern_compare, protocol_greater_than) { + const sourcemeta::core::URLPatternProtocol protocol1{"https"}; + const sourcemeta::core::URLPatternProtocol protocol2{"http"}; + EXPECT_TRUE(protocol1 > protocol2); + EXPECT_FALSE(protocol2 > protocol1); + EXPECT_TRUE(protocol1 >= protocol2); + EXPECT_FALSE(protocol2 >= protocol1); +} + +TEST(URLPattern_compare, protocol_less_or_equal_same) { + const sourcemeta::core::URLPatternProtocol protocol1{"https"}; + const sourcemeta::core::URLPatternProtocol protocol2{"https"}; + EXPECT_TRUE(protocol1 <= protocol2); + EXPECT_TRUE(protocol1 >= protocol2); +} + +TEST(URLPattern_compare, username_equality_same) { + const sourcemeta::core::URLPatternUsername username1{"user"}; + const sourcemeta::core::URLPatternUsername username2{"user"}; + EXPECT_TRUE(username1 == username2); + EXPECT_FALSE(username1 != username2); +} + +TEST(URLPattern_compare, username_equality_different) { + const sourcemeta::core::URLPatternUsername username1{"user1"}; + const sourcemeta::core::URLPatternUsername username2{"user2"}; + EXPECT_FALSE(username1 == username2); + EXPECT_TRUE(username1 != username2); +} + +TEST(URLPattern_compare, password_equality_same) { + const sourcemeta::core::URLPatternPassword password1{"pass"}; + const sourcemeta::core::URLPatternPassword password2{"pass"}; + EXPECT_TRUE(password1 == password2); + EXPECT_FALSE(password1 != password2); +} + +TEST(URLPattern_compare, password_equality_different) { + const sourcemeta::core::URLPatternPassword password1{"pass1"}; + const sourcemeta::core::URLPatternPassword password2{"pass2"}; + EXPECT_FALSE(password1 == password2); + EXPECT_TRUE(password1 != password2); +} + +TEST(URLPattern_compare, hostname_equality_same) { + const sourcemeta::core::URLPatternHostname hostname1{"example.com"}; + const sourcemeta::core::URLPatternHostname hostname2{"example.com"}; + EXPECT_TRUE(hostname1 == hostname2); + EXPECT_FALSE(hostname1 != hostname2); +} + +TEST(URLPattern_compare, hostname_equality_different) { + const sourcemeta::core::URLPatternHostname hostname1{"example.com"}; + const sourcemeta::core::URLPatternHostname hostname2{"test.com"}; + EXPECT_FALSE(hostname1 == hostname2); + EXPECT_TRUE(hostname1 != hostname2); +} + +TEST(URLPattern_compare, port_equality_same) { + const sourcemeta::core::URLPatternPort port1{"8080"}; + const sourcemeta::core::URLPatternPort port2{"8080"}; + EXPECT_TRUE(port1 == port2); + EXPECT_FALSE(port1 != port2); +} + +TEST(URLPattern_compare, port_equality_different) { + const sourcemeta::core::URLPatternPort port1{"8080"}; + const sourcemeta::core::URLPatternPort port2{"3000"}; + EXPECT_FALSE(port1 == port2); + EXPECT_TRUE(port1 != port2); +} + +TEST(URLPattern_compare, pathname_equality_same) { + const sourcemeta::core::URLPatternPathname pathname1{"/foo/bar"}; + const sourcemeta::core::URLPatternPathname pathname2{"/foo/bar"}; + EXPECT_TRUE(pathname1 == pathname2); + EXPECT_FALSE(pathname1 != pathname2); +} + +TEST(URLPattern_compare, pathname_equality_different) { + const sourcemeta::core::URLPatternPathname pathname1{"/foo/bar"}; + const sourcemeta::core::URLPatternPathname pathname2{"/baz"}; + EXPECT_FALSE(pathname1 == pathname2); + EXPECT_TRUE(pathname1 != pathname2); +} + +TEST(URLPattern_compare, search_equality_same) { + const sourcemeta::core::URLPatternSearch search1{"?q=test"}; + const sourcemeta::core::URLPatternSearch search2{"?q=test"}; + EXPECT_TRUE(search1 == search2); + EXPECT_FALSE(search1 != search2); +} + +TEST(URLPattern_compare, search_equality_different) { + const sourcemeta::core::URLPatternSearch search1{"?q=test"}; + const sourcemeta::core::URLPatternSearch search2{"?q=other"}; + EXPECT_FALSE(search1 == search2); + EXPECT_TRUE(search1 != search2); +} + +TEST(URLPattern_compare, hash_equality_same) { + const sourcemeta::core::URLPatternHash hash1{"#section"}; + const sourcemeta::core::URLPatternHash hash2{"#section"}; + EXPECT_TRUE(hash1 == hash2); + EXPECT_FALSE(hash1 != hash2); +} + +TEST(URLPattern_compare, hash_equality_different) { + const sourcemeta::core::URLPatternHash hash1{"#section1"}; + const sourcemeta::core::URLPatternHash hash2{"#section2"}; + EXPECT_FALSE(hash1 == hash2); + EXPECT_TRUE(hash1 != hash2); +} + +TEST(URLPattern_compare, protocol_char_vs_name) { + const sourcemeta::core::URLPatternProtocol protocol1{"https"}; + const sourcemeta::core::URLPatternProtocol protocol2{":protocol"}; + EXPECT_FALSE(protocol1 == protocol2); + EXPECT_TRUE(protocol1 != protocol2); + EXPECT_TRUE(protocol2 < protocol1); + EXPECT_FALSE(protocol1 < protocol2); +} + +TEST(URLPattern_compare, protocol_char_vs_asterisk) { + const sourcemeta::core::URLPatternProtocol protocol1{"https"}; + const sourcemeta::core::URLPatternProtocol protocol2{"*"}; + EXPECT_FALSE(protocol1 == protocol2); + EXPECT_TRUE(protocol1 != protocol2); + // WHATWG: less specific (asterisk) sorts first + EXPECT_TRUE(protocol2 < protocol1); + EXPECT_FALSE(protocol1 < protocol2); +} + +TEST(URLPattern_compare, protocol_char_vs_regex) { + const sourcemeta::core::URLPatternProtocol protocol1{"https"}; + const sourcemeta::core::URLPatternProtocol protocol2{"(https?)"}; + EXPECT_FALSE(protocol1 == protocol2); + EXPECT_TRUE(protocol1 != protocol2); + // WHATWG: less specific (regex) sorts first + EXPECT_TRUE(protocol2 < protocol1); + EXPECT_FALSE(protocol1 < protocol2); +} + +TEST(URLPattern_compare, protocol_name_vs_asterisk) { + const sourcemeta::core::URLPatternProtocol protocol1{":protocol"}; + const sourcemeta::core::URLPatternProtocol protocol2{"*"}; + EXPECT_FALSE(protocol1 == protocol2); + EXPECT_TRUE(protocol1 != protocol2); + // WHATWG: less specific (asterisk) sorts first + EXPECT_TRUE(protocol2 < protocol1); + EXPECT_FALSE(protocol1 < protocol2); +} + +TEST(URLPattern_compare, protocol_name_vs_regex) { + const sourcemeta::core::URLPatternProtocol protocol1{":protocol"}; + const sourcemeta::core::URLPatternProtocol protocol2{"(https?)"}; + EXPECT_FALSE(protocol1 == protocol2); + EXPECT_TRUE(protocol1 != protocol2); + // WHATWG: less specific (name/segment-wildcard) sorts first + EXPECT_TRUE(protocol1 < protocol2); + EXPECT_FALSE(protocol2 < protocol1); +} + +TEST(URLPattern_compare, protocol_asterisk_vs_regex) { + const sourcemeta::core::URLPatternProtocol protocol1{"*"}; + const sourcemeta::core::URLPatternProtocol protocol2{"(https?)"}; + EXPECT_FALSE(protocol1 == protocol2); + EXPECT_TRUE(protocol1 != protocol2); + // WHATWG: less specific (asterisk/full-wildcard) sorts first + EXPECT_TRUE(protocol1 < protocol2); + EXPECT_FALSE(protocol2 < protocol1); +} + +TEST(URLPattern_compare, username_char_vs_name) { + const sourcemeta::core::URLPatternUsername username1{"admin"}; + const sourcemeta::core::URLPatternUsername username2{":user"}; + EXPECT_FALSE(username1 == username2); + EXPECT_TRUE(username1 != username2); + EXPECT_TRUE(username2 < username1); + EXPECT_FALSE(username1 < username2); +} + +TEST(URLPattern_compare, username_char_vs_asterisk) { + const sourcemeta::core::URLPatternUsername username1{"admin"}; + const sourcemeta::core::URLPatternUsername username2{"*"}; + EXPECT_FALSE(username1 == username2); + EXPECT_TRUE(username1 != username2); + // WHATWG: less specific (asterisk) sorts first + EXPECT_TRUE(username2 < username1); + EXPECT_FALSE(username1 < username2); +} + +TEST(URLPattern_compare, password_char_vs_name) { + const sourcemeta::core::URLPatternPassword password1{"secret"}; + const sourcemeta::core::URLPatternPassword password2{":pass"}; + EXPECT_FALSE(password1 == password2); + EXPECT_TRUE(password1 != password2); + EXPECT_TRUE(password2 < password1); + EXPECT_FALSE(password1 < password2); +} + +TEST(URLPattern_compare, password_char_vs_asterisk) { + const sourcemeta::core::URLPatternPassword password1{"secret"}; + const sourcemeta::core::URLPatternPassword password2{"*"}; + EXPECT_FALSE(password1 == password2); + EXPECT_TRUE(password1 != password2); + // WHATWG: less specific (asterisk) sorts first + EXPECT_TRUE(password2 < password1); + EXPECT_FALSE(password1 < password2); +} + +TEST(URLPattern_compare, port_char_vs_name) { + const sourcemeta::core::URLPatternPort port1{"8080"}; + const sourcemeta::core::URLPatternPort port2{":port"}; + EXPECT_FALSE(port1 == port2); + EXPECT_TRUE(port1 != port2); + EXPECT_TRUE(port2 < port1); + EXPECT_FALSE(port1 < port2); +} + +TEST(URLPattern_compare, port_char_vs_asterisk) { + const sourcemeta::core::URLPatternPort port1{"8080"}; + const sourcemeta::core::URLPatternPort port2{"*"}; + EXPECT_FALSE(port1 == port2); + EXPECT_TRUE(port1 != port2); + // WHATWG: less specific (asterisk) sorts first + EXPECT_TRUE(port2 < port1); + EXPECT_FALSE(port1 < port2); +} + +TEST(URLPattern_compare, search_char_vs_name) { + const sourcemeta::core::URLPatternSearch search1{"?q=test"}; + const sourcemeta::core::URLPatternSearch search2{":search"}; + EXPECT_FALSE(search1 == search2); + EXPECT_TRUE(search1 != search2); + EXPECT_TRUE(search2 < search1); + EXPECT_FALSE(search1 < search2); +} + +TEST(URLPattern_compare, search_char_vs_asterisk) { + const sourcemeta::core::URLPatternSearch search1{"?q=test"}; + const sourcemeta::core::URLPatternSearch search2{"*"}; + EXPECT_FALSE(search1 == search2); + EXPECT_TRUE(search1 != search2); + // WHATWG: less specific (asterisk) sorts first + EXPECT_TRUE(search2 < search1); + EXPECT_FALSE(search1 < search2); +} + +TEST(URLPattern_compare, hash_char_vs_name) { + const sourcemeta::core::URLPatternHash hash1{"#section"}; + const sourcemeta::core::URLPatternHash hash2{":hash"}; + EXPECT_FALSE(hash1 == hash2); + EXPECT_TRUE(hash1 != hash2); + EXPECT_TRUE(hash2 < hash1); + EXPECT_FALSE(hash1 < hash2); +} + +TEST(URLPattern_compare, hash_char_vs_asterisk) { + const sourcemeta::core::URLPatternHash hash1{"#section"}; + const sourcemeta::core::URLPatternHash hash2{"*"}; + EXPECT_FALSE(hash1 == hash2); + EXPECT_TRUE(hash1 != hash2); + // WHATWG: less specific (asterisk) sorts first + EXPECT_TRUE(hash2 < hash1); + EXPECT_FALSE(hash1 < hash2); +} + +TEST(URLPattern_compare, hostname_char_vs_name) { + const sourcemeta::core::URLPatternHostname hostname1{"example.com"}; + const sourcemeta::core::URLPatternHostname hostname2{":domain.com"}; + EXPECT_FALSE(hostname1 == hostname2); + EXPECT_TRUE(hostname1 != hostname2); + EXPECT_TRUE(hostname2 < hostname1); + EXPECT_FALSE(hostname1 < hostname2); +} + +TEST(URLPattern_compare, hostname_char_vs_asterisk) { + const sourcemeta::core::URLPatternHostname hostname1{"example.com"}; + const sourcemeta::core::URLPatternHostname hostname2{"*.com"}; + EXPECT_FALSE(hostname1 == hostname2); + EXPECT_TRUE(hostname1 != hostname2); + // WHATWG: less specific (asterisk) sorts first + EXPECT_TRUE(hostname2 < hostname1); + EXPECT_FALSE(hostname1 < hostname2); +} + +TEST(URLPattern_compare, pathname_char_vs_name) { + const sourcemeta::core::URLPatternPathname pathname1{"/api/users"}; + const sourcemeta::core::URLPatternPathname pathname2{"/api/:id"}; + EXPECT_FALSE(pathname1 == pathname2); + EXPECT_TRUE(pathname1 != pathname2); + EXPECT_TRUE(pathname2 < pathname1); + EXPECT_FALSE(pathname1 < pathname2); +} + +TEST(URLPattern_compare, pathname_char_vs_asterisk) { + const sourcemeta::core::URLPatternPathname pathname1{"/api/users"}; + const sourcemeta::core::URLPatternPathname pathname2{"/api/*"}; + EXPECT_FALSE(pathname1 == pathname2); + EXPECT_TRUE(pathname1 != pathname2); + // WHATWG: less specific (asterisk) sorts first + EXPECT_TRUE(pathname2 < pathname1); + EXPECT_FALSE(pathname1 < pathname2); +} + +TEST(URLPattern_compare, pattern_default_constructed) { + const sourcemeta::core::URLPattern pattern1; + const sourcemeta::core::URLPattern pattern2; + EXPECT_TRUE(pattern1 == pattern2); + EXPECT_FALSE(pattern1 != pattern2); + EXPECT_TRUE(pattern1 <= pattern2); + EXPECT_TRUE(pattern1 >= pattern2); + EXPECT_FALSE(pattern1 < pattern2); + EXPECT_FALSE(pattern1 > pattern2); +} + +TEST(URLPattern_compare, pattern_same_protocol) { + const auto pattern1{ + sourcemeta::core::URLPattern::parse("https://example.com/foo")}; + const auto pattern2{ + sourcemeta::core::URLPattern::parse("https://example.com/foo")}; + EXPECT_TRUE(pattern1 == pattern2); + EXPECT_FALSE(pattern1 != pattern2); +} + +TEST(URLPattern_compare, pattern_different_protocol) { + const auto pattern1{ + sourcemeta::core::URLPattern::parse("http://example.com/foo")}; + const auto pattern2{ + sourcemeta::core::URLPattern::parse("https://example.com/foo")}; + EXPECT_FALSE(pattern1 == pattern2); + EXPECT_TRUE(pattern1 != pattern2); + EXPECT_TRUE(pattern1 < pattern2); + EXPECT_FALSE(pattern2 < pattern1); +} + +TEST(URLPattern_compare, pattern_different_hostname) { + const auto pattern1{ + sourcemeta::core::URLPattern::parse("https://aaa.com/foo")}; + const auto pattern2{ + sourcemeta::core::URLPattern::parse("https://zzz.com/foo")}; + EXPECT_FALSE(pattern1 == pattern2); + EXPECT_TRUE(pattern1 != pattern2); + EXPECT_TRUE(pattern1 < pattern2); + EXPECT_FALSE(pattern2 < pattern1); +} + +TEST(URLPattern_compare, pattern_different_pathname) { + const auto pattern1{ + sourcemeta::core::URLPattern::parse("https://example.com/aaa")}; + const auto pattern2{ + sourcemeta::core::URLPattern::parse("https://example.com/zzz")}; + EXPECT_FALSE(pattern1 == pattern2); + EXPECT_TRUE(pattern1 != pattern2); + EXPECT_TRUE(pattern1 < pattern2); + EXPECT_FALSE(pattern2 < pattern1); +} + +TEST(URLPattern_compare, pattern_different_port) { + const auto pattern1{ + sourcemeta::core::URLPattern::parse("https://example.com:3000/foo")}; + const auto pattern2{ + sourcemeta::core::URLPattern::parse("https://example.com:8080/foo")}; + EXPECT_FALSE(pattern1 == pattern2); + EXPECT_TRUE(pattern1 != pattern2); + EXPECT_TRUE(pattern1 < pattern2); + EXPECT_FALSE(pattern2 < pattern1); +} + +TEST(URLPattern_compare, pattern_protocol_takes_precedence) { + const auto pattern1{ + sourcemeta::core::URLPattern::parse("https://aaa.com/aaa")}; + const auto pattern2{ + sourcemeta::core::URLPattern::parse("http://zzz.com/zzz")}; + EXPECT_FALSE(pattern1 == pattern2); + EXPECT_TRUE(pattern1 != pattern2); + EXPECT_TRUE(pattern2 < pattern1); + EXPECT_FALSE(pattern1 < pattern2); +} diff --git a/test/urlpattern/urlpattern_match_hash_test.cc b/test/urlpattern/urlpattern_match_hash_test.cc new file mode 100644 index 000000000..4746ffff4 --- /dev/null +++ b/test/urlpattern/urlpattern_match_hash_test.cc @@ -0,0 +1,87 @@ +#include + +#include + +TEST(URLPattern_match, hash_char_exact_match) { + const sourcemeta::core::URLPattern pattern{.hash = "section"}; + const auto result{pattern.hash.match("section")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, hash_char_no_match) { + const sourcemeta::core::URLPattern pattern{.hash = "section"}; + const auto result{pattern.hash.match("intro")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, hash_name_basic) { + const sourcemeta::core::URLPattern pattern{.hash = ":fragment"}; + const auto result{pattern.hash.match("section")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "section"); + EXPECT_TRUE(result.value().at("fragment").has_value()); + EXPECT_EQ(result.value().at("fragment").value(), "section"); +} + +TEST(URLPattern_match, hash_name_any_value) { + const sourcemeta::core::URLPattern pattern{.hash = ":fragment"}; + const auto result{pattern.hash.match("heading-1")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "heading-1"); + EXPECT_TRUE(result.value().at("fragment").has_value()); + EXPECT_EQ(result.value().at("fragment").value(), "heading-1"); +} + +TEST(URLPattern_match, hash_name_regex_match) { + const sourcemeta::core::URLPattern pattern{.hash = ":fragment(\\w+)"}; + const auto result{pattern.hash.match("section")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "section"); + EXPECT_TRUE(result.value().at("fragment").has_value()); + EXPECT_EQ(result.value().at("fragment").value(), "section"); +} + +TEST(URLPattern_match, hash_name_regex_no_match) { + const sourcemeta::core::URLPattern pattern{.hash = ":fragment(\\d+)"}; + const auto result{pattern.hash.match("section")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, hash_regex_match) { + const sourcemeta::core::URLPattern pattern{.hash = "(\\w+)"}; + const auto result{pattern.hash.match("section")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "section"); +} + +TEST(URLPattern_match, hash_regex_no_match) { + const sourcemeta::core::URLPattern pattern{.hash = "(\\d+)"}; + const auto result{pattern.hash.match("section")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, hash_asterisk) { + const sourcemeta::core::URLPattern pattern{.hash = "*"}; + const auto result{pattern.hash.match("section")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "section"); +} + +TEST(URLPattern_match, hash_group_match) { + const sourcemeta::core::URLPattern pattern{.hash = "{section}"}; + const auto result{pattern.hash.match("section")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, hash_group_no_match) { + const sourcemeta::core::URLPattern pattern{.hash = "{section}"}; + const auto result{pattern.hash.match("intro")}; + EXPECT_FALSE(result.has_value()); +} diff --git a/test/urlpattern/urlpattern_match_hostname_test.cc b/test/urlpattern/urlpattern_match_hostname_test.cc new file mode 100644 index 000000000..90b9bde22 --- /dev/null +++ b/test/urlpattern/urlpattern_match_hostname_test.cc @@ -0,0 +1,779 @@ +#include + +#include + +TEST(URLPattern_match, hostname_name_basic) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo.:bar"}; + const auto result{pattern.hostname.match("foo.baz")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "baz"); + EXPECT_TRUE(result.value().at("bar").has_value()); + EXPECT_EQ(result.value().at("bar").value(), "baz"); +} + +TEST(URLPattern_match, hostname_name_too_few_segments) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo.:bar"}; + const auto result{pattern.hostname.match("foo")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, hostname_multiple_names) { + const sourcemeta::core::URLPattern pattern{.hostname = + "api.:version.users.:id"}; + const auto result{pattern.hostname.match("api.v1.users.123")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 2); + EXPECT_EQ(result.value().at(0), "v1"); + EXPECT_EQ(result.value().at(1), "123"); + EXPECT_TRUE(result.value().at("version").has_value()); + EXPECT_EQ(result.value().at("version").value(), "v1"); + EXPECT_TRUE(result.value().at("id").has_value()); + EXPECT_EQ(result.value().at("id").value(), "123"); +} + +TEST(URLPattern_match, hostname_name_optional_present) { + const sourcemeta::core::URLPattern pattern{.hostname = ":foo?"}; + const auto result{pattern.hostname.match("bar")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "bar"); + EXPECT_TRUE(result.value().at("foo").has_value()); + EXPECT_EQ(result.value().at("foo").value(), "bar"); +} + +TEST(URLPattern_match, hostname_name_optional_absent) { + const sourcemeta::core::URLPattern pattern{.hostname = ":foo?"}; + const auto result{pattern.hostname.match("")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, hostname_name_optional_with_prefix_present) { + const sourcemeta::core::URLPattern pattern{.hostname = "api.:version?"}; + const auto result{pattern.hostname.match("api.v1")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "v1"); + EXPECT_TRUE(result.value().at("version").has_value()); + EXPECT_EQ(result.value().at("version").value(), "v1"); +} + +TEST(URLPattern_match, hostname_name_optional_with_prefix_absent) { + const sourcemeta::core::URLPattern pattern{.hostname = "api.:version?"}; + const auto result{pattern.hostname.match("api")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, hostname_name_optional_with_suffix_present) { + const sourcemeta::core::URLPattern pattern{.hostname = ":id?.profile"}; + const auto result{pattern.hostname.match("123.profile")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "123"); + EXPECT_TRUE(result.value().at("id").has_value()); + EXPECT_EQ(result.value().at("id").value(), "123"); +} + +TEST(URLPattern_match, hostname_name_optional_with_suffix_absent) { + const sourcemeta::core::URLPattern pattern{.hostname = ":id?.profile"}; + const auto result{pattern.hostname.match("profile")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, hostname_multiple_name_optional_all_present) { + const sourcemeta::core::URLPattern pattern{.hostname = ":foo?.:bar?"}; + const auto result{pattern.hostname.match("first.second")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 2); + EXPECT_EQ(result.value().at(0), "first"); + EXPECT_EQ(result.value().at(1), "second"); + EXPECT_TRUE(result.value().at("foo").has_value()); + EXPECT_EQ(result.value().at("foo").value(), "first"); + EXPECT_TRUE(result.value().at("bar").has_value()); + EXPECT_EQ(result.value().at("bar").value(), "second"); +} + +TEST(URLPattern_match, hostname_multiple_name_optional_all_absent) { + const sourcemeta::core::URLPattern pattern{.hostname = ":foo?.:bar?"}; + const auto result{pattern.hostname.match("")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, hostname_name_optional_mixed_required_present) { + const sourcemeta::core::URLPattern pattern{.hostname = "users.:id.:action?"}; + const auto result{pattern.hostname.match("users.42.edit")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 2); + EXPECT_EQ(result.value().at(0), "42"); + EXPECT_EQ(result.value().at(1), "edit"); + EXPECT_TRUE(result.value().at("id").has_value()); + EXPECT_EQ(result.value().at("id").value(), "42"); + EXPECT_TRUE(result.value().at("action").has_value()); + EXPECT_EQ(result.value().at("action").value(), "edit"); +} + +TEST(URLPattern_match, hostname_name_optional_mixed_required_absent) { + const sourcemeta::core::URLPattern pattern{.hostname = "users.:id.:action?"}; + const auto result{pattern.hostname.match("users.42")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "42"); + EXPECT_TRUE(result.value().at("id").has_value()); + EXPECT_EQ(result.value().at("id").value(), "42"); +} + +TEST(URLPattern_match, hostname_name_multiple_single_segment) { + const sourcemeta::core::URLPattern pattern{.hostname = ":foo+"}; + const auto result{pattern.hostname.match("bar")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "bar"); + EXPECT_TRUE(result.value().at("foo").has_value()); + EXPECT_EQ(result.value().at("foo").value(), "bar"); +} + +TEST(URLPattern_match, hostname_name_multiple_two_segments) { + const sourcemeta::core::URLPattern pattern{.hostname = ":foo+"}; + const auto result{pattern.hostname.match("bar.baz")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "bar.baz"); + EXPECT_TRUE(result.value().at("foo").has_value()); + EXPECT_EQ(result.value().at("foo").value(), "bar.baz"); +} + +TEST(URLPattern_match, hostname_name_multiple_three_segments) { + const sourcemeta::core::URLPattern pattern{.hostname = ":foo+"}; + const auto result{pattern.hostname.match("bar.baz.qux")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "bar.baz.qux"); + EXPECT_TRUE(result.value().at("foo").has_value()); + EXPECT_EQ(result.value().at("foo").value(), "bar.baz.qux"); +} + +TEST(URLPattern_match, hostname_name_multiple_with_prefix) { + const sourcemeta::core::URLPattern pattern{.hostname = "api.:path+"}; + const auto result{pattern.hostname.match("api.v1.users")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "v1.users"); + EXPECT_TRUE(result.value().at("path").has_value()); + EXPECT_EQ(result.value().at("path").value(), "v1.users"); +} + +TEST(URLPattern_match, hostname_name_multiple_empty_fails) { + const sourcemeta::core::URLPattern pattern{.hostname = ":foo+"}; + const auto result{pattern.hostname.match("")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, hostname_name_multiple_with_required_before) { + const sourcemeta::core::URLPattern pattern{.hostname = ":id.:path+"}; + const auto result{pattern.hostname.match("123.a.b.c")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 2); + EXPECT_EQ(result.value().at(0), "123"); + EXPECT_EQ(result.value().at(1), "a.b.c"); + EXPECT_TRUE(result.value().at("id").has_value()); + EXPECT_EQ(result.value().at("id").value(), "123"); + EXPECT_TRUE(result.value().at("path").has_value()); + EXPECT_EQ(result.value().at("path").value(), "a.b.c"); +} + +TEST(URLPattern_match, hostname_name_multiple_mixed_tokens) { + const sourcemeta::core::URLPattern pattern{.hostname = + "api.:version.files.:path+"}; + const auto result{pattern.hostname.match("api.v1.files.docs.readme")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 2); + EXPECT_EQ(result.value().at(0), "v1"); + EXPECT_EQ(result.value().at(1), "docs.readme"); + EXPECT_TRUE(result.value().at("version").has_value()); + EXPECT_EQ(result.value().at("version").value(), "v1"); + EXPECT_TRUE(result.value().at("path").has_value()); + EXPECT_EQ(result.value().at("path").value(), "docs.readme"); +} + +TEST(URLPattern_match, hostname_name_asterisk_zero_segments) { + const sourcemeta::core::URLPattern pattern{.hostname = ":foo*"}; + const auto result{pattern.hostname.match("")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, hostname_name_asterisk_single_segment) { + const sourcemeta::core::URLPattern pattern{.hostname = ":foo*"}; + const auto result{pattern.hostname.match("bar")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "bar"); + EXPECT_TRUE(result.value().at("foo").has_value()); + EXPECT_EQ(result.value().at("foo").value(), "bar"); +} + +TEST(URLPattern_match, hostname_name_asterisk_two_segments) { + const sourcemeta::core::URLPattern pattern{.hostname = ":foo*"}; + const auto result{pattern.hostname.match("bar.baz")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "bar.baz"); + EXPECT_TRUE(result.value().at("foo").has_value()); + EXPECT_EQ(result.value().at("foo").value(), "bar.baz"); +} + +TEST(URLPattern_match, hostname_name_asterisk_three_segments) { + const sourcemeta::core::URLPattern pattern{.hostname = ":foo*"}; + const auto result{pattern.hostname.match("bar.baz.qux")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "bar.baz.qux"); + EXPECT_TRUE(result.value().at("foo").has_value()); + EXPECT_EQ(result.value().at("foo").value(), "bar.baz.qux"); +} + +TEST(URLPattern_match, hostname_name_asterisk_with_prefix_zero_segments) { + const sourcemeta::core::URLPattern pattern{.hostname = "api.:path*"}; + const auto result{pattern.hostname.match("api")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_hostname, match_name_asterisk_with_prefix_multiple_segments) { + const sourcemeta::core::URLPattern pattern{.hostname = "api.:path*"}; + const auto result{pattern.hostname.match("api.v1.users")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "v1.users"); + EXPECT_TRUE(result.value().at("path").has_value()); + EXPECT_EQ(result.value().at("path").value(), "v1.users"); +} + +TEST(URLPattern_hostname, + match_name_asterisk_with_required_before_zero_segments) { + const sourcemeta::core::URLPattern pattern{.hostname = ":id.:path*"}; + const auto result{pattern.hostname.match("123")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "123"); + EXPECT_TRUE(result.value().at("id").has_value()); + EXPECT_EQ(result.value().at("id").value(), "123"); +} + +TEST(URLPattern_hostname, + match_name_asterisk_with_required_before_multiple_segments) { + const sourcemeta::core::URLPattern pattern{.hostname = ":id.:path*"}; + const auto result{pattern.hostname.match("123.a.b.c")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 2); + EXPECT_EQ(result.value().at(0), "123"); + EXPECT_EQ(result.value().at(1), "a.b.c"); + EXPECT_TRUE(result.value().at("id").has_value()); + EXPECT_EQ(result.value().at("id").value(), "123"); + EXPECT_TRUE(result.value().at("path").has_value()); + EXPECT_EQ(result.value().at("path").value(), "a.b.c"); +} + +TEST(URLPattern_match, hostname_name_asterisk_mixed_tokens_zero_segments) { + const sourcemeta::core::URLPattern pattern{.hostname = + "api.:version.files.:path*"}; + const auto result{pattern.hostname.match("api.v1.files")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "v1"); + EXPECT_TRUE(result.value().at("version").has_value()); + EXPECT_EQ(result.value().at("version").value(), "v1"); +} + +TEST(URLPattern_hostname, match_name_asterisk_mixed_tokens_multiple_segments) { + const sourcemeta::core::URLPattern pattern{.hostname = + "api.:version.files.:path*"}; + const auto result{pattern.hostname.match("api.v1.files.docs.readme")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 2); + EXPECT_EQ(result.value().at(0), "v1"); + EXPECT_EQ(result.value().at(1), "docs.readme"); + EXPECT_TRUE(result.value().at("version").has_value()); + EXPECT_EQ(result.value().at("version").value(), "v1"); + EXPECT_TRUE(result.value().at("path").has_value()); + EXPECT_EQ(result.value().at("path").value(), "docs.readme"); +} + +TEST(URLPattern_match, hostname_name_regex_simple) { + const sourcemeta::core::URLPattern pattern{.hostname = ":foo(.*)"}; + const auto result{pattern.hostname.match("bar")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "bar"); + EXPECT_TRUE(result.value().at("foo").has_value()); + EXPECT_EQ(result.value().at("foo").value(), "bar"); +} + +TEST(URLPattern_match, hostname_name_regex_digits_match) { + const sourcemeta::core::URLPattern pattern{.hostname = ":id(\\d+)"}; + const auto result{pattern.hostname.match("123")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "123"); + EXPECT_TRUE(result.value().at("id").has_value()); + EXPECT_EQ(result.value().at("id").value(), "123"); +} + +TEST(URLPattern_match, hostname_name_regex_digits_no_match) { + const sourcemeta::core::URLPattern pattern{.hostname = ":id(\\d+)"}; + const auto result{pattern.hostname.match("abc")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, hostname_name_regex_with_prefix) { + const sourcemeta::core::URLPattern pattern{.hostname = "api.:version(v\\d+)"}; + const auto result{pattern.hostname.match("api.v2")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "v2"); + EXPECT_TRUE(result.value().at("version").has_value()); + EXPECT_EQ(result.value().at("version").value(), "v2"); +} + +TEST(URLPattern_match, hostname_name_regex_multiple) { + const sourcemeta::core::URLPattern pattern{.hostname = + ":category(\\w+).:id(\\d+)"}; + const auto result{pattern.hostname.match("products.123")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 2); + EXPECT_EQ(result.value().at(0), "products"); + EXPECT_EQ(result.value().at(1), "123"); + EXPECT_TRUE(result.value().at("category").has_value()); + EXPECT_EQ(result.value().at("category").value(), "products"); + EXPECT_TRUE(result.value().at("id").has_value()); + EXPECT_EQ(result.value().at("id").value(), "123"); +} + +TEST(URLPattern_match, hostname_name_regex_mixed_tokens) { + const sourcemeta::core::URLPattern pattern{ + .hostname = "api.:version(v\\d+).users.:id(\\d+)"}; + const auto result{pattern.hostname.match("api.v1.users.42")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 2); + EXPECT_EQ(result.value().at(0), "v1"); + EXPECT_EQ(result.value().at(1), "42"); + EXPECT_TRUE(result.value().at("version").has_value()); + EXPECT_EQ(result.value().at("version").value(), "v1"); + EXPECT_TRUE(result.value().at("id").has_value()); + EXPECT_EQ(result.value().at("id").value(), "42"); +} + +TEST(URLPattern_match, hostname_regex_simple) { + const sourcemeta::core::URLPattern pattern{.hostname = "(.*)"}; + const auto result{pattern.hostname.match("anything")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "anything"); +} + +TEST(URLPattern_match, hostname_regex_with_prefix) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo.(.*)"}; + const auto result{pattern.hostname.match("foo.bar")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "bar"); +} + +TEST(URLPattern_match, hostname_regex_digits_match) { + const sourcemeta::core::URLPattern pattern{.hostname = "(\\d+)"}; + const auto result{pattern.hostname.match("123")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "123"); +} + +TEST(URLPattern_match, hostname_regex_digits_no_match) { + const sourcemeta::core::URLPattern pattern{.hostname = "(\\d+)"}; + const auto result{pattern.hostname.match("abc")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, hostname_regex_complex) { + const sourcemeta::core::URLPattern pattern{.hostname = + "api.:version.(\\d+).details"}; + const auto result{pattern.hostname.match("api.v1.456.details")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 2); + EXPECT_EQ(result.value().at(0), "v1"); + EXPECT_EQ(result.value().at(1), "456"); +} + +TEST(URLPattern_match, hostname_regex_fail_pattern_mismatch) { + const sourcemeta::core::URLPattern pattern{.hostname = "(foo|bar)"}; + const auto result{pattern.hostname.match("baz")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, hostname_asterisk_alone) { + const sourcemeta::core::URLPattern pattern{.hostname = "*"}; + const auto result{pattern.hostname.match("anything")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "anything"); +} + +TEST(URLPattern_match, hostname_asterisk_with_prefix) { + const sourcemeta::core::URLPattern pattern{.hostname = "static.*"}; + const auto result{pattern.hostname.match("static.example")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "example"); +} + +TEST(URLPattern_match, hostname_asterisk_with_suffix) { + const sourcemeta::core::URLPattern pattern{.hostname = "*.foo"}; + const auto result{pattern.hostname.match("anything.foo")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "anything"); +} + +TEST(URLPattern_match, hostname_multiple_asterisks) { + const sourcemeta::core::URLPattern pattern{.hostname = "*.*"}; + const auto result{pattern.hostname.match("first.second")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 2); + EXPECT_EQ(result.value().at(0), "first"); + EXPECT_EQ(result.value().at(1), "second"); +} + +TEST(URLPattern_match, hostname_asterisk_optional_present) { + const sourcemeta::core::URLPattern pattern{.hostname = "*?"}; + const auto result{pattern.hostname.match("foo")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "foo"); +} + +TEST(URLPattern_match, hostname_asterisk_optional_absent) { + const sourcemeta::core::URLPattern pattern{.hostname = "*?"}; + const auto result{pattern.hostname.match("")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, hostname_asterisk_optional_with_prefix_present) { + const sourcemeta::core::URLPattern pattern{.hostname = "static.*?"}; + const auto result{pattern.hostname.match("static.example")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "example"); +} + +TEST(URLPattern_match, hostname_asterisk_optional_with_prefix_absent) { + const sourcemeta::core::URLPattern pattern{.hostname = "static.*?"}; + const auto result{pattern.hostname.match("static")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, hostname_asterisk_optional_with_suffix_present) { + const sourcemeta::core::URLPattern pattern{.hostname = "*?.foo"}; + const auto result{pattern.hostname.match("anything.foo")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "anything"); +} + +TEST(URLPattern_match, hostname_asterisk_optional_with_suffix_absent) { + const sourcemeta::core::URLPattern pattern{.hostname = "*?.foo"}; + const auto result{pattern.hostname.match("foo")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, hostname_asterisk_optional_mixed) { + const sourcemeta::core::URLPattern pattern{.hostname = + "api.:version.*?.users"}; + const auto result{pattern.hostname.match("api.v1.users")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "v1"); + EXPECT_TRUE(result.value().at("version").has_value()); + EXPECT_EQ(result.value().at("version").value(), "v1"); +} + +TEST(URLPattern_match, hostname_asterisk_asterisk_zero_segments) { + const sourcemeta::core::URLPattern pattern{.hostname = "**"}; + const auto result{pattern.hostname.match("")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, hostname_asterisk_asterisk_single_segment) { + const sourcemeta::core::URLPattern pattern{.hostname = "**"}; + const auto result{pattern.hostname.match("foo")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "foo"); +} + +TEST(URLPattern_match, hostname_asterisk_asterisk_multiple_segments) { + const sourcemeta::core::URLPattern pattern{.hostname = "**"}; + const auto result{pattern.hostname.match("foo.bar.baz")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "foo.bar.baz"); +} + +TEST(URLPattern_match, hostname_asterisk_asterisk_with_prefix_zero) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo.**"}; + const auto result{pattern.hostname.match("foo")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, hostname_asterisk_asterisk_with_prefix_single) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo.**"}; + const auto result{pattern.hostname.match("foo.bar")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "bar"); +} + +TEST(URLPattern_match, hostname_asterisk_asterisk_with_prefix_multiple) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo.**"}; + const auto result{pattern.hostname.match("foo.bar.baz")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "bar.baz"); +} + +TEST(URLPattern_match, hostname_asterisk_asterisk_mixed) { + const sourcemeta::core::URLPattern pattern{.hostname = "api.:version.**"}; + const auto result{pattern.hostname.match("api.v1.x.y.z")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 2); + EXPECT_EQ(result.value().at(0), "v1"); + EXPECT_EQ(result.value().at(1), "x.y.z"); + EXPECT_TRUE(result.value().at("version").has_value()); + EXPECT_EQ(result.value().at("version").value(), "v1"); +} + +TEST(URLPattern_match, hostname_all_token_types) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo.:bar.*"}; + const auto result{pattern.hostname.match("foo.baz.qux")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 2); + EXPECT_EQ(result.value().at(0), "baz"); + EXPECT_EQ(result.value().at(1), "qux"); + EXPECT_TRUE(result.value().at("bar").has_value()); + EXPECT_EQ(result.value().at("bar").value(), "baz"); +} + +TEST(URLPattern_match, hostname_complex_pattern) { + const sourcemeta::core::URLPattern pattern{ + .hostname = "api.:version.*.users.:id.profile"}; + const auto result{pattern.hostname.match("api.v2.public.users.456.profile")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 3); + EXPECT_EQ(result.value().at(0), "v2"); + EXPECT_EQ(result.value().at(1), "public"); + EXPECT_EQ(result.value().at(2), "456"); +} + +TEST(URLPattern_match, hostname_simple_char_token) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo"}; + const auto result{pattern.hostname.match("foo")}; + EXPECT_TRUE(result.has_value()); +} + +TEST(URLPattern_match, hostname_multiple_char_tokens) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo.bar.baz"}; + const auto result{pattern.hostname.match("foo.bar.baz")}; + EXPECT_TRUE(result.has_value()); +} + +TEST(URLPattern_match, hostname_empty_segment_pattern) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo..bar"}; + const auto result{pattern.hostname.match("foo..bar")}; + EXPECT_TRUE(result.has_value()); +} + +TEST(URLPattern_match, hostname_empty_segment_name) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo..:bar"}; + const auto result{pattern.hostname.match("foo..baz")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "baz"); +} + +TEST(URLPattern_match, hostname_fail_wrong_char) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo.bar"}; + const auto result{pattern.hostname.match("foo.baz")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, hostname_fail_too_many_segments) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo"}; + const auto result{pattern.hostname.match("foo.bar")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, hostname_fail_too_few_segments) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo.bar"}; + const auto result{pattern.hostname.match("foo")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, hostname_fail_empty_input) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo"}; + const auto result{pattern.hostname.match("")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, hostname_name_with_special_chars_in_value) { + const sourcemeta::core::URLPattern pattern{.hostname = ":filename"}; + const auto result{pattern.hostname.match("image-2024")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "image-2024"); +} + +TEST(URLPattern_match, hostname_asterisk_with_special_chars) { + const sourcemeta::core::URLPattern pattern{.hostname = "*"}; + const auto result{pattern.hostname.match("host-with_special")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "host-with_special"); +} + +TEST(URLPattern_match, hostname_group_simple) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo{.bar}"}; + const auto result{pattern.hostname.match("foo.bar")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, hostname_group_simple_mismatch) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo{.bar}"}; + const auto result{pattern.hostname.match("foo")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, hostname_group_optional_present) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo{.bar}?"}; + const auto result{pattern.hostname.match("foo.bar")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, hostname_group_optional_absent) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo{.bar}?"}; + const auto result{pattern.hostname.match("foo")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, hostname_group_multiple_single) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo{.bar}+"}; + const auto result{pattern.hostname.match("foo.bar")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, hostname_group_multiple_twice) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo{.bar}+"}; + const auto result{pattern.hostname.match("foo.bar.bar")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, hostname_group_multiple_fails_empty) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo{.bar}+"}; + const auto result{pattern.hostname.match("foo")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, hostname_group_asterisk_zero) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo{.bar}*"}; + const auto result{pattern.hostname.match("foo")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, hostname_group_asterisk_once) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo{.bar}*"}; + const auto result{pattern.hostname.match("foo.bar")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, hostname_group_asterisk_twice) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo{.bar}*"}; + const auto result{pattern.hostname.match("foo.bar.bar")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, hostname_group_with_name) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo{.:bar}"}; + const auto result{pattern.hostname.match("foo.baz")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "baz"); + EXPECT_TRUE(result.value().at("bar").has_value()); + EXPECT_EQ(result.value().at("bar").value(), "baz"); +} + +TEST(URLPattern_match, hostname_group_with_name_optional_present) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo{.:bar}?"}; + const auto result{pattern.hostname.match("foo.baz")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "baz"); + EXPECT_TRUE(result.value().at("bar").has_value()); + EXPECT_EQ(result.value().at("bar").value(), "baz"); +} + +TEST(URLPattern_match, hostname_group_with_name_optional_absent) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo{.:bar}?"}; + const auto result{pattern.hostname.match("foo")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, hostname_complex_segment_with_name) { + const sourcemeta::core::URLPattern pattern{.hostname = "file-:name"}; + const auto result{pattern.hostname.match("file-test")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "test"); + EXPECT_TRUE(result.value().at("name").has_value()); + EXPECT_EQ(result.value().at("name").value(), "test"); +} + +TEST(URLPattern_match, hostname_complex_segment_mismatch_prefix) { + const sourcemeta::core::URLPattern pattern{.hostname = "file-:name"}; + const auto result{pattern.hostname.match("doc-test")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, hostname_complex_segment_multiple_names) { + const sourcemeta::core::URLPattern pattern{.hostname = "file-:name.json"}; + const auto result{pattern.hostname.match("file-test.json")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "test"); + EXPECT_TRUE(result.value().at("name").has_value()); + EXPECT_EQ(result.value().at("name").value(), "test"); +} + +TEST(URLPattern_match, hostname_complex_segment_with_group) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo-:bar{baz}+"}; + const auto result{pattern.hostname.match("foo-testbaz")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "test"); + EXPECT_TRUE(result.value().at("bar").has_value()); + EXPECT_EQ(result.value().at("bar").value(), "test"); +} diff --git a/test/urlpattern/urlpattern_match_password_test.cc b/test/urlpattern/urlpattern_match_password_test.cc new file mode 100644 index 000000000..232b132c9 --- /dev/null +++ b/test/urlpattern/urlpattern_match_password_test.cc @@ -0,0 +1,87 @@ +#include + +#include + +TEST(URLPattern_match, password_char_exact_match) { + const sourcemeta::core::URLPattern pattern{.password = "secret"}; + const auto result{pattern.password.match("secret")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, password_char_no_match) { + const sourcemeta::core::URLPattern pattern{.password = "secret"}; + const auto result{pattern.password.match("password")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, password_name_basic) { + const sourcemeta::core::URLPattern pattern{.password = ":pass"}; + const auto result{pattern.password.match("secret123")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "secret123"); + EXPECT_TRUE(result.value().at("pass").has_value()); + EXPECT_EQ(result.value().at("pass").value(), "secret123"); +} + +TEST(URLPattern_match, password_name_any_value) { + const sourcemeta::core::URLPattern pattern{.password = ":pass"}; + const auto result{pattern.password.match("mypassword")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "mypassword"); + EXPECT_TRUE(result.value().at("pass").has_value()); + EXPECT_EQ(result.value().at("pass").value(), "mypassword"); +} + +TEST(URLPattern_match, password_name_regex_match) { + const sourcemeta::core::URLPattern pattern{.password = ":pass(\\w+)"}; + const auto result{pattern.password.match("pass123")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "pass123"); + EXPECT_TRUE(result.value().at("pass").has_value()); + EXPECT_EQ(result.value().at("pass").value(), "pass123"); +} + +TEST(URLPattern_match, password_name_regex_no_match) { + const sourcemeta::core::URLPattern pattern{.password = ":pass(\\d+)"}; + const auto result{pattern.password.match("secret")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, password_regex_match) { + const sourcemeta::core::URLPattern pattern{.password = "(\\w+)"}; + const auto result{pattern.password.match("secret")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "secret"); +} + +TEST(URLPattern_match, password_regex_no_match) { + const sourcemeta::core::URLPattern pattern{.password = "(\\d+)"}; + const auto result{pattern.password.match("secret")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, password_asterisk) { + const sourcemeta::core::URLPattern pattern{.password = "*"}; + const auto result{pattern.password.match("secret")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "secret"); +} + +TEST(URLPattern_match, password_group_match) { + const sourcemeta::core::URLPattern pattern{.password = "{secret}"}; + const auto result{pattern.password.match("secret")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, password_group_no_match) { + const sourcemeta::core::URLPattern pattern{.password = "{secret}"}; + const auto result{pattern.password.match("password")}; + EXPECT_FALSE(result.has_value()); +} diff --git a/test/urlpattern/urlpattern_match_pathname_test.cc b/test/urlpattern/urlpattern_match_pathname_test.cc new file mode 100644 index 000000000..bebb62f14 --- /dev/null +++ b/test/urlpattern/urlpattern_match_pathname_test.cc @@ -0,0 +1,1163 @@ +#include + +#include + +TEST(URLPattern_match, pathname_name_optional_present) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:foo?"}; + const auto result{pattern.pathname.match("/bar")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "bar"); + EXPECT_TRUE(result.value().at("foo").has_value()); + EXPECT_EQ(result.value().at("foo").value(), "bar"); +} + +TEST(URLPattern_match, pathname_name_optional_absent) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:foo?"}; + const auto result{pattern.pathname.match("/")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, pathname_name_optional_with_prefix_present) { + const sourcemeta::core::URLPattern pattern{.pathname = "/api/:version?"}; + const auto result{pattern.pathname.match("/api/v1")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "v1"); + EXPECT_TRUE(result.value().at("version").has_value()); + EXPECT_EQ(result.value().at("version").value(), "v1"); +} + +TEST(URLPattern_match, pathname_name_optional_with_prefix_absent) { + const sourcemeta::core::URLPattern pattern{.pathname = "/api/:version?"}; + const auto result{pattern.pathname.match("/api")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, pathname_name_optional_with_suffix_present) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:id?/profile"}; + const auto result{pattern.pathname.match("/123/profile")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "123"); + EXPECT_TRUE(result.value().at("id").has_value()); + EXPECT_EQ(result.value().at("id").value(), "123"); +} + +TEST(URLPattern_match, pathname_name_optional_with_suffix_absent) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:id?/profile"}; + const auto result{pattern.pathname.match("/profile")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, pathname_multiple_name_optional_all_present) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:foo?/:bar?"}; + const auto result{pattern.pathname.match("/first/second")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 2); + EXPECT_EQ(result.value().at(0), "first"); + EXPECT_EQ(result.value().at(1), "second"); + EXPECT_TRUE(result.value().at("foo").has_value()); + EXPECT_EQ(result.value().at("foo").value(), "first"); + EXPECT_TRUE(result.value().at("bar").has_value()); + EXPECT_EQ(result.value().at("bar").value(), "second"); +} + +TEST(URLPattern_match, pathname_multiple_name_optional_all_absent) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:foo?/:bar?"}; + const auto result{pattern.pathname.match("/")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, pathname_name_optional_mixed_required_present) { + const sourcemeta::core::URLPattern pattern{.pathname = "/users/:id/:action?"}; + const auto result{pattern.pathname.match("/users/42/edit")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 2); + EXPECT_EQ(result.value().at(0), "42"); + EXPECT_EQ(result.value().at(1), "edit"); + EXPECT_TRUE(result.value().at("id").has_value()); + EXPECT_EQ(result.value().at("id").value(), "42"); + EXPECT_TRUE(result.value().at("action").has_value()); + EXPECT_EQ(result.value().at("action").value(), "edit"); +} + +TEST(URLPattern_match, pathname_name_optional_mixed_required_absent) { + const sourcemeta::core::URLPattern pattern{.pathname = "/users/:id/:action?"}; + const auto result{pattern.pathname.match("/users/42")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "42"); + EXPECT_TRUE(result.value().at("id").has_value()); + EXPECT_EQ(result.value().at("id").value(), "42"); +} + +TEST(URLPattern_match, pathname_name_multiple_single_segment) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:foo+"}; + const auto result{pattern.pathname.match("/bar")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "bar"); + EXPECT_TRUE(result.value().at("foo").has_value()); + EXPECT_EQ(result.value().at("foo").value(), "bar"); +} + +TEST(URLPattern_match, pathname_name_multiple_two_segments) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:foo+"}; + const auto result{pattern.pathname.match("/bar/baz")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "bar/baz"); + EXPECT_TRUE(result.value().at("foo").has_value()); + EXPECT_EQ(result.value().at("foo").value(), "bar/baz"); +} + +TEST(URLPattern_match, pathname_name_multiple_three_segments) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:foo+"}; + const auto result{pattern.pathname.match("/bar/baz/qux")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "bar/baz/qux"); + EXPECT_TRUE(result.value().at("foo").has_value()); + EXPECT_EQ(result.value().at("foo").value(), "bar/baz/qux"); +} + +TEST(URLPattern_match, pathname_name_multiple_with_prefix) { + const sourcemeta::core::URLPattern pattern{.pathname = "/api/:path+"}; + const auto result{pattern.pathname.match("/api/v1/users")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "v1/users"); + EXPECT_TRUE(result.value().at("path").has_value()); + EXPECT_EQ(result.value().at("path").value(), "v1/users"); +} + +TEST(URLPattern_match, pathname_name_multiple_empty_fails) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:foo+"}; + const auto result{pattern.pathname.match("/")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, pathname_name_multiple_with_required_before) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:id/:path+"}; + const auto result{pattern.pathname.match("/123/a/b/c")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 2); + EXPECT_EQ(result.value().at(0), "123"); + EXPECT_EQ(result.value().at(1), "a/b/c"); + EXPECT_TRUE(result.value().at("id").has_value()); + EXPECT_EQ(result.value().at("id").value(), "123"); + EXPECT_TRUE(result.value().at("path").has_value()); + EXPECT_EQ(result.value().at("path").value(), "a/b/c"); +} + +TEST(URLPattern_match, pathname_name_multiple_mixed_tokens) { + const sourcemeta::core::URLPattern pattern{.pathname = + "/api/:version/files/:path+"}; + const auto result{pattern.pathname.match("/api/v1/files/docs/readme.md")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 2); + EXPECT_EQ(result.value().at(0), "v1"); + EXPECT_EQ(result.value().at(1), "docs/readme.md"); + EXPECT_TRUE(result.value().at("version").has_value()); + EXPECT_EQ(result.value().at("version").value(), "v1"); + EXPECT_TRUE(result.value().at("path").has_value()); + EXPECT_EQ(result.value().at("path").value(), "docs/readme.md"); +} + +TEST(URLPattern_match, pathname_name_asterisk_zero_segments) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:foo*"}; + const auto result{pattern.pathname.match("/")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, pathname_name_asterisk_single_segment) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:foo*"}; + const auto result{pattern.pathname.match("/bar")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "bar"); + EXPECT_TRUE(result.value().at("foo").has_value()); + EXPECT_EQ(result.value().at("foo").value(), "bar"); +} + +TEST(URLPattern_match, pathname_name_asterisk_two_segments) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:foo*"}; + const auto result{pattern.pathname.match("/bar/baz")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "bar/baz"); + EXPECT_TRUE(result.value().at("foo").has_value()); + EXPECT_EQ(result.value().at("foo").value(), "bar/baz"); +} + +TEST(URLPattern_match, pathname_name_asterisk_three_segments) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:foo*"}; + const auto result{pattern.pathname.match("/bar/baz/qux")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "bar/baz/qux"); + EXPECT_TRUE(result.value().at("foo").has_value()); + EXPECT_EQ(result.value().at("foo").value(), "bar/baz/qux"); +} + +TEST(URLPattern_match, pathname_name_asterisk_with_prefix_zero_segments) { + const sourcemeta::core::URLPattern pattern{.pathname = "/api/:path*"}; + const auto result{pattern.pathname.match("/api")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_pathname, match_name_asterisk_with_prefix_multiple_segments) { + const sourcemeta::core::URLPattern pattern{.pathname = "/api/:path*"}; + const auto result{pattern.pathname.match("/api/v1/users")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "v1/users"); + EXPECT_TRUE(result.value().at("path").has_value()); + EXPECT_EQ(result.value().at("path").value(), "v1/users"); +} + +TEST(URLPattern_pathname, + match_name_asterisk_with_required_before_zero_segments) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:id/:path*"}; + const auto result{pattern.pathname.match("/123")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "123"); + EXPECT_TRUE(result.value().at("id").has_value()); + EXPECT_EQ(result.value().at("id").value(), "123"); +} + +TEST(URLPattern_pathname, + match_name_asterisk_with_required_before_multiple_segments) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:id/:path*"}; + const auto result{pattern.pathname.match("/123/a/b/c")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 2); + EXPECT_EQ(result.value().at(0), "123"); + EXPECT_EQ(result.value().at(1), "a/b/c"); + EXPECT_TRUE(result.value().at("id").has_value()); + EXPECT_EQ(result.value().at("id").value(), "123"); + EXPECT_TRUE(result.value().at("path").has_value()); + EXPECT_EQ(result.value().at("path").value(), "a/b/c"); +} + +TEST(URLPattern_match, pathname_name_asterisk_mixed_tokens_zero_segments) { + const sourcemeta::core::URLPattern pattern{.pathname = + "/api/:version/files/:path*"}; + const auto result{pattern.pathname.match("/api/v1/files")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "v1"); + EXPECT_TRUE(result.value().at("version").has_value()); + EXPECT_EQ(result.value().at("version").value(), "v1"); +} + +TEST(URLPattern_pathname, match_name_asterisk_mixed_tokens_multiple_segments) { + const sourcemeta::core::URLPattern pattern{.pathname = + "/api/:version/files/:path*"}; + const auto result{pattern.pathname.match("/api/v1/files/docs/readme.md")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 2); + EXPECT_EQ(result.value().at(0), "v1"); + EXPECT_EQ(result.value().at(1), "docs/readme.md"); + EXPECT_TRUE(result.value().at("version").has_value()); + EXPECT_EQ(result.value().at("version").value(), "v1"); + EXPECT_TRUE(result.value().at("path").has_value()); + EXPECT_EQ(result.value().at("path").value(), "docs/readme.md"); +} + +TEST(URLPattern_match, pathname_name_regex_simple) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:foo(.*)"}; + const auto result{pattern.pathname.match("/bar")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "bar"); + EXPECT_TRUE(result.value().at("foo").has_value()); + EXPECT_EQ(result.value().at("foo").value(), "bar"); +} + +TEST(URLPattern_match, pathname_name_regex_digits_match) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:id(\\d+)"}; + const auto result{pattern.pathname.match("/123")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "123"); + EXPECT_TRUE(result.value().at("id").has_value()); + EXPECT_EQ(result.value().at("id").value(), "123"); +} + +TEST(URLPattern_match, pathname_name_regex_digits_no_match) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:id(\\d+)"}; + const auto result{pattern.pathname.match("/abc")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, pathname_name_regex_with_prefix) { + const sourcemeta::core::URLPattern pattern{.pathname = + "/api/:version(v\\d+)"}; + const auto result{pattern.pathname.match("/api/v2")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "v2"); + EXPECT_TRUE(result.value().at("version").has_value()); + EXPECT_EQ(result.value().at("version").value(), "v2"); +} + +TEST(URLPattern_match, pathname_name_regex_multiple) { + const sourcemeta::core::URLPattern pattern{.pathname = + "/:category(\\w+)/:id(\\d+)"}; + const auto result{pattern.pathname.match("/products/123")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 2); + EXPECT_EQ(result.value().at(0), "products"); + EXPECT_EQ(result.value().at(1), "123"); + EXPECT_TRUE(result.value().at("category").has_value()); + EXPECT_EQ(result.value().at("category").value(), "products"); + EXPECT_TRUE(result.value().at("id").has_value()); + EXPECT_EQ(result.value().at("id").value(), "123"); +} + +TEST(URLPattern_match, pathname_name_regex_mixed_tokens) { + const sourcemeta::core::URLPattern pattern{ + .pathname = "/api/:version(v\\d+)/users/:id(\\d+)"}; + const auto result{pattern.pathname.match("/api/v1/users/42")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 2); + EXPECT_EQ(result.value().at(0), "v1"); + EXPECT_EQ(result.value().at(1), "42"); + EXPECT_TRUE(result.value().at("version").has_value()); + EXPECT_EQ(result.value().at("version").value(), "v1"); + EXPECT_TRUE(result.value().at("id").has_value()); + EXPECT_EQ(result.value().at("id").value(), "42"); +} + +TEST(URLPattern_match, pathname_regex_simple) { + const sourcemeta::core::URLPattern pattern{.pathname = "/(.*)"}; + const auto result{pattern.pathname.match("/anything")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "anything"); +} + +TEST(URLPattern_match, pathname_regex_with_prefix) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/(.*)"}; + const auto result{pattern.pathname.match("/foo/bar")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "bar"); +} + +TEST(URLPattern_match, pathname_regex_digits_match) { + const sourcemeta::core::URLPattern pattern{.pathname = "/(\\d+)"}; + const auto result{pattern.pathname.match("/123")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "123"); +} + +TEST(URLPattern_match, pathname_regex_digits_no_match) { + const sourcemeta::core::URLPattern pattern{.pathname = "/(\\d+)"}; + const auto result{pattern.pathname.match("/abc")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, pathname_regex_complex) { + const sourcemeta::core::URLPattern pattern{ + .pathname = "/api/:version/(\\d+)/details"}; + const auto result{pattern.pathname.match("/api/v1/456/details")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 2); + EXPECT_EQ(result.value().at(0), "v1"); + EXPECT_EQ(result.value().at(1), "456"); +} + +TEST(URLPattern_match, pathname_regex_fail_pattern_mismatch) { + const sourcemeta::core::URLPattern pattern{.pathname = "/(foo|bar)"}; + const auto result{pattern.pathname.match("/baz")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, pathname_regex_optional_present) { + const sourcemeta::core::URLPattern pattern{.pathname = "/(.*)?"}; + const auto result{pattern.pathname.match("/foo")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "foo"); +} + +TEST(URLPattern_match, pathname_regex_optional_absent) { + const sourcemeta::core::URLPattern pattern{.pathname = "/(.*)?"}; + const auto result{pattern.pathname.match("/")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, pathname_regex_optional_with_prefix_present) { + const sourcemeta::core::URLPattern pattern{.pathname = "/api/(\\d+)?"}; + const auto result{pattern.pathname.match("/api/123")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "123"); +} + +TEST(URLPattern_match, pathname_regex_optional_with_prefix_absent) { + const sourcemeta::core::URLPattern pattern{.pathname = "/api/(\\d+)?"}; + const auto result{pattern.pathname.match("/api")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, pathname_regex_optional_with_suffix_present) { + const sourcemeta::core::URLPattern pattern{.pathname = "/(\\d+)?/users"}; + const auto result{pattern.pathname.match("/123/users")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "123"); +} + +TEST(URLPattern_match, pathname_regex_optional_with_suffix_absent) { + const sourcemeta::core::URLPattern pattern{.pathname = "/(\\d+)?/users"}; + const auto result{pattern.pathname.match("/users")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, pathname_regex_optional_mixed_with_required) { + const sourcemeta::core::URLPattern pattern{.pathname = + "/api/:version/(\\d+)?/users"}; + const auto result{pattern.pathname.match("/api/v1/users")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "v1"); + EXPECT_TRUE(result.value().at("version").has_value()); + EXPECT_EQ(result.value().at("version").value(), "v1"); +} + +TEST(URLPattern_match, pathname_regex_multiple_single_segment) { + const sourcemeta::core::URLPattern pattern{.pathname = "/(.*)+"}; + const auto result{pattern.pathname.match("/foo")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "foo"); +} + +TEST(URLPattern_match, pathname_regex_multiple_multiple_segments) { + const sourcemeta::core::URLPattern pattern{.pathname = "/(.*)+"}; + const auto result{pattern.pathname.match("/foo/bar/baz")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "foo/bar/baz"); +} + +TEST(URLPattern_match, pathname_regex_multiple_fails_empty) { + const sourcemeta::core::URLPattern pattern{.pathname = "/(.*)+"}; + const auto result{pattern.pathname.match("/")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, pathname_regex_multiple_with_prefix) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/(.*)+"}; + const auto result{pattern.pathname.match("/foo/bar/baz")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "bar/baz"); +} + +TEST(URLPattern_match, pathname_regex_multiple_with_prefix_fails) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/(.*)+"}; + const auto result{pattern.pathname.match("/foo")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, pathname_regex_multiple_pattern_match) { + const sourcemeta::core::URLPattern pattern{.pathname = "/(\\d+)+"}; + const auto result{pattern.pathname.match("/123/456")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "123/456"); +} + +TEST(URLPattern_match, pathname_regex_multiple_pattern_no_match) { + const sourcemeta::core::URLPattern pattern{.pathname = "/(\\d+)+"}; + const auto result{pattern.pathname.match("/abc")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, pathname_regex_asterisk_zero_segments) { + const sourcemeta::core::URLPattern pattern{.pathname = "/(.*)*"}; + const auto result{pattern.pathname.match("/")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, pathname_regex_asterisk_single_segment) { + const sourcemeta::core::URLPattern pattern{.pathname = "/(.*)*"}; + const auto result{pattern.pathname.match("/foo")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "foo"); +} + +TEST(URLPattern_match, pathname_regex_asterisk_multiple_segments) { + const sourcemeta::core::URLPattern pattern{.pathname = "/(.*)*"}; + const auto result{pattern.pathname.match("/foo/bar/baz")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "foo/bar/baz"); +} + +TEST(URLPattern_match, pathname_regex_asterisk_with_prefix_zero) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/(.*)*"}; + const auto result{pattern.pathname.match("/foo")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, pathname_regex_asterisk_with_prefix_single) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/(.*)*"}; + const auto result{pattern.pathname.match("/foo/bar")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "bar"); +} + +TEST(URLPattern_match, pathname_regex_asterisk_with_prefix_multiple) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/(.*)*"}; + const auto result{pattern.pathname.match("/foo/bar/baz")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "bar/baz"); +} + +TEST(URLPattern_match, pathname_regex_asterisk_pattern_match) { + const sourcemeta::core::URLPattern pattern{.pathname = "/(\\d+)*"}; + const auto result{pattern.pathname.match("/123/456")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "123/456"); +} + +TEST(URLPattern_match, pathname_regex_asterisk_pattern_no_match) { + const sourcemeta::core::URLPattern pattern{.pathname = "/(\\d+)*"}; + const auto result{pattern.pathname.match("/abc")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, pathname_regex_asterisk_pattern_zero) { + const sourcemeta::core::URLPattern pattern{.pathname = "/(\\d+)*"}; + const auto result{pattern.pathname.match("/")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, pathname_name_basic) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/:bar"}; + const auto result{pattern.pathname.match("/foo/baz")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "baz"); + EXPECT_TRUE(result.value().at("bar").has_value()); + EXPECT_EQ(result.value().at("bar").value(), "baz"); +} + +TEST(URLPattern_match, pathname_name_too_few_segments) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/:bar"}; + const auto result{pattern.pathname.match("/foo")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, pathname_multiple_names) { + const sourcemeta::core::URLPattern pattern{.pathname = + "/api/:version/users/:id"}; + const auto result{pattern.pathname.match("/api/v1/users/123")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 2); + EXPECT_EQ(result.value().at(0), "v1"); + EXPECT_EQ(result.value().at(1), "123"); + EXPECT_TRUE(result.value().at("version").has_value()); + EXPECT_EQ(result.value().at("version").value(), "v1"); + EXPECT_TRUE(result.value().at("id").has_value()); + EXPECT_EQ(result.value().at("id").value(), "123"); +} + +TEST(URLPattern_match, pathname_asterisk_alone) { + const sourcemeta::core::URLPattern pattern{.pathname = "/*"}; + const auto result{pattern.pathname.match("/anything")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "anything"); +} + +TEST(URLPattern_match, pathname_bare_asterisk_match) { + const sourcemeta::core::URLPattern pattern{.pathname = "*"}; + const auto result{pattern.pathname.match("anything")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "anything"); +} + +TEST(URLPattern_match, pathname_bare_asterisk_match_with_slash) { + const sourcemeta::core::URLPattern pattern{.pathname = "*"}; + const auto result{pattern.pathname.match("foo/bar")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "foo/bar"); +} + +TEST(URLPattern_match, pathname_bare_name_match) { + const sourcemeta::core::URLPattern pattern{.pathname = ":path"}; + const auto result{pattern.pathname.match("foo")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "foo"); + EXPECT_TRUE(result.value().at("path").has_value()); + EXPECT_EQ(result.value().at("path").value(), "foo"); +} + +TEST(URLPattern_match, pathname_bare_name_match_with_slash) { + const sourcemeta::core::URLPattern pattern{.pathname = ":path"}; + const auto result{pattern.pathname.match("foo/bar/baz")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "foo/bar/baz"); + EXPECT_TRUE(result.value().at("path").has_value()); + EXPECT_EQ(result.value().at("path").value(), "foo/bar/baz"); +} + +TEST(URLPattern_match, pathname_bare_char_exact_match) { + const sourcemeta::core::URLPattern pattern{.pathname = "foo"}; + const auto result{pattern.pathname.match("foo")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, pathname_bare_char_no_match) { + const sourcemeta::core::URLPattern pattern{.pathname = "foo"}; + const auto result{pattern.pathname.match("bar")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, pathname_bare_regex_match) { + const sourcemeta::core::URLPattern pattern{.pathname = "(foo|bar)"}; + const auto result{pattern.pathname.match("foo")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "foo"); +} + +TEST(URLPattern_match, pathname_bare_regex_no_match) { + const sourcemeta::core::URLPattern pattern{.pathname = "(foo|bar)"}; + const auto result{pattern.pathname.match("baz")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, pathname_bare_name_regex_match) { + const sourcemeta::core::URLPattern pattern{.pathname = ":path(foo|bar)"}; + const auto result{pattern.pathname.match("foo")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "foo"); + EXPECT_TRUE(result.value().at("path").has_value()); + EXPECT_EQ(result.value().at("path").value(), "foo"); +} + +TEST(URLPattern_match, pathname_bare_name_regex_no_match) { + const sourcemeta::core::URLPattern pattern{.pathname = ":path(foo|bar)"}; + const auto result{pattern.pathname.match("baz")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, pathname_bare_group_match) { + const sourcemeta::core::URLPattern pattern{.pathname = "{foo}"}; + const auto result{pattern.pathname.match("foo")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, pathname_bare_group_no_match) { + const sourcemeta::core::URLPattern pattern{.pathname = "{foo}"}; + const auto result{pattern.pathname.match("bar")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, pathname_asterisk_with_prefix) { + const sourcemeta::core::URLPattern pattern{.pathname = "/static/*"}; + const auto result{pattern.pathname.match("/static/image.png")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "image.png"); +} + +TEST(URLPattern_match, pathname_asterisk_with_suffix) { + const sourcemeta::core::URLPattern pattern{.pathname = "/*/foo"}; + const auto result{pattern.pathname.match("/anything/foo")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "anything"); +} + +TEST(URLPattern_match, pathname_multiple_asterisks) { + const sourcemeta::core::URLPattern pattern{.pathname = "/*/*"}; + const auto result{pattern.pathname.match("/first/second")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 2); + EXPECT_EQ(result.value().at(0), "first"); + EXPECT_EQ(result.value().at(1), "second"); +} + +TEST(URLPattern_match, pathname_asterisk_optional_present) { + const sourcemeta::core::URLPattern pattern{.pathname = "/*?"}; + const auto result{pattern.pathname.match("/foo")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "foo"); +} + +TEST(URLPattern_match, pathname_asterisk_optional_absent) { + const sourcemeta::core::URLPattern pattern{.pathname = "/*?"}; + const auto result{pattern.pathname.match("/")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, pathname_asterisk_optional_with_prefix_present) { + const sourcemeta::core::URLPattern pattern{.pathname = "/static/*?"}; + const auto result{pattern.pathname.match("/static/image.png")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "image.png"); +} + +TEST(URLPattern_match, pathname_asterisk_optional_with_prefix_absent) { + const sourcemeta::core::URLPattern pattern{.pathname = "/static/*?"}; + const auto result{pattern.pathname.match("/static")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, pathname_asterisk_optional_with_suffix_present) { + const sourcemeta::core::URLPattern pattern{.pathname = "/*?/foo"}; + const auto result{pattern.pathname.match("/anything/foo")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "anything"); +} + +TEST(URLPattern_match, pathname_asterisk_optional_with_suffix_absent) { + const sourcemeta::core::URLPattern pattern{.pathname = "/*?/foo"}; + const auto result{pattern.pathname.match("/foo")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, pathname_asterisk_optional_mixed) { + const sourcemeta::core::URLPattern pattern{.pathname = + "/api/:version/*?/users"}; + const auto result{pattern.pathname.match("/api/v1/users")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "v1"); + EXPECT_TRUE(result.value().at("version").has_value()); + EXPECT_EQ(result.value().at("version").value(), "v1"); +} + +TEST(URLPattern_match, pathname_asterisk_multiple_single_segment) { + const sourcemeta::core::URLPattern pattern{.pathname = "/*+"}; + const auto result{pattern.pathname.match("/foo")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "foo"); +} + +TEST(URLPattern_match, pathname_asterisk_multiple_multiple_segments) { + const sourcemeta::core::URLPattern pattern{.pathname = "/*+"}; + const auto result{pattern.pathname.match("/foo/bar/baz")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "foo/bar/baz"); +} + +TEST(URLPattern_match, pathname_asterisk_multiple_fails_empty) { + const sourcemeta::core::URLPattern pattern{.pathname = "/*+"}; + const auto result{pattern.pathname.match("/")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, pathname_asterisk_multiple_with_prefix) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/*+"}; + const auto result{pattern.pathname.match("/foo/bar/baz")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "bar/baz"); +} + +TEST(URLPattern_match, pathname_asterisk_multiple_with_prefix_fails) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/*+"}; + const auto result{pattern.pathname.match("/foo")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, pathname_asterisk_multiple_with_prefix_single) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/*+"}; + const auto result{pattern.pathname.match("/foo/bar")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "bar"); +} + +TEST(URLPattern_match, pathname_asterisk_multiple_mixed) { + const sourcemeta::core::URLPattern pattern{.pathname = "/api/:version/*+"}; + const auto result{pattern.pathname.match("/api/v1/x/y/z")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 2); + EXPECT_EQ(result.value().at(0), "v1"); + EXPECT_EQ(result.value().at(1), "x/y/z"); + EXPECT_TRUE(result.value().at("version").has_value()); + EXPECT_EQ(result.value().at("version").value(), "v1"); +} + +TEST(URLPattern_match, pathname_asterisk_asterisk_zero_segments) { + const sourcemeta::core::URLPattern pattern{.pathname = "/**"}; + const auto result{pattern.pathname.match("/")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, pathname_asterisk_asterisk_single_segment) { + const sourcemeta::core::URLPattern pattern{.pathname = "/**"}; + const auto result{pattern.pathname.match("/foo")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "foo"); +} + +TEST(URLPattern_match, pathname_asterisk_asterisk_multiple_segments) { + const sourcemeta::core::URLPattern pattern{.pathname = "/**"}; + const auto result{pattern.pathname.match("/foo/bar/baz")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "foo/bar/baz"); +} + +TEST(URLPattern_match, pathname_asterisk_asterisk_with_prefix_zero) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/**"}; + const auto result{pattern.pathname.match("/foo")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, pathname_asterisk_asterisk_with_prefix_single) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/**"}; + const auto result{pattern.pathname.match("/foo/bar")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "bar"); +} + +TEST(URLPattern_match, pathname_asterisk_asterisk_with_prefix_multiple) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/**"}; + const auto result{pattern.pathname.match("/foo/bar/baz")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "bar/baz"); +} + +TEST(URLPattern_match, pathname_asterisk_asterisk_mixed) { + const sourcemeta::core::URLPattern pattern{.pathname = "/api/:version/**"}; + const auto result{pattern.pathname.match("/api/v1/x/y/z")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 2); + EXPECT_EQ(result.value().at(0), "v1"); + EXPECT_EQ(result.value().at(1), "x/y/z"); + EXPECT_TRUE(result.value().at("version").has_value()); + EXPECT_EQ(result.value().at("version").value(), "v1"); +} + +TEST(URLPattern_match, pathname_all_token_types) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/:bar/*"}; + const auto result{pattern.pathname.match("/foo/baz/qux")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 2); + EXPECT_EQ(result.value().at(0), "baz"); + EXPECT_EQ(result.value().at(1), "qux"); + EXPECT_TRUE(result.value().at("bar").has_value()); + EXPECT_EQ(result.value().at("bar").value(), "baz"); +} + +TEST(URLPattern_match, pathname_complex_pattern) { + const sourcemeta::core::URLPattern pattern{ + .pathname = "/api/:version/*/users/:id/profile"}; + const auto result{pattern.pathname.match("/api/v2/public/users/456/profile")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 3); + EXPECT_EQ(result.value().at(0), "v2"); + EXPECT_EQ(result.value().at(1), "public"); + EXPECT_EQ(result.value().at(2), "456"); +} + +TEST(URLPattern_match, pathname_root_path) { + const sourcemeta::core::URLPattern pattern{.pathname = "/"}; + const auto result{pattern.pathname.match("/")}; + EXPECT_TRUE(result.has_value()); +} + +TEST(URLPattern_match, pathname_simple_char_token) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo"}; + const auto result{pattern.pathname.match("/foo")}; + EXPECT_TRUE(result.has_value()); +} + +TEST(URLPattern_match, pathname_multiple_char_tokens) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/bar/baz"}; + const auto result{pattern.pathname.match("/foo/bar/baz")}; + EXPECT_TRUE(result.has_value()); +} + +TEST(URLPattern_match, pathname_empty_segment_pattern) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo//bar"}; + const auto result{pattern.pathname.match("/foo//bar")}; + EXPECT_TRUE(result.has_value()); +} + +TEST(URLPattern_match, pathname_empty_segment_name) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo//:bar"}; + const auto result{pattern.pathname.match("/foo//baz")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "baz"); +} + +TEST(URLPattern_match, pathname_fail_wrong_char) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/bar"}; + const auto result{pattern.pathname.match("/foo/baz")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, pathname_fail_too_many_segments) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo"}; + const auto result{pattern.pathname.match("/foo/bar")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, pathname_fail_too_few_segments) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/bar"}; + const auto result{pattern.pathname.match("/foo")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, pathname_fail_no_leading_slash) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo"}; + const auto result{pattern.pathname.match("foo")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, pathname_fail_empty_input) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo"}; + const auto result{pattern.pathname.match("")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, pathname_name_with_special_chars_in_value) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:filename"}; + const auto result{pattern.pathname.match("/image-2024.png")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "image-2024.png"); +} + +TEST(URLPattern_match, pathname_asterisk_with_special_chars) { + const sourcemeta::core::URLPattern pattern{.pathname = "/*"}; + const auto result{pattern.pathname.match("/path-with_special.chars")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "path-with_special.chars"); +} + +TEST(URLPattern_match, pathname_trailing_slash_pattern) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/"}; + const auto result{pattern.pathname.match("/foo/")}; + EXPECT_TRUE(result.has_value()); +} + +TEST(URLPattern_match, pathname_trailing_slash_mismatch) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/"}; + const auto result{pattern.pathname.match("/foo")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, pathname_input_trailing_slash_mismatch) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/bar"}; + const auto result{pattern.pathname.match("/foo/bar/")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, pathname_group_simple) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo{/bar}"}; + const auto result{pattern.pathname.match("/foo/bar")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, pathname_group_simple_mismatch) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo{/bar}"}; + const auto result{pattern.pathname.match("/foo")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, pathname_group_optional_present) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo{/bar}?"}; + const auto result{pattern.pathname.match("/foo/bar")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, pathname_group_optional_absent) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo{/bar}?"}; + const auto result{pattern.pathname.match("/foo")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, pathname_group_multiple_single) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo{/bar}+"}; + const auto result{pattern.pathname.match("/foo/bar")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, pathname_group_multiple_twice) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo{/bar}+"}; + const auto result{pattern.pathname.match("/foo/bar/bar")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, pathname_group_multiple_fails_empty) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo{/bar}+"}; + const auto result{pattern.pathname.match("/foo")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, pathname_group_asterisk_zero) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo{/bar}*"}; + const auto result{pattern.pathname.match("/foo")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, pathname_group_asterisk_once) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo{/bar}*"}; + const auto result{pattern.pathname.match("/foo/bar")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, pathname_group_asterisk_twice) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo{/bar}*"}; + const auto result{pattern.pathname.match("/foo/bar/bar")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, pathname_group_with_name) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo{/:bar}"}; + const auto result{pattern.pathname.match("/foo/baz")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "baz"); + EXPECT_TRUE(result.value().at("bar").has_value()); + EXPECT_EQ(result.value().at("bar").value(), "baz"); +} + +TEST(URLPattern_match, pathname_group_with_name_optional_present) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo{/:bar}?"}; + const auto result{pattern.pathname.match("/foo/baz")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "baz"); + EXPECT_TRUE(result.value().at("bar").has_value()); + EXPECT_EQ(result.value().at("bar").value(), "baz"); +} + +TEST(URLPattern_match, pathname_group_with_name_optional_absent) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo{/:bar}?"}; + const auto result{pattern.pathname.match("/foo")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, pathname_complex_segment_with_name) { + const sourcemeta::core::URLPattern pattern{.pathname = "/file-:name.json"}; + const auto result{pattern.pathname.match("/file-test.json")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "test"); + EXPECT_TRUE(result.value().at("name").has_value()); + EXPECT_EQ(result.value().at("name").value(), "test"); +} + +TEST(URLPattern_match, pathname_complex_segment_mismatch_prefix) { + const sourcemeta::core::URLPattern pattern{.pathname = "/file-:name.json"}; + const auto result{pattern.pathname.match("/doc-test.json")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, pathname_complex_segment_mismatch_suffix) { + const sourcemeta::core::URLPattern pattern{.pathname = "/file-:name.json"}; + const auto result{pattern.pathname.match("/file-test.txt")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, pathname_complex_segment_multiple_names) { + const sourcemeta::core::URLPattern pattern{.pathname = "/v:major.:minor"}; + const auto result{pattern.pathname.match("/v1.2")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 2); + EXPECT_EQ(result.value().at(0), "1"); + EXPECT_EQ(result.value().at(1), "2"); + EXPECT_TRUE(result.value().at("major").has_value()); + EXPECT_EQ(result.value().at("major").value(), "1"); + EXPECT_TRUE(result.value().at("minor").has_value()); + EXPECT_EQ(result.value().at("minor").value(), "2"); +} + +TEST(URLPattern_match, pathname_complex_segment_with_group) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo-:bar{baz}+"}; + const auto result{pattern.pathname.match("/foo-test baz")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "test "); + EXPECT_TRUE(result.value().at("bar").has_value()); + EXPECT_EQ(result.value().at("bar").value(), "test "); +} + +TEST(URLPattern_match, pathname_complex_segment_group_first) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo{:bar}?baz"}; + const auto result{pattern.pathname.match("/foobaz")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, pathname_complex_segment_group_first_present) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo{:bar}?baz"}; + const auto result{pattern.pathname.match("/footestbaz")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "test"); + EXPECT_TRUE(result.value().at("bar").has_value()); + EXPECT_EQ(result.value().at("bar").value(), "test"); +} + +TEST(URLPattern_match, pathname_escaped_colon_at_end) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo\\:"}; + const auto result{pattern.pathname.match("/foo:")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} diff --git a/test/urlpattern/urlpattern_match_port_test.cc b/test/urlpattern/urlpattern_match_port_test.cc new file mode 100644 index 000000000..389d5b8bc --- /dev/null +++ b/test/urlpattern/urlpattern_match_port_test.cc @@ -0,0 +1,87 @@ +#include + +#include + +TEST(URLPattern_match, port_char_exact_match) { + const sourcemeta::core::URLPattern pattern{.port = "8080"}; + const auto result{pattern.port.match("8080")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, port_char_no_match) { + const sourcemeta::core::URLPattern pattern{.port = "8080"}; + const auto result{pattern.port.match("3000")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, port_name_basic) { + const sourcemeta::core::URLPattern pattern{.port = ":port"}; + const auto result{pattern.port.match("8080")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "8080"); + EXPECT_TRUE(result.value().at("port").has_value()); + EXPECT_EQ(result.value().at("port").value(), "8080"); +} + +TEST(URLPattern_match, port_name_any_value) { + const sourcemeta::core::URLPattern pattern{.port = ":port"}; + const auto result{pattern.port.match("443")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "443"); + EXPECT_TRUE(result.value().at("port").has_value()); + EXPECT_EQ(result.value().at("port").value(), "443"); +} + +TEST(URLPattern_match, port_name_regex_match) { + const sourcemeta::core::URLPattern pattern{.port = ":port(\\d+)"}; + const auto result{pattern.port.match("8080")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "8080"); + EXPECT_TRUE(result.value().at("port").has_value()); + EXPECT_EQ(result.value().at("port").value(), "8080"); +} + +TEST(URLPattern_match, port_name_regex_no_match) { + const sourcemeta::core::URLPattern pattern{.port = ":port(\\d+)"}; + const auto result{pattern.port.match("http")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, port_regex_match) { + const sourcemeta::core::URLPattern pattern{.port = "(\\d+)"}; + const auto result{pattern.port.match("8080")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "8080"); +} + +TEST(URLPattern_match, port_regex_no_match) { + const sourcemeta::core::URLPattern pattern{.port = "(\\d+)"}; + const auto result{pattern.port.match("http")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, port_asterisk) { + const sourcemeta::core::URLPattern pattern{.port = "*"}; + const auto result{pattern.port.match("8080")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "8080"); +} + +TEST(URLPattern_match, port_group_match) { + const sourcemeta::core::URLPattern pattern{.port = "{8080}"}; + const auto result{pattern.port.match("8080")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, port_group_no_match) { + const sourcemeta::core::URLPattern pattern{.port = "{8080}"}; + const auto result{pattern.port.match("3000")}; + EXPECT_FALSE(result.has_value()); +} diff --git a/test/urlpattern/urlpattern_match_protocol_test.cc b/test/urlpattern/urlpattern_match_protocol_test.cc new file mode 100644 index 000000000..ac0d11f09 --- /dev/null +++ b/test/urlpattern/urlpattern_match_protocol_test.cc @@ -0,0 +1,87 @@ +#include + +#include + +TEST(URLPattern_match, protocol_char_exact_match) { + const sourcemeta::core::URLPattern pattern{.protocol = "https"}; + const auto result{pattern.protocol.match("https")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, protocol_char_no_match) { + const sourcemeta::core::URLPattern pattern{.protocol = "https"}; + const auto result{pattern.protocol.match("http")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, protocol_name_basic) { + const sourcemeta::core::URLPattern pattern{.protocol = ":proto"}; + const auto result{pattern.protocol.match("https")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "https"); + EXPECT_TRUE(result.value().at("proto").has_value()); + EXPECT_EQ(result.value().at("proto").value(), "https"); +} + +TEST(URLPattern_match, protocol_name_any_value) { + const sourcemeta::core::URLPattern pattern{.protocol = ":proto"}; + const auto result{pattern.protocol.match("ftp")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "ftp"); + EXPECT_TRUE(result.value().at("proto").has_value()); + EXPECT_EQ(result.value().at("proto").value(), "ftp"); +} + +TEST(URLPattern_match, protocol_name_regex_match) { + const sourcemeta::core::URLPattern pattern{.protocol = ":proto(https?)"}; + const auto result{pattern.protocol.match("https")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "https"); + EXPECT_TRUE(result.value().at("proto").has_value()); + EXPECT_EQ(result.value().at("proto").value(), "https"); +} + +TEST(URLPattern_match, protocol_name_regex_no_match) { + const sourcemeta::core::URLPattern pattern{.protocol = ":proto(https?)"}; + const auto result{pattern.protocol.match("ftp")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, protocol_regex_match) { + const sourcemeta::core::URLPattern pattern{.protocol = "(https?)"}; + const auto result{pattern.protocol.match("http")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "http"); +} + +TEST(URLPattern_match, protocol_regex_no_match) { + const sourcemeta::core::URLPattern pattern{.protocol = "(https?)"}; + const auto result{pattern.protocol.match("ftp")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, protocol_asterisk) { + const sourcemeta::core::URLPattern pattern{.protocol = "*"}; + const auto result{pattern.protocol.match("https")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "https"); +} + +TEST(URLPattern_match, protocol_group_match) { + const sourcemeta::core::URLPattern pattern{.protocol = "{https}"}; + const auto result{pattern.protocol.match("https")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, protocol_group_no_match) { + const sourcemeta::core::URLPattern pattern{.protocol = "{https}"}; + const auto result{pattern.protocol.match("http")}; + EXPECT_FALSE(result.has_value()); +} diff --git a/test/urlpattern/urlpattern_match_search_test.cc b/test/urlpattern/urlpattern_match_search_test.cc new file mode 100644 index 000000000..f00f9aafa --- /dev/null +++ b/test/urlpattern/urlpattern_match_search_test.cc @@ -0,0 +1,87 @@ +#include + +#include + +TEST(URLPattern_match, search_char_exact_match) { + const sourcemeta::core::URLPattern pattern{.search = "q=test"}; + const auto result{pattern.search.match("q=test")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, search_char_no_match) { + const sourcemeta::core::URLPattern pattern{.search = "q=test"}; + const auto result{pattern.search.match("q=search")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, search_name_basic) { + const sourcemeta::core::URLPattern pattern{.search = ":query"}; + const auto result{pattern.search.match("q=test&lang=en")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "q=test&lang=en"); + EXPECT_TRUE(result.value().at("query").has_value()); + EXPECT_EQ(result.value().at("query").value(), "q=test&lang=en"); +} + +TEST(URLPattern_match, search_name_any_value) { + const sourcemeta::core::URLPattern pattern{.search = ":query"}; + const auto result{pattern.search.match("page=1")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "page=1"); + EXPECT_TRUE(result.value().at("query").has_value()); + EXPECT_EQ(result.value().at("query").value(), "page=1"); +} + +TEST(URLPattern_match, search_name_regex_match) { + const sourcemeta::core::URLPattern pattern{.search = ":query(q=.*)"}; + const auto result{pattern.search.match("q=test")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "q=test"); + EXPECT_TRUE(result.value().at("query").has_value()); + EXPECT_EQ(result.value().at("query").value(), "q=test"); +} + +TEST(URLPattern_match, search_name_regex_no_match) { + const sourcemeta::core::URLPattern pattern{.search = ":query(q=.*)"}; + const auto result{pattern.search.match("page=1")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, search_regex_match) { + const sourcemeta::core::URLPattern pattern{.search = "(\\w+=\\w+)"}; + const auto result{pattern.search.match("page=1")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "page=1"); +} + +TEST(URLPattern_match, search_regex_no_match) { + const sourcemeta::core::URLPattern pattern{.search = "(\\d+)"}; + const auto result{pattern.search.match("page=1")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, search_asterisk) { + const sourcemeta::core::URLPattern pattern{.search = "*"}; + const auto result{pattern.search.match("q=test&lang=en")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "q=test&lang=en"); +} + +TEST(URLPattern_match, search_group_match) { + const sourcemeta::core::URLPattern pattern{.search = "{q=test}"}; + const auto result{pattern.search.match("q=test")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, search_group_no_match) { + const sourcemeta::core::URLPattern pattern{.search = "{q=test}"}; + const auto result{pattern.search.match("page=1")}; + EXPECT_FALSE(result.has_value()); +} diff --git a/test/urlpattern/urlpattern_match_test.cc b/test/urlpattern/urlpattern_match_test.cc new file mode 100644 index 000000000..7da376939 --- /dev/null +++ b/test/urlpattern/urlpattern_match_test.cc @@ -0,0 +1,443 @@ +#include + +#include + +TEST(URLPattern_match, all_wildcard_single_segment_values) { + const sourcemeta::core::URLPattern pattern; + const sourcemeta::core::URLPatternInput input{.protocol = "https", + .username = "admin", + .password = "secret", + .hostname = "localhost", + .port = "8080", + .pathname = "/api", + .search = "q=test", + .hash = "section"}; + + const auto result{pattern.match(input)}; + + EXPECT_TRUE(result.protocol.has_value()); + EXPECT_EQ(result.protocol->size(), 1); + EXPECT_EQ(result.protocol->at(0), "https"); + + EXPECT_TRUE(result.username.has_value()); + EXPECT_EQ(result.username->size(), 1); + EXPECT_EQ(result.username->at(0), "admin"); + + EXPECT_TRUE(result.password.has_value()); + EXPECT_EQ(result.password->size(), 1); + EXPECT_EQ(result.password->at(0), "secret"); + + EXPECT_TRUE(result.hostname.has_value()); + EXPECT_EQ(result.hostname->size(), 1); + EXPECT_EQ(result.hostname->at(0), "localhost"); + + EXPECT_TRUE(result.port.has_value()); + EXPECT_EQ(result.port->size(), 1); + EXPECT_EQ(result.port->at(0), "8080"); + + EXPECT_TRUE(result.pathname.has_value()); + EXPECT_EQ(result.pathname->size(), 1); + EXPECT_EQ(result.pathname->at(0), "api"); + + EXPECT_TRUE(result.search.has_value()); + EXPECT_EQ(result.search->size(), 1); + EXPECT_EQ(result.search->at(0), "q=test"); + + EXPECT_TRUE(result.hash.has_value()); + EXPECT_EQ(result.hash->size(), 1); + EXPECT_EQ(result.hash->at(0), "section"); +} + +TEST(URLPattern_match, protocol_literal_match) { + const sourcemeta::core::URLPattern pattern{.protocol = "https"}; + const sourcemeta::core::URLPatternInput input{.protocol = "https", + .username = "", + .password = "", + .hostname = "localhost", + .port = "", + .pathname = "/", + .search = "", + .hash = ""}; + + const auto result{pattern.match(input)}; + + EXPECT_TRUE(result.protocol.has_value()); + EXPECT_EQ(result.protocol->size(), 0); + + EXPECT_TRUE(result.username.has_value()); + EXPECT_EQ(result.username->size(), 1); + EXPECT_EQ(result.username->at(0), ""); + + EXPECT_TRUE(result.password.has_value()); + EXPECT_EQ(result.password->size(), 1); + EXPECT_EQ(result.password->at(0), ""); + + EXPECT_TRUE(result.hostname.has_value()); + EXPECT_EQ(result.hostname->size(), 1); + EXPECT_EQ(result.hostname->at(0), "localhost"); + + EXPECT_TRUE(result.port.has_value()); + EXPECT_EQ(result.port->size(), 1); + EXPECT_EQ(result.port->at(0), ""); + + EXPECT_TRUE(result.pathname.has_value()); + EXPECT_EQ(result.pathname->size(), 1); + EXPECT_EQ(result.pathname->at(0), ""); + + EXPECT_TRUE(result.search.has_value()); + EXPECT_EQ(result.search->size(), 1); + EXPECT_EQ(result.search->at(0), ""); + + EXPECT_TRUE(result.hash.has_value()); + EXPECT_EQ(result.hash->size(), 1); + EXPECT_EQ(result.hash->at(0), ""); +} + +TEST(URLPattern_match, protocol_literal_no_match) { + const sourcemeta::core::URLPattern pattern{.protocol = "https"}; + const sourcemeta::core::URLPatternInput input{.protocol = "http", + .username = "", + .password = "", + .hostname = "localhost", + .port = "", + .pathname = "/", + .search = "", + .hash = ""}; + + const auto result{pattern.match(input)}; + + EXPECT_FALSE(result.protocol.has_value()); + EXPECT_FALSE(result.username.has_value()); + EXPECT_FALSE(result.password.has_value()); + EXPECT_FALSE(result.hostname.has_value()); + EXPECT_FALSE(result.port.has_value()); + EXPECT_FALSE(result.pathname.has_value()); + EXPECT_FALSE(result.search.has_value()); + EXPECT_FALSE(result.hash.has_value()); +} + +TEST(URLPattern_match, hostname_literal_match) { + const sourcemeta::core::URLPattern pattern{.hostname = "example.com"}; + const sourcemeta::core::URLPatternInput input{.protocol = "https", + .username = "", + .password = "", + .hostname = "example.com", + .port = "", + .pathname = "/", + .search = "", + .hash = ""}; + + const auto result{pattern.match(input)}; + + EXPECT_TRUE(result.protocol.has_value()); + EXPECT_EQ(result.protocol->size(), 1); + EXPECT_EQ(result.protocol->at(0), "https"); + + EXPECT_TRUE(result.username.has_value()); + EXPECT_EQ(result.username->size(), 1); + EXPECT_EQ(result.username->at(0), ""); + + EXPECT_TRUE(result.password.has_value()); + EXPECT_EQ(result.password->size(), 1); + EXPECT_EQ(result.password->at(0), ""); + + EXPECT_TRUE(result.hostname.has_value()); + EXPECT_EQ(result.hostname->size(), 0); + + EXPECT_TRUE(result.port.has_value()); + EXPECT_EQ(result.port->size(), 1); + EXPECT_EQ(result.port->at(0), ""); + + EXPECT_TRUE(result.pathname.has_value()); + EXPECT_EQ(result.pathname->size(), 1); + EXPECT_EQ(result.pathname->at(0), ""); + + EXPECT_TRUE(result.search.has_value()); + EXPECT_EQ(result.search->size(), 1); + EXPECT_EQ(result.search->at(0), ""); + + EXPECT_TRUE(result.hash.has_value()); + EXPECT_EQ(result.hash->size(), 1); + EXPECT_EQ(result.hash->at(0), ""); +} + +TEST(URLPattern_match, pathname_with_named_groups) { + const sourcemeta::core::URLPattern pattern{.pathname = "/api/:version/:id"}; + const sourcemeta::core::URLPatternInput input{.protocol = "", + .username = "", + .password = "", + .hostname = "localhost", + .port = "", + .pathname = "/api/v1/123", + .search = "", + .hash = ""}; + + const auto result{pattern.match(input)}; + + EXPECT_TRUE(result.protocol.has_value()); + EXPECT_EQ(result.protocol->size(), 1); + EXPECT_EQ(result.protocol->at(0), ""); + + EXPECT_TRUE(result.username.has_value()); + EXPECT_EQ(result.username->size(), 1); + EXPECT_EQ(result.username->at(0), ""); + + EXPECT_TRUE(result.password.has_value()); + EXPECT_EQ(result.password->size(), 1); + EXPECT_EQ(result.password->at(0), ""); + + EXPECT_TRUE(result.hostname.has_value()); + EXPECT_EQ(result.hostname->size(), 1); + EXPECT_EQ(result.hostname->at(0), "localhost"); + + EXPECT_TRUE(result.port.has_value()); + EXPECT_EQ(result.port->size(), 1); + EXPECT_EQ(result.port->at(0), ""); + + EXPECT_TRUE(result.pathname.has_value()); + EXPECT_EQ(result.pathname->size(), 2); + EXPECT_EQ(result.pathname->at(0), "v1"); + EXPECT_EQ(result.pathname->at(1), "123"); + EXPECT_EQ(result.pathname->at("version"), "v1"); + EXPECT_EQ(result.pathname->at("id"), "123"); + + EXPECT_TRUE(result.search.has_value()); + EXPECT_EQ(result.search->size(), 1); + EXPECT_EQ(result.search->at(0), ""); + + EXPECT_TRUE(result.hash.has_value()); + EXPECT_EQ(result.hash->size(), 1); + EXPECT_EQ(result.hash->at(0), ""); +} + +TEST(URLPattern_match, hostname_with_named_group) { + const sourcemeta::core::URLPattern pattern{.hostname = + ":subdomain.example.com"}; + const sourcemeta::core::URLPatternInput input{.protocol = "", + .username = "", + .password = "", + .hostname = "api.example.com", + .port = "", + .pathname = "/", + .search = "", + .hash = ""}; + + const auto result{pattern.match(input)}; + + EXPECT_TRUE(result.protocol.has_value()); + EXPECT_EQ(result.protocol->size(), 1); + EXPECT_EQ(result.protocol->at(0), ""); + + EXPECT_TRUE(result.username.has_value()); + EXPECT_EQ(result.username->size(), 1); + EXPECT_EQ(result.username->at(0), ""); + + EXPECT_TRUE(result.password.has_value()); + EXPECT_EQ(result.password->size(), 1); + EXPECT_EQ(result.password->at(0), ""); + + EXPECT_TRUE(result.hostname.has_value()); + EXPECT_EQ(result.hostname->size(), 1); + EXPECT_EQ(result.hostname->at(0), "api"); + EXPECT_EQ(result.hostname->at("subdomain"), "api"); + + EXPECT_TRUE(result.port.has_value()); + EXPECT_EQ(result.port->size(), 1); + EXPECT_EQ(result.port->at(0), ""); + + EXPECT_TRUE(result.pathname.has_value()); + EXPECT_EQ(result.pathname->size(), 1); + EXPECT_EQ(result.pathname->at(0), ""); + + EXPECT_TRUE(result.search.has_value()); + EXPECT_EQ(result.search->size(), 1); + EXPECT_EQ(result.search->at(0), ""); + + EXPECT_TRUE(result.hash.has_value()); + EXPECT_EQ(result.hash->size(), 1); + EXPECT_EQ(result.hash->at(0), ""); +} + +TEST(URLPattern_match, all_components_with_literals) { + const sourcemeta::core::URLPattern pattern{.protocol = "https", + .username = "admin", + .password = "secret", + .hostname = "example.com", + .port = "8080", + .pathname = "/api/v1", + .search = "q=test", + .hash = "section"}; + const sourcemeta::core::URLPatternInput input{.protocol = "https", + .username = "admin", + .password = "secret", + .hostname = "example.com", + .port = "8080", + .pathname = "/api/v1", + .search = "q=test", + .hash = "section"}; + + const auto result{pattern.match(input)}; + + EXPECT_TRUE(result.protocol.has_value()); + EXPECT_EQ(result.protocol->size(), 0); + + EXPECT_TRUE(result.username.has_value()); + EXPECT_EQ(result.username->size(), 0); + + EXPECT_TRUE(result.password.has_value()); + EXPECT_EQ(result.password->size(), 0); + + EXPECT_TRUE(result.hostname.has_value()); + EXPECT_EQ(result.hostname->size(), 0); + + EXPECT_TRUE(result.port.has_value()); + EXPECT_EQ(result.port->size(), 0); + + EXPECT_TRUE(result.pathname.has_value()); + EXPECT_EQ(result.pathname->size(), 0); + + EXPECT_TRUE(result.search.has_value()); + EXPECT_EQ(result.search->size(), 0); + + EXPECT_TRUE(result.hash.has_value()); + EXPECT_EQ(result.hash->size(), 0); +} + +TEST(URLPattern_match, wildcard_pathname) { + const sourcemeta::core::URLPattern pattern{.pathname = "/static/*"}; + const sourcemeta::core::URLPatternInput input{.protocol = "", + .username = "", + .password = "", + .hostname = "localhost", + .port = "", + .pathname = "/static/js", + .search = "", + .hash = ""}; + + const auto result{pattern.match(input)}; + + EXPECT_TRUE(result.protocol.has_value()); + EXPECT_EQ(result.protocol->size(), 1); + EXPECT_EQ(result.protocol->at(0), ""); + + EXPECT_TRUE(result.username.has_value()); + EXPECT_EQ(result.username->size(), 1); + EXPECT_EQ(result.username->at(0), ""); + + EXPECT_TRUE(result.password.has_value()); + EXPECT_EQ(result.password->size(), 1); + EXPECT_EQ(result.password->at(0), ""); + + EXPECT_TRUE(result.hostname.has_value()); + EXPECT_EQ(result.hostname->size(), 1); + EXPECT_EQ(result.hostname->at(0), "localhost"); + + EXPECT_TRUE(result.port.has_value()); + EXPECT_EQ(result.port->size(), 1); + EXPECT_EQ(result.port->at(0), ""); + + EXPECT_TRUE(result.pathname.has_value()); + EXPECT_EQ(result.pathname->size(), 1); + EXPECT_EQ(result.pathname->at(0), "js"); + + EXPECT_TRUE(result.search.has_value()); + EXPECT_EQ(result.search->size(), 1); + EXPECT_EQ(result.search->at(0), ""); + + EXPECT_TRUE(result.hash.has_value()); + EXPECT_EQ(result.hash->size(), 1); + EXPECT_EQ(result.hash->at(0), ""); +} + +TEST(URLPattern_match, wildcard_hostname) { + const sourcemeta::core::URLPattern pattern{.hostname = "*.example.com"}; + const sourcemeta::core::URLPatternInput input{.protocol = "", + .username = "", + .password = "", + .hostname = "api.example.com", + .port = "", + .pathname = "/", + .search = "", + .hash = ""}; + + const auto result{pattern.match(input)}; + + EXPECT_TRUE(result.protocol.has_value()); + EXPECT_EQ(result.protocol->size(), 1); + EXPECT_EQ(result.protocol->at(0), ""); + + EXPECT_TRUE(result.username.has_value()); + EXPECT_EQ(result.username->size(), 1); + EXPECT_EQ(result.username->at(0), ""); + + EXPECT_TRUE(result.password.has_value()); + EXPECT_EQ(result.password->size(), 1); + EXPECT_EQ(result.password->at(0), ""); + + EXPECT_TRUE(result.hostname.has_value()); + EXPECT_EQ(result.hostname->size(), 1); + EXPECT_EQ(result.hostname->at(0), "api"); + + EXPECT_TRUE(result.port.has_value()); + EXPECT_EQ(result.port->size(), 1); + EXPECT_EQ(result.port->at(0), ""); + + EXPECT_TRUE(result.pathname.has_value()); + EXPECT_EQ(result.pathname->size(), 1); + EXPECT_EQ(result.pathname->at(0), ""); + + EXPECT_TRUE(result.search.has_value()); + EXPECT_EQ(result.search->size(), 1); + EXPECT_EQ(result.search->at(0), ""); + + EXPECT_TRUE(result.hash.has_value()); + EXPECT_EQ(result.hash->size(), 1); + EXPECT_EQ(result.hash->at(0), ""); +} + +TEST(URLPattern_match, protocol_name_pattern) { + const sourcemeta::core::URLPattern pattern{.protocol = ":scheme"}; + const sourcemeta::core::URLPatternInput input{.protocol = "https", + .username = "", + .password = "", + .hostname = "localhost", + .port = "", + .pathname = "/", + .search = "", + .hash = ""}; + + const auto result{pattern.match(input)}; + + EXPECT_TRUE(result.protocol.has_value()); + EXPECT_EQ(result.protocol->size(), 1); + EXPECT_EQ(result.protocol->at(0), "https"); + EXPECT_EQ(result.protocol->at("scheme"), "https"); + + EXPECT_TRUE(result.username.has_value()); + EXPECT_EQ(result.username->size(), 1); + EXPECT_EQ(result.username->at(0), ""); + + EXPECT_TRUE(result.password.has_value()); + EXPECT_EQ(result.password->size(), 1); + EXPECT_EQ(result.password->at(0), ""); + + EXPECT_TRUE(result.hostname.has_value()); + EXPECT_EQ(result.hostname->size(), 1); + EXPECT_EQ(result.hostname->at(0), "localhost"); + + EXPECT_TRUE(result.port.has_value()); + EXPECT_EQ(result.port->size(), 1); + EXPECT_EQ(result.port->at(0), ""); + + EXPECT_TRUE(result.pathname.has_value()); + EXPECT_EQ(result.pathname->size(), 1); + EXPECT_EQ(result.pathname->at(0), ""); + + EXPECT_TRUE(result.search.has_value()); + EXPECT_EQ(result.search->size(), 1); + EXPECT_EQ(result.search->at(0), ""); + + EXPECT_TRUE(result.hash.has_value()); + EXPECT_EQ(result.hash->size(), 1); + EXPECT_EQ(result.hash->at(0), ""); +} diff --git a/test/urlpattern/urlpattern_match_username_test.cc b/test/urlpattern/urlpattern_match_username_test.cc new file mode 100644 index 000000000..622312da4 --- /dev/null +++ b/test/urlpattern/urlpattern_match_username_test.cc @@ -0,0 +1,87 @@ +#include + +#include + +TEST(URLPattern_match, username_char_exact_match) { + const sourcemeta::core::URLPattern pattern{.username = "admin"}; + const auto result{pattern.username.match("admin")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, username_char_no_match) { + const sourcemeta::core::URLPattern pattern{.username = "admin"}; + const auto result{pattern.username.match("user")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, username_name_basic) { + const sourcemeta::core::URLPattern pattern{.username = ":user"}; + const auto result{pattern.username.match("admin")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "admin"); + EXPECT_TRUE(result.value().at("user").has_value()); + EXPECT_EQ(result.value().at("user").value(), "admin"); +} + +TEST(URLPattern_match, username_name_any_value) { + const sourcemeta::core::URLPattern pattern{.username = ":user"}; + const auto result{pattern.username.match("john")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "john"); + EXPECT_TRUE(result.value().at("user").has_value()); + EXPECT_EQ(result.value().at("user").value(), "john"); +} + +TEST(URLPattern_match, username_name_regex_match) { + const sourcemeta::core::URLPattern pattern{.username = ":user(\\w+)"}; + const auto result{pattern.username.match("admin123")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "admin123"); + EXPECT_TRUE(result.value().at("user").has_value()); + EXPECT_EQ(result.value().at("user").value(), "admin123"); +} + +TEST(URLPattern_match, username_name_regex_no_match) { + const sourcemeta::core::URLPattern pattern{.username = ":user(\\d+)"}; + const auto result{pattern.username.match("admin")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, username_regex_match) { + const sourcemeta::core::URLPattern pattern{.username = "(\\w+)"}; + const auto result{pattern.username.match("user123")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "user123"); +} + +TEST(URLPattern_match, username_regex_no_match) { + const sourcemeta::core::URLPattern pattern{.username = "(\\d+)"}; + const auto result{pattern.username.match("admin")}; + EXPECT_FALSE(result.has_value()); +} + +TEST(URLPattern_match, username_asterisk) { + const sourcemeta::core::URLPattern pattern{.username = "*"}; + const auto result{pattern.username.match("admin")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 1); + EXPECT_EQ(result.value().at(0), "admin"); +} + +TEST(URLPattern_match, username_group_match) { + const sourcemeta::core::URLPattern pattern{.username = "{admin}"}; + const auto result{pattern.username.match("admin")}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 0); +} + +TEST(URLPattern_match, username_group_no_match) { + const sourcemeta::core::URLPattern pattern{.username = "{admin}"}; + const auto result{pattern.username.match("user")}; + EXPECT_FALSE(result.has_value()); +} diff --git a/test/urlpattern/urlpattern_parse_hash_test.cc b/test/urlpattern/urlpattern_parse_hash_test.cc new file mode 100644 index 000000000..6312a069c --- /dev/null +++ b/test/urlpattern/urlpattern_parse_hash_test.cc @@ -0,0 +1,158 @@ +#include + +#include + +#include "urlpattern_test_utils.h" + +TEST(URLPattern_parse, hash_copy_constructor) { + const sourcemeta::core::URLPatternHash original{"section"}; + const sourcemeta::core::URLPatternHash copy{original}; + EXPECT_SINGLE_PART_WITH_VALUE(copy, URLPatternPartChar, "section"); +} + +TEST(URLPattern_parse, hash_move_constructor) { + sourcemeta::core::URLPatternHash original{"section"}; + const sourcemeta::core::URLPatternHash moved{std::move(original)}; + EXPECT_SINGLE_PART_WITH_VALUE(moved, URLPatternPartChar, "section"); +} + +TEST(URLPattern_parse, hash_copy_assignment) { + sourcemeta::core::URLPattern pattern; + const sourcemeta::core::URLPatternHash original{"section"}; + pattern.hash = original; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.hash, URLPatternPartChar, "section"); +} + +TEST(URLPattern_parse, hash_move_assignment) { + sourcemeta::core::URLPattern pattern; + sourcemeta::core::URLPatternHash original{"section"}; + pattern.hash = std::move(original); + EXPECT_SINGLE_PART_WITH_VALUE(pattern.hash, URLPatternPartChar, "section"); +} + +TEST(URLPattern_parse, hash_default_constructor) { + const sourcemeta::core::URLPatternHash hash; + EXPECT_SINGLE_PART_WITHOUT_VALUE(hash, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse, hash_simple_char_token) { + const sourcemeta::core::URLPattern pattern{.hash = "section"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.hash, URLPatternPartChar, "section"); +} + +TEST(URLPattern_parse, hash_intro) { + const sourcemeta::core::URLPattern pattern{.hash = "intro"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.hash, URLPatternPartChar, "intro"); +} + +TEST(URLPattern_parse, hash_heading) { + const sourcemeta::core::URLPattern pattern{.hash = "heading-1"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.hash, URLPatternPartChar, "heading-1"); +} + +TEST(URLPattern_parse, hash_name_token) { + const sourcemeta::core::URLPattern pattern{.hash = ":fragment"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.hash, URLPatternPartName, "fragment"); +} + +TEST(URLPattern_parse, hash_name_optional) { + const sourcemeta::core::URLPattern pattern{.hash = ":fragment?"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.hash, URLPatternPartNameOptional, + "fragment"); +} + +TEST(URLPattern_parse, hash_name_multiple) { + const sourcemeta::core::URLPattern pattern{.hash = ":fragment+"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.hash, URLPatternPartNameMultiple, + "fragment"); +} + +TEST(URLPattern_parse, hash_name_asterisk) { + const sourcemeta::core::URLPattern pattern{.hash = ":fragment*"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.hash, URLPatternPartNameAsterisk, + "fragment"); +} + +TEST(URLPattern_parse, hash_name_regex) { + const sourcemeta::core::URLPattern pattern{.hash = ":frag(\\w+)"}; + EXPECT_SINGLE_PART_WITH_VALUE_AND_REGEX( + pattern.hash, URLPatternPartNameRegExp, "frag", "\\w+"); +} + +TEST(URLPattern_parse, hash_regex) { + const sourcemeta::core::URLPattern pattern{.hash = "(\\w+)"}; + EXPECT_SINGLE_PART_WITH_REGEX(pattern.hash, URLPatternPartRegExp, "\\w+"); +} + +TEST(URLPattern_parse, hash_regex_optional) { + const sourcemeta::core::URLPattern pattern{.hash = "(\\w+)?"}; + EXPECT_SINGLE_PART_WITH_REGEX(pattern.hash, URLPatternPartRegExpOptional, + "\\w+"); +} + +TEST(URLPattern_parse, hash_regex_multiple) { + const sourcemeta::core::URLPattern pattern{.hash = "(\\w+)+"}; + EXPECT_SINGLE_PART_WITH_REGEX(pattern.hash, URLPatternPartRegExpMultiple, + "\\w+"); +} + +TEST(URLPattern_parse, hash_regex_asterisk) { + const sourcemeta::core::URLPattern pattern{.hash = "(\\w+)*"}; + EXPECT_SINGLE_PART_WITH_REGEX(pattern.hash, URLPatternPartRegExpAsterisk, + "\\w+"); +} + +TEST(URLPattern_parse, hash_asterisk) { + const sourcemeta::core::URLPattern pattern{.hash = "*"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.hash, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse, hash_asterisk_optional) { + const sourcemeta::core::URLPattern pattern{.hash = "*?"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.hash, + URLPatternPartAsteriskOptional); +} + +TEST(URLPattern_parse, hash_asterisk_multiple) { + const sourcemeta::core::URLPattern pattern{.hash = "*+"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.hash, + URLPatternPartAsteriskMultiple); +} + +TEST(URLPattern_parse, hash_asterisk_asterisk) { + const sourcemeta::core::URLPattern pattern{.hash = "**"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.hash, + URLPatternPartAsteriskAsterisk); +} + +TEST(URLPattern_parse, hash_group_simple) { + const sourcemeta::core::URLPattern pattern{.hash = "{section}"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.hash, URLPatternPartGroup); +} + +TEST(URLPattern_parse, hash_group_optional) { + const sourcemeta::core::URLPattern pattern{.hash = "{section}?"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.hash, URLPatternPartGroupOptional); +} + +TEST(URLPattern_parse, hash_group_multiple) { + const sourcemeta::core::URLPattern pattern{.hash = "{section}+"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.hash, URLPatternPartGroupMultiple); +} + +TEST(URLPattern_parse, hash_group_asterisk) { + const sourcemeta::core::URLPattern pattern{.hash = "{section}*"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.hash, URLPatternPartGroupAsterisk); +} + +TEST(URLPattern_parse, hash_error_unclosed_group) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternHash, "{section", 8); +} + +TEST(URLPattern_parse, hash_error_unclosed_regex) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternHash, "(\\w+", 4); +} + +TEST(URLPattern_parse, hash_error_invalid_modifier) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternHash, ":frag%", 5); +} diff --git a/test/urlpattern/urlpattern_parse_hostname_test.cc b/test/urlpattern/urlpattern_parse_hostname_test.cc new file mode 100644 index 000000000..5bc38da2d --- /dev/null +++ b/test/urlpattern/urlpattern_parse_hostname_test.cc @@ -0,0 +1,623 @@ +#include + +#include + +#include "urlpattern_test_utils.h" + +TEST(URLPattern_parse, hostname_copy_constructor) { + const sourcemeta::core::URLPatternHostname original{"example.com"}; + const sourcemeta::core::URLPatternHostname copy{original}; + EXPECT_EQ(copy.value.size(), 2); + EXPECT_PART_WITH_VALUE(copy, 0, URLPatternPartChar, "example"); + EXPECT_PART_WITH_VALUE(copy, 1, URLPatternPartChar, "com"); +} + +TEST(URLPattern_parse, hostname_move_constructor) { + sourcemeta::core::URLPatternHostname original{"example.com"}; + const sourcemeta::core::URLPatternHostname moved{std::move(original)}; + EXPECT_EQ(moved.value.size(), 2); + EXPECT_PART_WITH_VALUE(moved, 0, URLPatternPartChar, "example"); + EXPECT_PART_WITH_VALUE(moved, 1, URLPatternPartChar, "com"); +} + +TEST(URLPattern_parse, hostname_copy_assignment) { + sourcemeta::core::URLPattern pattern; + const sourcemeta::core::URLPatternHostname original{"example.com"}; + pattern.hostname = original; + EXPECT_EQ(pattern.hostname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "example"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartChar, "com"); +} + +TEST(URLPattern_parse, hostname_move_assignment) { + sourcemeta::core::URLPattern pattern; + sourcemeta::core::URLPatternHostname original{"example.com"}; + pattern.hostname = std::move(original); + EXPECT_EQ(pattern.hostname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "example"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartChar, "com"); +} + +TEST(URLPattern_parse, hostname_default_constructor) { + const sourcemeta::core::URLPatternHostname hostname; + EXPECT_EQ(hostname.value.size(), 1); + EXPECT_PART_WITHOUT_VALUE(hostname, 0, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse, hostname_combined_all_token_types) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo.:bar.*"}; + EXPECT_EQ(pattern.hostname.value.size(), 3); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartName, "bar"); + EXPECT_PART_WITHOUT_VALUE(pattern.hostname, 2, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse, hostname_simple_char_token) { + const sourcemeta::core::URLPattern pattern{.hostname = "example.com"}; + EXPECT_EQ(pattern.hostname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "example"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartChar, "com"); +} + +TEST(URLPattern_parse, hostname_single_char_token) { + const sourcemeta::core::URLPattern pattern{.hostname = "localhost"}; + EXPECT_EQ(pattern.hostname.value.size(), 1); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "localhost"); +} + +TEST(URLPattern_parse, hostname_multiple_char_tokens) { + const sourcemeta::core::URLPattern pattern{.hostname = "api.example.com"}; + EXPECT_EQ(pattern.hostname.value.size(), 3); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "api"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartChar, "example"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 2, URLPatternPartChar, "com"); +} + +TEST(URLPattern_parse, hostname_empty_segment_double_dot) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo..bar"}; + EXPECT_EQ(pattern.hostname.value.size(), 3); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartChar, ""); + EXPECT_PART_WITH_VALUE(pattern.hostname, 2, URLPatternPartChar, "bar"); +} + +TEST(URLPattern_parse, hostname_empty_segment_trailing_dot) { + const sourcemeta::core::URLPattern pattern{.hostname = "example.com."}; + EXPECT_EQ(pattern.hostname.value.size(), 3); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "example"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartChar, "com"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 2, URLPatternPartChar, ""); +} + +TEST(URLPattern_parse, hostname_empty_segment_leading_dot) { + const sourcemeta::core::URLPattern pattern{.hostname = ".example.com"}; + EXPECT_EQ(pattern.hostname.value.size(), 3); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, ""); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartChar, "example"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 2, URLPatternPartChar, "com"); +} + +TEST(URLPattern_parse, hostname_name_token_simple) { + const sourcemeta::core::URLPattern pattern{.hostname = ":name.example.com"}; + EXPECT_EQ(pattern.hostname.value.size(), 3); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartName, "name"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartChar, "example"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 2, URLPatternPartChar, "com"); +} + +TEST(URLPattern_parse, hostname_error_name_starts_with_digit) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternHostname, ":123", 1); +} + +TEST(URLPattern_parse, hostname_error_name_starts_with_hyphen) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternHostname, ":foo-bar", 4); +} + +TEST(URLPattern_parse, hostname_error_name_with_at_sign) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternHostname, ":foo@bar", 4); +} + +TEST(URLPattern_parse, hostname_error_empty_name_token) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternHostname, ":", 1); +} + +TEST(URLPattern_parse, hostname_error_empty_name_before_dot) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternHostname, ":.example.com", 1); +} + +TEST(URLPattern_parse, hostname_error_empty_name_before_asterisk) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternHostname, ":*", 1); +} + +TEST(URLPattern_parse, hostname_name_token_valid_start_lowercase) { + const sourcemeta::core::URLPattern pattern{.hostname = ":foo"}; + EXPECT_EQ(pattern.hostname.value.size(), 1); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartName, "foo"); +} + +TEST(URLPattern_parse, hostname_name_token_valid_start_uppercase) { + const sourcemeta::core::URLPattern pattern{.hostname = ":Foo"}; + EXPECT_EQ(pattern.hostname.value.size(), 1); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartName, "Foo"); +} + +TEST(URLPattern_parse, hostname_name_token_valid_start_underscore) { + const sourcemeta::core::URLPattern pattern{.hostname = ":_foo"}; + EXPECT_EQ(pattern.hostname.value.size(), 1); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartName, "_foo"); +} + +TEST(URLPattern_parse, hostname_name_token_valid_start_dollar) { + const sourcemeta::core::URLPattern pattern{.hostname = ":$foo"}; + EXPECT_EQ(pattern.hostname.value.size(), 1); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartName, "$foo"); +} + +TEST(URLPattern_parse, hostname_asterisk_alone) { + const sourcemeta::core::URLPattern pattern{.hostname = "*"}; + EXPECT_EQ(pattern.hostname.value.size(), 1); + EXPECT_PART_WITHOUT_VALUE(pattern.hostname, 0, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse, hostname_asterisk_with_segments) { + const sourcemeta::core::URLPattern pattern{.hostname = "*.example.com"}; + EXPECT_EQ(pattern.hostname.value.size(), 3); + EXPECT_PART_WITHOUT_VALUE(pattern.hostname, 0, URLPatternPartAsterisk); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartChar, "example"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 2, URLPatternPartChar, "com"); +} + +TEST(URLPattern_parse, hostname_multiple_asterisks) { + const sourcemeta::core::URLPattern pattern{.hostname = "*.*"}; + EXPECT_EQ(pattern.hostname.value.size(), 2); + EXPECT_PART_WITHOUT_VALUE(pattern.hostname, 0, URLPatternPartAsterisk); + EXPECT_PART_WITHOUT_VALUE(pattern.hostname, 1, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse, hostname_combined_char_name_asterisk) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo.:bar.*"}; + EXPECT_EQ(pattern.hostname.value.size(), 3); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartName, "bar"); + EXPECT_PART_WITHOUT_VALUE(pattern.hostname, 2, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse, hostname_asterisk_optional_simple) { + const sourcemeta::core::URLPattern pattern{.hostname = "*?"}; + EXPECT_EQ(pattern.hostname.value.size(), 1); + EXPECT_PART_WITHOUT_VALUE(pattern.hostname, 0, + URLPatternPartAsteriskOptional); +} + +TEST(URLPattern_parse, hostname_asterisk_optional_with_prefix) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo.*?"}; + EXPECT_EQ(pattern.hostname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITHOUT_VALUE(pattern.hostname, 1, + URLPatternPartAsteriskOptional); +} + +TEST(URLPattern_parse, hostname_asterisk_optional_with_suffix) { + const sourcemeta::core::URLPattern pattern{.hostname = "*?.bar"}; + EXPECT_EQ(pattern.hostname.value.size(), 2); + EXPECT_PART_WITHOUT_VALUE(pattern.hostname, 0, + URLPatternPartAsteriskOptional); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartChar, "bar"); +} + +TEST(URLPattern_parse, hostname_multiple_asterisk_optional) { + const sourcemeta::core::URLPattern pattern{.hostname = "*?.*?"}; + EXPECT_EQ(pattern.hostname.value.size(), 2); + EXPECT_PART_WITHOUT_VALUE(pattern.hostname, 0, + URLPatternPartAsteriskOptional); + EXPECT_PART_WITHOUT_VALUE(pattern.hostname, 1, + URLPatternPartAsteriskOptional); +} + +TEST(URLPattern_parse, hostname_asterisk_optional_mixed) { + const sourcemeta::core::URLPattern pattern{.hostname = + "api.:version.*?.example.com"}; + EXPECT_EQ(pattern.hostname.value.size(), 5); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "api"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartName, "version"); + EXPECT_PART_WITHOUT_VALUE(pattern.hostname, 2, + URLPatternPartAsteriskOptional); + EXPECT_PART_WITH_VALUE(pattern.hostname, 3, URLPatternPartChar, "example"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 4, URLPatternPartChar, "com"); +} + +TEST(URLPattern_parse, hostname_asterisk_multiple_simple) { + const sourcemeta::core::URLPattern pattern{.hostname = "*+"}; + EXPECT_EQ(pattern.hostname.value.size(), 1); + EXPECT_PART_WITHOUT_VALUE(pattern.hostname, 0, + URLPatternPartAsteriskMultiple); +} + +TEST(URLPattern_parse, hostname_asterisk_multiple_with_prefix) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo.*+"}; + EXPECT_EQ(pattern.hostname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITHOUT_VALUE(pattern.hostname, 1, + URLPatternPartAsteriskMultiple); +} + +TEST(URLPattern_parse, hostname_asterisk_multiple_with_suffix) { + const sourcemeta::core::URLPattern pattern{.hostname = "*+.bar"}; + EXPECT_EQ(pattern.hostname.value.size(), 2); + EXPECT_PART_WITHOUT_VALUE(pattern.hostname, 0, + URLPatternPartAsteriskMultiple); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartChar, "bar"); +} + +TEST(URLPattern_parse, hostname_multiple_asterisk_multiple) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo.*+.bar.*+"}; + EXPECT_EQ(pattern.hostname.value.size(), 4); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITHOUT_VALUE(pattern.hostname, 1, + URLPatternPartAsteriskMultiple); + EXPECT_PART_WITH_VALUE(pattern.hostname, 2, URLPatternPartChar, "bar"); + EXPECT_PART_WITHOUT_VALUE(pattern.hostname, 3, + URLPatternPartAsteriskMultiple); +} + +TEST(URLPattern_parse, hostname_asterisk_asterisk_simple) { + const sourcemeta::core::URLPattern pattern{.hostname = "**"}; + EXPECT_EQ(pattern.hostname.value.size(), 1); + EXPECT_PART_WITHOUT_VALUE(pattern.hostname, 0, + URLPatternPartAsteriskAsterisk); +} + +TEST(URLPattern_parse, hostname_asterisk_asterisk_with_prefix) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo.**"}; + EXPECT_EQ(pattern.hostname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITHOUT_VALUE(pattern.hostname, 1, + URLPatternPartAsteriskAsterisk); +} + +TEST(URLPattern_parse, hostname_asterisk_asterisk_with_suffix) { + const sourcemeta::core::URLPattern pattern{.hostname = "**.bar"}; + EXPECT_EQ(pattern.hostname.value.size(), 2); + EXPECT_PART_WITHOUT_VALUE(pattern.hostname, 0, + URLPatternPartAsteriskAsterisk); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartChar, "bar"); +} + +TEST(URLPattern_parse, hostname_multiple_asterisk_asterisk) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo.**.bar.**"}; + EXPECT_EQ(pattern.hostname.value.size(), 4); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITHOUT_VALUE(pattern.hostname, 1, + URLPatternPartAsteriskAsterisk); + EXPECT_PART_WITH_VALUE(pattern.hostname, 2, URLPatternPartChar, "bar"); + EXPECT_PART_WITHOUT_VALUE(pattern.hostname, 3, + URLPatternPartAsteriskAsterisk); +} + +TEST(URLPattern_parse, hostname_escape_colon) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo\\:bar"}; + EXPECT_EQ(pattern.hostname.value.size(), 1); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "foo:bar"); +} + +TEST(URLPattern_parse, hostname_escape_asterisk) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo\\*bar"}; + EXPECT_EQ(pattern.hostname.value.size(), 1); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "foo*bar"); +} + +TEST(URLPattern_parse, hostname_escape_backslash) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo\\\\bar"}; + EXPECT_EQ(pattern.hostname.value.size(), 1); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "foo\\bar"); +} + +TEST(URLPattern_parse, hostname_escape_dot) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo\\.bar"}; + EXPECT_EQ(pattern.hostname.value.size(), 1); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "foo.bar"); +} + +TEST(URLPattern_parse, hostname_multiple_escapes) { + const sourcemeta::core::URLPattern pattern{.hostname = "\\:\\*\\\\"}; + EXPECT_EQ(pattern.hostname.value.size(), 1); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, ":*\\"); +} + +TEST(URLPattern_parse, hostname_name_token_with_digits) { + const sourcemeta::core::URLPattern pattern{.hostname = ":foo123"}; + EXPECT_EQ(pattern.hostname.value.size(), 1); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartName, "foo123"); +} + +TEST(URLPattern_parse, hostname_name_token_with_underscores) { + const sourcemeta::core::URLPattern pattern{.hostname = ":foo_bar_baz"}; + EXPECT_EQ(pattern.hostname.value.size(), 1); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartName, + "foo_bar_baz"); +} + +TEST(URLPattern_parse, hostname_name_token_with_dollar_signs) { + const sourcemeta::core::URLPattern pattern{.hostname = ":$foo$bar"}; + EXPECT_EQ(pattern.hostname.value.size(), 1); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartName, "$foo$bar"); +} + +TEST(URLPattern_parse, hostname_multiple_name_tokens) { + const sourcemeta::core::URLPattern pattern{.hostname = ":foo.:bar.:baz"}; + EXPECT_EQ(pattern.hostname.value.size(), 3); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartName, "foo"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartName, "bar"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 2, URLPatternPartName, "baz"); +} + +TEST(URLPattern_parse, hostname_regex_simple) { + const sourcemeta::core::URLPattern pattern{.hostname = "(.*)"}; + EXPECT_EQ(pattern.hostname.value.size(), 1); + EXPECT_PART_WITH_REGEX(pattern.hostname, 0, URLPatternPartRegExp, ".*"); +} + +TEST(URLPattern_parse, hostname_regex_with_prefix) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo.(.*)"}; + EXPECT_EQ(pattern.hostname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITH_REGEX(pattern.hostname, 1, URLPatternPartRegExp, ".*"); +} + +TEST(URLPattern_parse, hostname_regex_with_suffix) { + const sourcemeta::core::URLPattern pattern{.hostname = "(.*).bar"}; + EXPECT_EQ(pattern.hostname.value.size(), 2); + EXPECT_PART_WITH_REGEX(pattern.hostname, 0, URLPatternPartRegExp, ".*"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartChar, "bar"); +} + +TEST(URLPattern_parse, hostname_multiple_regexes) { + const sourcemeta::core::URLPattern pattern{.hostname = "(foo).(bar)"}; + EXPECT_EQ(pattern.hostname.value.size(), 2); + EXPECT_PART_WITH_REGEX(pattern.hostname, 0, URLPatternPartRegExp, "foo"); + EXPECT_PART_WITH_REGEX(pattern.hostname, 1, URLPatternPartRegExp, "bar"); +} + +TEST(URLPattern_parse, hostname_regex_unclosed) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternHostname, "foo.(.*", 7); +} + +TEST(URLPattern_parse, hostname_regex_empty) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternHostname, "()", 1); +} + +TEST(URLPattern_parse, hostname_name_regex_simple) { + const sourcemeta::core::URLPattern pattern{.hostname = ":foo(.*)"}; + EXPECT_EQ(pattern.hostname.value.size(), 1); + EXPECT_PART_WITH_VALUE_AND_REGEX(pattern.hostname, 0, + URLPatternPartNameRegExp, "foo", ".*"); +} + +TEST(URLPattern_parse, hostname_name_regex_with_prefix) { + const sourcemeta::core::URLPattern pattern{.hostname = "api.:version(\\d+)"}; + EXPECT_EQ(pattern.hostname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "api"); + EXPECT_PART_WITH_VALUE_AND_REGEX(pattern.hostname, 1, + URLPatternPartNameRegExp, "version", "\\d+"); +} + +TEST(URLPattern_parse, hostname_name_regex_unclosed) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternHostname, ":foo(.*", 7); +} + +TEST(URLPattern_parse, hostname_name_regex_empty) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternHostname, ":foo()", 5); +} + +TEST(URLPattern_parse, hostname_name_optional_simple) { + const sourcemeta::core::URLPattern pattern{.hostname = ":foo?"}; + EXPECT_EQ(pattern.hostname.value.size(), 1); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartNameOptional, + "foo"); +} + +TEST(URLPattern_parse, hostname_name_optional_with_prefix) { + const sourcemeta::core::URLPattern pattern{.hostname = "api.:version?"}; + EXPECT_EQ(pattern.hostname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "api"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartNameOptional, + "version"); +} + +TEST(URLPattern_parse, hostname_name_optional_with_suffix) { + const sourcemeta::core::URLPattern pattern{.hostname = ":id?.example.com"}; + EXPECT_EQ(pattern.hostname.value.size(), 3); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartNameOptional, "id"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartChar, "example"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 2, URLPatternPartChar, "com"); +} + +TEST(URLPattern_parse, hostname_multiple_name_optional) { + const sourcemeta::core::URLPattern pattern{.hostname = ":foo?.:bar?"}; + EXPECT_EQ(pattern.hostname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartNameOptional, + "foo"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartNameOptional, + "bar"); +} + +TEST(URLPattern_parse, hostname_name_multiple_simple) { + const sourcemeta::core::URLPattern pattern{.hostname = ":foo+"}; + EXPECT_EQ(pattern.hostname.value.size(), 1); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartNameMultiple, + "foo"); +} + +TEST(URLPattern_parse, hostname_name_multiple_with_prefix) { + const sourcemeta::core::URLPattern pattern{.hostname = "api.:version+"}; + EXPECT_EQ(pattern.hostname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "api"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartNameMultiple, + "version"); +} + +TEST(URLPattern_parse, hostname_name_asterisk_simple) { + const sourcemeta::core::URLPattern pattern{.hostname = ":foo*"}; + EXPECT_EQ(pattern.hostname.value.size(), 1); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartNameAsterisk, + "foo"); +} + +TEST(URLPattern_parse, hostname_name_asterisk_with_prefix) { + const sourcemeta::core::URLPattern pattern{.hostname = "api.:version*"}; + EXPECT_EQ(pattern.hostname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "api"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartNameAsterisk, + "version"); +} + +TEST(URLPattern_parse, hostname_regex_optional_simple) { + const sourcemeta::core::URLPattern pattern{.hostname = "(.*)?"}; + EXPECT_EQ(pattern.hostname.value.size(), 1); + EXPECT_PART_WITH_REGEX(pattern.hostname, 0, URLPatternPartRegExpOptional, + ".*"); +} + +TEST(URLPattern_parse, hostname_regex_optional_with_prefix) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo.(.*)?"}; + EXPECT_EQ(pattern.hostname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITH_REGEX(pattern.hostname, 1, URLPatternPartRegExpOptional, + ".*"); +} + +TEST(URLPattern_parse, hostname_regex_multiple_simple) { + const sourcemeta::core::URLPattern pattern{.hostname = "(.*)+"}; + EXPECT_EQ(pattern.hostname.value.size(), 1); + EXPECT_PART_WITH_REGEX(pattern.hostname, 0, URLPatternPartRegExpMultiple, + ".*"); +} + +TEST(URLPattern_parse, hostname_regex_multiple_with_prefix) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo.(.*)+"}; + EXPECT_EQ(pattern.hostname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITH_REGEX(pattern.hostname, 1, URLPatternPartRegExpMultiple, + ".*"); +} + +TEST(URLPattern_parse, hostname_regex_asterisk_simple) { + const sourcemeta::core::URLPattern pattern{.hostname = "(.*)*"}; + EXPECT_EQ(pattern.hostname.value.size(), 1); + EXPECT_PART_WITH_REGEX(pattern.hostname, 0, URLPatternPartRegExpAsterisk, + ".*"); +} + +TEST(URLPattern_parse, hostname_regex_asterisk_with_prefix) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo.(.*)*"}; + EXPECT_EQ(pattern.hostname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITH_REGEX(pattern.hostname, 1, URLPatternPartRegExpAsterisk, + ".*"); +} + +TEST(URLPattern_parse, hostname_group_simple_char) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo{.bar}"}; + EXPECT_EQ(pattern.hostname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_GROUP(pattern.hostname, 1, URLPatternPartGroup, 1, true, false); + EXPECT_PART_NESTED_WITH_VALUE(pattern.hostname, 1, URLPatternPartGroup, 0, + URLPatternPartChar, "bar"); +} + +TEST(URLPattern_parse, hostname_group_multiple_tokens) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo{.bar.:baz}"}; + EXPECT_EQ(pattern.hostname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_GROUP(pattern.hostname, 1, URLPatternPartGroup, 2, true, false); + EXPECT_PART_NESTED_WITH_VALUE(pattern.hostname, 1, URLPatternPartGroup, 0, + URLPatternPartChar, "bar"); + EXPECT_PART_NESTED_WITH_VALUE(pattern.hostname, 1, URLPatternPartGroup, 1, + URLPatternPartName, "baz"); +} + +TEST(URLPattern_parse, hostname_group_with_asterisk) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo{.*}"}; + EXPECT_EQ(pattern.hostname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_GROUP(pattern.hostname, 1, URLPatternPartGroup, 1, true, false); + EXPECT_PART_NESTED_WITHOUT_VALUE(pattern.hostname, 1, URLPatternPartGroup, 0, + URLPatternPartAsterisk); +} + +TEST(URLPattern_parse, hostname_group_optional_simple) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo{.bar}?"}; + EXPECT_EQ(pattern.hostname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_GROUP(pattern.hostname, 1, URLPatternPartGroupOptional, 1, true, + false); + EXPECT_PART_NESTED_WITH_VALUE(pattern.hostname, 1, + URLPatternPartGroupOptional, 0, + URLPatternPartChar, "bar"); +} + +TEST(URLPattern_parse, hostname_group_multiple_simple) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo{.bar}+"}; + EXPECT_EQ(pattern.hostname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_GROUP(pattern.hostname, 1, URLPatternPartGroupMultiple, 1, true, + false); + EXPECT_PART_NESTED_WITH_VALUE(pattern.hostname, 1, + URLPatternPartGroupMultiple, 0, + URLPatternPartChar, "bar"); +} + +TEST(URLPattern_parse, hostname_group_asterisk_simple) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo{.bar}*"}; + EXPECT_EQ(pattern.hostname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_GROUP(pattern.hostname, 1, URLPatternPartGroupAsterisk, 1, true, + false); + EXPECT_PART_NESTED_WITH_VALUE(pattern.hostname, 1, + URLPatternPartGroupAsterisk, 0, + URLPatternPartChar, "bar"); +} + +TEST(URLPattern_parse, hostname_complex_segment_char_name_char) { + const sourcemeta::core::URLPattern pattern{.hostname = "file-:name.json"}; + EXPECT_EQ(pattern.hostname.value.size(), 2); + EXPECT_PART_COMPLEX_SIZE(pattern.hostname, 0, 2); + EXPECT_PART_COMPLEX_WITH_VALUE(pattern.hostname, 0, 0, URLPatternPartChar, + "file-"); + EXPECT_PART_COMPLEX_WITH_VALUE(pattern.hostname, 0, 1, URLPatternPartName, + "name"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartChar, "json"); +} + +TEST(URLPattern_parse, hostname_complex_segment_multiple_names) { + const sourcemeta::core::URLPattern pattern{.hostname = + "v:major:minor.:resource"}; + EXPECT_EQ(pattern.hostname.value.size(), 2); + EXPECT_PART_COMPLEX_SIZE(pattern.hostname, 0, 3); + EXPECT_PART_COMPLEX_WITH_VALUE(pattern.hostname, 0, 0, URLPatternPartChar, + "v"); + EXPECT_PART_COMPLEX_WITH_VALUE(pattern.hostname, 0, 1, URLPatternPartName, + "major"); + EXPECT_PART_COMPLEX_WITH_VALUE(pattern.hostname, 0, 2, URLPatternPartName, + "minor"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartName, "resource"); +} + +TEST(URLPattern_parse, hostname_complex_segment_with_group) { + const sourcemeta::core::URLPattern pattern{.hostname = "foo-:bar{baz}+"}; + EXPECT_EQ(pattern.hostname.value.size(), 1); + EXPECT_PART_COMPLEX_SIZE(pattern.hostname, 0, 3); + EXPECT_PART_COMPLEX_WITH_VALUE(pattern.hostname, 0, 0, URLPatternPartChar, + "foo-"); + EXPECT_PART_COMPLEX_WITH_VALUE(pattern.hostname, 0, 1, URLPatternPartName, + "bar"); + EXPECT_PART_COMPLEX_NESTED_SIZE(pattern.hostname, 0, 2, + URLPatternPartGroupMultiple, 1); + EXPECT_PART_COMPLEX_NESTED_WITH_VALUE(pattern.hostname, 0, 2, + URLPatternPartGroupMultiple, 0, + URLPatternPartChar, "baz"); +} + +TEST(URLPattern_parse, hostname_error_trailing_backslash) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternHostname, "foo\\", 3); +} diff --git a/test/urlpattern/urlpattern_parse_input_json_test.cc b/test/urlpattern/urlpattern_parse_input_json_test.cc new file mode 100644 index 000000000..2e39c725d --- /dev/null +++ b/test/urlpattern/urlpattern_parse_input_json_test.cc @@ -0,0 +1,172 @@ +#include + +#include +#include + +TEST(URLPatternInput_parse_json, empty_object) { + const sourcemeta::core::JSON input{sourcemeta::core::parse_json("{}")}; + const auto result{sourcemeta::core::URLPatternInput::parse(input)}; + EXPECT_TRUE(result.has_value()); + EXPECT_TRUE(result->protocol.empty()); + EXPECT_TRUE(result->username.empty()); + EXPECT_TRUE(result->password.empty()); + EXPECT_TRUE(result->hostname.empty()); + EXPECT_TRUE(result->port.empty()); + EXPECT_TRUE(result->pathname.empty()); + EXPECT_TRUE(result->search.empty()); + EXPECT_TRUE(result->hash.empty()); +} + +TEST(URLPatternInput_parse_json, protocol_only) { + const sourcemeta::core::JSON input{ + sourcemeta::core::parse_json(R"({"protocol": "https"})")}; + const auto result{sourcemeta::core::URLPatternInput::parse(input)}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result->protocol, "https"); + EXPECT_TRUE(result->username.empty()); + EXPECT_TRUE(result->password.empty()); + EXPECT_TRUE(result->hostname.empty()); + EXPECT_TRUE(result->port.empty()); + EXPECT_TRUE(result->pathname.empty()); + EXPECT_TRUE(result->search.empty()); + EXPECT_TRUE(result->hash.empty()); +} + +TEST(URLPatternInput_parse_json, username_only) { + const sourcemeta::core::JSON input{ + sourcemeta::core::parse_json(R"({"username": "admin"})")}; + const auto result{sourcemeta::core::URLPatternInput::parse(input)}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result->username, "admin"); +} + +TEST(URLPatternInput_parse_json, password_only) { + const sourcemeta::core::JSON input{ + sourcemeta::core::parse_json(R"({"password": "secret"})")}; + const auto result{sourcemeta::core::URLPatternInput::parse(input)}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result->password, "secret"); +} + +TEST(URLPatternInput_parse_json, hostname_only) { + const sourcemeta::core::JSON input{ + sourcemeta::core::parse_json(R"({"hostname": "example.com"})")}; + const auto result{sourcemeta::core::URLPatternInput::parse(input)}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result->hostname, "example.com"); +} + +TEST(URLPatternInput_parse_json, port_only) { + const sourcemeta::core::JSON input{ + sourcemeta::core::parse_json(R"({"port": "8080"})")}; + const auto result{sourcemeta::core::URLPatternInput::parse(input)}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result->port, "8080"); +} + +TEST(URLPatternInput_parse_json, pathname_only) { + const sourcemeta::core::JSON input{ + sourcemeta::core::parse_json(R"({"pathname": "/foo/bar"})")}; + const auto result{sourcemeta::core::URLPatternInput::parse(input)}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result->pathname, "/foo/bar"); +} + +TEST(URLPatternInput_parse_json, search_only) { + const sourcemeta::core::JSON input{ + sourcemeta::core::parse_json(R"({"search": "foo=bar&baz=qux"})")}; + const auto result{sourcemeta::core::URLPatternInput::parse(input)}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result->search, "foo=bar&baz=qux"); +} + +TEST(URLPatternInput_parse_json, hash_only) { + const sourcemeta::core::JSON input{ + sourcemeta::core::parse_json(R"({"hash": "section"})")}; + const auto result{sourcemeta::core::URLPatternInput::parse(input)}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result->hash, "section"); +} + +TEST(URLPatternInput_parse_json, all_components) { + const sourcemeta::core::JSON input{sourcemeta::core::parse_json(R"({ + "protocol": "https", + "username": "user", + "password": "pass", + "hostname": "example.com", + "port": "8080", + "pathname": "/api/v1", + "search": "key=value", + "hash": "section" + })")}; + const auto result{sourcemeta::core::URLPatternInput::parse(input)}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result->protocol, "https"); + EXPECT_EQ(result->username, "user"); + EXPECT_EQ(result->password, "pass"); + EXPECT_EQ(result->hostname, "example.com"); + EXPECT_EQ(result->port, "8080"); + EXPECT_EQ(result->pathname, "/api/v1"); + EXPECT_EQ(result->search, "key=value"); + EXPECT_EQ(result->hash, "section"); +} + +TEST(URLPatternInput_parse_json, ignores_unknown_properties) { + const sourcemeta::core::JSON input{sourcemeta::core::parse_json(R"({ + "pathname": "/foo", + "unknown": "ignored", + "another": 123 + })")}; + const auto result{sourcemeta::core::URLPatternInput::parse(input)}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result->pathname, "/foo"); +} + +TEST(URLPatternInput_parse_json, input_is_string) { + const sourcemeta::core::JSON input{sourcemeta::core::parse_json("\"foo\"")}; + EXPECT_FALSE(sourcemeta::core::URLPatternInput::parse(input).has_value()); +} + +TEST(URLPatternInput_parse_json, input_is_array) { + const sourcemeta::core::JSON input{sourcemeta::core::parse_json("[1, 2, 3]")}; + EXPECT_FALSE(sourcemeta::core::URLPatternInput::parse(input).has_value()); +} + +TEST(URLPatternInput_parse_json, input_is_number) { + const sourcemeta::core::JSON input{sourcemeta::core::parse_json("42")}; + EXPECT_FALSE(sourcemeta::core::URLPatternInput::parse(input).has_value()); +} + +TEST(URLPatternInput_parse_json, input_is_null) { + const sourcemeta::core::JSON input{sourcemeta::core::parse_json("null")}; + EXPECT_FALSE(sourcemeta::core::URLPatternInput::parse(input).has_value()); +} + +TEST(URLPatternInput_parse_json, input_is_boolean) { + const sourcemeta::core::JSON input{sourcemeta::core::parse_json("true")}; + EXPECT_FALSE(sourcemeta::core::URLPatternInput::parse(input).has_value()); +} + +TEST(URLPatternInput_parse_json, non_string_pathname) { + const sourcemeta::core::JSON input{ + sourcemeta::core::parse_json(R"({"pathname": 123})")}; + EXPECT_FALSE(sourcemeta::core::URLPatternInput::parse(input).has_value()); +} + +TEST(URLPatternInput_parse_json, non_string_protocol) { + const sourcemeta::core::JSON input{ + sourcemeta::core::parse_json(R"({"protocol": true})")}; + EXPECT_FALSE(sourcemeta::core::URLPatternInput::parse(input).has_value()); +} + +TEST(URLPatternInput_parse_json, non_string_hostname) { + const sourcemeta::core::JSON input{ + sourcemeta::core::parse_json(R"({"hostname": null})")}; + EXPECT_FALSE(sourcemeta::core::URLPatternInput::parse(input).has_value()); +} + +TEST(URLPatternInput_parse_json, non_string_port) { + const sourcemeta::core::JSON input{ + sourcemeta::core::parse_json(R"({"port": 8080})")}; + EXPECT_FALSE(sourcemeta::core::URLPatternInput::parse(input).has_value()); +} diff --git a/test/urlpattern/urlpattern_parse_json_test.cc b/test/urlpattern/urlpattern_parse_json_test.cc new file mode 100644 index 000000000..6b455bb33 --- /dev/null +++ b/test/urlpattern/urlpattern_parse_json_test.cc @@ -0,0 +1,239 @@ +#include + +#include +#include + +#include "urlpattern_test_utils.h" + +TEST(URLPattern_parse_json, empty_object) { + const sourcemeta::core::JSON input{sourcemeta::core::parse_json("{}")}; + const auto pattern{sourcemeta::core::URLPattern::parse(input)}; + EXPECT_TRUE(pattern.has_value()); + + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern->protocol, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern->username, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern->password, URLPatternPartAsterisk); + EXPECT_PART_VECTOR_SIZE(pattern->hostname, 1); + EXPECT_PART_WITHOUT_VALUE(pattern->hostname, 0, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern->port, URLPatternPartAsterisk); + EXPECT_PART_VECTOR_SIZE(pattern->pathname, 1); + EXPECT_PART_WITHOUT_VALUE(pattern->pathname, 0, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern->search, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern->hash, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse_json, pathname_only) { + const sourcemeta::core::JSON input{ + sourcemeta::core::parse_json(R"({"pathname": "/foo/bar"})")}; + const auto pattern{sourcemeta::core::URLPattern::parse(input)}; + EXPECT_TRUE(pattern.has_value()); + + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern->protocol, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern->username, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern->password, URLPatternPartAsterisk); + EXPECT_PART_VECTOR_SIZE(pattern->hostname, 1); + EXPECT_PART_WITHOUT_VALUE(pattern->hostname, 0, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern->port, URLPatternPartAsterisk); + + EXPECT_PART_VECTOR_SIZE(pattern->pathname, 2); + EXPECT_PART_WITH_VALUE(pattern->pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITH_VALUE(pattern->pathname, 1, URLPatternPartChar, "bar"); + + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern->search, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern->hash, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse_json, pathname_with_named_group) { + const sourcemeta::core::JSON input{ + sourcemeta::core::parse_json(R"({"pathname": "/foo/:bar"})")}; + const auto pattern{sourcemeta::core::URLPattern::parse(input)}; + EXPECT_TRUE(pattern.has_value()); + + EXPECT_PART_VECTOR_SIZE(pattern->pathname, 2); + EXPECT_PART_WITH_VALUE(pattern->pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITH_VALUE(pattern->pathname, 1, URLPatternPartName, "bar"); +} + +TEST(URLPattern_parse_json, pathname_with_wildcard) { + const sourcemeta::core::JSON input{ + sourcemeta::core::parse_json(R"({"pathname": "/foo/*"})")}; + const auto pattern{sourcemeta::core::URLPattern::parse(input)}; + EXPECT_TRUE(pattern.has_value()); + + EXPECT_PART_VECTOR_SIZE(pattern->pathname, 2); + EXPECT_PART_WITH_VALUE(pattern->pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITHOUT_VALUE(pattern->pathname, 1, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse_json, protocol_only) { + const sourcemeta::core::JSON input{ + sourcemeta::core::parse_json(R"({"protocol": "https"})")}; + const auto pattern{sourcemeta::core::URLPattern::parse(input)}; + EXPECT_TRUE(pattern.has_value()); + + EXPECT_SINGLE_PART_WITH_VALUE(pattern->protocol, URLPatternPartChar, "https"); +} + +TEST(URLPattern_parse_json, hostname_only) { + const sourcemeta::core::JSON input{ + sourcemeta::core::parse_json(R"({"hostname": "example.com"})")}; + const auto pattern{sourcemeta::core::URLPattern::parse(input)}; + EXPECT_TRUE(pattern.has_value()); + + EXPECT_PART_VECTOR_SIZE(pattern->hostname, 2); + EXPECT_PART_WITH_VALUE(pattern->hostname, 0, URLPatternPartChar, "example"); + EXPECT_PART_WITH_VALUE(pattern->hostname, 1, URLPatternPartChar, "com"); +} + +TEST(URLPattern_parse_json, port_only) { + const sourcemeta::core::JSON input{ + sourcemeta::core::parse_json(R"({"port": "8080"})")}; + const auto pattern{sourcemeta::core::URLPattern::parse(input)}; + EXPECT_TRUE(pattern.has_value()); + + EXPECT_SINGLE_PART_WITH_VALUE(pattern->port, URLPatternPartChar, "8080"); +} + +TEST(URLPattern_parse_json, username_only) { + const sourcemeta::core::JSON input{ + sourcemeta::core::parse_json(R"({"username": "admin"})")}; + const auto pattern{sourcemeta::core::URLPattern::parse(input)}; + EXPECT_TRUE(pattern.has_value()); + + EXPECT_SINGLE_PART_WITH_VALUE(pattern->username, URLPatternPartChar, "admin"); +} + +TEST(URLPattern_parse_json, password_only) { + const sourcemeta::core::JSON input{ + sourcemeta::core::parse_json(R"({"password": "secret"})")}; + const auto pattern{sourcemeta::core::URLPattern::parse(input)}; + EXPECT_TRUE(pattern.has_value()); + + EXPECT_SINGLE_PART_WITH_VALUE(pattern->password, URLPatternPartChar, + "secret"); +} + +TEST(URLPattern_parse_json, search_only) { + const sourcemeta::core::JSON input{ + sourcemeta::core::parse_json(R"({"search": "foo=bar"})")}; + const auto pattern{sourcemeta::core::URLPattern::parse(input)}; + EXPECT_TRUE(pattern.has_value()); + + EXPECT_SINGLE_PART_WITH_VALUE(pattern->search, URLPatternPartChar, "foo=bar"); +} + +TEST(URLPattern_parse_json, hash_only) { + const sourcemeta::core::JSON input{ + sourcemeta::core::parse_json(R"({"hash": "section"})")}; + const auto pattern{sourcemeta::core::URLPattern::parse(input)}; + EXPECT_TRUE(pattern.has_value()); + + EXPECT_SINGLE_PART_WITH_VALUE(pattern->hash, URLPatternPartChar, "section"); +} + +TEST(URLPattern_parse_json, all_components) { + const sourcemeta::core::JSON input{sourcemeta::core::parse_json(R"({ + "protocol": "https", + "username": "user", + "password": "pass", + "hostname": "example.com", + "port": "8080", + "pathname": "/api/:id", + "search": "key=value", + "hash": "section" + })")}; + const auto pattern{sourcemeta::core::URLPattern::parse(input)}; + EXPECT_TRUE(pattern.has_value()); + + EXPECT_SINGLE_PART_WITH_VALUE(pattern->protocol, URLPatternPartChar, "https"); + EXPECT_SINGLE_PART_WITH_VALUE(pattern->username, URLPatternPartChar, "user"); + EXPECT_SINGLE_PART_WITH_VALUE(pattern->password, URLPatternPartChar, "pass"); + + EXPECT_PART_VECTOR_SIZE(pattern->hostname, 2); + EXPECT_PART_WITH_VALUE(pattern->hostname, 0, URLPatternPartChar, "example"); + EXPECT_PART_WITH_VALUE(pattern->hostname, 1, URLPatternPartChar, "com"); + + EXPECT_SINGLE_PART_WITH_VALUE(pattern->port, URLPatternPartChar, "8080"); + + EXPECT_PART_VECTOR_SIZE(pattern->pathname, 2); + EXPECT_PART_WITH_VALUE(pattern->pathname, 0, URLPatternPartChar, "api"); + EXPECT_PART_WITH_VALUE(pattern->pathname, 1, URLPatternPartName, "id"); + + EXPECT_SINGLE_PART_WITH_VALUE(pattern->search, URLPatternPartChar, + "key=value"); + EXPECT_SINGLE_PART_WITH_VALUE(pattern->hash, URLPatternPartChar, "section"); +} + +TEST(URLPattern_parse_json, ignores_unknown_properties) { + const sourcemeta::core::JSON input{sourcemeta::core::parse_json(R"({ + "pathname": "/foo", + "unknown": "ignored", + "another": 123 + })")}; + const auto pattern{sourcemeta::core::URLPattern::parse(input)}; + EXPECT_TRUE(pattern.has_value()); + + EXPECT_PART_VECTOR_SIZE(pattern->pathname, 1); + EXPECT_PART_WITH_VALUE(pattern->pathname, 0, URLPatternPartChar, "foo"); +} + +TEST(URLPattern_parse_json, input_is_string) { + const sourcemeta::core::JSON input{sourcemeta::core::parse_json("\"foo\"")}; + EXPECT_FALSE(sourcemeta::core::URLPattern::parse(input).has_value()); +} + +TEST(URLPattern_parse_json, input_is_array) { + const sourcemeta::core::JSON input{sourcemeta::core::parse_json("[1, 2, 3]")}; + EXPECT_FALSE(sourcemeta::core::URLPattern::parse(input).has_value()); +} + +TEST(URLPattern_parse_json, input_is_number) { + const sourcemeta::core::JSON input{sourcemeta::core::parse_json("42")}; + EXPECT_FALSE(sourcemeta::core::URLPattern::parse(input).has_value()); +} + +TEST(URLPattern_parse_json, input_is_null) { + const sourcemeta::core::JSON input{sourcemeta::core::parse_json("null")}; + EXPECT_FALSE(sourcemeta::core::URLPattern::parse(input).has_value()); +} + +TEST(URLPattern_parse_json, input_is_boolean) { + const sourcemeta::core::JSON input{sourcemeta::core::parse_json("true")}; + EXPECT_FALSE(sourcemeta::core::URLPattern::parse(input).has_value()); +} + +TEST(URLPattern_parse_json, non_string_pathname) { + const sourcemeta::core::JSON input{ + sourcemeta::core::parse_json(R"({"pathname": 123})")}; + EXPECT_FALSE(sourcemeta::core::URLPattern::parse(input).has_value()); +} + +TEST(URLPattern_parse_json, non_string_protocol) { + const sourcemeta::core::JSON input{ + sourcemeta::core::parse_json(R"({"protocol": true})")}; + EXPECT_FALSE(sourcemeta::core::URLPattern::parse(input).has_value()); +} + +TEST(URLPattern_parse_json, non_string_hostname) { + const sourcemeta::core::JSON input{ + sourcemeta::core::parse_json(R"({"hostname": null})")}; + EXPECT_FALSE(sourcemeta::core::URLPattern::parse(input).has_value()); +} + +TEST(URLPattern_parse_json, non_string_port) { + const sourcemeta::core::JSON input{ + sourcemeta::core::parse_json(R"({"port": 8080})")}; + EXPECT_FALSE(sourcemeta::core::URLPattern::parse(input).has_value()); +} + +TEST(URLPattern_parse_json, invalid_pattern) { + const sourcemeta::core::JSON input{ + sourcemeta::core::parse_json(R"({"pathname": "/foo/:"})")}; + EXPECT_FALSE(sourcemeta::core::URLPattern::parse(input).has_value()); +} + +TEST(URLPattern_parse_json, invalid_regex_pattern) { + const sourcemeta::core::JSON input{ + sourcemeta::core::parse_json(R"JSON({"pathname": "/foo/([)"})JSON")}; + EXPECT_FALSE(sourcemeta::core::URLPattern::parse(input).has_value()); +} diff --git a/test/urlpattern/urlpattern_parse_password_test.cc b/test/urlpattern/urlpattern_parse_password_test.cc new file mode 100644 index 000000000..23192de57 --- /dev/null +++ b/test/urlpattern/urlpattern_parse_password_test.cc @@ -0,0 +1,163 @@ +#include + +#include + +#include "urlpattern_test_utils.h" + +TEST(URLPattern_parse, password_copy_constructor) { + const sourcemeta::core::URLPatternPassword original{"secret"}; + const sourcemeta::core::URLPatternPassword copy{original}; + EXPECT_SINGLE_PART_WITH_VALUE(copy, URLPatternPartChar, "secret"); +} + +TEST(URLPattern_parse, password_move_constructor) { + sourcemeta::core::URLPatternPassword original{"secret"}; + const sourcemeta::core::URLPatternPassword moved{std::move(original)}; + EXPECT_SINGLE_PART_WITH_VALUE(moved, URLPatternPartChar, "secret"); +} + +TEST(URLPattern_parse, password_copy_assignment) { + sourcemeta::core::URLPattern pattern; + const sourcemeta::core::URLPatternPassword original{"secret"}; + pattern.password = original; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.password, URLPatternPartChar, "secret"); +} + +TEST(URLPattern_parse, password_move_assignment) { + sourcemeta::core::URLPattern pattern; + sourcemeta::core::URLPatternPassword original{"secret"}; + pattern.password = std::move(original); + EXPECT_SINGLE_PART_WITH_VALUE(pattern.password, URLPatternPartChar, "secret"); +} + +TEST(URLPattern_parse, password_default_constructor) { + const sourcemeta::core::URLPatternPassword password; + EXPECT_SINGLE_PART_WITHOUT_VALUE(password, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse, password_simple_char_token) { + const sourcemeta::core::URLPattern pattern{.password = "secret"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.password, URLPatternPartChar, "secret"); +} + +TEST(URLPattern_parse, password_pass) { + const sourcemeta::core::URLPattern pattern{.password = "pass123"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.password, URLPatternPartChar, + "pass123"); +} + +TEST(URLPattern_parse, password_with_special_chars) { + const sourcemeta::core::URLPattern pattern{.password = "p@ss"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.password, URLPatternPartChar, "p@ss"); +} + +TEST(URLPattern_parse, password_name_token) { + const sourcemeta::core::URLPattern pattern{.password = ":password"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.password, URLPatternPartName, + "password"); +} + +TEST(URLPattern_parse, password_name_optional) { + const sourcemeta::core::URLPattern pattern{.password = ":password?"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.password, URLPatternPartNameOptional, + "password"); +} + +TEST(URLPattern_parse, password_name_multiple) { + const sourcemeta::core::URLPattern pattern{.password = ":password+"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.password, URLPatternPartNameMultiple, + "password"); +} + +TEST(URLPattern_parse, password_name_asterisk) { + const sourcemeta::core::URLPattern pattern{.password = ":password*"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.password, URLPatternPartNameAsterisk, + "password"); +} + +TEST(URLPattern_parse, password_name_regex) { + const sourcemeta::core::URLPattern pattern{.password = ":pass(\\w+)"}; + EXPECT_SINGLE_PART_WITH_VALUE_AND_REGEX( + pattern.password, URLPatternPartNameRegExp, "pass", "\\w+"); +} + +TEST(URLPattern_parse, password_regex) { + const sourcemeta::core::URLPattern pattern{.password = "(\\w+)"}; + EXPECT_SINGLE_PART_WITH_REGEX(pattern.password, URLPatternPartRegExp, "\\w+"); +} + +TEST(URLPattern_parse, password_regex_optional) { + const sourcemeta::core::URLPattern pattern{.password = "(\\w+)?"}; + EXPECT_SINGLE_PART_WITH_REGEX(pattern.password, URLPatternPartRegExpOptional, + "\\w+"); +} + +TEST(URLPattern_parse, password_regex_multiple) { + const sourcemeta::core::URLPattern pattern{.password = "(\\w+)+"}; + EXPECT_SINGLE_PART_WITH_REGEX(pattern.password, URLPatternPartRegExpMultiple, + "\\w+"); +} + +TEST(URLPattern_parse, password_regex_asterisk) { + const sourcemeta::core::URLPattern pattern{.password = "(\\w+)*"}; + EXPECT_SINGLE_PART_WITH_REGEX(pattern.password, URLPatternPartRegExpAsterisk, + "\\w+"); +} + +TEST(URLPattern_parse, password_asterisk) { + const sourcemeta::core::URLPattern pattern{.password = "*"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse, password_asterisk_optional) { + const sourcemeta::core::URLPattern pattern{.password = "*?"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, + URLPatternPartAsteriskOptional); +} + +TEST(URLPattern_parse, password_asterisk_multiple) { + const sourcemeta::core::URLPattern pattern{.password = "*+"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, + URLPatternPartAsteriskMultiple); +} + +TEST(URLPattern_parse, password_asterisk_asterisk) { + const sourcemeta::core::URLPattern pattern{.password = "**"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, + URLPatternPartAsteriskAsterisk); +} + +TEST(URLPattern_parse, password_group_simple) { + const sourcemeta::core::URLPattern pattern{.password = "{secret}"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, URLPatternPartGroup); +} + +TEST(URLPattern_parse, password_group_optional) { + const sourcemeta::core::URLPattern pattern{.password = "{secret}?"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, + URLPatternPartGroupOptional); +} + +TEST(URLPattern_parse, password_group_multiple) { + const sourcemeta::core::URLPattern pattern{.password = "{secret}+"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, + URLPatternPartGroupMultiple); +} + +TEST(URLPattern_parse, password_group_asterisk) { + const sourcemeta::core::URLPattern pattern{.password = "{secret}*"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, + URLPatternPartGroupAsterisk); +} + +TEST(URLPattern_parse, password_error_unclosed_group) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternPassword, "{secret", 7); +} + +TEST(URLPattern_parse, password_error_unclosed_regex) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternPassword, "(\\w+", 4); +} + +TEST(URLPattern_parse, password_error_invalid_modifier) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternPassword, ":pass%", 5); +} diff --git a/test/urlpattern/urlpattern_parse_pathname_test.cc b/test/urlpattern/urlpattern_parse_pathname_test.cc new file mode 100644 index 000000000..41a2b4c23 --- /dev/null +++ b/test/urlpattern/urlpattern_parse_pathname_test.cc @@ -0,0 +1,1079 @@ +#include + +#include + +#include "urlpattern_test_utils.h" + +TEST(URLPattern_parse, pathname_copy_constructor) { + const sourcemeta::core::URLPatternPathname original{"/api/v1"}; + const sourcemeta::core::URLPatternPathname copy{original}; + EXPECT_EQ(copy.value.size(), 2); + EXPECT_PART_WITH_VALUE(copy, 0, URLPatternPartChar, "api"); + EXPECT_PART_WITH_VALUE(copy, 1, URLPatternPartChar, "v1"); +} + +TEST(URLPattern_parse, pathname_move_constructor) { + sourcemeta::core::URLPatternPathname original{"/api/v1"}; + const sourcemeta::core::URLPatternPathname moved{std::move(original)}; + EXPECT_EQ(moved.value.size(), 2); + EXPECT_PART_WITH_VALUE(moved, 0, URLPatternPartChar, "api"); + EXPECT_PART_WITH_VALUE(moved, 1, URLPatternPartChar, "v1"); +} + +TEST(URLPattern_parse, pathname_copy_assignment) { + sourcemeta::core::URLPattern pattern; + const sourcemeta::core::URLPatternPathname original{"/api/v1"}; + pattern.pathname = original; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "api"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartChar, "v1"); +} + +TEST(URLPattern_parse, pathname_move_assignment) { + sourcemeta::core::URLPattern pattern; + sourcemeta::core::URLPatternPathname original{"/api/v1"}; + pattern.pathname = std::move(original); + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "api"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartChar, "v1"); +} + +TEST(URLPattern_parse, pathname_default_constructor) { + const sourcemeta::core::URLPatternPathname pathname; + EXPECT_EQ(pathname.value.size(), 1); + EXPECT_PART_WITHOUT_VALUE(pathname, 0, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse, pathname_combined_all_token_types) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/:bar/*"}; + EXPECT_EQ(pattern.pathname.value.size(), 3); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartName, "bar"); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 2, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse, pathname_error_empty_input) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternPathname, "", 0); +} + +TEST(URLPattern_parse, pathname_error_trailing_backslash) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternPathname, "/foo\\", 4); +} + +TEST(URLPattern_parse, pathname_error_name_starts_with_digit) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternPathname, "/:123", 2); +} + +TEST(URLPattern_parse, pathname_error_name_starts_with_hyphen) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternPathname, "/:foo-bar", 5); +} + +TEST(URLPattern_parse, pathname_error_name_with_at_sign) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternPathname, "/:foo@bar", 5); +} + +TEST(URLPattern_parse, pathname_error_empty_name_token) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternPathname, "/:", 2); +} + +TEST(URLPattern_parse, pathname_error_empty_name_before_slash) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternPathname, "/:/foo", 2); +} + +TEST(URLPattern_parse, pathname_error_empty_name_before_asterisk) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternPathname, "/:*", 2); +} + +TEST(URLPattern_parse, pathname_name_token_valid_start_lowercase) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:foo"}; + EXPECT_EQ(pattern.pathname.value.size(), 1); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartName, "foo"); +} + +TEST(URLPattern_parse, pathname_name_token_valid_start_uppercase) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:Foo"}; + EXPECT_EQ(pattern.pathname.value.size(), 1); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartName, "Foo"); +} + +TEST(URLPattern_parse, pathname_name_token_valid_start_underscore) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:_foo"}; + EXPECT_EQ(pattern.pathname.value.size(), 1); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartName, "_foo"); +} + +TEST(URLPattern_parse, pathname_name_token_valid_start_dollar) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:$foo"}; + EXPECT_EQ(pattern.pathname.value.size(), 1); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartName, "$foo"); +} + +TEST(URLPattern_parse, pathname_simple_char_token) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo"}; + EXPECT_EQ(pattern.pathname.value.size(), 1); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); +} + +TEST(URLPattern_parse, pathname_multiple_char_tokens) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/bar/baz"}; + EXPECT_EQ(pattern.pathname.value.size(), 3); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartChar, "bar"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 2, URLPatternPartChar, "baz"); +} + +TEST(URLPattern_parse, pathname_empty_segment_double_slash) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo//bar"}; + EXPECT_EQ(pattern.pathname.value.size(), 3); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartChar, ""); + EXPECT_PART_WITH_VALUE(pattern.pathname, 2, URLPatternPartChar, "bar"); +} + +TEST(URLPattern_parse, pathname_empty_segment_trailing_slash) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartChar, ""); +} + +TEST(URLPattern_parse, pathname_single_slash) { + const sourcemeta::core::URLPattern pattern{.pathname = "/"}; + EXPECT_EQ(pattern.pathname.value.size(), 0); +} + +TEST(URLPattern_parse, pathname_asterisk_alone) { + const sourcemeta::core::URLPattern pattern{.pathname = "/*"}; + EXPECT_EQ(pattern.pathname.value.size(), 1); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 0, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse, pathname_bare_asterisk) { + const sourcemeta::core::URLPattern pattern{.pathname = "*"}; + EXPECT_EQ(pattern.pathname.value.size(), 1); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 0, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse, pathname_bare_name) { + const sourcemeta::core::URLPattern pattern{.pathname = ":path"}; + EXPECT_EQ(pattern.pathname.value.size(), 1); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartName, "path"); +} + +TEST(URLPattern_parse, pathname_bare_char) { + const sourcemeta::core::URLPattern pattern{.pathname = "foo"}; + EXPECT_EQ(pattern.pathname.value.size(), 1); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); +} + +TEST(URLPattern_parse, pathname_bare_regex) { + const sourcemeta::core::URLPattern pattern{.pathname = "(\\\\w+)"}; + EXPECT_EQ(pattern.pathname.value.size(), 1); + EXPECT_PART_WITH_REGEX(pattern.pathname, 0, URLPatternPartRegExp, "\\\\w+"); +} + +TEST(URLPattern_parse, pathname_bare_name_regex) { + const sourcemeta::core::URLPattern pattern{.pathname = ":path(\\\\d+)"}; + EXPECT_EQ(pattern.pathname.value.size(), 1); + EXPECT_PART_WITH_VALUE_AND_REGEX(pattern.pathname, 0, + URLPatternPartNameRegExp, "path", "\\\\d+"); +} + +TEST(URLPattern_parse, pathname_bare_group) { + const sourcemeta::core::URLPattern pattern{.pathname = "{foo}"}; + EXPECT_EQ(pattern.pathname.value.size(), 1); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 0, URLPatternPartGroup); +} + +TEST(URLPattern_parse, pathname_bare_asterisk_with_segment) { + const sourcemeta::core::URLPattern pattern{.pathname = "*/foo"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 0, URLPatternPartAsterisk); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartChar, "foo"); +} + +TEST(URLPattern_parse, pathname_asterisk_with_segments) { + const sourcemeta::core::URLPattern pattern{.pathname = "/*/foo"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 0, URLPatternPartAsterisk); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartChar, "foo"); +} + +TEST(URLPattern_parse, pathname_multiple_asterisks) { + const sourcemeta::core::URLPattern pattern{.pathname = "/*/*"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 0, URLPatternPartAsterisk); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 1, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse, pathname_combined_char_name_asterisk) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/:bar/*"}; + EXPECT_EQ(pattern.pathname.value.size(), 3); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartName, "bar"); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 2, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse, pathname_asterisk_optional_simple) { + const sourcemeta::core::URLPattern pattern{.pathname = "/*?"}; + EXPECT_EQ(pattern.pathname.value.size(), 1); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 0, + URLPatternPartAsteriskOptional); +} + +TEST(URLPattern_parse, pathname_asterisk_optional_with_prefix) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/*?"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 1, + URLPatternPartAsteriskOptional); +} + +TEST(URLPattern_parse, pathname_asterisk_optional_with_suffix) { + const sourcemeta::core::URLPattern pattern{.pathname = "/*?/bar"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 0, + URLPatternPartAsteriskOptional); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartChar, "bar"); +} + +TEST(URLPattern_parse, pathname_multiple_asterisk_optional) { + const sourcemeta::core::URLPattern pattern{.pathname = "/*?/*?"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 0, + URLPatternPartAsteriskOptional); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 1, + URLPatternPartAsteriskOptional); +} + +TEST(URLPattern_parse, pathname_asterisk_optional_mixed) { + const sourcemeta::core::URLPattern pattern{.pathname = + "/api/:version/*?/users"}; + EXPECT_EQ(pattern.pathname.value.size(), 4); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "api"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartName, "version"); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 2, + URLPatternPartAsteriskOptional); + EXPECT_PART_WITH_VALUE(pattern.pathname, 3, URLPatternPartChar, "users"); +} + +TEST(URLPattern_parse, pathname_asterisk_optional_complex) { + const sourcemeta::core::URLPattern pattern{.pathname = + "/foo/*?/:bar/*/baz/*?"}; + EXPECT_EQ(pattern.pathname.value.size(), 6); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 1, + URLPatternPartAsteriskOptional); + EXPECT_PART_WITH_VALUE(pattern.pathname, 2, URLPatternPartName, "bar"); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 3, URLPatternPartAsterisk); + EXPECT_PART_WITH_VALUE(pattern.pathname, 4, URLPatternPartChar, "baz"); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 5, + URLPatternPartAsteriskOptional); +} + +TEST(URLPattern_parse, pathname_asterisk_multiple_simple) { + const sourcemeta::core::URLPattern pattern{.pathname = "/*+"}; + EXPECT_EQ(pattern.pathname.value.size(), 1); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 0, + URLPatternPartAsteriskMultiple); +} + +TEST(URLPattern_parse, pathname_asterisk_multiple_with_prefix) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/*+"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 1, + URLPatternPartAsteriskMultiple); +} + +TEST(URLPattern_parse, pathname_asterisk_multiple_with_suffix) { + const sourcemeta::core::URLPattern pattern{.pathname = "/*+/bar"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 0, + URLPatternPartAsteriskMultiple); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartChar, "bar"); +} + +TEST(URLPattern_parse, pathname_multiple_asterisk_multiple) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/*+/bar/*+"}; + EXPECT_EQ(pattern.pathname.value.size(), 4); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 1, + URLPatternPartAsteriskMultiple); + EXPECT_PART_WITH_VALUE(pattern.pathname, 2, URLPatternPartChar, "bar"); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 3, + URLPatternPartAsteriskMultiple); +} + +TEST(URLPattern_parse, pathname_asterisk_multiple_mixed) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/*+/bar/:name"}; + EXPECT_EQ(pattern.pathname.value.size(), 4); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 1, + URLPatternPartAsteriskMultiple); + EXPECT_PART_WITH_VALUE(pattern.pathname, 2, URLPatternPartChar, "bar"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 3, URLPatternPartName, "name"); +} + +TEST(URLPattern_parse, pathname_asterisk_multiple_complex) { + const sourcemeta::core::URLPattern pattern{.pathname = + "/foo/*+/:bar/*/baz/*+"}; + EXPECT_EQ(pattern.pathname.value.size(), 6); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 1, + URLPatternPartAsteriskMultiple); + EXPECT_PART_WITH_VALUE(pattern.pathname, 2, URLPatternPartName, "bar"); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 3, URLPatternPartAsterisk); + EXPECT_PART_WITH_VALUE(pattern.pathname, 4, URLPatternPartChar, "baz"); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 5, + URLPatternPartAsteriskMultiple); +} + +TEST(URLPattern_parse, pathname_asterisk_asterisk_simple) { + const sourcemeta::core::URLPattern pattern{.pathname = "/**"}; + EXPECT_EQ(pattern.pathname.value.size(), 1); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 0, + URLPatternPartAsteriskAsterisk); +} + +TEST(URLPattern_parse, pathname_asterisk_asterisk_with_prefix) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/**"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 1, + URLPatternPartAsteriskAsterisk); +} + +TEST(URLPattern_parse, pathname_asterisk_asterisk_with_suffix) { + const sourcemeta::core::URLPattern pattern{.pathname = "/**/bar"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 0, + URLPatternPartAsteriskAsterisk); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartChar, "bar"); +} + +TEST(URLPattern_parse, pathname_multiple_asterisk_asterisk) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/**/bar/**"}; + EXPECT_EQ(pattern.pathname.value.size(), 4); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 1, + URLPatternPartAsteriskAsterisk); + EXPECT_PART_WITH_VALUE(pattern.pathname, 2, URLPatternPartChar, "bar"); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 3, + URLPatternPartAsteriskAsterisk); +} + +TEST(URLPattern_parse, pathname_asterisk_asterisk_mixed) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/**/bar/:name"}; + EXPECT_EQ(pattern.pathname.value.size(), 4); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 1, + URLPatternPartAsteriskAsterisk); + EXPECT_PART_WITH_VALUE(pattern.pathname, 2, URLPatternPartChar, "bar"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 3, URLPatternPartName, "name"); +} + +TEST(URLPattern_parse, pathname_asterisk_asterisk_complex) { + const sourcemeta::core::URLPattern pattern{.pathname = + "/foo/**/:bar/*/baz/**"}; + EXPECT_EQ(pattern.pathname.value.size(), 6); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 1, + URLPatternPartAsteriskAsterisk); + EXPECT_PART_WITH_VALUE(pattern.pathname, 2, URLPatternPartName, "bar"); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 3, URLPatternPartAsterisk); + EXPECT_PART_WITH_VALUE(pattern.pathname, 4, URLPatternPartChar, "baz"); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 5, + URLPatternPartAsteriskAsterisk); +} + +TEST(URLPattern_parse, pathname_escape_colon) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo\\:bar"}; + EXPECT_EQ(pattern.pathname.value.size(), 1); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo:bar"); +} + +TEST(URLPattern_parse, pathname_escape_colon_at_end) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo\\:"}; + EXPECT_EQ(pattern.pathname.value.size(), 1); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo:"); +} + +TEST(URLPattern_parse, pathname_escape_asterisk) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo\\*bar"}; + EXPECT_EQ(pattern.pathname.value.size(), 1); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo*bar"); +} + +TEST(URLPattern_parse, pathname_escape_backslash) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo\\\\bar"}; + EXPECT_EQ(pattern.pathname.value.size(), 1); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo\\bar"); +} + +TEST(URLPattern_parse, pathname_escape_slash) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo\\/bar"}; + EXPECT_EQ(pattern.pathname.value.size(), 1); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo/bar"); +} + +TEST(URLPattern_parse, pathname_multiple_escapes) { + const sourcemeta::core::URLPattern pattern{.pathname = "/\\:\\*\\\\"}; + EXPECT_EQ(pattern.pathname.value.size(), 1); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, ":*\\"); +} + +TEST(URLPattern_parse, pathname_name_token_with_digits) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:foo123"}; + EXPECT_EQ(pattern.pathname.value.size(), 1); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartName, "foo123"); +} + +TEST(URLPattern_parse, pathname_name_token_with_underscores) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:foo_bar_baz"}; + EXPECT_EQ(pattern.pathname.value.size(), 1); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartName, + "foo_bar_baz"); +} + +TEST(URLPattern_parse, pathname_name_token_with_dollar_signs) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:$foo$bar"}; + EXPECT_EQ(pattern.pathname.value.size(), 1); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartName, "$foo$bar"); +} + +TEST(URLPattern_parse, pathname_multiple_name_tokens) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:foo/:bar/:baz"}; + EXPECT_EQ(pattern.pathname.value.size(), 3); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartName, "foo"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartName, "bar"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 2, URLPatternPartName, "baz"); +} + +TEST(URLPattern_parse, pathname_complex_realistic_pattern) { + const sourcemeta::core::URLPattern pattern{.pathname = + "/api/:version/users/:id"}; + EXPECT_EQ(pattern.pathname.value.size(), 4); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "api"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartName, "version"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 2, URLPatternPartChar, "users"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 3, URLPatternPartName, "id"); +} + +TEST(URLPattern_parse, pathname_wildcard_prefix_pattern) { + const sourcemeta::core::URLPattern pattern{.pathname = "/static/*"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "static"); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 1, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse, pathname_escaped_characters_in_segment) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo\\:\\*\\\\/:bar"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo:*\\"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartName, "bar"); +} + +TEST(URLPattern_parse, pathname_regex_simple) { + const sourcemeta::core::URLPattern pattern{.pathname = "/(.*)"}; + EXPECT_EQ(pattern.pathname.value.size(), 1); + EXPECT_PART_WITH_REGEX(pattern.pathname, 0, URLPatternPartRegExp, ".*"); +} + +TEST(URLPattern_parse, pathname_regex_with_prefix) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/(.*)"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITH_REGEX(pattern.pathname, 1, URLPatternPartRegExp, ".*"); +} + +TEST(URLPattern_parse, pathname_regex_with_suffix) { + const sourcemeta::core::URLPattern pattern{.pathname = "/(.*)/bar"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_REGEX(pattern.pathname, 0, URLPatternPartRegExp, ".*"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartChar, "bar"); +} + +TEST(URLPattern_parse, pathname_multiple_regexes) { + const sourcemeta::core::URLPattern pattern{.pathname = "/(foo)/(bar)"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_REGEX(pattern.pathname, 0, URLPatternPartRegExp, "foo"); + EXPECT_PART_WITH_REGEX(pattern.pathname, 1, URLPatternPartRegExp, "bar"); +} + +TEST(URLPattern_parse, pathname_regex_complex_pattern) { + const sourcemeta::core::URLPattern pattern{.pathname = + "/foo/:bar/(.*)/*/([0-9]+)"}; + EXPECT_EQ(pattern.pathname.value.size(), 5); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartName, "bar"); + EXPECT_PART_WITH_REGEX(pattern.pathname, 2, URLPatternPartRegExp, ".*"); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 3, URLPatternPartAsterisk); + EXPECT_PART_WITH_REGEX(pattern.pathname, 4, URLPatternPartRegExp, "[0-9]+"); +} + +TEST(URLPattern_parse, pathname_regex_unclosed) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternPathname, "/foo/(.*", 8); +} + +TEST(URLPattern_parse, pathname_regex_empty) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternPathname, "/()", 2); +} + +TEST(URLPattern_parse, pathname_name_regex_simple) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:foo(.*)"}; + EXPECT_EQ(pattern.pathname.value.size(), 1); + EXPECT_PART_WITH_VALUE_AND_REGEX(pattern.pathname, 0, + URLPatternPartNameRegExp, "foo", ".*"); +} + +TEST(URLPattern_parse, pathname_name_regex_with_prefix) { + const sourcemeta::core::URLPattern pattern{.pathname = "/api/:version(\\d+)"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "api"); + EXPECT_PART_WITH_VALUE_AND_REGEX(pattern.pathname, 1, + URLPatternPartNameRegExp, "version", "\\d+"); +} + +TEST(URLPattern_parse, pathname_name_regex_with_suffix) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:id(\\d+)/profile"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE_AND_REGEX(pattern.pathname, 0, + URLPatternPartNameRegExp, "id", "\\d+"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartChar, "profile"); +} + +TEST(URLPattern_parse, pathname_multiple_name_regex) { + const sourcemeta::core::URLPattern pattern{.pathname = + "/:foo(\\w+)/:bar(\\d+)"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE_AND_REGEX(pattern.pathname, 0, + URLPatternPartNameRegExp, "foo", "\\w+"); + EXPECT_PART_WITH_VALUE_AND_REGEX(pattern.pathname, 1, + URLPatternPartNameRegExp, "bar", "\\d+"); +} + +TEST(URLPattern_parse, pathname_name_regex_complex) { + const sourcemeta::core::URLPattern pattern{ + .pathname = "/api/:version(v\\d+)/users/:id(\\d+)/*/posts/(\\w+)"}; + EXPECT_EQ(pattern.pathname.value.size(), 7); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "api"); + EXPECT_PART_WITH_VALUE_AND_REGEX( + pattern.pathname, 1, URLPatternPartNameRegExp, "version", "v\\d+"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 2, URLPatternPartChar, "users"); + EXPECT_PART_WITH_VALUE_AND_REGEX(pattern.pathname, 3, + URLPatternPartNameRegExp, "id", "\\d+"); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 4, URLPatternPartAsterisk); + EXPECT_PART_WITH_VALUE(pattern.pathname, 5, URLPatternPartChar, "posts"); + EXPECT_PART_WITH_REGEX(pattern.pathname, 6, URLPatternPartRegExp, "\\w+"); +} + +TEST(URLPattern_parse, pathname_name_regex_unclosed) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternPathname, "/:foo(.*", 8); +} + +TEST(URLPattern_parse, pathname_name_regex_empty) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternPathname, "/:foo()", 6); +} + +TEST(URLPattern_parse, pathname_name_optional_simple) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:foo?"}; + EXPECT_EQ(pattern.pathname.value.size(), 1); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartNameOptional, + "foo"); +} + +TEST(URLPattern_parse, pathname_name_optional_with_prefix) { + const sourcemeta::core::URLPattern pattern{.pathname = "/api/:version?"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "api"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartNameOptional, + "version"); +} + +TEST(URLPattern_parse, pathname_name_optional_with_suffix) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:id?/profile"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartNameOptional, "id"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartChar, "profile"); +} + +TEST(URLPattern_parse, pathname_multiple_name_optional) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:foo?/:bar?"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartNameOptional, + "foo"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartNameOptional, + "bar"); +} + +TEST(URLPattern_parse, pathname_name_optional_mixed_with_required) { + const sourcemeta::core::URLPattern pattern{.pathname = + "/:required/:optional?"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartName, "required"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartNameOptional, + "optional"); +} + +TEST(URLPattern_parse, pathname_name_optional_complex) { + const sourcemeta::core::URLPattern pattern{ + .pathname = "/api/:version?/users/:id/:action?"}; + EXPECT_EQ(pattern.pathname.value.size(), 5); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "api"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartNameOptional, + "version"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 2, URLPatternPartChar, "users"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 3, URLPatternPartName, "id"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 4, URLPatternPartNameOptional, + "action"); +} + +TEST(URLPattern_parse, pathname_name_multiple_simple) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:foo+"}; + EXPECT_EQ(pattern.pathname.value.size(), 1); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartNameMultiple, + "foo"); +} + +TEST(URLPattern_parse, pathname_name_multiple_with_prefix) { + const sourcemeta::core::URLPattern pattern{.pathname = "/api/:version+"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "api"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartNameMultiple, + "version"); +} + +TEST(URLPattern_parse, pathname_name_multiple_with_suffix) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:id+/profile"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartNameMultiple, "id"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartChar, "profile"); +} + +TEST(URLPattern_parse, pathname_multiple_name_multiple) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:foo+/:bar+"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartNameMultiple, + "foo"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartNameMultiple, + "bar"); +} + +TEST(URLPattern_parse, pathname_name_multiple_mixed_with_required) { + const sourcemeta::core::URLPattern pattern{.pathname = + "/:required/:multiple+"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartName, "required"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartNameMultiple, + "multiple"); +} + +TEST(URLPattern_parse, pathname_name_multiple_complex) { + const sourcemeta::core::URLPattern pattern{.pathname = + "/api/:version/users/:path+"}; + EXPECT_EQ(pattern.pathname.value.size(), 4); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "api"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartName, "version"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 2, URLPatternPartChar, "users"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 3, URLPatternPartNameMultiple, + "path"); +} + +TEST(URLPattern_parse, pathname_name_asterisk_simple) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:foo*"}; + EXPECT_EQ(pattern.pathname.value.size(), 1); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartNameAsterisk, + "foo"); +} + +TEST(URLPattern_parse, pathname_name_asterisk_with_prefix) { + const sourcemeta::core::URLPattern pattern{.pathname = "/api/:version*"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "api"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartNameAsterisk, + "version"); +} + +TEST(URLPattern_parse, pathname_name_asterisk_with_suffix) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:id*/profile"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartNameAsterisk, "id"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartChar, "profile"); +} + +TEST(URLPattern_parse, pathname_multiple_name_asterisk) { + const sourcemeta::core::URLPattern pattern{.pathname = "/:foo*/:bar*"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartNameAsterisk, + "foo"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartNameAsterisk, + "bar"); +} + +TEST(URLPattern_parse, pathname_name_asterisk_mixed_with_required) { + const sourcemeta::core::URLPattern pattern{.pathname = + "/:required/:asterisk*"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartName, "required"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartNameAsterisk, + "asterisk"); +} + +TEST(URLPattern_parse, pathname_name_asterisk_complex) { + const sourcemeta::core::URLPattern pattern{.pathname = + "/api/:version/files/:path*"}; + EXPECT_EQ(pattern.pathname.value.size(), 4); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "api"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartName, "version"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 2, URLPatternPartChar, "files"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 3, URLPatternPartNameAsterisk, + "path"); +} + +TEST(URLPattern_parse, pathname_regex_optional_simple) { + const sourcemeta::core::URLPattern pattern{.pathname = "/(.*)?"}; + EXPECT_EQ(pattern.pathname.value.size(), 1); + EXPECT_PART_WITH_REGEX(pattern.pathname, 0, URLPatternPartRegExpOptional, + ".*"); +} + +TEST(URLPattern_parse, pathname_regex_optional_with_prefix) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/(.*)?"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITH_REGEX(pattern.pathname, 1, URLPatternPartRegExpOptional, + ".*"); +} + +TEST(URLPattern_parse, pathname_regex_optional_with_suffix) { + const sourcemeta::core::URLPattern pattern{.pathname = "/(.*)?/bar"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_REGEX(pattern.pathname, 0, URLPatternPartRegExpOptional, + ".*"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartChar, "bar"); +} + +TEST(URLPattern_parse, pathname_multiple_regex_optional) { + const sourcemeta::core::URLPattern pattern{.pathname = "/(foo)?/(bar)?"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_REGEX(pattern.pathname, 0, URLPatternPartRegExpOptional, + "foo"); + EXPECT_PART_WITH_REGEX(pattern.pathname, 1, URLPatternPartRegExpOptional, + "bar"); +} + +TEST(URLPattern_parse, pathname_regex_optional_mixed_with_required) { + const sourcemeta::core::URLPattern pattern{.pathname = "/(\\d+)/(.*)?/:name"}; + EXPECT_EQ(pattern.pathname.value.size(), 3); + EXPECT_PART_WITH_REGEX(pattern.pathname, 0, URLPatternPartRegExp, "\\d+"); + EXPECT_PART_WITH_REGEX(pattern.pathname, 1, URLPatternPartRegExpOptional, + ".*"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 2, URLPatternPartName, "name"); +} + +TEST(URLPattern_parse, pathname_regex_optional_complex) { + const sourcemeta::core::URLPattern pattern{.pathname = + "/api/:version/(\\d+)?/users"}; + EXPECT_EQ(pattern.pathname.value.size(), 4); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "api"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartName, "version"); + EXPECT_PART_WITH_REGEX(pattern.pathname, 2, URLPatternPartRegExpOptional, + "\\d+"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 3, URLPatternPartChar, "users"); +} + +TEST(URLPattern_parse, pathname_regex_multiple_simple) { + const sourcemeta::core::URLPattern pattern{.pathname = "/(.*)+"}; + EXPECT_EQ(pattern.pathname.value.size(), 1); + EXPECT_PART_WITH_REGEX(pattern.pathname, 0, URLPatternPartRegExpMultiple, + ".*"); +} + +TEST(URLPattern_parse, pathname_regex_multiple_with_prefix) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/(.*)+"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITH_REGEX(pattern.pathname, 1, URLPatternPartRegExpMultiple, + ".*"); +} + +TEST(URLPattern_parse, pathname_regex_multiple_with_suffix) { + const sourcemeta::core::URLPattern pattern{.pathname = "/(.* )+/bar"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_REGEX(pattern.pathname, 0, URLPatternPartRegExpMultiple, + ".* "); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartChar, "bar"); +} + +TEST(URLPattern_parse, pathname_multiple_regex_multiple) { + const sourcemeta::core::URLPattern pattern{.pathname = "/(foo)+/(bar)+"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_REGEX(pattern.pathname, 0, URLPatternPartRegExpMultiple, + "foo"); + EXPECT_PART_WITH_REGEX(pattern.pathname, 1, URLPatternPartRegExpMultiple, + "bar"); +} + +TEST(URLPattern_parse, pathname_regex_multiple_mixed) { + const sourcemeta::core::URLPattern pattern{.pathname = + "/foo/(.* )+/bar/:name"}; + EXPECT_EQ(pattern.pathname.value.size(), 4); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITH_REGEX(pattern.pathname, 1, URLPatternPartRegExpMultiple, + ".* "); + EXPECT_PART_WITH_VALUE(pattern.pathname, 2, URLPatternPartChar, "bar"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 3, URLPatternPartName, "name"); +} + +TEST(URLPattern_parse, pathname_regex_multiple_complex) { + const sourcemeta::core::URLPattern pattern{.pathname = + "/api/:version/(\\d+)+/users"}; + EXPECT_EQ(pattern.pathname.value.size(), 4); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "api"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartName, "version"); + EXPECT_PART_WITH_REGEX(pattern.pathname, 2, URLPatternPartRegExpMultiple, + "\\d+"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 3, URLPatternPartChar, "users"); +} + +TEST(URLPattern_parse, pathname_regex_asterisk_simple) { + const sourcemeta::core::URLPattern pattern{.pathname = "/(.*)*"}; + EXPECT_EQ(pattern.pathname.value.size(), 1); + EXPECT_PART_WITH_REGEX(pattern.pathname, 0, URLPatternPartRegExpAsterisk, + ".*"); +} + +TEST(URLPattern_parse, pathname_regex_asterisk_with_prefix) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/(.*)*"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITH_REGEX(pattern.pathname, 1, URLPatternPartRegExpAsterisk, + ".*"); +} + +TEST(URLPattern_parse, pathname_regex_asterisk_with_suffix) { + const sourcemeta::core::URLPattern pattern{.pathname = "/(.* )*/bar"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_REGEX(pattern.pathname, 0, URLPatternPartRegExpAsterisk, + ".* "); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartChar, "bar"); +} + +TEST(URLPattern_parse, pathname_multiple_regex_asterisk) { + const sourcemeta::core::URLPattern pattern{.pathname = "/(foo)*/(bar)*"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_REGEX(pattern.pathname, 0, URLPatternPartRegExpAsterisk, + "foo"); + EXPECT_PART_WITH_REGEX(pattern.pathname, 1, URLPatternPartRegExpAsterisk, + "bar"); +} + +TEST(URLPattern_parse, pathname_regex_asterisk_mixed) { + const sourcemeta::core::URLPattern pattern{.pathname = + "/foo/(.* )*/bar/:name"}; + EXPECT_EQ(pattern.pathname.value.size(), 4); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITH_REGEX(pattern.pathname, 1, URLPatternPartRegExpAsterisk, + ".* "); + EXPECT_PART_WITH_VALUE(pattern.pathname, 2, URLPatternPartChar, "bar"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 3, URLPatternPartName, "name"); +} + +TEST(URLPattern_parse, pathname_regex_asterisk_complex) { + const sourcemeta::core::URLPattern pattern{.pathname = + "/api/:version/(\\d+)*/users"}; + EXPECT_EQ(pattern.pathname.value.size(), 4); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "api"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartName, "version"); + EXPECT_PART_WITH_REGEX(pattern.pathname, 2, URLPatternPartRegExpAsterisk, + "\\d+"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 3, URLPatternPartChar, "users"); +} + +TEST(URLPattern_parse, pathname_group_simple_char) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo{/bar}"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_GROUP(pattern.pathname, 1, URLPatternPartGroup, 1, true, false); + EXPECT_PART_NESTED_WITH_VALUE(pattern.pathname, 1, URLPatternPartGroup, 0, + URLPatternPartChar, "bar"); +} + +TEST(URLPattern_parse, pathname_group_multiple_tokens) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo{/bar/:baz}"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_GROUP(pattern.pathname, 1, URLPatternPartGroup, 2, true, false); + EXPECT_PART_NESTED_WITH_VALUE(pattern.pathname, 1, URLPatternPartGroup, 0, + URLPatternPartChar, "bar"); + EXPECT_PART_NESTED_WITH_VALUE(pattern.pathname, 1, URLPatternPartGroup, 1, + URLPatternPartName, "baz"); +} + +TEST(URLPattern_parse, pathname_group_with_asterisk) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo{/*}"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_GROUP(pattern.pathname, 1, URLPatternPartGroup, 1, true, false); + EXPECT_PART_NESTED_WITHOUT_VALUE(pattern.pathname, 1, URLPatternPartGroup, 0, + URLPatternPartAsterisk); +} + +TEST(URLPattern_parse, pathname_group_optional_simple) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo{/bar}?"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_GROUP(pattern.pathname, 1, URLPatternPartGroupOptional, 1, true, + false); + EXPECT_PART_NESTED_WITH_VALUE(pattern.pathname, 1, + URLPatternPartGroupOptional, 0, + URLPatternPartChar, "bar"); +} + +TEST(URLPattern_parse, pathname_group_optional_multiple_tokens) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo{/bar/:baz}?"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_GROUP(pattern.pathname, 1, URLPatternPartGroupOptional, 2, true, + false); + EXPECT_PART_NESTED_WITH_VALUE(pattern.pathname, 1, + URLPatternPartGroupOptional, 0, + URLPatternPartChar, "bar"); + EXPECT_PART_NESTED_WITH_VALUE(pattern.pathname, 1, + URLPatternPartGroupOptional, 1, + URLPatternPartName, "baz"); +} + +TEST(URLPattern_parse, pathname_group_multiple_simple) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo{/bar}+"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_GROUP(pattern.pathname, 1, URLPatternPartGroupMultiple, 1, true, + false); + EXPECT_PART_NESTED_WITH_VALUE(pattern.pathname, 1, + URLPatternPartGroupMultiple, 0, + URLPatternPartChar, "bar"); +} + +TEST(URLPattern_parse, pathname_group_multiple_multiple_tokens) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo{/bar/:baz}+"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_GROUP(pattern.pathname, 1, URLPatternPartGroupMultiple, 2, true, + false); + EXPECT_PART_NESTED_WITH_VALUE(pattern.pathname, 1, + URLPatternPartGroupMultiple, 0, + URLPatternPartChar, "bar"); + EXPECT_PART_NESTED_WITH_VALUE(pattern.pathname, 1, + URLPatternPartGroupMultiple, 1, + URLPatternPartName, "baz"); +} + +TEST(URLPattern_parse, pathname_group_asterisk_simple) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo{/bar}*"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_GROUP(pattern.pathname, 1, URLPatternPartGroupAsterisk, 1, true, + false); + EXPECT_PART_NESTED_WITH_VALUE(pattern.pathname, 1, + URLPatternPartGroupAsterisk, 0, + URLPatternPartChar, "bar"); +} + +TEST(URLPattern_parse, pathname_group_asterisk_multiple_tokens) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo{/bar/:baz}*"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_GROUP(pattern.pathname, 1, URLPatternPartGroupAsterisk, 2, true, + false); + EXPECT_PART_NESTED_WITH_VALUE(pattern.pathname, 1, + URLPatternPartGroupAsterisk, 0, + URLPatternPartChar, "bar"); + EXPECT_PART_NESTED_WITH_VALUE(pattern.pathname, 1, + URLPatternPartGroupAsterisk, 1, + URLPatternPartName, "baz"); +} + +TEST(URLPattern_parse, pathname_group_optional_no_inner_prefix) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo/{:bar}?"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_GROUP(pattern.pathname, 1, URLPatternPartGroupOptional, 1, false, + false); + EXPECT_PART_NESTED_WITH_VALUE(pattern.pathname, 1, + URLPatternPartGroupOptional, 0, + URLPatternPartName, "bar"); +} + +TEST(URLPattern_parse, pathname_group_optional_with_inner_suffix) { + const sourcemeta::core::URLPattern pattern{.pathname = "foo{/:bar/}?"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_GROUP(pattern.pathname, 1, URLPatternPartGroupOptional, 2, true, + true); + EXPECT_PART_NESTED_WITH_VALUE(pattern.pathname, 1, + URLPatternPartGroupOptional, 0, + URLPatternPartName, "bar"); + EXPECT_PART_NESTED_WITH_VALUE(pattern.pathname, 1, + URLPatternPartGroupOptional, 1, + URLPatternPartChar, ""); +} + +TEST(URLPattern_parse, pathname_complex_segment_char_name_char) { + const sourcemeta::core::URLPattern pattern{.pathname = "/file-:name.json"}; + EXPECT_EQ(pattern.pathname.value.size(), 1); + EXPECT_PART_COMPLEX_SIZE(pattern.pathname, 0, 3); + EXPECT_PART_COMPLEX_WITH_VALUE(pattern.pathname, 0, 0, URLPatternPartChar, + "file-"); + EXPECT_PART_COMPLEX_WITH_VALUE(pattern.pathname, 0, 1, URLPatternPartName, + "name"); + EXPECT_PART_COMPLEX_WITH_VALUE(pattern.pathname, 0, 2, URLPatternPartChar, + ".json"); +} + +TEST(URLPattern_parse, pathname_complex_segment_multiple_names) { + const sourcemeta::core::URLPattern pattern{.pathname = + "/v:major.:minor/:resource"}; + EXPECT_EQ(pattern.pathname.value.size(), 2); + EXPECT_PART_COMPLEX_SIZE(pattern.pathname, 0, 4); + EXPECT_PART_COMPLEX_WITH_VALUE(pattern.pathname, 0, 0, URLPatternPartChar, + "v"); + EXPECT_PART_COMPLEX_WITH_VALUE(pattern.pathname, 0, 1, URLPatternPartName, + "major"); + EXPECT_PART_COMPLEX_WITH_VALUE(pattern.pathname, 0, 2, URLPatternPartChar, + "."); + EXPECT_PART_COMPLEX_WITH_VALUE(pattern.pathname, 0, 3, URLPatternPartName, + "minor"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartName, "resource"); +} + +TEST(URLPattern_parse, pathname_complex_segment_with_group) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo-:bar{baz}+"}; + EXPECT_EQ(pattern.pathname.value.size(), 1); + EXPECT_PART_COMPLEX_SIZE(pattern.pathname, 0, 3); + EXPECT_PART_COMPLEX_WITH_VALUE(pattern.pathname, 0, 0, URLPatternPartChar, + "foo-"); + EXPECT_PART_COMPLEX_WITH_VALUE(pattern.pathname, 0, 1, URLPatternPartName, + "bar"); + EXPECT_PART_COMPLEX_NESTED_SIZE(pattern.pathname, 0, 2, + URLPatternPartGroupMultiple, 1); + EXPECT_PART_COMPLEX_NESTED_WITH_VALUE(pattern.pathname, 0, 2, + URLPatternPartGroupMultiple, 0, + URLPatternPartChar, "baz"); +} + +TEST(URLPattern_parse, pathname_complex_segment_group_first) { + const sourcemeta::core::URLPattern pattern{.pathname = "/foo{:bar}?baz"}; + EXPECT_EQ(pattern.pathname.value.size(), 1); + EXPECT_PART_COMPLEX_SIZE(pattern.pathname, 0, 3); + EXPECT_PART_COMPLEX_WITH_VALUE(pattern.pathname, 0, 0, URLPatternPartChar, + "foo"); + EXPECT_PART_COMPLEX_NESTED_SIZE(pattern.pathname, 0, 1, + URLPatternPartGroupOptional, 1); + EXPECT_PART_COMPLEX_NESTED_WITH_VALUE(pattern.pathname, 0, 1, + URLPatternPartGroupOptional, 0, + URLPatternPartName, "bar"); + EXPECT_PART_COMPLEX_WITH_VALUE(pattern.pathname, 0, 2, URLPatternPartChar, + "baz"); +} diff --git a/test/urlpattern/urlpattern_parse_port_test.cc b/test/urlpattern/urlpattern_parse_port_test.cc new file mode 100644 index 000000000..7e8f9ec61 --- /dev/null +++ b/test/urlpattern/urlpattern_parse_port_test.cc @@ -0,0 +1,163 @@ +#include + +#include + +#include "urlpattern_test_utils.h" + +TEST(URLPattern_parse, port_copy_constructor) { + const sourcemeta::core::URLPatternPort original{"8080"}; + const sourcemeta::core::URLPatternPort copy{original}; + EXPECT_SINGLE_PART_WITH_VALUE(copy, URLPatternPartChar, "8080"); +} + +TEST(URLPattern_parse, port_move_constructor) { + sourcemeta::core::URLPatternPort original{"8080"}; + const sourcemeta::core::URLPatternPort moved{std::move(original)}; + EXPECT_SINGLE_PART_WITH_VALUE(moved, URLPatternPartChar, "8080"); +} + +TEST(URLPattern_parse, port_copy_assignment) { + sourcemeta::core::URLPattern pattern; + const sourcemeta::core::URLPatternPort original{"8080"}; + pattern.port = original; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.port, URLPatternPartChar, "8080"); +} + +TEST(URLPattern_parse, port_move_assignment) { + sourcemeta::core::URLPattern pattern; + sourcemeta::core::URLPatternPort original{"8080"}; + pattern.port = std::move(original); + EXPECT_SINGLE_PART_WITH_VALUE(pattern.port, URLPatternPartChar, "8080"); +} + +TEST(URLPattern_parse, port_default_constructor) { + const sourcemeta::core::URLPatternPort port; + EXPECT_SINGLE_PART_WITHOUT_VALUE(port, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse, port_simple_char_token) { + const sourcemeta::core::URLPattern pattern{.port = "8080"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.port, URLPatternPartChar, "8080"); +} + +TEST(URLPattern_parse, port_80) { + const sourcemeta::core::URLPattern pattern{.port = "80"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.port, URLPatternPartChar, "80"); +} + +TEST(URLPattern_parse, port_443) { + const sourcemeta::core::URLPattern pattern{.port = "443"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.port, URLPatternPartChar, "443"); +} + +TEST(URLPattern_parse, port_3000) { + const sourcemeta::core::URLPattern pattern{.port = "3000"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.port, URLPatternPartChar, "3000"); +} + +TEST(URLPattern_parse, port_name_token) { + const sourcemeta::core::URLPattern pattern{.port = ":port"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.port, URLPatternPartName, "port"); +} + +TEST(URLPattern_parse, port_name_optional) { + const sourcemeta::core::URLPattern pattern{.port = ":port?"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.port, URLPatternPartNameOptional, + "port"); +} + +TEST(URLPattern_parse, port_name_multiple) { + const sourcemeta::core::URLPattern pattern{.port = ":port+"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.port, URLPatternPartNameMultiple, + "port"); +} + +TEST(URLPattern_parse, port_name_asterisk) { + const sourcemeta::core::URLPattern pattern{.port = ":port*"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.port, URLPatternPartNameAsterisk, + "port"); +} + +TEST(URLPattern_parse, port_name_regex) { + const sourcemeta::core::URLPattern pattern{.port = ":port(\\d+)"}; + EXPECT_SINGLE_PART_WITH_VALUE_AND_REGEX( + pattern.port, URLPatternPartNameRegExp, "port", "\\d+"); +} + +TEST(URLPattern_parse, port_regex) { + const sourcemeta::core::URLPattern pattern{.port = "(\\d+)"}; + EXPECT_SINGLE_PART_WITH_REGEX(pattern.port, URLPatternPartRegExp, "\\d+"); +} + +TEST(URLPattern_parse, port_regex_optional) { + const sourcemeta::core::URLPattern pattern{.port = "(\\d+)?"}; + EXPECT_SINGLE_PART_WITH_REGEX(pattern.port, URLPatternPartRegExpOptional, + "\\d+"); +} + +TEST(URLPattern_parse, port_regex_multiple) { + const sourcemeta::core::URLPattern pattern{.port = "(\\d+)+"}; + EXPECT_SINGLE_PART_WITH_REGEX(pattern.port, URLPatternPartRegExpMultiple, + "\\d+"); +} + +TEST(URLPattern_parse, port_regex_asterisk) { + const sourcemeta::core::URLPattern pattern{.port = "(\\d+)*"}; + EXPECT_SINGLE_PART_WITH_REGEX(pattern.port, URLPatternPartRegExpAsterisk, + "\\d+"); +} + +TEST(URLPattern_parse, port_asterisk) { + const sourcemeta::core::URLPattern pattern{.port = "*"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse, port_asterisk_optional) { + const sourcemeta::core::URLPattern pattern{.port = "*?"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, + URLPatternPartAsteriskOptional); +} + +TEST(URLPattern_parse, port_asterisk_multiple) { + const sourcemeta::core::URLPattern pattern{.port = "*+"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, + URLPatternPartAsteriskMultiple); +} + +TEST(URLPattern_parse, port_asterisk_asterisk) { + const sourcemeta::core::URLPattern pattern{.port = "**"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, + URLPatternPartAsteriskAsterisk); +} + +TEST(URLPattern_parse, port_group_simple) { + const sourcemeta::core::URLPattern pattern{.port = "{8080}"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, URLPatternPartGroup); +} + +TEST(URLPattern_parse, port_group_optional) { + const sourcemeta::core::URLPattern pattern{.port = "{8080}?"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, URLPatternPartGroupOptional); +} + +TEST(URLPattern_parse, port_group_multiple) { + const sourcemeta::core::URLPattern pattern{.port = "{8080}+"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, URLPatternPartGroupMultiple); +} + +TEST(URLPattern_parse, port_group_asterisk) { + const sourcemeta::core::URLPattern pattern{.port = "{8080}*"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, URLPatternPartGroupAsterisk); +} + +TEST(URLPattern_parse, port_error_unclosed_group) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternPort, "{8080", 5); +} + +TEST(URLPattern_parse, port_error_unclosed_regex) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternPort, "(\\d+", 4); +} + +TEST(URLPattern_parse, port_error_invalid_modifier) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternPort, ":port%", 5); +} diff --git a/test/urlpattern/urlpattern_parse_protocol_test.cc b/test/urlpattern/urlpattern_parse_protocol_test.cc new file mode 100644 index 000000000..5bbe45a83 --- /dev/null +++ b/test/urlpattern/urlpattern_parse_protocol_test.cc @@ -0,0 +1,163 @@ +#include + +#include + +#include "urlpattern_test_utils.h" + +TEST(URLPattern_parse, protocol_copy_constructor) { + const sourcemeta::core::URLPatternProtocol original{"https"}; + const sourcemeta::core::URLPatternProtocol copy{original}; + EXPECT_SINGLE_PART_WITH_VALUE(copy, URLPatternPartChar, "https"); +} + +TEST(URLPattern_parse, protocol_move_constructor) { + sourcemeta::core::URLPatternProtocol original{"https"}; + const sourcemeta::core::URLPatternProtocol moved{std::move(original)}; + EXPECT_SINGLE_PART_WITH_VALUE(moved, URLPatternPartChar, "https"); +} + +TEST(URLPattern_parse, protocol_copy_assignment) { + sourcemeta::core::URLPattern pattern; + const sourcemeta::core::URLPatternProtocol original{"https"}; + pattern.protocol = original; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.protocol, URLPatternPartChar, "https"); +} + +TEST(URLPattern_parse, protocol_move_assignment) { + sourcemeta::core::URLPattern pattern; + sourcemeta::core::URLPatternProtocol original{"https"}; + pattern.protocol = std::move(original); + EXPECT_SINGLE_PART_WITH_VALUE(pattern.protocol, URLPatternPartChar, "https"); +} + +TEST(URLPattern_parse, protocol_default_constructor) { + const sourcemeta::core::URLPatternProtocol protocol; + EXPECT_SINGLE_PART_WITHOUT_VALUE(protocol, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse, protocol_simple_char_token) { + const sourcemeta::core::URLPattern pattern{.protocol = "https"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.protocol, URLPatternPartChar, "https"); +} + +TEST(URLPattern_parse, protocol_http) { + const sourcemeta::core::URLPattern pattern{.protocol = "http"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.protocol, URLPatternPartChar, "http"); +} + +TEST(URLPattern_parse, protocol_ftp) { + const sourcemeta::core::URLPattern pattern{.protocol = "ftp"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.protocol, URLPatternPartChar, "ftp"); +} + +TEST(URLPattern_parse, protocol_name_token) { + const sourcemeta::core::URLPattern pattern{.protocol = ":protocol"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.protocol, URLPatternPartName, + "protocol"); +} + +TEST(URLPattern_parse, protocol_name_optional) { + const sourcemeta::core::URLPattern pattern{.protocol = ":protocol?"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.protocol, URLPatternPartNameOptional, + "protocol"); +} + +TEST(URLPattern_parse, protocol_name_multiple) { + const sourcemeta::core::URLPattern pattern{.protocol = ":protocol+"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.protocol, URLPatternPartNameMultiple, + "protocol"); +} + +TEST(URLPattern_parse, protocol_name_asterisk) { + const sourcemeta::core::URLPattern pattern{.protocol = ":protocol*"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.protocol, URLPatternPartNameAsterisk, + "protocol"); +} + +TEST(URLPattern_parse, protocol_name_regex) { + const sourcemeta::core::URLPattern pattern{.protocol = ":proto(https?)"}; + EXPECT_SINGLE_PART_WITH_VALUE_AND_REGEX( + pattern.protocol, URLPatternPartNameRegExp, "proto", "https?"); +} + +TEST(URLPattern_parse, protocol_regex) { + const sourcemeta::core::URLPattern pattern{.protocol = "(https?)"}; + EXPECT_SINGLE_PART_WITH_REGEX(pattern.protocol, URLPatternPartRegExp, + "https?"); +} + +TEST(URLPattern_parse, protocol_regex_optional) { + const sourcemeta::core::URLPattern pattern{.protocol = "(https?)?"}; + EXPECT_SINGLE_PART_WITH_REGEX(pattern.protocol, URLPatternPartRegExpOptional, + "https?"); +} + +TEST(URLPattern_parse, protocol_regex_multiple) { + const sourcemeta::core::URLPattern pattern{.protocol = "(https?)+"}; + EXPECT_SINGLE_PART_WITH_REGEX(pattern.protocol, URLPatternPartRegExpMultiple, + "https?"); +} + +TEST(URLPattern_parse, protocol_regex_asterisk) { + const sourcemeta::core::URLPattern pattern{.protocol = "(https?)*"}; + EXPECT_SINGLE_PART_WITH_REGEX(pattern.protocol, URLPatternPartRegExpAsterisk, + "https?"); +} + +TEST(URLPattern_parse, protocol_asterisk) { + const sourcemeta::core::URLPattern pattern{.protocol = "*"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.protocol, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse, protocol_asterisk_optional) { + const sourcemeta::core::URLPattern pattern{.protocol = "*?"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.protocol, + URLPatternPartAsteriskOptional); +} + +TEST(URLPattern_parse, protocol_asterisk_multiple) { + const sourcemeta::core::URLPattern pattern{.protocol = "*+"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.protocol, + URLPatternPartAsteriskMultiple); +} + +TEST(URLPattern_parse, protocol_asterisk_asterisk) { + const sourcemeta::core::URLPattern pattern{.protocol = "**"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.protocol, + URLPatternPartAsteriskAsterisk); +} + +TEST(URLPattern_parse, protocol_group_simple) { + const sourcemeta::core::URLPattern pattern{.protocol = "{https}"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.protocol, URLPatternPartGroup); +} + +TEST(URLPattern_parse, protocol_group_optional) { + const sourcemeta::core::URLPattern pattern{.protocol = "{https}?"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.protocol, + URLPatternPartGroupOptional); +} + +TEST(URLPattern_parse, protocol_group_multiple) { + const sourcemeta::core::URLPattern pattern{.protocol = "{https}+"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.protocol, + URLPatternPartGroupMultiple); +} + +TEST(URLPattern_parse, protocol_group_asterisk) { + const sourcemeta::core::URLPattern pattern{.protocol = "{https}*"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.protocol, + URLPatternPartGroupAsterisk); +} + +TEST(URLPattern_parse, protocol_error_unclosed_group) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternProtocol, "{http", 5); +} + +TEST(URLPattern_parse, protocol_error_unclosed_regex) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternProtocol, "(https?", 7); +} + +TEST(URLPattern_parse, protocol_error_invalid_modifier) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternProtocol, ":proto%", 6); +} diff --git a/test/urlpattern/urlpattern_parse_search_test.cc b/test/urlpattern/urlpattern_parse_search_test.cc new file mode 100644 index 000000000..6e82d2825 --- /dev/null +++ b/test/urlpattern/urlpattern_parse_search_test.cc @@ -0,0 +1,159 @@ +#include + +#include + +#include "urlpattern_test_utils.h" + +TEST(URLPattern_parse, search_copy_constructor) { + const sourcemeta::core::URLPatternSearch original{"q=test"}; + const sourcemeta::core::URLPatternSearch copy{original}; + EXPECT_SINGLE_PART_WITH_VALUE(copy, URLPatternPartChar, "q=test"); +} + +TEST(URLPattern_parse, search_move_constructor) { + sourcemeta::core::URLPatternSearch original{"q=test"}; + const sourcemeta::core::URLPatternSearch moved{std::move(original)}; + EXPECT_SINGLE_PART_WITH_VALUE(moved, URLPatternPartChar, "q=test"); +} + +TEST(URLPattern_parse, search_copy_assignment) { + sourcemeta::core::URLPattern pattern; + const sourcemeta::core::URLPatternSearch original{"q=test"}; + pattern.search = original; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.search, URLPatternPartChar, "q=test"); +} + +TEST(URLPattern_parse, search_move_assignment) { + sourcemeta::core::URLPattern pattern; + sourcemeta::core::URLPatternSearch original{"q=test"}; + pattern.search = std::move(original); + EXPECT_SINGLE_PART_WITH_VALUE(pattern.search, URLPatternPartChar, "q=test"); +} + +TEST(URLPattern_parse, search_default_constructor) { + const sourcemeta::core::URLPatternSearch search; + EXPECT_SINGLE_PART_WITHOUT_VALUE(search, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse, search_simple_char_token) { + const sourcemeta::core::URLPattern pattern{.search = "q=test"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.search, URLPatternPartChar, "q=test"); +} + +TEST(URLPattern_parse, search_query_params) { + const sourcemeta::core::URLPattern pattern{.search = "q=search&lang=en"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.search, URLPatternPartChar, + "q=search&lang=en"); +} + +TEST(URLPattern_parse, search_single_param) { + const sourcemeta::core::URLPattern pattern{.search = "page=1"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.search, URLPatternPartChar, "page=1"); +} + +TEST(URLPattern_parse, search_name_token) { + const sourcemeta::core::URLPattern pattern{.search = ":query"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.search, URLPatternPartName, "query"); +} + +TEST(URLPattern_parse, search_name_optional) { + const sourcemeta::core::URLPattern pattern{.search = ":query?"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.search, URLPatternPartNameOptional, + "query"); +} + +TEST(URLPattern_parse, search_name_multiple) { + const sourcemeta::core::URLPattern pattern{.search = ":query+"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.search, URLPatternPartNameMultiple, + "query"); +} + +TEST(URLPattern_parse, search_name_asterisk) { + const sourcemeta::core::URLPattern pattern{.search = ":query*"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.search, URLPatternPartNameAsterisk, + "query"); +} + +TEST(URLPattern_parse, search_name_regex) { + const sourcemeta::core::URLPattern pattern{.search = ":q([^&]+)"}; + EXPECT_SINGLE_PART_WITH_VALUE_AND_REGEX( + pattern.search, URLPatternPartNameRegExp, "q", "[^&]+"); +} + +TEST(URLPattern_parse, search_regex) { + const sourcemeta::core::URLPattern pattern{.search = "([^&]+)"}; + EXPECT_SINGLE_PART_WITH_REGEX(pattern.search, URLPatternPartRegExp, "[^&]+"); +} + +TEST(URLPattern_parse, search_regex_optional) { + const sourcemeta::core::URLPattern pattern{.search = "([^&]+)?"}; + EXPECT_SINGLE_PART_WITH_REGEX(pattern.search, URLPatternPartRegExpOptional, + "[^&]+"); +} + +TEST(URLPattern_parse, search_regex_multiple) { + const sourcemeta::core::URLPattern pattern{.search = "([^&]+)+"}; + EXPECT_SINGLE_PART_WITH_REGEX(pattern.search, URLPatternPartRegExpMultiple, + "[^&]+"); +} + +TEST(URLPattern_parse, search_regex_asterisk) { + const sourcemeta::core::URLPattern pattern{.search = "([^&]+)*"}; + EXPECT_SINGLE_PART_WITH_REGEX(pattern.search, URLPatternPartRegExpAsterisk, + "[^&]+"); +} + +TEST(URLPattern_parse, search_asterisk) { + const sourcemeta::core::URLPattern pattern{.search = "*"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.search, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse, search_asterisk_optional) { + const sourcemeta::core::URLPattern pattern{.search = "*?"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.search, + URLPatternPartAsteriskOptional); +} + +TEST(URLPattern_parse, search_asterisk_multiple) { + const sourcemeta::core::URLPattern pattern{.search = "*+"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.search, + URLPatternPartAsteriskMultiple); +} + +TEST(URLPattern_parse, search_asterisk_asterisk) { + const sourcemeta::core::URLPattern pattern{.search = "**"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.search, + URLPatternPartAsteriskAsterisk); +} + +TEST(URLPattern_parse, search_group_simple) { + const sourcemeta::core::URLPattern pattern{.search = "{q=test}"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.search, URLPatternPartGroup); +} + +TEST(URLPattern_parse, search_group_optional) { + const sourcemeta::core::URLPattern pattern{.search = "{q=test}?"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.search, URLPatternPartGroupOptional); +} + +TEST(URLPattern_parse, search_group_multiple) { + const sourcemeta::core::URLPattern pattern{.search = "{q=test}+"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.search, URLPatternPartGroupMultiple); +} + +TEST(URLPattern_parse, search_group_asterisk) { + const sourcemeta::core::URLPattern pattern{.search = "{q=test}*"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.search, URLPatternPartGroupAsterisk); +} + +TEST(URLPattern_parse, search_error_unclosed_group) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternSearch, "{q=test", 7); +} + +TEST(URLPattern_parse, search_error_unclosed_regex) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternSearch, "([^&]+", 6); +} + +TEST(URLPattern_parse, search_error_invalid_modifier) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternSearch, ":query%", 6); +} diff --git a/test/urlpattern/urlpattern_parse_string_test.cc b/test/urlpattern/urlpattern_parse_string_test.cc new file mode 100644 index 000000000..483fb37f6 --- /dev/null +++ b/test/urlpattern/urlpattern_parse_string_test.cc @@ -0,0 +1,374 @@ +#include + +#include + +#include "urlpattern_test_utils.h" + +TEST(URLPattern_parse_string, simple_pathname_pattern) { + const auto pattern{sourcemeta::core::URLPattern::parse("/foo/bar")}; + + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.protocol, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.username, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, URLPatternPartAsterisk); + EXPECT_PART_VECTOR_SIZE(pattern.hostname, 1); + EXPECT_PART_WITHOUT_VALUE(pattern.hostname, 0, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, URLPatternPartAsterisk); + + EXPECT_PART_VECTOR_SIZE(pattern.pathname, 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartChar, "bar"); + + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.search, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.hash, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse_string, pathname_with_named_group) { + const auto pattern{sourcemeta::core::URLPattern::parse("/foo/:bar")}; + + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.protocol, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.username, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, URLPatternPartAsterisk); + EXPECT_PART_VECTOR_SIZE(pattern.hostname, 1); + EXPECT_PART_WITHOUT_VALUE(pattern.hostname, 0, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, URLPatternPartAsterisk); + + EXPECT_PART_VECTOR_SIZE(pattern.pathname, 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartName, "bar"); + + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.search, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.hash, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse_string, pathname_with_wildcard) { + const auto pattern{sourcemeta::core::URLPattern::parse("/foo/*")}; + + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.protocol, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.username, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, URLPatternPartAsterisk); + EXPECT_PART_VECTOR_SIZE(pattern.hostname, 1); + EXPECT_PART_WITHOUT_VALUE(pattern.hostname, 0, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, URLPatternPartAsterisk); + + EXPECT_PART_VECTOR_SIZE(pattern.pathname, 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 1, URLPatternPartAsterisk); + + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.search, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.hash, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse_string, full_url_with_protocol_host_path) { + const auto pattern{ + sourcemeta::core::URLPattern::parse("https://example.com/foo")}; + + EXPECT_SINGLE_PART_WITH_VALUE(pattern.protocol, URLPatternPartChar, "https"); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.username, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, URLPatternPartAsterisk); + + EXPECT_PART_VECTOR_SIZE(pattern.hostname, 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "example"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartChar, "com"); + + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, URLPatternPartAsterisk); + + EXPECT_PART_VECTOR_SIZE(pattern.pathname, 1); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.search, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.hash, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse_string, url_with_named_path_segment) { + const auto pattern{ + sourcemeta::core::URLPattern::parse("https://example.com/:foo")}; + + EXPECT_SINGLE_PART_WITH_VALUE(pattern.protocol, URLPatternPartChar, "https"); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.username, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, URLPatternPartAsterisk); + + EXPECT_PART_VECTOR_SIZE(pattern.hostname, 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "example"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartChar, "com"); + + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, URLPatternPartAsterisk); + + EXPECT_PART_VECTOR_SIZE(pattern.pathname, 1); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartName, "foo"); + + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.search, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.hash, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse_string, url_with_port) { + const auto pattern{ + sourcemeta::core::URLPattern::parse("https://example.com:8080/foo")}; + + EXPECT_SINGLE_PART_WITH_VALUE(pattern.protocol, URLPatternPartChar, "https"); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.username, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, URLPatternPartAsterisk); + + EXPECT_PART_VECTOR_SIZE(pattern.hostname, 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "example"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartChar, "com"); + + EXPECT_SINGLE_PART_WITH_VALUE(pattern.port, URLPatternPartChar, "8080"); + + EXPECT_PART_VECTOR_SIZE(pattern.pathname, 1); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.search, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.hash, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse_string, url_with_username) { + const auto pattern{ + sourcemeta::core::URLPattern::parse("https://user@example.com/foo")}; + + EXPECT_SINGLE_PART_WITH_VALUE(pattern.protocol, URLPatternPartChar, "https"); + EXPECT_SINGLE_PART_WITH_VALUE(pattern.username, URLPatternPartChar, "user"); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, URLPatternPartAsterisk); + + EXPECT_PART_VECTOR_SIZE(pattern.hostname, 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "example"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartChar, "com"); + + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, URLPatternPartAsterisk); + + EXPECT_PART_VECTOR_SIZE(pattern.pathname, 1); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.search, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.hash, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse_string, url_with_username_and_password) { + const auto pattern{ + sourcemeta::core::URLPattern::parse("https://user:pass@example.com/foo")}; + + EXPECT_SINGLE_PART_WITH_VALUE(pattern.protocol, URLPatternPartChar, "https"); + EXPECT_SINGLE_PART_WITH_VALUE(pattern.username, URLPatternPartChar, "user"); + EXPECT_SINGLE_PART_WITH_VALUE(pattern.password, URLPatternPartChar, "pass"); + + EXPECT_PART_VECTOR_SIZE(pattern.hostname, 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "example"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartChar, "com"); + + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, URLPatternPartAsterisk); + + EXPECT_PART_VECTOR_SIZE(pattern.pathname, 1); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.search, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.hash, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse_string, url_with_query_string) { + const auto pattern{ + sourcemeta::core::URLPattern::parse("https://example.com/foo?bar=baz")}; + + EXPECT_SINGLE_PART_WITH_VALUE(pattern.protocol, URLPatternPartChar, "https"); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.username, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, URLPatternPartAsterisk); + + EXPECT_PART_VECTOR_SIZE(pattern.hostname, 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "example"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartChar, "com"); + + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, URLPatternPartAsterisk); + + EXPECT_PART_VECTOR_SIZE(pattern.pathname, 1); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + + EXPECT_SINGLE_PART_WITH_VALUE(pattern.search, URLPatternPartChar, "bar=baz"); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.hash, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse_string, url_with_named_query) { + const auto pattern{ + sourcemeta::core::URLPattern::parse("https://example.com/foo?:query")}; + + EXPECT_SINGLE_PART_WITH_VALUE(pattern.protocol, URLPatternPartChar, "https"); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.username, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, URLPatternPartAsterisk); + + EXPECT_PART_VECTOR_SIZE(pattern.hostname, 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "example"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartChar, "com"); + + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, URLPatternPartAsterisk); + + EXPECT_PART_VECTOR_SIZE(pattern.pathname, 1); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + + EXPECT_SINGLE_PART_WITH_VALUE(pattern.search, URLPatternPartName, "query"); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.hash, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse_string, url_with_hash) { + const auto pattern{ + sourcemeta::core::URLPattern::parse("https://example.com/foo#section")}; + + EXPECT_SINGLE_PART_WITH_VALUE(pattern.protocol, URLPatternPartChar, "https"); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.username, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, URLPatternPartAsterisk); + + EXPECT_PART_VECTOR_SIZE(pattern.hostname, 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "example"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartChar, "com"); + + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, URLPatternPartAsterisk); + + EXPECT_PART_VECTOR_SIZE(pattern.pathname, 1); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.search, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITH_VALUE(pattern.hash, URLPatternPartChar, "section"); +} + +TEST(URLPattern_parse_string, url_with_named_hash) { + const auto pattern{ + sourcemeta::core::URLPattern::parse("https://example.com/foo#:anchor")}; + + EXPECT_SINGLE_PART_WITH_VALUE(pattern.protocol, URLPatternPartChar, "https"); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.username, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, URLPatternPartAsterisk); + + EXPECT_PART_VECTOR_SIZE(pattern.hostname, 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "example"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartChar, "com"); + + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, URLPatternPartAsterisk); + + EXPECT_PART_VECTOR_SIZE(pattern.pathname, 1); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.search, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITH_VALUE(pattern.hash, URLPatternPartName, "anchor"); +} + +TEST(URLPattern_parse_string, url_with_query_and_hash) { + const auto pattern{sourcemeta::core::URLPattern::parse( + "https://example.com/foo?bar=baz#section")}; + + EXPECT_SINGLE_PART_WITH_VALUE(pattern.protocol, URLPatternPartChar, "https"); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.username, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, URLPatternPartAsterisk); + + EXPECT_PART_VECTOR_SIZE(pattern.hostname, 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "example"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartChar, "com"); + + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, URLPatternPartAsterisk); + + EXPECT_PART_VECTOR_SIZE(pattern.pathname, 1); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + + EXPECT_SINGLE_PART_WITH_VALUE(pattern.search, URLPatternPartChar, "bar=baz"); + EXPECT_SINGLE_PART_WITH_VALUE(pattern.hash, URLPatternPartChar, "section"); +} + +TEST(URLPattern_parse_string, complex_multi_segment_path) { + const auto pattern{sourcemeta::core::URLPattern::parse( + "https://example.com/api/v1/:resource/:id")}; + + EXPECT_SINGLE_PART_WITH_VALUE(pattern.protocol, URLPatternPartChar, "https"); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.username, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, URLPatternPartAsterisk); + + EXPECT_PART_VECTOR_SIZE(pattern.hostname, 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "example"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartChar, "com"); + + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, URLPatternPartAsterisk); + + EXPECT_PART_VECTOR_SIZE(pattern.pathname, 4); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "api"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartChar, "v1"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 2, URLPatternPartName, "resource"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 3, URLPatternPartName, "id"); + + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.search, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.hash, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse_string, wildcard_protocol) { + const auto pattern{ + sourcemeta::core::URLPattern::parse("*://example.com/foo")}; + + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.protocol, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.username, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, URLPatternPartAsterisk); + + EXPECT_PART_VECTOR_SIZE(pattern.hostname, 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "example"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartChar, "com"); + + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, URLPatternPartAsterisk); + + EXPECT_PART_VECTOR_SIZE(pattern.pathname, 1); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.search, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.hash, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse_string, named_protocol) { + const auto pattern{ + sourcemeta::core::URLPattern::parse(":protocol://example.com/foo")}; + + EXPECT_SINGLE_PART_WITH_VALUE(pattern.protocol, URLPatternPartName, + "protocol"); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.username, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, URLPatternPartAsterisk); + + EXPECT_PART_VECTOR_SIZE(pattern.hostname, 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "example"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartChar, "com"); + + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, URLPatternPartAsterisk); + + EXPECT_PART_VECTOR_SIZE(pattern.pathname, 1); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "foo"); + + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.search, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.hash, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse_string, empty_string) { + const auto pattern{sourcemeta::core::URLPattern::parse("")}; + + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.protocol, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.username, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, URLPatternPartAsterisk); + EXPECT_PART_VECTOR_SIZE(pattern.hostname, 1); + EXPECT_PART_WITHOUT_VALUE(pattern.hostname, 0, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, URLPatternPartAsterisk); + EXPECT_PART_VECTOR_SIZE(pattern.pathname, 1); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 0, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.search, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.hash, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse_string, all_components_full_pattern) { + const auto pattern{sourcemeta::core::URLPattern::parse( + "https://user:pass@example.com:8080/api/:id?key=value#section")}; + + EXPECT_SINGLE_PART_WITH_VALUE(pattern.protocol, URLPatternPartChar, "https"); + EXPECT_SINGLE_PART_WITH_VALUE(pattern.username, URLPatternPartChar, "user"); + EXPECT_SINGLE_PART_WITH_VALUE(pattern.password, URLPatternPartChar, "pass"); + + EXPECT_PART_VECTOR_SIZE(pattern.hostname, 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "example"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartChar, "com"); + + EXPECT_SINGLE_PART_WITH_VALUE(pattern.port, URLPatternPartChar, "8080"); + + EXPECT_PART_VECTOR_SIZE(pattern.pathname, 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "api"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartName, "id"); + + EXPECT_SINGLE_PART_WITH_VALUE(pattern.search, URLPatternPartChar, + "key=value"); + EXPECT_SINGLE_PART_WITH_VALUE(pattern.hash, URLPatternPartChar, "section"); +} diff --git a/test/urlpattern/urlpattern_parse_test.cc b/test/urlpattern/urlpattern_parse_test.cc new file mode 100644 index 000000000..df6224a7f --- /dev/null +++ b/test/urlpattern/urlpattern_parse_test.cc @@ -0,0 +1,305 @@ +#include + +#include + +#include "urlpattern_test_utils.h" + +TEST(URLPattern_parse, equality_default_constructed) { + const sourcemeta::core::URLPattern pattern1; + const sourcemeta::core::URLPattern pattern2; + EXPECT_TRUE(pattern1 == pattern2); + EXPECT_FALSE(pattern1 != pattern2); +} + +TEST(URLPattern_parse, equality_same_protocol) { + const sourcemeta::core::URLPattern pattern1{.protocol = "https"}; + const sourcemeta::core::URLPattern pattern2{.protocol = "https"}; + EXPECT_TRUE(pattern1 == pattern2); + EXPECT_FALSE(pattern1 != pattern2); +} + +TEST(URLPattern_parse, equality_different_protocol) { + const sourcemeta::core::URLPattern pattern1{.protocol = "https"}; + const sourcemeta::core::URLPattern pattern2{.protocol = "http"}; + EXPECT_FALSE(pattern1 == pattern2); + EXPECT_TRUE(pattern1 != pattern2); +} + +TEST(URLPattern_parse, equality_same_all_components) { + const sourcemeta::core::URLPattern pattern1{.protocol = "https", + .username = "user", + .password = "pass", + .hostname = "example.com", + .port = "8080", + .pathname = "/api/:id", + .search = "key=value", + .hash = "section"}; + const sourcemeta::core::URLPattern pattern2{.protocol = "https", + .username = "user", + .password = "pass", + .hostname = "example.com", + .port = "8080", + .pathname = "/api/:id", + .search = "key=value", + .hash = "section"}; + EXPECT_TRUE(pattern1 == pattern2); + EXPECT_FALSE(pattern1 != pattern2); +} + +TEST(URLPattern_parse, equality_different_pathname) { + const sourcemeta::core::URLPattern pattern1{.pathname = "/foo/bar"}; + const sourcemeta::core::URLPattern pattern2{.pathname = "/foo/baz"}; + EXPECT_FALSE(pattern1 == pattern2); + EXPECT_TRUE(pattern1 != pattern2); +} + +TEST(URLPattern_parse, equality_different_hostname) { + const sourcemeta::core::URLPattern pattern1{.hostname = "example.com"}; + const sourcemeta::core::URLPattern pattern2{.hostname = "example.org"}; + EXPECT_FALSE(pattern1 == pattern2); + EXPECT_TRUE(pattern1 != pattern2); +} + +TEST(URLPattern_parse, default_constructor_all_asterisks) { + const sourcemeta::core::URLPattern pattern; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.protocol, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.username, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, URLPatternPartAsterisk); + EXPECT_PART_VECTOR_SIZE(pattern.hostname, 1); + EXPECT_PART_WITHOUT_VALUE(pattern.hostname, 0, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, URLPatternPartAsterisk); + EXPECT_PART_VECTOR_SIZE(pattern.pathname, 1); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 0, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.search, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.hash, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse, single_component_protocol_only) { + const sourcemeta::core::URLPattern pattern{.protocol = "https"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.protocol, URLPatternPartChar, "https"); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.username, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, URLPatternPartAsterisk); + EXPECT_PART_VECTOR_SIZE(pattern.hostname, 1); + EXPECT_PART_WITHOUT_VALUE(pattern.hostname, 0, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, URLPatternPartAsterisk); + EXPECT_PART_VECTOR_SIZE(pattern.pathname, 1); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 0, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.search, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.hash, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse, single_component_hostname_only) { + const sourcemeta::core::URLPattern pattern{.hostname = "example.com"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.protocol, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.username, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, URLPatternPartAsterisk); + EXPECT_PART_VECTOR_SIZE(pattern.hostname, 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "example"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartChar, "com"); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, URLPatternPartAsterisk); + EXPECT_PART_VECTOR_SIZE(pattern.pathname, 1); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 0, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.search, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.hash, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse, single_component_pathname_only) { + const sourcemeta::core::URLPattern pattern{.pathname = "/api/v1"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.protocol, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.username, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, URLPatternPartAsterisk); + EXPECT_PART_VECTOR_SIZE(pattern.hostname, 1); + EXPECT_PART_WITHOUT_VALUE(pattern.hostname, 0, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, URLPatternPartAsterisk); + EXPECT_PART_VECTOR_SIZE(pattern.pathname, 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "api"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartChar, "v1"); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.search, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.hash, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse, single_component_hash_only) { + const sourcemeta::core::URLPattern pattern{.hash = "section"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.protocol, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.username, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, URLPatternPartAsterisk); + EXPECT_PART_VECTOR_SIZE(pattern.hostname, 1); + EXPECT_PART_WITHOUT_VALUE(pattern.hostname, 0, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, URLPatternPartAsterisk); + EXPECT_PART_VECTOR_SIZE(pattern.pathname, 1); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 0, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.search, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITH_VALUE(pattern.hash, URLPatternPartChar, "section"); +} + +TEST(URLPattern_parse, protocol_and_hostname) { + const sourcemeta::core::URLPattern pattern{.protocol = "https", + .hostname = "example.com"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.protocol, URLPatternPartChar, "https"); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.username, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, URLPatternPartAsterisk); + EXPECT_PART_VECTOR_SIZE(pattern.hostname, 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "example"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartChar, "com"); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, URLPatternPartAsterisk); + EXPECT_PART_VECTOR_SIZE(pattern.pathname, 1); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 0, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.search, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.hash, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse, protocol_hostname_and_pathname) { + const sourcemeta::core::URLPattern pattern{ + .protocol = "https", .hostname = "example.com", .pathname = "/api/v1"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.protocol, URLPatternPartChar, "https"); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.username, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, URLPatternPartAsterisk); + EXPECT_PART_VECTOR_SIZE(pattern.hostname, 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "example"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartChar, "com"); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, URLPatternPartAsterisk); + EXPECT_PART_VECTOR_SIZE(pattern.pathname, 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "api"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartChar, "v1"); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.search, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.hash, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse, protocol_hostname_pathname_and_hash) { + const sourcemeta::core::URLPattern pattern{.protocol = "https", + .hostname = "example.com", + .pathname = "/api/v1", + .hash = "section"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.protocol, URLPatternPartChar, "https"); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.username, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, URLPatternPartAsterisk); + EXPECT_PART_VECTOR_SIZE(pattern.hostname, 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "example"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartChar, "com"); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, URLPatternPartAsterisk); + EXPECT_PART_VECTOR_SIZE(pattern.pathname, 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "api"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartChar, "v1"); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.search, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITH_VALUE(pattern.hash, URLPatternPartChar, "section"); +} + +TEST(URLPattern_parse, all_components_specified) { + const sourcemeta::core::URLPattern pattern{.protocol = "https", + .username = "admin", + .password = "secret", + .hostname = "example.com", + .port = "8080", + .pathname = "/api/v1", + .search = "q=test", + .hash = "section"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.protocol, URLPatternPartChar, "https"); + EXPECT_SINGLE_PART_WITH_VALUE(pattern.username, URLPatternPartChar, "admin"); + EXPECT_SINGLE_PART_WITH_VALUE(pattern.password, URLPatternPartChar, "secret"); + EXPECT_PART_VECTOR_SIZE(pattern.hostname, 2); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartChar, "example"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartChar, "com"); + EXPECT_SINGLE_PART_WITH_VALUE(pattern.port, URLPatternPartChar, "8080"); + EXPECT_PART_VECTOR_SIZE(pattern.pathname, 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "api"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartChar, "v1"); + EXPECT_SINGLE_PART_WITH_VALUE(pattern.search, URLPatternPartChar, "q=test"); + EXPECT_SINGLE_PART_WITH_VALUE(pattern.hash, URLPatternPartChar, "section"); +} + +TEST(URLPattern_parse, pattern_with_names) { + const sourcemeta::core::URLPattern pattern{.protocol = ":protocol", + .hostname = ":host.example.com", + .pathname = "/api/:version/:id"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.protocol, URLPatternPartName, + "protocol"); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.username, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, URLPatternPartAsterisk); + EXPECT_PART_VECTOR_SIZE(pattern.hostname, 3); + EXPECT_PART_WITH_VALUE(pattern.hostname, 0, URLPatternPartName, "host"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartChar, "example"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 2, URLPatternPartChar, "com"); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, URLPatternPartAsterisk); + EXPECT_PART_VECTOR_SIZE(pattern.pathname, 3); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "api"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 1, URLPatternPartName, "version"); + EXPECT_PART_WITH_VALUE(pattern.pathname, 2, URLPatternPartName, "id"); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.search, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.hash, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse, username_and_password_only) { + const sourcemeta::core::URLPattern pattern{.username = "admin", + .password = "secret"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.protocol, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITH_VALUE(pattern.username, URLPatternPartChar, "admin"); + EXPECT_SINGLE_PART_WITH_VALUE(pattern.password, URLPatternPartChar, "secret"); + EXPECT_PART_VECTOR_SIZE(pattern.hostname, 1); + EXPECT_PART_WITHOUT_VALUE(pattern.hostname, 0, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, URLPatternPartAsterisk); + EXPECT_PART_VECTOR_SIZE(pattern.pathname, 1); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 0, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.search, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.hash, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse, port_and_pathname) { + const sourcemeta::core::URLPattern pattern{.port = "8080", + .pathname = "/api"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.protocol, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.username, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, URLPatternPartAsterisk); + EXPECT_PART_VECTOR_SIZE(pattern.hostname, 1); + EXPECT_PART_WITHOUT_VALUE(pattern.hostname, 0, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITH_VALUE(pattern.port, URLPatternPartChar, "8080"); + EXPECT_PART_VECTOR_SIZE(pattern.pathname, 1); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "api"); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.search, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.hash, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse, search_and_hash) { + const sourcemeta::core::URLPattern pattern{.search = "q=test", + .hash = "section"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.protocol, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.username, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, URLPatternPartAsterisk); + EXPECT_PART_VECTOR_SIZE(pattern.hostname, 1); + EXPECT_PART_WITHOUT_VALUE(pattern.hostname, 0, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, URLPatternPartAsterisk); + EXPECT_PART_VECTOR_SIZE(pattern.pathname, 1); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 0, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITH_VALUE(pattern.search, URLPatternPartChar, "q=test"); + EXPECT_SINGLE_PART_WITH_VALUE(pattern.hash, URLPatternPartChar, "section"); +} + +TEST(URLPattern_parse, hostname_with_wildcard) { + const sourcemeta::core::URLPattern pattern{.hostname = "*.example.com"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.protocol, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.username, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, URLPatternPartAsterisk); + EXPECT_PART_VECTOR_SIZE(pattern.hostname, 3); + EXPECT_PART_WITHOUT_VALUE(pattern.hostname, 0, URLPatternPartAsterisk); + EXPECT_PART_WITH_VALUE(pattern.hostname, 1, URLPatternPartChar, "example"); + EXPECT_PART_WITH_VALUE(pattern.hostname, 2, URLPatternPartChar, "com"); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, URLPatternPartAsterisk); + EXPECT_PART_VECTOR_SIZE(pattern.pathname, 1); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 0, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.search, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.hash, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse, pathname_with_wildcard) { + const sourcemeta::core::URLPattern pattern{.pathname = "/static/*"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.protocol, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.username, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.password, URLPatternPartAsterisk); + EXPECT_PART_VECTOR_SIZE(pattern.hostname, 1); + EXPECT_PART_WITHOUT_VALUE(pattern.hostname, 0, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.port, URLPatternPartAsterisk); + EXPECT_PART_VECTOR_SIZE(pattern.pathname, 2); + EXPECT_PART_WITH_VALUE(pattern.pathname, 0, URLPatternPartChar, "static"); + EXPECT_PART_WITHOUT_VALUE(pattern.pathname, 1, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.search, URLPatternPartAsterisk); + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.hash, URLPatternPartAsterisk); +} diff --git a/test/urlpattern/urlpattern_parse_username_test.cc b/test/urlpattern/urlpattern_parse_username_test.cc new file mode 100644 index 000000000..3fcadb9b1 --- /dev/null +++ b/test/urlpattern/urlpattern_parse_username_test.cc @@ -0,0 +1,163 @@ +#include + +#include + +#include "urlpattern_test_utils.h" + +TEST(URLPattern_parse, username_copy_constructor) { + const sourcemeta::core::URLPatternUsername original{"admin"}; + const sourcemeta::core::URLPatternUsername copy{original}; + EXPECT_SINGLE_PART_WITH_VALUE(copy, URLPatternPartChar, "admin"); +} + +TEST(URLPattern_parse, username_move_constructor) { + sourcemeta::core::URLPatternUsername original{"admin"}; + const sourcemeta::core::URLPatternUsername moved{std::move(original)}; + EXPECT_SINGLE_PART_WITH_VALUE(moved, URLPatternPartChar, "admin"); +} + +TEST(URLPattern_parse, username_copy_assignment) { + sourcemeta::core::URLPattern pattern; + const sourcemeta::core::URLPatternUsername original{"admin"}; + pattern.username = original; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.username, URLPatternPartChar, "admin"); +} + +TEST(URLPattern_parse, username_move_assignment) { + sourcemeta::core::URLPattern pattern; + sourcemeta::core::URLPatternUsername original{"admin"}; + pattern.username = std::move(original); + EXPECT_SINGLE_PART_WITH_VALUE(pattern.username, URLPatternPartChar, "admin"); +} + +TEST(URLPattern_parse, username_default_constructor) { + const sourcemeta::core::URLPatternUsername username; + EXPECT_SINGLE_PART_WITHOUT_VALUE(username, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse, username_simple_char_token) { + const sourcemeta::core::URLPattern pattern{.username = "admin"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.username, URLPatternPartChar, "admin"); +} + +TEST(URLPattern_parse, username_user) { + const sourcemeta::core::URLPattern pattern{.username = "user"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.username, URLPatternPartChar, "user"); +} + +TEST(URLPattern_parse, username_with_numbers) { + const sourcemeta::core::URLPattern pattern{.username = "user123"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.username, URLPatternPartChar, + "user123"); +} + +TEST(URLPattern_parse, username_name_token) { + const sourcemeta::core::URLPattern pattern{.username = ":username"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.username, URLPatternPartName, + "username"); +} + +TEST(URLPattern_parse, username_name_optional) { + const sourcemeta::core::URLPattern pattern{.username = ":username?"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.username, URLPatternPartNameOptional, + "username"); +} + +TEST(URLPattern_parse, username_name_multiple) { + const sourcemeta::core::URLPattern pattern{.username = ":username+"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.username, URLPatternPartNameMultiple, + "username"); +} + +TEST(URLPattern_parse, username_name_asterisk) { + const sourcemeta::core::URLPattern pattern{.username = ":username*"}; + EXPECT_SINGLE_PART_WITH_VALUE(pattern.username, URLPatternPartNameAsterisk, + "username"); +} + +TEST(URLPattern_parse, username_name_regex) { + const sourcemeta::core::URLPattern pattern{.username = ":user(\\w+)"}; + EXPECT_SINGLE_PART_WITH_VALUE_AND_REGEX( + pattern.username, URLPatternPartNameRegExp, "user", "\\w+"); +} + +TEST(URLPattern_parse, username_regex) { + const sourcemeta::core::URLPattern pattern{.username = "(\\w+)"}; + EXPECT_SINGLE_PART_WITH_REGEX(pattern.username, URLPatternPartRegExp, "\\w+"); +} + +TEST(URLPattern_parse, username_regex_optional) { + const sourcemeta::core::URLPattern pattern{.username = "(\\w+)?"}; + EXPECT_SINGLE_PART_WITH_REGEX(pattern.username, URLPatternPartRegExpOptional, + "\\w+"); +} + +TEST(URLPattern_parse, username_regex_multiple) { + const sourcemeta::core::URLPattern pattern{.username = "(\\w+)+"}; + EXPECT_SINGLE_PART_WITH_REGEX(pattern.username, URLPatternPartRegExpMultiple, + "\\w+"); +} + +TEST(URLPattern_parse, username_regex_asterisk) { + const sourcemeta::core::URLPattern pattern{.username = "(\\w+)*"}; + EXPECT_SINGLE_PART_WITH_REGEX(pattern.username, URLPatternPartRegExpAsterisk, + "\\w+"); +} + +TEST(URLPattern_parse, username_asterisk) { + const sourcemeta::core::URLPattern pattern{.username = "*"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.username, URLPatternPartAsterisk); +} + +TEST(URLPattern_parse, username_asterisk_optional) { + const sourcemeta::core::URLPattern pattern{.username = "*?"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.username, + URLPatternPartAsteriskOptional); +} + +TEST(URLPattern_parse, username_asterisk_multiple) { + const sourcemeta::core::URLPattern pattern{.username = "*+"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.username, + URLPatternPartAsteriskMultiple); +} + +TEST(URLPattern_parse, username_asterisk_asterisk) { + const sourcemeta::core::URLPattern pattern{.username = "**"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.username, + URLPatternPartAsteriskAsterisk); +} + +TEST(URLPattern_parse, username_group_simple) { + const sourcemeta::core::URLPattern pattern{.username = "{admin}"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.username, URLPatternPartGroup); +} + +TEST(URLPattern_parse, username_group_optional) { + const sourcemeta::core::URLPattern pattern{.username = "{admin}?"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.username, + URLPatternPartGroupOptional); +} + +TEST(URLPattern_parse, username_group_multiple) { + const sourcemeta::core::URLPattern pattern{.username = "{admin}+"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.username, + URLPatternPartGroupMultiple); +} + +TEST(URLPattern_parse, username_group_asterisk) { + const sourcemeta::core::URLPattern pattern{.username = "{admin}*"}; + EXPECT_SINGLE_PART_WITHOUT_VALUE(pattern.username, + URLPatternPartGroupAsterisk); +} + +TEST(URLPattern_parse, username_error_unclosed_group) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternUsername, "{admin", 6); +} + +TEST(URLPattern_parse, username_error_unclosed_regex) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternUsername, "(\\w+", 4); +} + +TEST(URLPattern_parse, username_error_invalid_modifier) { + EXPECT_COMPONENT_PARSE_ERROR(URLPatternUsername, ":user%", 5); +} diff --git a/test/urlpattern/urlpattern_test_utils.h b/test/urlpattern/urlpattern_test_utils.h new file mode 100644 index 000000000..61b4c739f --- /dev/null +++ b/test/urlpattern/urlpattern_test_utils.h @@ -0,0 +1,195 @@ +#ifndef SOURCEMETA_CORE_URLPATTERN_TEST_UTILS_H_ +#define SOURCEMETA_CORE_URLPATTERN_TEST_UTILS_H_ + +#define EXPECT_COMPONENT_PARSE_ERROR(ComponentType, input, expected_column) \ + try { \ + sourcemeta::core::ComponentType result{(input)}; \ + FAIL(); \ + } catch (const sourcemeta::core::URLPatternParseError &error) { \ + EXPECT_EQ(error.column(), (expected_column)); \ + SUCCEED(); \ + } catch (...) { \ + FAIL(); \ + } + +#define EXPECT_PART_WITHOUT_VALUE(result, index, expected_type) \ + EXPECT_TRUE(std::holds_alternative( \ + (result).value.at(index))); + +#define EXPECT_PART_WITH_VALUE(result, index, expected_type, expected_value) \ + EXPECT_PART_WITHOUT_VALUE(result, index, expected_type); \ + EXPECT_EQ( \ + std::get((result).value.at(index)) \ + .value, \ + (expected_value)); + +#define EXPECT_PART_WITH_REGEX(result, index, expected_type, expected_pattern) \ + EXPECT_PART_WITHOUT_VALUE(result, index, expected_type); \ + EXPECT_EQ( \ + std::get((result).value.at(index)) \ + .original_pattern, \ + (expected_pattern)); + +#define EXPECT_PART_WITH_VALUE_AND_REGEX(result, index, expected_type, \ + expected_value, expected_pattern) \ + EXPECT_PART_WITHOUT_VALUE(result, index, expected_type); \ + EXPECT_EQ( \ + std::get((result).value.at(index)) \ + .value, \ + (expected_value)); \ + EXPECT_EQ( \ + std::get((result).value.at(index)) \ + .original_pattern, \ + (expected_pattern)); + +#define EXPECT_PART_SIZE(result, index, expected_type, expected_size) \ + EXPECT_PART_WITHOUT_VALUE(result, index, expected_type) \ + EXPECT_EQ( \ + std::get((result).value.at(index)) \ + .value.size(), \ + (expected_size)); + +#define EXPECT_PART_GROUP(result, index, expected_type, expected_size, \ + expected_has_inner_segment_prefix, \ + expected_has_inner_segment_suffix) \ + EXPECT_PART_SIZE(result, index, expected_type, expected_size) \ + EXPECT_EQ( \ + std::get((result).value.at(index)) \ + .has_inner_segment_prefix, \ + (expected_has_inner_segment_prefix)); \ + EXPECT_EQ( \ + std::get((result).value.at(index)) \ + .has_inner_segment_suffix, \ + (expected_has_inner_segment_suffix)); + +#define EXPECT_PART_NESTED_WITHOUT_VALUE(result, index, expected_type, \ + subindex, expected_subtype) \ + EXPECT_PART_WITHOUT_VALUE(result, index, expected_type); \ + EXPECT_TRUE(std::holds_alternative( \ + std::get((result).value.at(index)) \ + .value.at(subindex))); + +#define EXPECT_PART_NESTED_WITH_VALUE(result, index, expected_type, subindex, \ + expected_subtype, expected_value) \ + EXPECT_PART_NESTED_WITHOUT_VALUE(result, index, expected_type, subindex, \ + expected_subtype); \ + EXPECT_EQ( \ + std::get( \ + std::get((result).value.at(index)) \ + .value.at(subindex)) \ + .value, \ + (expected_value)); + +#define EXPECT_PART_COMPLEX_SIZE(result, index, expected_size) \ + EXPECT_PART_WITHOUT_VALUE(result, index, URLPatternPartComplexSegment) \ + EXPECT_EQ(std::get( \ + (result).value.at(index)) \ + .value.size(), \ + (expected_size)); + +#define EXPECT_PART_COMPLEX_WITHOUT_VALUE(result, index, subindex, \ + expected_subtype) \ + EXPECT_PART_WITHOUT_VALUE(result, index, URLPatternPartComplexSegment); \ + EXPECT_TRUE(std::holds_alternative( \ + std::get( \ + (result).value.at(index)) \ + .value.at(subindex))); + +#define EXPECT_PART_COMPLEX_WITH_VALUE(result, index, subindex, \ + expected_subtype, expected_value) \ + EXPECT_PART_COMPLEX_WITHOUT_VALUE(result, index, subindex, \ + expected_subtype); \ + EXPECT_EQ(std::get( \ + std::get( \ + (result).value.at(index)) \ + .value.at(subindex)) \ + .value, \ + (expected_value)); + +#define EXPECT_PART_COMPLEX_NESTED_SIZE(result, index, subindex, \ + expected_subtype, expected_size) \ + EXPECT_PART_COMPLEX_WITHOUT_VALUE(result, index, subindex, expected_subtype) \ + EXPECT_EQ(std::get( \ + std::get( \ + (result).value.at(index)) \ + .value.at(subindex)) \ + .value.size(), \ + (expected_size)); + +#define EXPECT_PART_COMPLEX_GROUP( \ + result, index, subindex, expected_subtype, expected_size, \ + expected_has_inner_segment_prefix, expected_has_inner_segment_suffix) \ + EXPECT_PART_COMPLEX_NESTED_SIZE(result, index, subindex, expected_subtype, \ + expected_size) \ + EXPECT_EQ(std::get( \ + std::get( \ + (result).value.at(index)) \ + .value.at(subindex)) \ + .has_inner_segment_prefix, \ + (expected_has_inner_segment_prefix)); \ + EXPECT_EQ(std::get( \ + std::get( \ + (result).value.at(index)) \ + .value.at(subindex)) \ + .has_inner_segment_suffix, \ + (expected_has_inner_segment_suffix)); + +#define EXPECT_PART_COMPLEX_NESTED_WITHOUT_VALUE( \ + result, index, subindex, expected_subtype, subsubindex, \ + expected_subsubtype) \ + EXPECT_PART_COMPLEX_WITHOUT_VALUE(result, index, subindex, \ + expected_subtype); \ + EXPECT_TRUE(std::holds_alternative( \ + std::get( \ + std::get( \ + (result).value.at(index)) \ + .value.at(subindex)) \ + .value.at(subsubindex))); + +#define EXPECT_PART_COMPLEX_NESTED_WITH_VALUE( \ + result, index, subindex, expected_subtype, subsubindex, \ + expected_subsubtype, expected_value) \ + EXPECT_PART_COMPLEX_NESTED_WITHOUT_VALUE(result, index, subindex, \ + expected_subtype, subsubindex, \ + expected_subsubtype); \ + EXPECT_EQ(std::get( \ + std::get( \ + std::get( \ + (result).value.at(index)) \ + .value.at(subindex)) \ + .value.at(subsubindex)) \ + .value, \ + (expected_value)); + +#define EXPECT_SINGLE_PART_WITHOUT_VALUE(component, expected_type) \ + EXPECT_TRUE(std::holds_alternative( \ + (component).value)); + +#define EXPECT_SINGLE_PART_WITH_VALUE(component, expected_type, \ + expected_value) \ + EXPECT_SINGLE_PART_WITHOUT_VALUE(component, expected_type); \ + EXPECT_EQ( \ + std::get((component).value).value, \ + (expected_value)); + +#define EXPECT_SINGLE_PART_WITH_REGEX(component, expected_type, \ + expected_pattern) \ + EXPECT_SINGLE_PART_WITHOUT_VALUE(component, expected_type); \ + EXPECT_EQ(std::get((component).value) \ + .original_pattern, \ + (expected_pattern)); + +#define EXPECT_SINGLE_PART_WITH_VALUE_AND_REGEX( \ + component, expected_type, expected_value, expected_pattern) \ + EXPECT_SINGLE_PART_WITHOUT_VALUE(component, expected_type); \ + EXPECT_EQ( \ + std::get((component).value).value, \ + (expected_value)); \ + EXPECT_EQ(std::get((component).value) \ + .original_pattern, \ + (expected_pattern)); + +#define EXPECT_PART_VECTOR_SIZE(component, expected_size) \ + EXPECT_EQ((component).value.size(), (expected_size)); + +#endif diff --git a/test/urlpattern/urlpattern_wpt.cc b/test/urlpattern/urlpattern_wpt.cc new file mode 100644 index 000000000..fea20308e --- /dev/null +++ b/test/urlpattern/urlpattern_wpt.cc @@ -0,0 +1,454 @@ +#include + +#include +#include + +#include // std::int64_t +#include // std::filesystem +#include // std::cerr +#include // std::string, std::to_string + +static auto compare_to_int(const std::strong_ordering ordering) + -> std::int64_t { + if (ordering == std::strong_ordering::less) { + return -1; + } else if (ordering == std::strong_ordering::greater) { + return 1; + } else { + return 0; + } +} + +static auto is_url_component(const std::string &key) -> bool { + return key == "protocol" || key == "username" || key == "password" || + key == "hostname" || key == "port" || key == "pathname" || + key == "search" || key == "hash"; +} + +// TODO: Remove this filter and handle all tests +static auto do_we_handle_match_test(const sourcemeta::core::JSON &test_case) + -> bool { + if (!test_case.defines("pattern") || !test_case.defines("inputs") || + !test_case.defines("expected_match")) { + return false; + } + + const auto &pattern = test_case.at("pattern"); + const auto &inputs = test_case.at("inputs"); + + if (!pattern.is_array() || pattern.size() != 1) { + return false; + } + + if (!inputs.is_array() || inputs.size() != 1) { + return false; + } + + const auto &pattern_item = pattern.at(0); + const auto &input_item = inputs.at(0); + + // Pattern must be an object (string patterns require full URL parsing) + if (!pattern_item.is_object()) { + return false; + } + + // Input can be object or string (URL) + if (!input_item.is_object() && !input_item.is_string()) { + return false; + } + + // Skip tests with baseURL (not yet supported) + if (pattern_item.defines("baseURL")) { + return false; + } + if (input_item.is_object() && input_item.defines("baseURL")) { + return false; + } + + // Skip tests with exactly_empty_components (requires special port/protocol + // normalization semantics that are not yet implemented) + if (test_case.defines("exactly_empty_components")) { + return false; + } + + // Skip tests involving javascript: protocol (special opaque path handling) + if (input_item.is_object() && input_item.defines("protocol")) { + const auto &protocol = input_item.at("protocol").to_string(); + if (protocol == "javascript") { + return false; + } + } + + // Skip tests that rely on default port normalization + // (e.g., http + port 80 should normalize port to empty) + if (input_item.is_object() && input_item.defines("protocol") && + input_item.defines("port")) { + const auto &protocol = input_item.at("protocol").to_string(); + const auto &port = input_item.at("port").to_string(); + if ((protocol == "http" && port == "80") || + (protocol == "https" && port == "443") || + (protocol == "ws" && port == "80") || + (protocol == "wss" && port == "443") || + (protocol == "ftp" && port == "21")) { + return false; + } + } + + // Skip hostname patterns with groups containing IPv6 addresses + // (e.g., "{[\:\:ab\::num]}") - these require complex segment matching + // within groups, which is not yet implemented + if (pattern_item.defines("hostname")) { + const auto &hostname_pattern = pattern_item.at("hostname").to_string(); + if (hostname_pattern.find('{') != std::string::npos && + hostname_pattern.find('[') != std::string::npos) { + return false; + } + } + + // All pattern keys must be URL components + for (const auto &entry : pattern_item.as_object()) { + if (!is_url_component(entry.first)) { + return false; + } + } + + // All input keys must be URL components (only check if input is an object) + if (input_item.is_object()) { + for (const auto &entry : input_item.as_object()) { + if (!is_url_component(entry.first)) { + return false; + } + } + } + + // Skip tests with empty pattern and string input (requires special default + // wildcard capture semantics) + if (pattern_item.as_object().empty() && input_item.is_string()) { + return false; + } + + // Skip tests with protocol patterns containing colons (e.g., "http{s}?:") + if (pattern_item.defines("protocol")) { + const auto &protocol = pattern_item.at("protocol").to_string(); + if (protocol.find(':') != std::string::npos) { + return false; + } + } + + // Skip tests with search patterns containing leading ? + if (pattern_item.defines("search")) { + const auto &search = pattern_item.at("search").to_string(); + if (!search.empty() && search[0] == '?') { + return false; + } + } + + // Skip tests with hostname patterns containing path/query/hash delimiters + // that should be truncated (these require special canonicalization) + if (pattern_item.defines("hostname")) { + const auto &hostname = pattern_item.at("hostname").to_string(); + if (hostname.find('/') != std::string::npos || + hostname.find('?') != std::string::npos || + hostname.find('#') != std::string::npos) { + return false; + } + } + + return true; +} + +static auto check_component_result( + const std::optional &result, + const sourcemeta::core::JSON &expected_groups) -> void { + EXPECT_TRUE(result.has_value()); + if (!result.has_value()) { + return; + } + + std::size_t expected_non_null_count = 0; + for (const auto &entry : expected_groups.as_object()) { + if (!entry.second.is_null()) { + expected_non_null_count += 1; + } + } + EXPECT_EQ(result->size(), expected_non_null_count); + + for (const auto &entry : expected_groups.as_object()) { + const auto &key = entry.first; + const auto &value = entry.second; + + bool is_numeric = !key.empty(); + for (const auto character : key) { + if (character < '0' || character > '9') { + is_numeric = false; + break; + } + } + + if (value.is_null()) { + if (!is_numeric) { + const auto actual = result->at(key); + EXPECT_FALSE(actual.has_value()); + } + continue; + } + + EXPECT_TRUE(value.is_string()); + if (!value.is_string()) { + continue; + } + + const auto expected_value = value.to_string(); + + if (is_numeric) { + // For numeric keys, first try name lookup (bare patterns store "0", "1", + // etc. as names), then fall back to positional lookup (regular patterns) + const auto by_name = result->at(key); + if (by_name.has_value()) { + EXPECT_EQ(by_name.value(), expected_value); + } else { + const auto position = static_cast(std::stoul(key)); + if (position < result->size()) { + const auto by_position = result->at(position); + EXPECT_EQ(by_position, expected_value); + } else { + FAIL() << "Could not find value for numeric key " << key; + } + } + } else { + const auto actual = result->at(key); + EXPECT_TRUE(actual.has_value()); + if (actual.has_value()) { + EXPECT_EQ(actual.value(), expected_value); + } + } + } +} + +class URLPatternMatchTest : public testing::Test { +public: + explicit URLPatternMatchTest(const sourcemeta::core::JSON &pattern_input, + const sourcemeta::core::JSON &input_value, + const sourcemeta::core::JSON &expected) + : pattern_json{pattern_input}, input_json{input_value}, + expected_match{expected} {} + + void TestBody() override { + std::optional maybe_input; + if (this->input_json.is_string()) { + maybe_input = + sourcemeta::core::URLPatternInput::parse(this->input_json.to_string()); + } else { + maybe_input = + sourcemeta::core::URLPatternInput::parse(this->input_json); + } + const auto maybe_pattern{ + sourcemeta::core::URLPattern::parse(this->pattern_json)}; + + if (!maybe_input.has_value() || !maybe_pattern.has_value()) { + EXPECT_TRUE(this->expected_match.is_null()); + return; + } + + const auto &input{maybe_input.value()}; + const auto &pattern{maybe_pattern.value()}; + const auto result{pattern.match(input)}; + + if (this->expected_match.is_null()) { + EXPECT_FALSE(result.protocol.has_value()); + EXPECT_FALSE(result.username.has_value()); + EXPECT_FALSE(result.password.has_value()); + EXPECT_FALSE(result.hostname.has_value()); + EXPECT_FALSE(result.port.has_value()); + EXPECT_FALSE(result.pathname.has_value()); + EXPECT_FALSE(result.search.has_value()); + EXPECT_FALSE(result.hash.has_value()); + } else { + EXPECT_TRUE(this->expected_match.is_object()); + + if (this->expected_match.defines("protocol")) { + const auto &expected = this->expected_match.at("protocol"); + EXPECT_TRUE(expected.is_object()); + EXPECT_TRUE(expected.defines("groups")); + check_component_result(result.protocol, expected.at("groups")); + } + + if (this->expected_match.defines("username")) { + const auto &expected = this->expected_match.at("username"); + EXPECT_TRUE(expected.is_object()); + EXPECT_TRUE(expected.defines("groups")); + check_component_result(result.username, expected.at("groups")); + } + + if (this->expected_match.defines("password")) { + const auto &expected = this->expected_match.at("password"); + EXPECT_TRUE(expected.is_object()); + EXPECT_TRUE(expected.defines("groups")); + check_component_result(result.password, expected.at("groups")); + } + + if (this->expected_match.defines("hostname")) { + const auto &expected = this->expected_match.at("hostname"); + EXPECT_TRUE(expected.is_object()); + EXPECT_TRUE(expected.defines("groups")); + check_component_result(result.hostname, expected.at("groups")); + } + + if (this->expected_match.defines("port")) { + const auto &expected = this->expected_match.at("port"); + EXPECT_TRUE(expected.is_object()); + EXPECT_TRUE(expected.defines("groups")); + check_component_result(result.port, expected.at("groups")); + } + + if (this->expected_match.defines("pathname")) { + const auto &expected = this->expected_match.at("pathname"); + EXPECT_TRUE(expected.is_object()); + EXPECT_TRUE(expected.defines("groups")); + check_component_result(result.pathname, expected.at("groups")); + } + + if (this->expected_match.defines("search")) { + const auto &expected = this->expected_match.at("search"); + EXPECT_TRUE(expected.is_object()); + EXPECT_TRUE(expected.defines("groups")); + check_component_result(result.search, expected.at("groups")); + } + + if (this->expected_match.defines("hash")) { + const auto &expected = this->expected_match.at("hash"); + EXPECT_TRUE(expected.is_object()); + EXPECT_TRUE(expected.defines("groups")); + check_component_result(result.hash, expected.at("groups")); + } + } + } + +private: + const sourcemeta::core::JSON pattern_json; + const sourcemeta::core::JSON input_json; + const sourcemeta::core::JSON expected_match; +}; + +class URLPatternCompareTest : public testing::Test { +public: + explicit URLPatternCompareTest(const sourcemeta::core::JSON &left_input, + const sourcemeta::core::JSON &right_input, + const std::int64_t expected_value, + const std::string &component_name) + : left{left_input}, right{right_input}, expected{expected_value}, + component{component_name} {} + + void TestBody() override { + sourcemeta::core::URLPattern left_pattern; + sourcemeta::core::URLPattern right_pattern; + + if (this->left.is_object()) { + left_pattern = sourcemeta::core::URLPattern::parse(this->left).value(); + } else if (this->left.is_string()) { + left_pattern = + sourcemeta::core::URLPattern::parse(this->left.to_string()); + } + + if (this->right.is_object()) { + right_pattern = sourcemeta::core::URLPattern::parse(this->right).value(); + } else if (this->right.is_string()) { + right_pattern = + sourcemeta::core::URLPattern::parse(this->right.to_string()); + } + + // When inputs are URL strings (not objects), compare only the specified + // component. This allows testing component-specific comparison behavior + // with full URL inputs. + std::int64_t result{0}; + const bool both_are_strings = + this->left.is_string() && this->right.is_string(); + + if (both_are_strings && this->component == "pathname") { + result = compare_to_int(left_pattern.pathname <=> right_pattern.pathname); + } else if (both_are_strings && this->component == "protocol") { + result = compare_to_int(left_pattern.protocol <=> right_pattern.protocol); + } else if (both_are_strings && this->component == "username") { + result = compare_to_int(left_pattern.username <=> right_pattern.username); + } else if (both_are_strings && this->component == "password") { + result = compare_to_int(left_pattern.password <=> right_pattern.password); + } else if (both_are_strings && this->component == "hostname") { + result = compare_to_int(left_pattern.hostname <=> right_pattern.hostname); + } else if (both_are_strings && this->component == "port") { + result = compare_to_int(left_pattern.port <=> right_pattern.port); + } else if (both_are_strings && this->component == "search") { + result = compare_to_int(left_pattern.search <=> right_pattern.search); + } else if (both_are_strings && this->component == "hash") { + result = compare_to_int(left_pattern.hash <=> right_pattern.hash); + } else { + result = compare_to_int(left_pattern <=> right_pattern); + } + + EXPECT_EQ(result, this->expected); + } + +private: + const sourcemeta::core::JSON left; + const sourcemeta::core::JSON right; + const std::int64_t expected; + const std::string component; +}; + +int main(int argc, char **argv) { + testing::InitGoogleTest(&argc, argv); + + const std::filesystem::path compare_test_path = + std::filesystem::path{WPT_URLPATTERN_PATH} / "resources" / + "urlpattern-compare-test-data.json"; + + const auto compare_test_data{sourcemeta::core::read_json(compare_test_path)}; + + std::size_t compare_index = 0; + for (const auto &test_case : compare_test_data.as_array()) { + const auto &left = test_case.at("left"); + const auto &right = test_case.at("right"); + const auto expected = test_case.at("expected").to_integer(); + const std::string component = test_case.at("component").to_string(); + + const std::string test_name = "compare_" + std::to_string(compare_index); + + testing::RegisterTest( + "URLPattern_wpt_compare", test_name.c_str(), nullptr, nullptr, __FILE__, + __LINE__, [=]() -> URLPatternCompareTest * { + return new URLPatternCompareTest(left, right, expected, component); + }); + + compare_index += 1; + } + + const std::filesystem::path match_test_path = + std::filesystem::path{WPT_URLPATTERN_PATH} / "resources" / + "urlpatterntestdata.json"; + + const auto match_test_data{sourcemeta::core::read_json(match_test_path)}; + + std::size_t match_index = 0; + for (const auto &test_case : match_test_data.as_array()) { + if (!do_we_handle_match_test(test_case)) { + std::cerr << "WARNING: Skipping match test index " << match_index << "\n"; + match_index += 1; + continue; + } + + const auto &pattern = test_case.at("pattern").at(0); + const auto &input = test_case.at("inputs").at(0); + const auto &expected_match = test_case.at("expected_match"); + + const std::string test_name = "match_" + std::to_string(match_index); + + testing::RegisterTest( + "URLPattern_wpt_match", test_name.c_str(), nullptr, nullptr, __FILE__, + __LINE__, [=]() -> URLPatternMatchTest * { + return new URLPatternMatchTest(pattern, input, expected_match); + }); + + match_index += 1; + } + + return RUN_ALL_TESTS(); +} diff --git a/vendor/wpt.mask b/vendor/wpt.mask new file mode 100644 index 000000000..b51326320 --- /dev/null +++ b/vendor/wpt.mask @@ -0,0 +1,303 @@ +accelerometer/ +accessibility/ +accname/ +acid/ +ai/ +ambient-light/ +animation-worklet/ +annotation-model/ +annotation-protocol/ +annotation-vocab/ +apng/ +appmanifest/ +attribution-reporting/ +audio-output/ +autoplay-policy-detection/ +avif/ +background-fetch/ +background-sync/ +badging/ +battery-status/ +beacon/ +bluetooth/ +browsing-topics/ +captured-mouse-events/ +clear-site-data/ +client-hints/ +clipboard-apis/ +close-watcher/ +CODE_OF_CONDUCT.md +CODEOWNERS +common/ +compat/ +compression/ +compute-pressure/ +conformance-checkers/ +connection-allowlist/ +console/ +contacts/ +container-timing/ +content-dpr/ +content-index/ +content-security-policy/ +contenteditable/ +CONTRIBUTING.md +cookies/ +cookiestore/ +core-aam/ +cors/ +cpu-performance/ +credential-management/ +css/ +custom-elements/ +delegated-ink/ +density-size-correction/ +deprecation-reporting/ +device-bound-session-credentials/ +device-memory/ +device-posture/ +digital-credentials/ +direct-sockets/ +docs/ +document-picture-in-picture/ +document-policy/ +dom/ +domparsing/ +domxpath/ +dpub-aam/ +dpub-aria/ +ecmascript/ +editing/ +element-timing/ +encoding/ +encoding-detection/ +encrypted-media/ +entries-api/ +event-timing/ +eventsource/ +eyedropper/ +feature-policy/ +fedcm/ +fenced-frame/ +fetch/ +file-system-access/ +FileAPI/ +fledge/ +focus/ +font-access/ +fonts/ +forced-colors-mode/ +fs/ +fullscreen/ +gamepad/ +generic-sensor/ +geolocation/ +geolocation-sensor/ +gif/ +gpc/ +graphics-aam/ +graphics-aria/ +gyroscope/ +hr-time/ +hsts/ +html/ +html-aam/ +html-longdesc/ +html-media-capture/ +https-upgrades/ +idle-detection/ +imagebitmap-renderingcontext/ +images/ +import-maps/ +IndexedDB/ +inert/ +infrastructure/ +input-device-capabilities/ +input-events/ +installedapp/ +interfaces/ +intersection-observer/ +intervention-reporting/ +is-input-pending/ +jpegxl/ +js/ +js-self-profiling/ +keyboard-lock/ +keyboard-map/ +largest-contentful-paint/ +layout-instability/ +lint.ignore +loading/ +long-animation-frame/ +longtask-timing/ +magnetometer/ +managed/ +mathml/ +measure-memory/ +media/ +media-capabilities/ +media-playback-quality/ +media-source/ +mediacapture-extensions/ +mediacapture-fromelement/ +mediacapture-handle/ +mediacapture-image/ +mediacapture-insertable-streams/ +mediacapture-record/ +mediacapture-region/ +mediacapture-streams/ +mediasession/ +merchant-validation/ +mimesniff/ +mixed-content/ +mst-content-hint/ +nav-tracking-mitigations/ +navigation-api/ +navigation-timing/ +netinfo/ +network-error-logging/ +notifications/ +old-tests/ +orientation-event/ +orientation-sensor/ +page-lifecycle/ +page-visibility/ +paint-timing/ +parakeet/ +payment-handler/ +payment-method-basic-card/ +payment-method-id/ +payment-request/ +performance-timeline/ +periodic-background-sync/ +permissions/ +permissions-policy/ +permissions-request/ +permissions-revoke/ +picture-in-picture/ +png/ +pointerevents/ +pointerlock/ +preload/ +presentation-api/ +print/ +private-aggregation/ +private-click-measurement/ +proximity/ +push-api/ +quirks/ +README.md +referrer-policy/ +remote-playback/ +reporting/ +requestidlecallback/ +resize-observer/ +resource-timing/ +resources/ +sanitizer-api/ +savedata/ +scheduler/ +screen-capture/ +screen-details/ +screen-orientation/ +screen-wake-lock/ +scroll-animations/ +scroll-to-text-fragment/ +secure-contexts/ +secure-payment-confirmation/ +selection/ +serial/ +server-timing/ +service-workers/ +shadow-dom/ +shape-detection/ +shared-storage/ +shared-storage-selecturl-limit/ +signed-exchange/ +soft-navigation-heuristics/ +speculation-rules/ +speech-api/ +storage/ +storage-access-api/ +streams/ +subapps/ +subresource-integrity/ +svg/ +svg-aam/ +timing-entrytypes-registry/ +tools/ +top-level-storage-access-api/ +touch-events/ +trust-tokens/ +trusted-types/ +ua-client-hints/ +uievents/ +upgrade-insecure-requests/ +url/ +user-timing/ +vibration/ +video-rvfc/ +viewport/ +viewport-segments/ +virtual-keyboard/ +visual-viewport/ +wai-aria/ +wasm/ +web-animations/ +web-bundle/ +web-extensions/ +web-locks/ +web-nfc/ +web-otp/ +web-share/ +webaudio/ +webauthn/ +webcodecs/ +WebCryptoAPI/ +webdriver/ +webgl/ +webgpu/ +webhid/ +webidl/ +webmessaging/ +webmidi/ +webnn/ +webrtc/ +webrtc-encoded-transform/ +webrtc-extensions/ +webrtc-ice/ +webrtc-identity/ +webrtc-priority/ +webrtc-stats/ +webrtc-svc/ +websockets/ +webstorage/ +webtransport/ +webusb/ +webvr/ +webvtt/ +webxr/ +window-management/ +workers/ +worklets/ +wpt +wpt.py +x-frame-options/ +xhr/ +xml/ +.well-known +.azure-pipelines.yml +.mailmap +.taskcluster.yml +urlpattern/META.yml +urlpattern/WEB_FEATURES.yml +urlpattern/resources/urlpattern-compare-tests.tentative.js +urlpattern/resources/urlpattern-hasregexpgroups-tests.js +urlpattern/resources/urlpatterntests.js +urlpattern/urlpattern-compare.tentative.any.js +urlpattern/urlpattern-compare.tentative.https.any.js +urlpattern/urlpattern-constructor.html +urlpattern/urlpattern-generate.tentative.any.js +urlpattern/urlpattern-hasregexpgroups.any.js +urlpattern/urlpattern.any.js +urlpattern/urlpattern.https.any.js diff --git a/vendor/wpt/LICENSE.md b/vendor/wpt/LICENSE.md new file mode 100644 index 000000000..39c46d03a --- /dev/null +++ b/vendor/wpt/LICENSE.md @@ -0,0 +1,11 @@ +# The 3-Clause BSD License + +Copyright © web-platform-tests contributors + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/wpt/urlpattern/resources/urlpattern-compare-test-data.json b/vendor/wpt/urlpattern/resources/urlpattern-compare-test-data.json new file mode 100644 index 000000000..0043ac08b --- /dev/null +++ b/vendor/wpt/urlpattern/resources/urlpattern-compare-test-data.json @@ -0,0 +1,152 @@ +[ + { + "component": "pathname", + "left": { "pathname": "/foo/a" }, + "right": { "pathname": "/foo/b" }, + "expected": -1 + }, + { + "component": "pathname", + "left": { "pathname": "/foo/b" }, + "right": { "pathname": "/foo/bar" }, + "expected": -1 + }, + { + "component": "pathname", + "left": { "pathname": "/foo/bar" }, + "right": { "pathname": "/foo/:bar" }, + "expected": 1 + }, + { + "component": "pathname", + "left": { "pathname": "/foo/" }, + "right": { "pathname": "/foo/:bar" }, + "expected": 1 + }, + { + "component": "pathname", + "left": { "pathname": "/foo/:bar" }, + "right": { "pathname": "/foo/*" }, + "expected": 1 + }, + { + "component": "pathname", + "left": { "pathname": "/foo/{bar}" }, + "right": { "pathname": "/foo/(bar)" }, + "expected": 1 + }, + { + "component": "pathname", + "left": { "pathname": "/foo/{bar}" }, + "right": { "pathname": "/foo/{bar}+" }, + "expected": 1 + }, + { + "component": "pathname", + "left": { "pathname": "/foo/{bar}+" }, + "right": { "pathname": "/foo/{bar}?" }, + "expected": 1 + }, + { + "component": "pathname", + "left": { "pathname": "/foo/{bar}?" }, + "right": { "pathname": "/foo/{bar}*" }, + "expected": 1 + }, + { + "component": "pathname", + "left": { "pathname": "/foo/(123)" }, + "right": { "pathname": "/foo/(12)" }, + "expected": 1 + }, + { + "component": "pathname", + "left": { "pathname": "/foo/:b" }, + "right": { "pathname": "/foo/:a" }, + "expected": 0 + }, + { + "component": "pathname", + "left": { "pathname": "*/foo" }, + "right": { "pathname": "*" }, + "expected": 1 + }, + { + "component": "port", + "left": { "port": "9" }, + "right": { "port": "100" }, + "expected": 1 + }, + { + "component": "pathname", + "left": { "pathname": "foo/:bar?/baz" }, + "right": { "pathname": "foo/{:bar}?/baz" }, + "expected": -1 + }, + { + "component": "pathname", + "left": { "pathname": "foo/:bar?/baz" }, + "right": { "pathname": "foo{/:bar}?/baz" }, + "expected": 0 + }, + { + "component": "pathname", + "left": { "pathname": "foo/:bar?/baz" }, + "right": { "pathname": "fo{o/:bar}?/baz" }, + "expected": 1 + }, + { + "component": "pathname", + "left": { "pathname": "foo/:bar?/baz" }, + "right": { "pathname": "foo{/:bar/}?baz" }, + "expected": -1 + }, + { + "component": "pathname", + "left": "https://a.example.com/b?a", + "right": "https://b.example.com/a?b", + "expected": 1 + }, + { + "component": "pathname", + "left": { "pathname": "/foo/{bar}/baz" }, + "right": { "pathname": "/foo/bar/baz" }, + "expected": 0 + }, + { + "component": "protocol", + "left": { "protocol": "a" }, + "right": { "protocol": "b" }, + "expected": -1 + }, + { + "component": "username", + "left": { "username": "a" }, + "right": { "username": "b" }, + "expected": -1 + }, + { + "component": "password", + "left": { "password": "a" }, + "right": { "password": "b" }, + "expected": -1 + }, + { + "component": "hostname", + "left": { "hostname": "a" }, + "right": { "hostname": "b" }, + "expected": -1 + }, + { + "component": "search", + "left": { "search": "a" }, + "right": { "search": "b" }, + "expected": -1 + }, + { + "component": "hash", + "left": { "hash": "a" }, + "right": { "hash": "b" }, + "expected": -1 + } +] diff --git a/vendor/wpt/urlpattern/resources/urlpattern-generate-test-data.json b/vendor/wpt/urlpattern/resources/urlpattern-generate-test-data.json new file mode 100644 index 000000000..c118f0a73 --- /dev/null +++ b/vendor/wpt/urlpattern/resources/urlpattern-generate-test-data.json @@ -0,0 +1,116 @@ +[ + { + "pattern": { "pathname": "/foo" }, + "component": "invalid", + "groups": {}, + "expected": null + }, + { + "pattern": { "pathname": "/foo" }, + "component": "pathname", + "groups": {}, + "expected": "/foo" + }, + { + "pattern": { "pathname": "/:foo" }, + "component": "pathname", + "groups": { "foo": "bar" }, + "expected": "/bar" + }, + { + "pattern": { "pathname": "/:foo" }, + "component": "pathname", + "groups": { "foo": "🍅" }, + "expected": "/%F0%9F%8D%85" + }, + { + "pattern": { "hostname": "{:foo}.example.com" }, + "component": "hostname", + "groups": { "foo": "🍅" }, + "expected": "xn--fi8h.example.com" + }, + { + "pattern": { "pathname": "/:foo" }, + "component": "pathname", + "groups": {}, + "expected": null + }, + { + "pattern": { "pathname": "/foo/:bar" }, + "component": "pathname", + "groups": { "bar": "baz" }, + "expected": "/foo/baz" + }, + { + "pattern": { "pathname": "/foo:bar" }, + "component": "pathname", + "groups": { "bar": "baz" }, + "expected": "/foobaz" + }, + { + "pattern": { "pathname": "/:foo/:bar" }, + "component": "pathname", + "groups": { "foo": "baz", "bar": "qux" }, + "expected": "/baz/qux" + }, + { + "pattern": "https://example.com/:foo", + "component": "pathname", + "groups": { "foo": " " }, + "expected": "/%20" + }, + { + "pattern": "original-scheme://example.com/:foo", + "component": "pathname", + "groups": { "foo": " " }, + "expected": "/ " + }, + { + "pattern": { "pathname": "/:foo" }, + "component": "pathname", + "groups": { "foo": "bar/baz" }, + "expected": null + }, + { + "pattern": { "pathname": "*" }, + "component": "pathname", + "groups": {}, + "expected": null + }, + { + "pattern": { "pathname": "/{foo}+" }, + "component": "pathname", + "groups": {}, + "expected": null + }, + { + "pattern": { "pathname": "/{foo}?" }, + "component": "pathname", + "groups": {}, + "expected": null + }, + { + "pattern": { "pathname": "/{foo}*" }, + "component": "pathname", + "groups": {}, + "expected": null + }, + { + "pattern": { "pathname": "/(regexp)" }, + "component": "pathname", + "groups": {}, + "expected": null + }, + { + "pattern": { "pathname": "/([^\\/]+?)" }, + "component": "pathname", + "groups": {}, + "expected": null + }, + { + "pattern": { "port": "([^\\:]+?)" }, + "component": "port", + "groups": {}, + "expected": null + } +] diff --git a/vendor/wpt/urlpattern/resources/urlpatterntestdata.json b/vendor/wpt/urlpattern/resources/urlpatterntestdata.json new file mode 100644 index 000000000..ae10c412a --- /dev/null +++ b/vendor/wpt/urlpattern/resources/urlpatterntestdata.json @@ -0,0 +1,3092 @@ +[ + { + "pattern": [{ "pathname": "/foo/bar" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": {} } + } + }, + { + "pattern": [{ "pathname": "/foo/bar" }], + "inputs": [{ "pathname": "/foo/ba" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/bar" }], + "inputs": [{ "pathname": "/foo/bar/" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/bar" }], + "inputs": [{ "pathname": "/foo/bar/baz" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/bar" }], + "inputs": [ "https://example.com/foo/bar" ], + "expected_match": { + "hostname": { "input": "example.com", "groups": { "0": "example.com" } }, + "pathname": { "input": "/foo/bar", "groups": {} }, + "protocol": { "input": "https", "groups": { "0": "https" } } + } + }, + { + "pattern": [{ "pathname": "/foo/bar" }], + "inputs": [ "https://example.com/foo/bar/baz" ], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/bar" }], + "inputs": [{ "hostname": "example.com", "pathname": "/foo/bar" }], + "expected_match": { + "hostname": { "input": "example.com", "groups": { "0": "example.com" } }, + "pathname": { "input": "/foo/bar", "groups": {} } + } + }, + { + "pattern": [{ "pathname": "/foo/bar" }], + "inputs": [{ "hostname": "example.com", "pathname": "/foo/bar/baz" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/bar" }], + "inputs": [{ "pathname": "/foo/bar", "baseURL": "https://example.com" }], + "expected_match": { + "hostname": { "input": "example.com", "groups": { "0": "example.com" } }, + "pathname": { "input": "/foo/bar", "groups": {} }, + "protocol": { "input": "https", "groups": { "0": "https" } } + } + }, + { + "pattern": [{ "pathname": "/foo/bar" }], + "inputs": [{ "pathname": "/foo/bar/baz", + "baseURL": "https://example.com" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com?query#hash" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com?query#hash" }], + "inputs": [{ "hostname": "example.com", "pathname": "/foo/bar" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com?query#hash" }], + "inputs": [{ "protocol": "https", "hostname": "example.com", + "pathname": "/foo/bar" }], + "exactly_empty_components": [ "port" ], + "expected_match": { + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/foo/bar", "groups": {} }, + "protocol": { "input": "https", "groups": {} } + } + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com" }], + "inputs": [{ "protocol": "https", "hostname": "example.com", + "pathname": "/foo/bar" }], + "exactly_empty_components": [ "port" ], + "expected_match": { + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/foo/bar", "groups": {} }, + "protocol": { "input": "https", "groups": {} } + } + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com" }], + "inputs": [{ "protocol": "https", "hostname": "example.com", + "pathname": "/foo/bar/baz" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com?query#hash" }], + "inputs": [{ "protocol": "https", "hostname": "example.com", + "pathname": "/foo/bar", "search": "otherquery", + "hash": "otherhash" }], + "exactly_empty_components": [ "port" ], + "expected_match": { + "hash": { "input": "otherhash", "groups": { "0": "otherhash" } }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/foo/bar", "groups": {} }, + "protocol": { "input": "https", "groups": {} }, + "search": { "input": "otherquery", "groups": { "0": "otherquery" } } + } + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com" }], + "inputs": [{ "protocol": "https", "hostname": "example.com", + "pathname": "/foo/bar", "search": "otherquery", + "hash": "otherhash" }], + "exactly_empty_components": [ "port" ], + "expected_match": { + "hash": { "input": "otherhash", "groups": { "0": "otherhash" } }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/foo/bar", "groups": {} }, + "protocol": { "input": "https", "groups": {} }, + "search": { "input": "otherquery", "groups": { "0": "otherquery" } } + } + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com?otherquery#otherhash" }], + "inputs": [{ "protocol": "https", "hostname": "example.com", + "pathname": "/foo/bar", "search": "otherquery", + "hash": "otherhash" }], + "exactly_empty_components": [ "port" ], + "expected_match": { + "hash": { "input": "otherhash", "groups": { "0": "otherhash" } }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/foo/bar", "groups": {} }, + "protocol": { "input": "https", "groups": {} }, + "search": { "input": "otherquery", "groups": { "0": "otherquery" } } + } + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com?query#hash" }], + "inputs": [ "https://example.com/foo/bar" ], + "exactly_empty_components": [ "port" ], + "expected_match": { + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/foo/bar", "groups": {} }, + "protocol": { "input": "https", "groups": {} } + } + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com?query#hash" }], + "inputs": [ "https://example.com/foo/bar?otherquery#otherhash" ], + "exactly_empty_components": [ "port" ], + "expected_match": { + "hash": { "input": "otherhash", "groups": { "0": "otherhash" } }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/foo/bar", "groups": {} }, + "protocol": { "input": "https", "groups": {} }, + "search": { "input": "otherquery", "groups": { "0": "otherquery" } } + } + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com?query#hash" }], + "inputs": [ "https://example.com/foo/bar?query#hash" ], + "exactly_empty_components": [ "port" ], + "expected_match": { + "hash": { "input": "hash", "groups": { "0": "hash" } }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/foo/bar", "groups": {} }, + "protocol": { "input": "https", "groups": {} }, + "search": { "input": "query", "groups": { "0": "query" } } + } + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com?query#hash" }], + "inputs": [ "https://example.com/foo/bar/baz" ], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com?query#hash" }], + "inputs": [ "https://other.com/foo/bar" ], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com?query#hash" }], + "inputs": [ "http://other.com/foo/bar" ], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com?query#hash" }], + "inputs": [{ "pathname": "/foo/bar", "baseURL": "https://example.com" }], + "exactly_empty_components": [ "port" ], + "expected_match": { + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/foo/bar", "groups": {} }, + "protocol": { "input": "https", "groups": {} } + } + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com?query#hash" }], + "inputs": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com?query#hash" }], + "exactly_empty_components": [ "port" ], + "expected_match": { + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/foo/bar", "groups": {} }, + "protocol": { "input": "https", "groups": {} } + } + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com?query#hash" }], + "inputs": [{ "pathname": "/foo/bar/baz", + "baseURL": "https://example.com" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com?query#hash" }], + "inputs": [{ "pathname": "/foo/bar", "baseURL": "https://other.com" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com?query#hash" }], + "inputs": [{ "pathname": "/foo/bar", "baseURL": "http://example.com" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/:bar" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": { "bar": "bar" } } + } + }, + { + "pattern": [{ "pathname": "/foo/([^\\/]+?)" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": { "0": "bar" } } + } + }, + { + "pattern": [{ "pathname": "/foo/:bar" }], + "inputs": [{ "pathname": "/foo/index.html" }], + "expected_match": { + "pathname": { "input": "/foo/index.html", "groups": { "bar": "index.html" } } + } + }, + { + "pattern": [{ "pathname": "/foo/:bar" }], + "inputs": [{ "pathname": "/foo/bar/" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/:bar" }], + "inputs": [{ "pathname": "/foo/" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/(.*)" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_obj": { + "pathname": "/foo/*" + }, + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": { "0": "bar" } } + } + }, + { + "pattern": [{ "pathname": "/foo/*" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": { "0": "bar" } } + } + }, + { + "pattern": [{ "pathname": "/foo/(.*)" }], + "inputs": [{ "pathname": "/foo/bar/baz" }], + "expected_obj": { + "pathname": "/foo/*" + }, + "expected_match": { + "pathname": { "input": "/foo/bar/baz", "groups": { "0": "bar/baz" } } + } + }, + { + "pattern": [{ "pathname": "/foo/*" }], + "inputs": [{ "pathname": "/foo/bar/baz" }], + "expected_match": { + "pathname": { "input": "/foo/bar/baz", "groups": { "0": "bar/baz" } } + } + }, + { + "pattern": [{ "pathname": "/foo/(.*)" }], + "inputs": [{ "pathname": "/foo/" }], + "expected_obj": { + "pathname": "/foo/*" + }, + "expected_match": { + "pathname": { "input": "/foo/", "groups": { "0": "" } } + } + }, + { + "pattern": [{ "pathname": "/foo/*" }], + "inputs": [{ "pathname": "/foo/" }], + "expected_match": { + "pathname": { "input": "/foo/", "groups": { "0": "" } } + } + }, + { + "pattern": [{ "pathname": "/foo/(.*)" }], + "inputs": [{ "pathname": "/foo" }], + "expected_obj": { + "pathname": "/foo/*" + }, + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/*" }], + "inputs": [{ "pathname": "/foo" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/:bar(.*)" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": { "bar": "bar" } } + } + }, + { + "pattern": [{ "pathname": "/foo/:bar(.*)" }], + "inputs": [{ "pathname": "/foo/bar/baz" }], + "expected_match": { + "pathname": { "input": "/foo/bar/baz", "groups": { "bar": "bar/baz" } } + } + }, + { + "pattern": [{ "pathname": "/foo/:bar(.*)" }], + "inputs": [{ "pathname": "/foo/" }], + "expected_match": { + "pathname": { "input": "/foo/", "groups": { "bar": "" } } + } + }, + { + "pattern": [{ "pathname": "/foo/:bar(.*)" }], + "inputs": [{ "pathname": "/foo" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/:bar?" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": { "bar": "bar" } } + } + }, + { + "pattern": [{ "pathname": "/foo/:bar?" }], + "inputs": [{ "pathname": "/foo" }], + "//": "The `null` below is translated to undefined in the test harness.", + "expected_match": { + "pathname": { "input": "/foo", "groups": { "bar": null } } + } + }, + { + "pattern": [{ "pathname": "/foo/:bar?" }], + "inputs": [{ "pathname": "/foo/" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/:bar?" }], + "inputs": [{ "pathname": "/foobar" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/:bar?" }], + "inputs": [{ "pathname": "/foo/bar/baz" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/:bar+" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": { "bar": "bar" } } + } + }, + { + "pattern": [{ "pathname": "/foo/:bar+" }], + "inputs": [{ "pathname": "/foo/bar/baz" }], + "expected_match": { + "pathname": { "input": "/foo/bar/baz", "groups": { "bar": "bar/baz" } } + } + }, + { + "pattern": [{ "pathname": "/foo/:bar+" }], + "inputs": [{ "pathname": "/foo" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/:bar+" }], + "inputs": [{ "pathname": "/foo/" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/:bar+" }], + "inputs": [{ "pathname": "/foobar" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/:bar*" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": { "bar": "bar" } } + } + }, + { + "pattern": [{ "pathname": "/foo/:bar*" }], + "inputs": [{ "pathname": "/foo/bar/baz" }], + "expected_match": { + "pathname": { "input": "/foo/bar/baz", "groups": { "bar": "bar/baz" } } + } + }, + { + "pattern": [{ "pathname": "/foo/:bar*" }], + "inputs": [{ "pathname": "/foo" }], + "//": "The `null` below is translated to undefined in the test harness.", + "expected_match": { + "pathname": { "input": "/foo", "groups": { "bar": null } } + } + }, + { + "pattern": [{ "pathname": "/foo/:bar*" }], + "inputs": [{ "pathname": "/foo/" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/:bar*" }], + "inputs": [{ "pathname": "/foobar" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/(.*)?" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_obj": { + "pathname": "/foo/*?" + }, + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": { "0": "bar" } } + } + }, + { + "pattern": [{ "pathname": "/foo/*?" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": { "0": "bar" } } + } + }, + { + "pattern": [{ "pathname": "/foo/(.*)?" }], + "inputs": [{ "pathname": "/foo/bar/baz" }], + "expected_obj": { + "pathname": "/foo/*?" + }, + "expected_match": { + "pathname": { "input": "/foo/bar/baz", "groups": { "0": "bar/baz" } } + } + }, + { + "pattern": [{ "pathname": "/foo/*?" }], + "inputs": [{ "pathname": "/foo/bar/baz" }], + "expected_match": { + "pathname": { "input": "/foo/bar/baz", "groups": { "0": "bar/baz" } } + } + }, + { + "pattern": [{ "pathname": "/foo/(.*)?" }], + "inputs": [{ "pathname": "/foo" }], + "expected_obj": { + "pathname": "/foo/*?" + }, + "//": "The `null` below is translated to undefined in the test harness.", + "expected_match": { + "pathname": { "input": "/foo", "groups": { "0": null } } + } + }, + { + "pattern": [{ "pathname": "/foo/*?" }], + "inputs": [{ "pathname": "/foo" }], + "//": "The `null` below is translated to undefined in the test harness.", + "expected_match": { + "pathname": { "input": "/foo", "groups": { "0": null } } + } + }, + { + "pattern": [{ "pathname": "/foo/(.*)?" }], + "inputs": [{ "pathname": "/foo/" }], + "expected_obj": { + "pathname": "/foo/*?" + }, + "expected_match": { + "pathname": { "input": "/foo/", "groups": { "0": "" } } + } + }, + { + "pattern": [{ "pathname": "/foo/*?" }], + "inputs": [{ "pathname": "/foo/" }], + "expected_match": { + "pathname": { "input": "/foo/", "groups": { "0": "" } } + } + }, + { + "pattern": [{ "pathname": "/foo/(.*)?" }], + "inputs": [{ "pathname": "/foobar" }], + "expected_obj": { + "pathname": "/foo/*?" + }, + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/*?" }], + "inputs": [{ "pathname": "/foobar" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/(.*)?" }], + "inputs": [{ "pathname": "/fo" }], + "expected_obj": { + "pathname": "/foo/*?" + }, + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/*?" }], + "inputs": [{ "pathname": "/fo" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/(.*)+" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_obj": { + "pathname": "/foo/*+" + }, + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": { "0": "bar" } } + } + }, + { + "pattern": [{ "pathname": "/foo/*+" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": { "0": "bar" } } + } + }, + { + "pattern": [{ "pathname": "/foo/(.*)+" }], + "inputs": [{ "pathname": "/foo/bar/baz" }], + "expected_obj": { + "pathname": "/foo/*+" + }, + "expected_match": { + "pathname": { "input": "/foo/bar/baz", "groups": { "0": "bar/baz" } } + } + }, + { + "pattern": [{ "pathname": "/foo/*+" }], + "inputs": [{ "pathname": "/foo/bar/baz" }], + "expected_match": { + "pathname": { "input": "/foo/bar/baz", "groups": { "0": "bar/baz" } } + } + }, + { + "pattern": [{ "pathname": "/foo/(.*)+" }], + "inputs": [{ "pathname": "/foo" }], + "expected_obj": { + "pathname": "/foo/*+" + }, + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/*+" }], + "inputs": [{ "pathname": "/foo" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/(.*)+" }], + "inputs": [{ "pathname": "/foo/" }], + "expected_obj": { + "pathname": "/foo/*+" + }, + "expected_match": { + "pathname": { "input": "/foo/", "groups": { "0": "" } } + } + }, + { + "pattern": [{ "pathname": "/foo/*+" }], + "inputs": [{ "pathname": "/foo/" }], + "expected_match": { + "pathname": { "input": "/foo/", "groups": { "0": "" } } + } + }, + { + "pattern": [{ "pathname": "/foo/(.*)+" }], + "inputs": [{ "pathname": "/foobar" }], + "expected_obj": { + "pathname": "/foo/*+" + }, + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/*+" }], + "inputs": [{ "pathname": "/foobar" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/(.*)+" }], + "inputs": [{ "pathname": "/fo" }], + "expected_obj": { + "pathname": "/foo/*+" + }, + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/*+" }], + "inputs": [{ "pathname": "/fo" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/(.*)*" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_obj": { + "pathname": "/foo/**" + }, + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": { "0": "bar" } } + } + }, + { + "pattern": [{ "pathname": "/foo/**" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": { "0": "bar" } } + } + }, + { + "pattern": [{ "pathname": "/foo/(.*)*" }], + "inputs": [{ "pathname": "/foo/bar/baz" }], + "expected_obj": { + "pathname": "/foo/**" + }, + "expected_match": { + "pathname": { "input": "/foo/bar/baz", "groups": { "0": "bar/baz" } } + } + }, + { + "pattern": [{ "pathname": "/foo/**" }], + "inputs": [{ "pathname": "/foo/bar/baz" }], + "expected_match": { + "pathname": { "input": "/foo/bar/baz", "groups": { "0": "bar/baz" } } + } + }, + { + "pattern": [{ "pathname": "/foo/(.*)*" }], + "inputs": [{ "pathname": "/foo" }], + "expected_obj": { + "pathname": "/foo/**" + }, + "//": "The `null` below is translated to undefined in the test harness.", + "expected_match": { + "pathname": { "input": "/foo", "groups": { "0": null } } + } + }, + { + "pattern": [{ "pathname": "/foo/**" }], + "inputs": [{ "pathname": "/foo" }], + "//": "The `null` below is translated to undefined in the test harness.", + "expected_match": { + "pathname": { "input": "/foo", "groups": { "0": null } } + } + }, + { + "pattern": [{ "pathname": "/foo/(.*)*" }], + "inputs": [{ "pathname": "/foo/" }], + "expected_obj": { + "pathname": "/foo/**" + }, + "expected_match": { + "pathname": { "input": "/foo/", "groups": { "0": "" } } + } + }, + { + "pattern": [{ "pathname": "/foo/**" }], + "inputs": [{ "pathname": "/foo/" }], + "expected_match": { + "pathname": { "input": "/foo/", "groups": { "0": "" } } + } + }, + { + "pattern": [{ "pathname": "/foo/(.*)*" }], + "inputs": [{ "pathname": "/foobar" }], + "expected_obj": { + "pathname": "/foo/**" + }, + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/**" }], + "inputs": [{ "pathname": "/foobar" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/(.*)*" }], + "inputs": [{ "pathname": "/fo" }], + "expected_obj": { + "pathname": "/foo/**" + }, + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/**" }], + "inputs": [{ "pathname": "/fo" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo{/bar}" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_obj": { + "pathname": "/foo/bar" + }, + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": {} } + } + }, + { + "pattern": [{ "pathname": "/foo{/bar}" }], + "inputs": [{ "pathname": "/foo/bar/baz" }], + "expected_obj": { + "pathname": "/foo/bar" + }, + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo{/bar}" }], + "inputs": [{ "pathname": "/foo" }], + "expected_obj": { + "pathname": "/foo/bar" + }, + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo{/bar}" }], + "inputs": [{ "pathname": "/foo/" }], + "expected_obj": { + "pathname": "/foo/bar" + }, + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo{/bar}?" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": {} } + } + }, + { + "pattern": [{ "pathname": "/foo{/bar}?" }], + "inputs": [{ "pathname": "/foo/bar/baz" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo{/bar}?" }], + "inputs": [{ "pathname": "/foo" }], + "expected_match": { + "pathname": { "input": "/foo", "groups": {} } + } + }, + { + "pattern": [{ "pathname": "/foo{/bar}?" }], + "inputs": [{ "pathname": "/foo/" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo{/bar}+" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": {} } + } + }, + { + "pattern": [{ "pathname": "/foo{/bar}+" }], + "inputs": [{ "pathname": "/foo/bar/bar" }], + "expected_match": { + "pathname": { "input": "/foo/bar/bar", "groups": {} } + } + }, + { + "pattern": [{ "pathname": "/foo{/bar}+" }], + "inputs": [{ "pathname": "/foo/bar/baz" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo{/bar}+" }], + "inputs": [{ "pathname": "/foo" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo{/bar}+" }], + "inputs": [{ "pathname": "/foo/" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo{/bar}*" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": {} } + } + }, + { + "pattern": [{ "pathname": "/foo{/bar}*" }], + "inputs": [{ "pathname": "/foo/bar/bar" }], + "expected_match": { + "pathname": { "input": "/foo/bar/bar", "groups": {} } + } + }, + { + "pattern": [{ "pathname": "/foo{/bar}*" }], + "inputs": [{ "pathname": "/foo/bar/baz" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo{/bar}*" }], + "inputs": [{ "pathname": "/foo" }], + "expected_match": { + "pathname": { "input": "/foo", "groups": {} } + } + }, + { + "pattern": [{ "pathname": "/foo{/bar}*" }], + "inputs": [{ "pathname": "/foo/" }], + "expected_match": null + }, + { + "pattern": [{ "protocol": "(café)" }], + "expected_obj": "error" + }, + { + "pattern": [{ "username": "(café)" }], + "expected_obj": "error" + }, + { + "pattern": [{ "password": "(café)" }], + "expected_obj": "error" + }, + { + "pattern": [{ "hostname": "(café)" }], + "expected_obj": "error" + }, + { + "pattern": [{ "pathname": "(café)" }], + "expected_obj": "error" + }, + { + "pattern": [{ "search": "(café)" }], + "expected_obj": "error" + }, + { + "pattern": [{ "hash": "(café)" }], + "expected_obj": "error" + }, + { + "pattern": [{ "protocol": ":café" }], + "inputs": [{ "protocol": "foo" }], + "expected_match": { + "protocol": { "input": "foo", "groups": { "café": "foo" } } + } + }, + { + "pattern": [{ "username": ":café" }], + "inputs": [{ "username": "foo" }], + "expected_match": { + "username": { "input": "foo", "groups": { "café": "foo" } } + } + }, + { + "pattern": [{ "password": ":café" }], + "inputs": [{ "password": "foo" }], + "expected_match": { + "password": { "input": "foo", "groups": { "café": "foo" } } + } + }, + { + "pattern": [{ "hostname": ":café" }], + "inputs": [{ "hostname": "foo" }], + "expected_match": { + "hostname": { "input": "foo", "groups": { "café": "foo" } } + } + }, + { + "pattern": [{ "pathname": "/:café" }], + "inputs": [{ "pathname": "/foo" }], + "expected_match": { + "pathname": { "input": "/foo", "groups": { "café": "foo" } } + } + }, + { + "pattern": [{ "search": ":café" }], + "inputs": [{ "search": "foo" }], + "expected_match": { + "search": { "input": "foo", "groups": { "café": "foo" } } + } + }, + { + "pattern": [{ "hash": ":café" }], + "inputs": [{ "hash": "foo" }], + "expected_match": { + "hash": { "input": "foo", "groups": { "café": "foo" } } + } + }, + { + "pattern": [{ "protocol": ":\u2118" }], + "inputs": [{ "protocol": "foo" }], + "expected_match": { + "protocol": { "input": "foo", "groups": { "\u2118": "foo" } } + } + }, + { + "pattern": [{ "username": ":\u2118" }], + "inputs": [{ "username": "foo" }], + "expected_match": { + "username": { "input": "foo", "groups": { "\u2118": "foo" } } + } + }, + { + "pattern": [{ "password": ":\u2118" }], + "inputs": [{ "password": "foo" }], + "expected_match": { + "password": { "input": "foo", "groups": { "\u2118": "foo" } } + } + }, + { + "pattern": [{ "hostname": ":\u2118" }], + "inputs": [{ "hostname": "foo" }], + "expected_match": { + "hostname": { "input": "foo", "groups": { "\u2118": "foo" } } + } + }, + { + "pattern": [{ "pathname": "/:\u2118" }], + "inputs": [{ "pathname": "/foo" }], + "expected_match": { + "pathname": { "input": "/foo", "groups": { "\u2118": "foo" } } + } + }, + { + "pattern": [{ "search": ":\u2118" }], + "inputs": [{ "search": "foo" }], + "expected_match": { + "search": { "input": "foo", "groups": { "\u2118": "foo" } } + } + }, + { + "pattern": [{ "hash": ":\u2118" }], + "inputs": [{ "hash": "foo" }], + "expected_match": { + "hash": { "input": "foo", "groups": { "\u2118": "foo" } } + } + }, + { + "pattern": [{ "protocol": ":\u3400" }], + "inputs": [{ "protocol": "foo" }], + "expected_match": { + "protocol": { "input": "foo", "groups": { "\u3400": "foo" } } + } + }, + { + "pattern": [{ "username": ":\u3400" }], + "inputs": [{ "username": "foo" }], + "expected_match": { + "username": { "input": "foo", "groups": { "\u3400": "foo" } } + } + }, + { + "pattern": [{ "password": ":\u3400" }], + "inputs": [{ "password": "foo" }], + "expected_match": { + "password": { "input": "foo", "groups": { "\u3400": "foo" } } + } + }, + { + "pattern": [{ "hostname": ":\u3400" }], + "inputs": [{ "hostname": "foo" }], + "expected_match": { + "hostname": { "input": "foo", "groups": { "\u3400": "foo" } } + } + }, + { + "pattern": [{ "pathname": "/:\u3400" }], + "inputs": [{ "pathname": "/foo" }], + "expected_match": { + "pathname": { "input": "/foo", "groups": { "\u3400": "foo" } } + } + }, + { + "pattern": [{ "search": ":\u3400" }], + "inputs": [{ "search": "foo" }], + "expected_match": { + "search": { "input": "foo", "groups": { "\u3400": "foo" } } + } + }, + { + "pattern": [{ "hash": ":\u3400" }], + "inputs": [{ "hash": "foo" }], + "expected_match": { + "hash": { "input": "foo", "groups": { "\u3400": "foo" } } + } + }, + { + "pattern": [{ "protocol": "(.*)" }], + "inputs": [{ "protocol" : "café" }], + "expected_obj": { + "protocol": "*" + }, + "expected_match": null + }, + { + "pattern": [{ "protocol": "(.*)" }], + "inputs": [{ "protocol": "cafe" }], + "expected_obj": { + "protocol": "*" + }, + "expected_match": { + "protocol": { "input": "cafe", "groups": { "0": "cafe" }} + } + }, + { + "pattern": [{ "protocol": "foo-bar" }], + "inputs": [{ "protocol": "foo-bar" }], + "expected_match": { + "protocol": { "input": "foo-bar", "groups": {} } + } + }, + { + "pattern": [{ "username": "caf%C3%A9" }], + "inputs": [{ "username" : "café" }], + "expected_match": { + "username": { "input": "caf%C3%A9", "groups": {}} + } + }, + { + "pattern": [{ "username": "café" }], + "inputs": [{ "username" : "café" }], + "expected_obj": { + "username": "caf%C3%A9" + }, + "expected_match": { + "username": { "input": "caf%C3%A9", "groups": {}} + } + }, + { + "pattern": [{ "username": "caf%c3%a9" }], + "inputs": [{ "username" : "café" }], + "expected_match": null + }, + { + "pattern": [{ "password": "caf%C3%A9" }], + "inputs": [{ "password" : "café" }], + "expected_match": { + "password": { "input": "caf%C3%A9", "groups": {}} + } + }, + { + "pattern": [{ "password": "café" }], + "inputs": [{ "password" : "café" }], + "expected_obj": { + "password": "caf%C3%A9" + }, + "expected_match": { + "password": { "input": "caf%C3%A9", "groups": {}} + } + }, + { + "pattern": [{ "password": "caf%c3%a9" }], + "inputs": [{ "password" : "café" }], + "expected_match": null + }, + { + "pattern": [{ "hostname": "xn--caf-dma.com" }], + "inputs": [{ "hostname" : "café.com" }], + "expected_match": { + "hostname": { "input": "xn--caf-dma.com", "groups": {}} + } + }, + { + "pattern": [{ "hostname": "café.com" }], + "inputs": [{ "hostname" : "café.com" }], + "expected_obj": { + "hostname": "xn--caf-dma.com" + }, + "expected_match": { + "hostname": { "input": "xn--caf-dma.com", "groups": {}} + } + }, + { + "pattern": ["http://\uD83D\uDEB2.com/"], + "inputs": ["http://\uD83D\uDEB2.com/"], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "http", + "hostname": "xn--h78h.com", + "pathname": "/" + }, + "expected_match": { + "protocol": { "input": "http", "groups": {}}, + "hostname": { "input": "xn--h78h.com", "groups": {}}, + "pathname": { "input": "/", "groups": {}} + } + }, + { + "pattern": [{"pathname":":a\uDB40\uDD00b"}], + "inputs": [], + "expected_obj": { + "pathname": ":a\uDB40\uDD00b" + }, + "expected_match": null + }, + { + "pattern": [{"pathname":"test/:a\uD801\uDC50b"}], + "inputs": [{"pathname":"test/foo"}], + "expected_obj": { + "pathname": "test/:a\uD801\uDC50b" + }, + "expected_match": { + "pathname": { "input": "test/foo", "groups": { "a\uD801\uDC50b": "foo" }} + } + }, + { + "pattern": [{"pathname":":\uD83D\uDEB2"}], + "expected_obj": "error" + }, + { + "pattern": [{ "port": "" }], + "inputs": [{ "protocol": "http", "port": "80" }], + "exactly_empty_components": [ "port" ], + "expected_match": { + "protocol": { "input": "http", "groups": { "0": "http" }} + } + }, + { + "pattern": [{ "protocol": "http", "port": "80" }], + "inputs": [{ "protocol": "http", "port": "80" }], + "exactly_empty_components": [ "port" ], + "expected_match": { + "protocol": { "input": "http", "groups": {}} + } + }, + { + "pattern": [{ "protocol": "http", "port": "80{20}?" }], + "inputs": [{ "protocol": "http", "port": "80" }], + "expected_match": null + }, + { + "pattern": [{ "protocol": "http", "port": "80 " }], + "inputs": [{ "protocol": "http", "port": "80" }], + "expected_obj": { + "protocol": "http", + "port": "80" + }, + "expected_match": null + }, + { + "pattern": [{ "protocol": "http", "port": "100000" }], + "inputs": [{ "protocol": "http", "port": "100000" }], + "expected_obj": "error" + }, + { + "pattern": [{ "port": "80" }], + "inputs": [{ "protocol": "http", "port": "80" }], + "expected_match": null + }, + { + "pattern": [{ "protocol": "http{s}?", "port": "80" }], + "inputs": [{ "protocol": "http", "port": "80" }], + "expected_match": null + }, + { + "pattern": [{ "port": "80" }], + "inputs": [{ "port": "80" }], + "expected_match": { + "port": { "input": "80", "groups": {}} + } + }, + { + "pattern": [{ "port": "80" }], + "inputs": [{ "port": "8\t0" }], + "expected_match": { + "port": { "input": "80", "groups": {}} + } + }, + { + "pattern": [{ "port": "80" }], + "inputs": [{ "port": "80x" }], + "expected_match": { + "port": { "input": "80", "groups": {}} + } + }, + { + "pattern": [{ "port": "80" }], + "inputs": [{ "port": "80?x" }], + "expected_match": { + "port": { "input": "80", "groups": {}} + } + }, + { + "pattern": [{ "port": "80" }], + "inputs": [{ "port": "80\\x" }], + "expected_match": { + "port": { "input": "80", "groups": {}} + } + }, + { + "pattern": [{ "port": "(.*)" }], + "inputs": [{ "port": "invalid80" }], + "expected_obj": { + "port": "*" + }, + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/bar" }], + "inputs": [{ "pathname": "/foo/./bar" }], + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": {}} + } + }, + { + "pattern": [{ "pathname": "/foo/baz" }], + "inputs": [{ "pathname": "/foo/bar/../baz" }], + "expected_match": { + "pathname": { "input": "/foo/baz", "groups": {}} + } + }, + { + "pattern": [{ "pathname": "/caf%C3%A9" }], + "inputs": [{ "pathname": "/café" }], + "expected_match": { + "pathname": { "input": "/caf%C3%A9", "groups": {}} + } + }, + { + "pattern": [{ "pathname": "/café" }], + "inputs": [{ "pathname": "/café" }], + "expected_obj": { + "pathname": "/caf%C3%A9" + }, + "expected_match": { + "pathname": { "input": "/caf%C3%A9", "groups": {}} + } + }, + { + "pattern": [{ "pathname": "/caf%c3%a9" }], + "inputs": [{ "pathname": "/café" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/bar" }], + "inputs": [{ "pathname": "foo/bar" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/bar" }], + "inputs": [{ "pathname": "foo/bar", "baseURL": "https://example.com" }], + "expected_match": { + "protocol": { "input": "https", "groups": { "0": "https" }}, + "hostname": { "input": "example.com", "groups": { "0": "example.com" }}, + "pathname": { "input": "/foo/bar", "groups": {}} + } + }, + { + "pattern": [{ "pathname": "/foo/../bar" }], + "inputs": [{ "pathname": "/bar" }], + "expected_obj": { + "pathname": "/bar" + }, + "expected_match": { + "pathname": { "input": "/bar", "groups": {}} + } + }, + { + "pattern": [{ "pathname": "./foo/bar", "baseURL": "https://example.com" }], + "inputs": [{ "pathname": "foo/bar", "baseURL": "https://example.com" }], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "pathname": "/foo/bar" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {}}, + "hostname": { "input": "example.com", "groups": {}}, + "pathname": { "input": "/foo/bar", "groups": {}} + } + }, + { + "pattern": [{ "pathname": "", "baseURL": "https://example.com" }], + "inputs": [{ "pathname": "/", "baseURL": "https://example.com" }], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "pathname": "/" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {}}, + "hostname": { "input": "example.com", "groups": {}}, + "pathname": { "input": "/", "groups": {}} + } + }, + { + "pattern": [{ "pathname": "{/bar}", "baseURL": "https://example.com/foo/" }], + "inputs": [{ "pathname": "./bar", "baseURL": "https://example.com/foo/" }], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "pathname": "/bar" + }, + "expected_match": null + }, + { + "pattern": [{ "pathname": "\\/bar", "baseURL": "https://example.com/foo/" }], + "inputs": [{ "pathname": "./bar", "baseURL": "https://example.com/foo/" }], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "pathname": "/bar" + }, + "expected_match": null + }, + { + "pattern": [{ "pathname": "b", "baseURL": "https://example.com/foo/" }], + "inputs": [{ "pathname": "./b", "baseURL": "https://example.com/foo/" }], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "pathname": "/foo/b" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {}}, + "hostname": { "input": "example.com", "groups": {}}, + "pathname": { "input": "/foo/b", "groups": {}} + } + }, + { + "pattern": [{ "pathname": "foo/bar" }], + "inputs": [ "https://example.com/foo/bar" ], + "expected_match": null + }, + { + "pattern": [{ "pathname": "foo/bar", "baseURL": "https://example.com" }], + "inputs": [ "https://example.com/foo/bar" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "pathname": "/foo/bar" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {}}, + "hostname": { "input": "example.com", "groups": {}}, + "pathname": { "input": "/foo/bar", "groups": {}} + } + }, + { + "pattern": [{ "pathname": ":name.html", "baseURL": "https://example.com" }], + "inputs": [ "https://example.com/foo.html"] , + "exactly_empty_components": [ "port" ], + "expected_obj": { + "pathname": "/:name.html" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {}}, + "hostname": { "input": "example.com", "groups": {}}, + "pathname": { "input": "/foo.html", "groups": { "name": "foo" }} + } + }, + { + "pattern": [{ "search": "q=caf%C3%A9" }], + "inputs": [{ "search": "q=café" }], + "expected_match": { + "search": { "input": "q=caf%C3%A9", "groups": {}} + } + }, + { + "pattern": [{ "search": "q=café" }], + "inputs": [{ "search": "q=café" }], + "expected_obj": { + "search": "q=caf%C3%A9" + }, + "expected_match": { + "search": { "input": "q=caf%C3%A9", "groups": {}} + } + }, + { + "pattern": [{ "search": "q=caf%c3%a9" }], + "inputs": [{ "search": "q=café" }], + "expected_match": null + }, + { + "pattern": [{ "hash": "caf%C3%A9" }], + "inputs": [{ "hash": "café" }], + "expected_match": { + "hash": { "input": "caf%C3%A9", "groups": {}} + } + }, + { + "pattern": [{ "hash": "café" }], + "inputs": [{ "hash": "café" }], + "expected_obj": { + "hash": "caf%C3%A9" + }, + "expected_match": { + "hash": { "input": "caf%C3%A9", "groups": {}} + } + }, + { + "pattern": [{ "hash": "caf%c3%a9" }], + "inputs": [{ "hash": "café" }], + "expected_match": null + }, + { + "pattern": [{ "protocol": "about", "pathname": "(blank|sourcedoc)" }], + "inputs": [ "about:blank" ], + "expected_match": { + "protocol": { "input": "about", "groups": {}}, + "pathname": { "input": "blank", "groups": { "0": "blank" }} + } + }, + { + "pattern": [{ "protocol": "data", "pathname": ":number([0-9]+)" }], + "inputs": [ "data:8675309" ], + "expected_match": { + "protocol": { "input": "data", "groups": {}}, + "pathname": { "input": "8675309", "groups": { "number": "8675309" }} + } + }, + { + "pattern": [{ "pathname": "/(\\m)" }], + "expected_obj": "error" + }, + { + "pattern": [{ "pathname": "/foo!" }], + "inputs": [{ "pathname": "/foo!" }], + "expected_match": { + "pathname": { "input": "/foo!", "groups": {}} + } + }, + { + "pattern": [{ "pathname": "/foo\\:" }], + "inputs": [{ "pathname": "/foo:" }], + "expected_match": { + "pathname": { "input": "/foo:", "groups": {}} + } + }, + { + "pattern": [{ "pathname": "/foo\\{" }], + "inputs": [{ "pathname": "/foo{" }], + "expected_obj": { + "pathname": "/foo%7B" + }, + "expected_match": { + "pathname": { "input": "/foo%7B", "groups": {}} + } + }, + { + "pattern": [{ "pathname": "/foo\\(" }], + "inputs": [{ "pathname": "/foo(" }], + "expected_match": { + "pathname": { "input": "/foo(", "groups": {}} + } + }, + { + "pattern": [{ "protocol": "javascript", "pathname": "var x = 1;" }], + "inputs": [{ "protocol": "javascript", "pathname": "var x = 1;" }], + "expected_match": { + "protocol": { "input": "javascript", "groups": {}}, + "pathname": { "input": "var x = 1;", "groups": {}} + } + }, + { + "pattern": [{ "pathname": "var x = 1;" }], + "inputs": [{ "protocol": "javascript", "pathname": "var x = 1;" }], + "expected_obj": { + "pathname": "var%20x%20=%201;" + }, + "expected_match": null + }, + { + "pattern": [{ "protocol": "javascript", "pathname": "var x = 1;" }], + "inputs": [{ "baseURL": "javascript:var x = 1;" }], + "expected_match": { + "protocol": { "input": "javascript", "groups": {}}, + "pathname": { "input": "var x = 1;", "groups": {}} + } + }, + { + "pattern": [{ "protocol": "(data|javascript)", "pathname": "var x = 1;" }], + "inputs": [{ "protocol": "javascript", "pathname": "var x = 1;" }], + "expected_match": { + "protocol": { "input": "javascript", "groups": {"0": "javascript"}}, + "pathname": { "input": "var x = 1;", "groups": {}} + } + }, + { + "pattern": [{ "protocol": "(https|javascript)", "pathname": "var x = 1;" }], + "inputs": [{ "protocol": "javascript", "pathname": "var x = 1;" }], + "expected_obj": { + "pathname": "var%20x%20=%201;" + }, + "expected_match": null + }, + { + "pattern": [{ "pathname": "var x = 1;" }], + "inputs": [{ "pathname": "var x = 1;" }], + "expected_obj": { + "pathname": "var%20x%20=%201;" + }, + "expected_match": { + "pathname": { "input": "var%20x%20=%201;", "groups": {}} + } + }, + { + "pattern": [{ "pathname": "/foo/bar" }], + "inputs": [ "./foo/bar", "https://example.com" ], + "expected_match": { + "hostname": { "input": "example.com", "groups": { "0": "example.com" } }, + "pathname": { "input": "/foo/bar", "groups": {} }, + "protocol": { "input": "https", "groups": { "0": "https" } } + } + }, + { + "pattern": [{ "pathname": "/foo/bar" }], + "inputs": [ { "pathname": "/foo/bar" }, "https://example.com" ], + "expected_match": "error" + }, + { + "pattern": [ "https://example.com:8080/foo?bar#baz" ], + "inputs": [{ "pathname": "/foo", "search": "bar", "hash": "baz", + "baseURL": "https://example.com:8080" }], + "expected_obj": { + "protocol": "https", + "username": "*", + "password": "*", + "hostname": "example.com", + "port": "8080", + "pathname": "/foo", + "search": "bar", + "hash": "baz" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "port": { "input": "8080", "groups": {} }, + "pathname": { "input": "/foo", "groups": {} }, + "search": { "input": "bar", "groups": {} }, + "hash": { "input": "baz", "groups": {} } + } + }, + { + "pattern": [ "/foo?bar#baz", "https://example.com:8080" ], + "inputs": [{ "pathname": "/foo", "search": "bar", "hash": "baz", + "baseURL": "https://example.com:8080" }], + "expected_obj": { + "pathname": "/foo", + "search": "bar", + "hash": "baz" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "port": { "input": "8080", "groups": {} }, + "pathname": { "input": "/foo", "groups": {} }, + "search": { "input": "bar", "groups": {} }, + "hash": { "input": "baz", "groups": {} } + } + }, + { + "pattern": [ "/foo" ], + "expected_obj": "error" + }, + { + "pattern": [ "example.com/foo" ], + "expected_obj": "error" + }, + { + "pattern": [ "http{s}?://{*.}?example.com/:product/:endpoint" ], + "inputs": [ "https://sub.example.com/foo/bar" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "http{s}?", + "hostname": "{*.}?example.com", + "pathname": "/:product/:endpoint" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "sub.example.com", "groups": { "0": "sub" } }, + "pathname": { "input": "/foo/bar", "groups": { "product": "foo", + "endpoint": "bar" } } + } + }, + { + "pattern": [ "https://example.com?foo" ], + "inputs": [ "https://example.com/?foo" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "pathname": "/", + "search": "foo" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/", "groups": {} }, + "search": { "input": "foo", "groups": {} } + } + }, + { + "pattern": [ "https://example.com#foo" ], + "inputs": [ "https://example.com/#foo" ], + "exactly_empty_components": [ "port", "search" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "pathname": "/", + "hash": "foo" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/", "groups": {} }, + "hash": { "input": "foo", "groups": {} } + } + }, + { + "pattern": [ "https://example.com:8080?foo" ], + "inputs": [ "https://example.com:8080/?foo" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "port": "8080", + "pathname": "/", + "search": "foo" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "port": { "input": "8080", "groups": {} }, + "pathname": { "input": "/", "groups": {} }, + "search": { "input": "foo", "groups": {} } + } + }, + { + "pattern": [ "https://example.com:8080#foo" ], + "inputs": [ "https://example.com:8080/#foo" ], + "exactly_empty_components": [ "search" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "port": "8080", + "pathname": "/", + "hash": "foo" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "port": { "input": "8080", "groups": {} }, + "pathname": { "input": "/", "groups": {} }, + "hash": { "input": "foo", "groups": {} } + } + }, + { + "pattern": [ "https://example.com/?foo" ], + "inputs": [ "https://example.com/?foo" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "pathname": "/", + "search": "foo" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/", "groups": {} }, + "search": { "input": "foo", "groups": {} } + } + }, + { + "pattern": [ "https://example.com/#foo" ], + "inputs": [ "https://example.com/#foo" ], + "exactly_empty_components": [ "port", "search" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "pathname": "/", + "hash": "foo" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/", "groups": {} }, + "hash": { "input": "foo", "groups": {} } + } + }, + { + "pattern": [ "https://example.com/*?foo" ], + "inputs": [ "https://example.com/?foo" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "pathname": "/*?foo" + }, + "expected_match": null + }, + { + "pattern": [ "https://example.com/*\\?foo" ], + "inputs": [ "https://example.com/?foo" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "pathname": "/*", + "search": "foo" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/", "groups": { "0": "" } }, + "search": { "input": "foo", "groups": {} } + } + }, + { + "pattern": [ "https://example.com/:name?foo" ], + "inputs": [ "https://example.com/bar?foo" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "pathname": "/:name?foo" + }, + "expected_match": null + }, + { + "pattern": [ "https://example.com/:name\\?foo" ], + "inputs": [ "https://example.com/bar?foo" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "pathname": "/:name", + "search": "foo" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/bar", "groups": { "name": "bar" } }, + "search": { "input": "foo", "groups": {} } + } + }, + { + "pattern": [ "https://example.com/(bar)?foo" ], + "inputs": [ "https://example.com/bar?foo" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "pathname": "/(bar)?foo" + }, + "expected_match": null + }, + { + "pattern": [ "https://example.com/(bar)\\?foo" ], + "inputs": [ "https://example.com/bar?foo" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "pathname": "/(bar)", + "search": "foo" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/bar", "groups": { "0": "bar" } }, + "search": { "input": "foo", "groups": {} } + } + }, + { + "pattern": [ "https://example.com/{bar}?foo" ], + "inputs": [ "https://example.com/bar?foo" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "pathname": "/{bar}?foo" + }, + "expected_match": null + }, + { + "pattern": [ "https://example.com/{bar}\\?foo" ], + "inputs": [ "https://example.com/bar?foo" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "pathname": "/bar", + "search": "foo" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/bar", "groups": {} }, + "search": { "input": "foo", "groups": {} } + } + }, + { + "pattern": [ "https://example.com/" ], + "inputs": [ "https://example.com:8080/" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "port": "", + "pathname": "/" + }, + "expected_match": null + }, + { + "pattern": [ "data:foobar" ], + "inputs": [ "data:foobar" ], + "expected_obj": "error" + }, + { + "pattern": [ "data\\:foobar" ], + "inputs": [ "data:foobar" ], + "exactly_empty_components": [ "hostname", "port" ], + "expected_obj": { + "protocol": "data", + "pathname": "foobar" + }, + "expected_match": { + "protocol": { "input": "data", "groups": {} }, + "pathname": { "input": "foobar", "groups": {} } + } + }, + { + "pattern": [ "https://{sub.}?example.com/foo" ], + "inputs": [ "https://example.com/foo" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "{sub.}?example.com", + "pathname": "/foo" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/foo", "groups": {} } + } + }, + { + "pattern": [ "https://{sub.}?example{.com/}foo" ], + "inputs": [ "https://example.com/foo" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "{sub.}?example.com", + "pathname": "*" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/foo", "groups": { "0": "/foo" } } + } + }, + { + "pattern": [ "{https://}example.com/foo" ], + "inputs": [ "https://example.com/foo" ], + "expected_obj": "error" + }, + { + "pattern": [ "https://(sub.)?example.com/foo" ], + "inputs": [ "https://example.com/foo" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "(sub.)?example.com", + "pathname": "/foo" + }, + "//": "The `null` below is translated to undefined in the test harness.", + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": { "0": null } }, + "pathname": { "input": "/foo", "groups": {} } + } + }, + { + "pattern": [ "https://(sub.)?example(.com/)foo" ], + "inputs": [ "https://example.com/foo" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "(sub.)?example(.com/)foo", + "pathname": "*" + }, + "expected_match": null + }, + { + "pattern": [ "(https://)example.com/foo" ], + "inputs": [ "https://example.com/foo" ], + "expected_obj": "error" + }, + { + "pattern": [ "https://{sub{.}}example.com/foo" ], + "inputs": [ "https://example.com/foo" ], + "expected_obj": "error" + }, + { + "pattern": [ "https://(sub(?:.))?example.com/foo" ], + "inputs": [ "https://example.com/foo" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "(sub(?:.))?example.com", + "pathname": "/foo" + }, + "//": "The `null` below is translated to undefined in the test harness.", + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": { "0": null } }, + "pathname": { "input": "/foo", "groups": {} } + } + }, + { + "pattern": [ "file:///foo/bar" ], + "inputs": [ "file:///foo/bar" ], + "exactly_empty_components": [ "hostname", "port" ], + "expected_obj": { + "protocol": "file", + "pathname": "/foo/bar" + }, + "expected_match": { + "protocol": { "input": "file", "groups": {} }, + "pathname": { "input": "/foo/bar", "groups": {} } + } + }, + { + "pattern": [ "data:" ], + "inputs": [ "data:" ], + "exactly_empty_components": [ "hostname", "port", "pathname" ], + "expected_obj": { + "protocol": "data" + }, + "expected_match": { + "protocol": { "input": "data", "groups": {} } + } + }, + { + "pattern": [ "foo://bar" ], + "inputs": [ "foo://bad_url_browser_interop" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "foo", + "hostname": "bar" + }, + "expected_match": null + }, + { + "pattern": [ "(café)://foo" ], + "expected_obj": "error" + }, + { + "pattern": [ "https://example.com/foo?bar#baz" ], + "inputs": [{ "protocol": "https:", + "search": "?bar", + "hash": "#baz", + "baseURL": "http://example.com/foo" }], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "pathname": "/foo", + "search": "bar", + "hash": "baz" + }, + "expected_match": null + }, + { + "pattern": [{ "protocol": "http{s}?:", + "search": "?bar", + "hash": "#baz" }], + "inputs": [ "http://example.com/foo?bar#baz" ], + "expected_obj": { + "protocol": "http{s}?", + "search": "bar", + "hash": "baz" + }, + "expected_match": { + "protocol": { "input": "http", "groups": {} }, + "hostname": { "input": "example.com", "groups": { "0": "example.com" }}, + "pathname": { "input": "/foo", "groups": { "0": "/foo" }}, + "search": { "input": "bar", "groups": {} }, + "hash": { "input": "baz", "groups": {} } + } + }, + { + "pattern": [ "?bar#baz", "https://example.com/foo" ], + "inputs": [ "?bar#baz", "https://example.com/foo" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "pathname": "/foo", + "search": "bar", + "hash": "baz" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/foo", "groups": {} }, + "search": { "input": "bar", "groups": {} }, + "hash": { "input": "baz", "groups": {} } + } + }, + { + "pattern": [ "?bar", "https://example.com/foo#baz" ], + "inputs": [ "?bar", "https://example.com/foo#snafu" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "pathname": "/foo", + "search": "bar", + "hash": "*" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/foo", "groups": {} }, + "search": { "input": "bar", "groups": {} } + } + }, + { + "pattern": [ "#baz", "https://example.com/foo?bar" ], + "inputs": [ "#baz", "https://example.com/foo?bar" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "pathname": "/foo", + "search": "bar", + "hash": "baz" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/foo", "groups": {} }, + "search": { "input": "bar", "groups": {} }, + "hash": { "input": "baz", "groups": {} } + } + }, + { + "pattern": [ "#baz", "https://example.com/foo" ], + "inputs": [ "#baz", "https://example.com/foo" ], + "exactly_empty_components": [ "port", "search" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "pathname": "/foo", + "hash": "baz" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/foo", "groups": {} }, + "hash": { "input": "baz", "groups": {} } + } + }, + { + "pattern": [{ "pathname": "*" }], + "inputs": [ "foo", "data:data-urls-cannot-be-base-urls" ], + "expected_match": null + }, + { + "pattern": [{ "pathname": "*" }], + "inputs": [ "foo", "not|a|valid|url" ], + "expected_match": null + }, + { + "pattern": [ "https://foo\\:bar@example.com" ], + "inputs": [ "https://foo:bar@example.com" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "username": "foo", + "password": "bar", + "hostname": "example.com", + "pathname": "*" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "username": { "input": "foo", "groups": {} }, + "password": { "input": "bar", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/", "groups": { "0": "/" } } + } + }, + { + "pattern": [ "https://foo@example.com" ], + "inputs": [ "https://foo@example.com" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "username": "foo", + "hostname": "example.com", + "pathname": "*" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "username": { "input": "foo", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/", "groups": { "0": "/" } } + } + }, + { + "pattern": [ "https://\\:bar@example.com" ], + "inputs": [ "https://:bar@example.com" ], + "exactly_empty_components": [ "username", "port" ], + "expected_obj": { + "protocol": "https", + "password": "bar", + "hostname": "example.com", + "pathname": "*" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "password": { "input": "bar", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/", "groups": { "0": "/" } } + } + }, + { + "pattern": [ "https://:user::pass@example.com" ], + "inputs": [ "https://foo:bar@example.com" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "username": ":user", + "password": ":pass", + "hostname": "example.com", + "pathname": "*" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "username": { "input": "foo", "groups": { "user": "foo" } }, + "password": { "input": "bar", "groups": { "pass": "bar" } }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/", "groups": { "0": "/" } } + } + }, + { + "pattern": [ "https\\:foo\\:bar@example.com" ], + "inputs": [ "https:foo:bar@example.com" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "username": "foo", + "password": "bar", + "hostname": "example.com", + "pathname": "*" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "username": { "input": "foo", "groups": {} }, + "password": { "input": "bar", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/", "groups": { "0": "/" } } + } + }, + { + "pattern": [ "data\\:foo\\:bar@example.com" ], + "inputs": [ "data:foo:bar@example.com" ], + "exactly_empty_components": [ "hostname", "port" ], + "expected_obj": { + "protocol": "data", + "pathname": "foo\\:bar@example.com" + }, + "expected_match": { + "protocol": { "input": "data", "groups": {} }, + "pathname": { "input": "foo:bar@example.com", "groups": {} } + } + }, + { + "pattern": [ "https://foo{\\:}bar@example.com" ], + "inputs": [ "https://foo:bar@example.com" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "username": "foo%3Abar", + "hostname": "example.com" + }, + "expected_match": null + }, + { + "pattern": [ "data{\\:}channel.html", "https://example.com" ], + "inputs": [ "https://example.com/data:channel.html" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "pathname": "/data\\:channel.html", + "search": "*", + "hash": "*" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/data:channel.html", "groups": {} } + } + }, + { + "pattern": [ "http://[\\:\\:1]/" ], + "inputs": [ "http://[::1]/" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "http", + "hostname": "[\\:\\:1]", + "pathname": "/" + }, + "expected_match": { + "protocol": { "input": "http", "groups": {} }, + "hostname": { "input": "[::1]", "groups": {} }, + "pathname": { "input": "/", "groups": {} } + } + }, + { + "pattern": [ "http://[\\:\\:1]:8080/" ], + "inputs": [ "http://[::1]:8080/" ], + "expected_obj": { + "protocol": "http", + "hostname": "[\\:\\:1]", + "port": "8080", + "pathname": "/" + }, + "expected_match": { + "protocol": { "input": "http", "groups": {} }, + "hostname": { "input": "[::1]", "groups": {} }, + "port": { "input": "8080", "groups": {} }, + "pathname": { "input": "/", "groups": {} } + } + }, + { + "pattern": [ "http://[\\:\\:a]/" ], + "inputs": [ "http://[::a]/" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "http", + "hostname": "[\\:\\:a]", + "pathname": "/" + }, + "expected_match": { + "protocol": { "input": "http", "groups": {} }, + "hostname": { "input": "[::a]", "groups": {} }, + "pathname": { "input": "/", "groups": {} } + } + }, + { + "pattern": [ "http://[:address]/" ], + "inputs": [ "http://[::1]/" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "http", + "hostname": "[:address]", + "pathname": "/" + }, + "expected_match": { + "protocol": { "input": "http", "groups": {} }, + "hostname": { "input": "[::1]", "groups": { "address": "::1" }}, + "pathname": { "input": "/", "groups": {} } + } + }, + { + "pattern": [ "http://[\\:\\:AB\\::num]/" ], + "inputs": [ "http://[::ab:1]/" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "http", + "hostname": "[\\:\\:ab\\::num]", + "pathname": "/" + }, + "expected_match": { + "protocol": { "input": "http", "groups": {} }, + "hostname": { "input": "[::ab:1]", "groups": { "num": "1" }}, + "pathname": { "input": "/", "groups": {} } + } + }, + { + "pattern": [{ "hostname": "[\\:\\:AB\\::num]" }], + "inputs": [{ "hostname": "[::ab:1]" }], + "expected_obj": { + "hostname": "[\\:\\:ab\\::num]" + }, + "expected_match": { + "hostname": { "input": "[::ab:1]", "groups": { "num": "1" }} + } + }, + { + "pattern": [{ "hostname": "[\\:\\:xY\\::num]" }], + "expected_obj": "error" + }, + { + "pattern": [{ "hostname": "{[\\:\\:ab\\::num]}" }], + "inputs": [{ "hostname": "[::ab:1]" }], + "expected_match": { + "hostname": { "input": "[::ab:1]", "groups": { "num": "1" }} + } + }, + { + "pattern": [{ "hostname": "{[\\:\\:fé\\::num]}" }], + "expected_obj": "error" + }, + { + "pattern": [{ "hostname": "{[\\:\\::num\\:1]}" }], + "inputs": [{ "hostname": "[::ab:1]" }], + "expected_match": { + "hostname": { "input": "[::ab:1]", "groups": { "num": "ab" }} + } + }, + { + "pattern": [{ "hostname": "{[\\:\\::num\\:fé]}" }], + "expected_obj": "error" + }, + { + "pattern": [{ "hostname": "[*\\:1]" }], + "inputs": [{ "hostname": "[::ab:1]" }], + "expected_match": { + "hostname": { "input": "[::ab:1]", "groups": { "0": "::ab" }} + } + }, + { + "pattern": [{ "hostname": "*\\:1]" }], + "expected_obj": "error" + }, + { + "pattern": [ "https://foo{{@}}example.com" ], + "inputs": [ "https://foo@example.com" ], + "expected_obj": "error" + }, + { + "pattern": [ "https://foo{@example.com" ], + "inputs": [ "https://foo@example.com" ], + "expected_obj": "error" + }, + { + "pattern": [ "data\\:text/javascript,let x = 100/:tens?5;" ], + "inputs": [ "data:text/javascript,let x = 100/5;" ], + "exactly_empty_components": [ "hostname", "port" ], + "expected_obj": { + "protocol": "data", + "pathname": "text/javascript,let x = 100/:tens?5;" + }, + "//": "The `null` below is translated to undefined in the test harness.", + "expected_match": { + "protocol": { "input": "data", "groups": {} }, + "pathname": { "input": "text/javascript,let x = 100/5;", "groups": { "tens": null } } + } + }, + { + "pattern": [{ "pathname": "/:id/:id" }], + "expected_obj": "error" + }, + { + "pattern": [{ "pathname": "/foo", "baseURL": "" }], + "expected_obj": "error" + }, + { + "pattern": [ "/foo", "" ], + "expected_obj": "error" + }, + { + "pattern": [{ "pathname": "/foo" }, "https://example.com" ], + "expected_obj": "error" + }, + { + "pattern": [{ "pathname": ":name*" }], + "inputs": [{ "pathname": "foobar" }], + "expected_match": { + "pathname": { "input": "foobar", "groups": { "name": "foobar" }} + } + }, + { + "pattern": [{ "pathname": ":name+" }], + "inputs": [{ "pathname": "foobar" }], + "expected_match": { + "pathname": { "input": "foobar", "groups": { "name": "foobar" }} + } + }, + { + "pattern": [{ "pathname": ":name" }], + "inputs": [{ "pathname": "foobar" }], + "expected_match": { + "pathname": { "input": "foobar", "groups": { "name": "foobar" }} + } + }, + { + "pattern": [{ "protocol": ":name*" }], + "inputs": [{ "protocol": "foobar" }], + "expected_match": { + "protocol": { "input": "foobar", "groups": { "name": "foobar" }} + } + }, + { + "pattern": [{ "protocol": ":name+" }], + "inputs": [{ "protocol": "foobar" }], + "expected_match": { + "protocol": { "input": "foobar", "groups": { "name": "foobar" }} + } + }, + { + "pattern": [{ "protocol": ":name" }], + "inputs": [{ "protocol": "foobar" }], + "expected_match": { + "protocol": { "input": "foobar", "groups": { "name": "foobar" }} + } + }, + { + "pattern": [{ "hostname": "bad hostname" }], + "expected_obj": "error" + }, + { + "pattern": [{ "hostname": "bad#hostname" }], + "inputs": [{ "hostname": "bad" }], + "expected_obj": { + "hostname": "bad" + }, + "expected_match": { + "hostname": { "input": "bad", "groups": {} } + } + }, + { + "pattern": [{ "hostname": "bad%hostname" }], + "expected_obj": "error" + }, + { + "pattern": [{ "hostname": "bad/hostname" }], + "inputs": [{ "hostname": "bad" }], + "expected_obj": { + "hostname": "bad" + }, + "expected_match": { + "hostname": { "input": "bad", "groups": {} } + } + }, + { + "pattern": [{ "hostname": "bad\\:hostname" }], + "expected_obj": "error" + }, + { + "pattern": [{ "hostname": "badhostname" }], + "expected_obj": "error" + }, + { + "pattern": [{ "hostname": "bad?hostname" }], + "expected_obj": "error" + }, + { + "pattern": [{ "hostname": "bad@hostname" }], + "expected_obj": "error" + }, + { + "pattern": [{ "hostname": "bad[hostname" }], + "expected_obj": "error" + }, + { + "pattern": [{ "hostname": "bad]hostname" }], + "expected_obj": "error" + }, + { + "pattern": [{ "hostname": "bad\\\\hostname" }], + "inputs": [{ "hostname": "badhostname" }], + "expected_obj": { + "hostname": "bad" + }, + "expected_match": null + }, + { + "pattern": [{ "hostname": "bad^hostname" }], + "expected_obj": "error" + }, + { + "pattern": [{ "hostname": "bad|hostname" }], + "expected_obj": "error" + }, + { + "pattern": [{ "hostname": "bad\nhostname" }], + "inputs": [{ "hostname": "badhostname" }], + "expected_obj": { + "hostname": "badhostname" + }, + "expected_match": { + "hostname": { "input": "badhostname", "groups": {} } + } + }, + { + "pattern": [{ "hostname": "bad\rhostname" }], + "inputs": [{ "hostname": "badhostname" }], + "expected_obj": { + "hostname": "badhostname" + }, + "expected_match": { + "hostname": { "input": "badhostname", "groups": {} } + } + }, + { + "pattern": [{ "hostname": "bad\thostname" }], + "inputs": [{ "hostname": "badhostname" }], + "expected_obj": { + "hostname": "badhostname" + }, + "expected_match": { + "hostname": { "input": "badhostname", "groups": {} } + } + }, + { + "pattern": [{}], + "inputs": ["https://example.com/"], + "expected_match": { + "protocol": { "input": "https", "groups": { "0": "https" }}, + "hostname": { "input": "example.com", "groups": { "0": "example.com" }}, + "pathname": { "input": "/", "groups": { "0": "/" }} + } + }, + { + "pattern": [], + "inputs": ["https://example.com/"], + "expected_match": { + "protocol": { "input": "https", "groups": { "0": "https" }}, + "hostname": { "input": "example.com", "groups": { "0": "example.com" }}, + "pathname": { "input": "/", "groups": { "0": "/" }} + } + }, + { + "pattern": [], + "inputs": [{}], + "expected_match": {} + }, + { + "pattern": [], + "inputs": [], + "expected_match": { "inputs": [{}] } + }, + { + "pattern": [{ "pathname": "(foo)(.*)" }], + "inputs": [{ "pathname": "foobarbaz" }], + "expected_match": { + "pathname": { "input": "foobarbaz", "groups": { "0": "foo", "1": "barbaz" }} + } + }, + { + "pattern": [{ "pathname": "{(foo)bar}(.*)" }], + "inputs": [{ "pathname": "foobarbaz" }], + "expected_match": { + "pathname": { "input": "foobarbaz", "groups": { "0": "foo", "1": "baz" }} + } + }, + { + "pattern": [{ "pathname": "(foo)?(.*)" }], + "inputs": [{ "pathname": "foobarbaz" }], + "expected_obj": { + "pathname": "(foo)?*" + }, + "expected_match": { + "pathname": { "input": "foobarbaz", "groups": { "0": "foo", "1": "barbaz" }} + } + }, + { + "pattern": [{ "pathname": "{:foo}(.*)" }], + "inputs": [{ "pathname": "foobarbaz" }], + "expected_match": { + "pathname": { "input": "foobarbaz", "groups": { "foo": "f", "0": "oobarbaz" }} + } + }, + { + "pattern": [{ "pathname": "{:foo}(barbaz)" }], + "inputs": [{ "pathname": "foobarbaz" }], + "expected_match": { + "pathname": { "input": "foobarbaz", "groups": { "foo": "foo", "0": "barbaz" }} + } + }, + { + "pattern": [{ "pathname": "{:foo}{(.*)}" }], + "inputs": [{ "pathname": "foobarbaz" }], + "expected_obj": { + "pathname": "{:foo}(.*)" + }, + "expected_match": { + "pathname": { "input": "foobarbaz", "groups": { "foo": "f", "0": "oobarbaz" }} + } + }, + { + "pattern": [{ "pathname": "{:foo}{(.*)bar}" }], + "inputs": [{ "pathname": "foobarbaz" }], + "expected_obj": { + "pathname": ":foo{*bar}" + }, + "expected_match": null + }, + { + "pattern": [{ "pathname": "{:foo}{bar(.*)}" }], + "inputs": [{ "pathname": "foobarbaz" }], + "expected_obj": { + "pathname": ":foo{bar*}" + }, + "expected_match": { + "pathname": { "input": "foobarbaz", "groups": { "foo": "foo", "0": "baz" }} + } + }, + { + "pattern": [{ "pathname": "{:foo}:bar(.*)" }], + "inputs": [{ "pathname": "foobarbaz" }], + "expected_obj": { + "pathname": ":foo:bar(.*)" + }, + "expected_match": { + "pathname": { "input": "foobarbaz", "groups": { "foo": "f", "bar": "oobarbaz" }} + } + }, + { + "pattern": [{ "pathname": "{:foo}?(.*)" }], + "inputs": [{ "pathname": "foobarbaz" }], + "expected_obj": { + "pathname": ":foo?*" + }, + "expected_match": { + "pathname": { "input": "foobarbaz", "groups": { "foo": "f", "0": "oobarbaz" }} + } + }, + { + "pattern": [{ "pathname": "{:foo\\bar}" }], + "inputs": [{ "pathname": "foobar" }], + "expected_match": { + "pathname": { "input": "foobar", "groups": { "foo": "foo" }} + } + }, + { + "pattern": [{ "pathname": "{:foo\\.bar}" }], + "inputs": [{ "pathname": "foo.bar" }], + "expected_obj": { + "pathname": "{:foo.bar}" + }, + "expected_match": { + "pathname": { "input": "foo.bar", "groups": { "foo": "foo" }} + } + }, + { + "pattern": [{ "pathname": "{:foo(foo)bar}" }], + "inputs": [{ "pathname": "foobar" }], + "expected_match": { + "pathname": { "input": "foobar", "groups": { "foo": "foo" }} + } + }, + { + "pattern": [{ "pathname": "{:foo}bar" }], + "inputs": [{ "pathname": "foobar" }], + "expected_match": { + "pathname": { "input": "foobar", "groups": { "foo": "foo" }} + } + }, + { + "pattern": [{ "pathname": ":foo\\bar" }], + "inputs": [{ "pathname": "foobar" }], + "expected_obj": { + "pathname": "{:foo}bar" + }, + "expected_match": { + "pathname": { "input": "foobar", "groups": { "foo": "foo" }} + } + }, + { + "pattern": [{ "pathname": ":foo{}(.*)" }], + "inputs": [{ "pathname": "foobar" }], + "expected_obj": { + "pathname": "{:foo}(.*)" + }, + "expected_match": { + "pathname": { "input": "foobar", "groups": { "foo": "f", "0": "oobar" }} + } + }, + { + "pattern": [{ "pathname": ":foo{}bar" }], + "inputs": [{ "pathname": "foobar" }], + "expected_obj": { + "pathname": "{:foo}bar" + }, + "expected_match": { + "pathname": { "input": "foobar", "groups": { "foo": "foo" }} + } + }, + { + "pattern": [{ "pathname": ":foo{}?bar" }], + "inputs": [{ "pathname": "foobar" }], + "expected_obj": { + "pathname": "{:foo}bar" + }, + "expected_match": { + "pathname": { "input": "foobar", "groups": { "foo": "foo" }} + } + }, + { + "pattern": [{ "pathname": "*{}**?" }], + "inputs": [{ "pathname": "foobar" }], + "expected_obj": { + "pathname": "*(.*)?" + }, + "//": "The `null` below is translated to undefined in the test harness.", + "expected_match": { + "pathname": { "input": "foobar", "groups": { "0": "foobar", "1": null }} + } + }, + { + "pattern": [{ "pathname": ":foo(baz)(.*)" }], + "inputs": [{ "pathname": "bazbar" }], + "expected_match": { + "pathname": { "input": "bazbar", "groups": { "foo": "baz", "0": "bar" }} + } + }, + { + "pattern": [{ "pathname": ":foo(baz)bar" }], + "inputs": [{ "pathname": "bazbar" }], + "expected_match": { + "pathname": { "input": "bazbar", "groups": { "foo": "baz" }} + } + }, + { + "pattern": [{ "pathname": "*/*" }], + "inputs": [{ "pathname": "foo/bar" }], + "expected_match": { + "pathname": { "input": "foo/bar", "groups": { "0": "foo", "1": "bar" }} + } + }, + { + "pattern": [{ "pathname": "*\\/*" }], + "inputs": [{ "pathname": "foo/bar" }], + "expected_obj": { + "pathname": "*/{*}" + }, + "expected_match": { + "pathname": { "input": "foo/bar", "groups": { "0": "foo", "1": "bar" }} + } + }, + { + "pattern": [{ "pathname": "*/{*}" }], + "inputs": [{ "pathname": "foo/bar" }], + "expected_match": { + "pathname": { "input": "foo/bar", "groups": { "0": "foo", "1": "bar" }} + } + }, + { + "pattern": [{ "pathname": "*//*" }], + "inputs": [{ "pathname": "foo/bar" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/:foo." }], + "inputs": [{ "pathname": "/bar." }], + "expected_match": { + "pathname": { "input": "/bar.", "groups": { "foo": "bar" } } + } + }, + { + "pattern": [{ "pathname": "/:foo.." }], + "inputs": [{ "pathname": "/bar.." }], + "expected_match": { + "pathname": { "input": "/bar..", "groups": { "foo": "bar" } } + } + }, + { + "pattern": [{ "pathname": "./foo" }], + "inputs": [{ "pathname": "./foo" }], + "expected_match": { + "pathname": { "input": "./foo", "groups": {}} + } + }, + { + "pattern": [{ "pathname": "../foo" }], + "inputs": [{ "pathname": "../foo" }], + "expected_match": { + "pathname": { "input": "../foo", "groups": {}} + } + }, + { + "pattern": [{ "pathname": ":foo./" }], + "inputs": [{ "pathname": "bar./" }], + "expected_match": { + "pathname": { "input": "bar./", "groups": { "foo": "bar" }} + } + }, + { + "pattern": [{ "pathname": ":foo../" }], + "inputs": [{ "pathname": "bar../" }], + "expected_match": { + "pathname": { "input": "bar../", "groups": { "foo": "bar" }} + } + }, + { + "pattern": [{ "pathname": "/:foo\\bar" }], + "inputs": [{ "pathname": "/bazbar" }], + "expected_obj": { + "pathname": "{/:foo}bar" + }, + "expected_match": { + "pathname": { "input": "/bazbar", "groups": { "foo": "baz" }} + } + }, + { + "pattern": [{ "pathname": "/foo/bar" }, { "ignoreCase": true }], + "inputs": [{ "pathname": "/FOO/BAR" }], + "expected_match": { + "pathname": { "input": "/FOO/BAR", "groups": {} } + } + }, + { + "pattern": [{ "ignoreCase": true }], + "inputs": [{ "pathname": "/FOO/BAR" }], + "expected_match": { + "pathname": { "input": "/FOO/BAR", "groups": { "0": "/FOO/BAR" } } + } + }, + { + "pattern": [ "https://example.com:8080/foo?bar#baz", + { "ignoreCase": true }], + "inputs": [{ "pathname": "/FOO", "search": "BAR", "hash": "BAZ", + "baseURL": "https://example.com:8080" }], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "port": "8080", + "pathname": "/foo", + "search": "bar", + "hash": "baz" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "port": { "input": "8080", "groups": {} }, + "pathname": { "input": "/FOO", "groups": {} }, + "search": { "input": "BAR", "groups": {} }, + "hash": { "input": "BAZ", "groups": {} } + } + }, + { + "pattern": [ "/foo?bar#baz", "https://example.com:8080", + { "ignoreCase": true }], + "inputs": [{ "pathname": "/FOO", "search": "BAR", "hash": "BAZ", + "baseURL": "https://example.com:8080" }], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "port": "8080", + "pathname": "/foo", + "search": "bar", + "hash": "baz" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "port": { "input": "8080", "groups": {} }, + "pathname": { "input": "/FOO", "groups": {} }, + "search": { "input": "BAR", "groups": {} }, + "hash": { "input": "BAZ", "groups": {} } + } + }, + { + "pattern": [ "/foo?bar#baz", { "ignoreCase": true }, + "https://example.com:8080" ], + "inputs": [{ "pathname": "/FOO", "search": "BAR", "hash": "BAZ", + "baseURL": "https://example.com:8080" }], + "expected_obj": "error" + }, + { + "pattern": [{ "search": "foo", "baseURL": "https://example.com/a/+/b" }], + "inputs": [{ "search": "foo", "baseURL": "https://example.com/a/+/b" }], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "pathname": "/a/\\+/b" + }, + "expected_match": { + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/a/+/b", "groups": {} }, + "protocol": { "input": "https", "groups": {} }, + "search": { "input": "foo", "groups": {} } + } + }, + { + "pattern": [{ "hash": "foo", "baseURL": "https://example.com/?q=*&v=?&hmm={}&umm=()" }], + "inputs": [{ "hash": "foo", "baseURL": "https://example.com/?q=*&v=?&hmm={}&umm=()" }], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "search": "q=\\*&v=\\?&hmm=\\{\\}&umm=\\(\\)" + }, + "expected_match": { + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/", "groups": {} }, + "protocol": { "input": "https", "groups": {} }, + "search": { "input": "q=*&v=?&hmm={}&umm=()", "groups": {} }, + "hash": { "input": "foo", "groups": {} } + } + }, + { + "pattern": [ "#foo", "https://example.com/?q=*&v=?&hmm={}&umm=()" ], + "inputs": [ "https://example.com/?q=*&v=?&hmm={}&umm=()#foo" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "search": "q=\\*&v=\\?&hmm=\\{\\}&umm=\\(\\)", + "hash": "foo" + }, + "expected_match": { + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/", "groups": {} }, + "protocol": { "input": "https", "groups": {} }, + "search": { "input": "q=*&v=?&hmm={}&umm=()", "groups": {} }, + "hash": { "input": "foo", "groups": {} } + } + }, + { + "pattern": [{ "pathname": "/([[a-z]--a])" }], + "inputs": [{ "pathname": "/a" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/([[a-z]--a])" }], + "inputs": [{ "pathname": "/z" }], + "expected_match": { + "pathname": { "input": "/z", "groups": { "0": "z" } } + } + }, + { + "pattern": [{ "pathname": "/([\\d&&[0-1]])" }], + "inputs": [{ "pathname": "/0" }], + "expected_match": { + "pathname": { "input": "/0", "groups": { "0": "0" } } + } + }, + { + "pattern": [{ "pathname": "/([\\d&&[0-1]])" }], + "inputs": [{ "pathname": "/3" }], + "expected_match": null + }, + { + "pattern": [{ "protocol": "http", "hostname": "example.com/ignoredpath" }], + "inputs": ["http://example.com/"], + "expected_obj": { + "protocol": "http", + "hostname": "example.com", + "pathname": "*" + }, + "expected_match": { + "protocol": { "input": "http", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/", "groups": { "0": "/" } } + } + }, + { + "pattern": [{ "protocol": "http", "hostname": "example.com\\?ignoredsearch" }], + "inputs": ["http://example.com/"], + "expected_obj": { + "protocol": "http", + "hostname": "example.com", + "search": "*" + }, + "expected_match": { + "protocol": { "input": "http", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/", "groups": { "0": "/" } } + } + }, + { + "pattern": [{ "protocol": "http", "hostname": "example.com#ignoredhash" }], + "inputs": ["http://example.com/"], + "expected_obj": { + "protocol": "http", + "hostname": "example.com", + "hash": "*" + }, + "expected_match": { + "protocol": { "input": "http", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/", "groups": { "0": "/" } } + } + }, + { + "pattern": ["https://www.example.com/*"], + "inputs": ["https://www.example.com/x"], + "exactly_empty_components": ["port"], + "expected_obj": { + "protocol": "https", + "hostname": "www.example.com", + "pathname": "/*" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "www.example.com", "groups": {} }, + "pathname": { "input": "/x", "groups": { "0": "x" } } + } + }, + { + "pattern": ["https://www.example.com/*"], + "inputs": ["https://www.example.com/xyz"], + "exactly_empty_components": ["port"], + "expected_obj": { + "protocol": "https", + "hostname": "www.example.com", + "pathname": "/*" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "www.example.com", "groups": {} }, + "pathname": { "input": "/xyz", "groups": { "0": "xyz" } } + } + }, + { + "pattern": ["https://www.example.com/*"], + "inputs": ["https://www.example.com/example"], + "exactly_empty_components": ["port"], + "expected_obj": { + "protocol": "https", + "hostname": "www.example.com", + "pathname": "/*" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "www.example.com", "groups": {} }, + "pathname": { "input": "/example", "groups": { "0": "example" } } + } + }, + { + "pattern": ["https://www.example.com/*"], + "inputs": ["https://www.example.com/text"], + "exactly_empty_components": ["port"], + "expected_obj": { + "protocol": "https", + "hostname": "www.example.com", + "pathname": "/*" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "www.example.com", "groups": {} }, + "pathname": { "input": "/text", "groups": { "0": "text" } } + } + }, + { + "pattern": ["https://www.example.com/*"], + "inputs": ["https://www.example.com/path/with/x"], + "exactly_empty_components": ["port"], + "expected_obj": { + "protocol": "https", + "hostname": "www.example.com", + "pathname": "/*" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "www.example.com", "groups": {} }, + "pathname": { "input": "/path/with/x", "groups": { "0": "path/with/x" } } + } + } +]