From 24771db506e4c180fa3b68c38342527bdb5e4c1a Mon Sep 17 00:00:00 2001 From: ghost-bot Date: Sun, 23 Nov 2025 11:20:49 +0000 Subject: [PATCH] CI: Update Pages (2025-11-23 11:20:49) --- v0.1.0/CHANGELOG.md | 43 ++++ v0.1.0/VERSION | 1 + v0.1.0/cliff.toml | 104 +++++++++ v0.1.0/deno.jsonc | 32 +++ v0.1.0/deno.lock | 155 ++++++++++++++ v0.1.0/src/Errors/InvalidHttpMethodError.ts | 25 +++ v0.1.0/src/Errors/mod.ts | 3 + v0.1.0/src/HttpKernel.ts | 199 ++++++++++++++++++ v0.1.0/src/Interfaces/IContext.ts | 53 +++++ v0.1.0/src/Interfaces/IHttpErrorHandlers.ts | 40 ++++ v0.1.0/src/Interfaces/IHttpKernel.ts | 49 +++++ v0.1.0/src/Interfaces/IHttpKernelConfig.ts | 10 + v0.1.0/src/Interfaces/IInternalRoute.ts | 39 ++++ v0.1.0/src/Interfaces/IRouteBuilder.ts | 39 ++++ v0.1.0/src/Interfaces/IRouteDefinition.ts | 91 ++++++++ v0.1.0/src/Interfaces/IRouteMatch.ts | 6 + v0.1.0/src/Interfaces/IRouteMatcher.ts | 36 ++++ .../__tests__/routeDefinitionGuards.test.ts | 43 ++++ v0.1.0/src/Interfaces/mod.ts | 19 ++ v0.1.0/src/RouteBuilder.ts | 67 ++++++ v0.1.0/src/Types/DeepPartial.ts | 4 + v0.1.0/src/Types/Handler.ts | 57 +++++ v0.1.0/src/Types/HttpErrorHandler.ts | 28 +++ v0.1.0/src/Types/HttpMethod.ts | 52 +++++ v0.1.0/src/Types/HttpStatusCode.ts | 189 +++++++++++++++++ v0.1.0/src/Types/Middleware.ts | 51 +++++ v0.1.0/src/Types/Params.ts | 10 + v0.1.0/src/Types/Query.ts | 12 ++ v0.1.0/src/Types/RegisterRoute.ts | 16 ++ v0.1.0/src/Types/ResponseDecorator.ts | 30 +++ v0.1.0/src/Types/State.ts | 9 + v0.1.0/src/Types/__tests__/HttpMethod.test.ts | 40 ++++ .../Types/__tests__/HttpStatusCode.test.ts | 35 +++ v0.1.0/src/Types/mod.ts | 45 ++++ .../__tests__/createEmptyContext.test.ts | 28 +++ .../__tests__/createRouteMatcher.test.ts | 118 +++++++++++ .../Utils/__tests__/normalizeError.test.ts | 35 +++ v0.1.0/src/Utils/createEmptyContext.ts | 30 +++ v0.1.0/src/Utils/createRouteMatcher.ts | 54 +++++ v0.1.0/src/Utils/mod.ts | 5 + v0.1.0/src/Utils/normalizeError.ts | 32 +++ v0.1.0/src/__tests__/HttpKernel.test.ts | 180 ++++++++++++++++ v0.1.0/src/__tests__/RouteBuilder.test.ts | 118 +++++++++++ v0.1.0/src/mod.ts | 4 + v0.2.0/CHANGELOG.md | 93 ++++++++ v0.2.0/LICENSE | 18 ++ v0.2.0/README.md | 135 ++++++++++++ v0.2.0/VERSION | 1 + v0.2.0/cliff.toml | 104 +++++++++ v0.2.0/deno.jsonc | 36 ++++ v0.2.0/deno.lock | 155 ++++++++++++++ v0.2.0/src/Errors/InvalidHttpMethodError.ts | 25 +++ v0.2.0/src/Errors/mod.ts | 3 + v0.2.0/src/HttpKernel.ts | 144 +++++++++++++ v0.2.0/src/Interfaces/IContext.ts | 53 +++++ v0.2.0/src/Interfaces/IHttpErrorHandlers.ts | 40 ++++ v0.2.0/src/Interfaces/IHttpKernel.ts | 49 +++++ v0.2.0/src/Interfaces/IHttpKernelConfig.ts | 10 + v0.2.0/src/Interfaces/IInternalRoute.ts | 64 ++++++ v0.2.0/src/Interfaces/IRouteBuilder.ts | 39 ++++ v0.2.0/src/Interfaces/IRouteDefinition.ts | 91 ++++++++ v0.2.0/src/Interfaces/IRouteMatch.ts | 6 + v0.2.0/src/Interfaces/IRouteMatcher.ts | 35 +++ .../__tests__/routeDefinitionGuards.test.ts | 43 ++++ v0.2.0/src/Interfaces/mod.ts | 19 ++ v0.2.0/src/RouteBuilder.ts | 148 +++++++++++++ v0.2.0/src/Types/DeepPartial.ts | 4 + v0.2.0/src/Types/Handler.ts | 57 +++++ v0.2.0/src/Types/HttpErrorHandler.ts | 28 +++ v0.2.0/src/Types/HttpMethod.ts | 52 +++++ v0.2.0/src/Types/HttpStatusCode.ts | 189 +++++++++++++++++ v0.2.0/src/Types/Middleware.ts | 51 +++++ v0.2.0/src/Types/Params.ts | 10 + v0.2.0/src/Types/Query.ts | 12 ++ v0.2.0/src/Types/RegisterRoute.ts | 16 ++ v0.2.0/src/Types/ResponseDecorator.ts | 30 +++ v0.2.0/src/Types/State.ts | 9 + v0.2.0/src/Types/__tests__/HttpMethod.test.ts | 40 ++++ .../Types/__tests__/HttpStatusCode.test.ts | 35 +++ v0.2.0/src/Types/mod.ts | 45 ++++ .../__tests__/createEmptyContext.test.ts | 28 +++ .../__tests__/createRouteMatcher.test.ts | 118 +++++++++++ .../Utils/__tests__/normalizeError.test.ts | 35 +++ v0.2.0/src/Utils/createEmptyContext.ts | 30 +++ v0.2.0/src/Utils/createRouteMatcher.ts | 54 +++++ v0.2.0/src/Utils/mod.ts | 5 + v0.2.0/src/Utils/normalizeError.ts | 30 +++ v0.2.0/src/__bench__/HttpKernel.bench.ts | 87 ++++++++ v0.2.0/src/__tests__/HttpKernel.test.ts | 185 ++++++++++++++++ v0.2.0/src/__tests__/RouteBuilder.test.ts | 140 ++++++++++++ v0.2.0/src/mod.ts | 4 + v0.2.1/CHANGELOG.md | 107 ++++++++++ v0.2.1/LICENSE | 18 ++ v0.2.1/README.md | 135 ++++++++++++ v0.2.1/VERSION | 1 + v0.2.1/cliff.toml | 104 +++++++++ v0.2.1/deno.jsonc | 36 ++++ v0.2.1/deno.lock | 155 ++++++++++++++ v0.2.1/src/Errors/InvalidHttpMethodError.ts | 25 +++ v0.2.1/src/Errors/mod.ts | 3 + v0.2.1/src/HttpKernel.ts | 144 +++++++++++++ v0.2.1/src/Interfaces/IContext.ts | 53 +++++ v0.2.1/src/Interfaces/IHttpErrorHandlers.ts | 40 ++++ v0.2.1/src/Interfaces/IHttpKernel.ts | 49 +++++ v0.2.1/src/Interfaces/IHttpKernelConfig.ts | 10 + v0.2.1/src/Interfaces/IInternalRoute.ts | 64 ++++++ v0.2.1/src/Interfaces/IRouteBuilder.ts | 39 ++++ v0.2.1/src/Interfaces/IRouteDefinition.ts | 91 ++++++++ v0.2.1/src/Interfaces/IRouteMatch.ts | 6 + v0.2.1/src/Interfaces/IRouteMatcher.ts | 35 +++ .../__tests__/routeDefinitionGuards.test.ts | 43 ++++ v0.2.1/src/Interfaces/mod.ts | 19 ++ v0.2.1/src/RouteBuilder.ts | 148 +++++++++++++ v0.2.1/src/Types/DeepPartial.ts | 4 + v0.2.1/src/Types/Handler.ts | 57 +++++ v0.2.1/src/Types/HttpErrorHandler.ts | 28 +++ v0.2.1/src/Types/HttpMethod.ts | 52 +++++ v0.2.1/src/Types/HttpStatusCode.ts | 189 +++++++++++++++++ v0.2.1/src/Types/Middleware.ts | 51 +++++ v0.2.1/src/Types/Params.ts | 10 + v0.2.1/src/Types/Query.ts | 12 ++ v0.2.1/src/Types/RegisterRoute.ts | 16 ++ v0.2.1/src/Types/ResponseDecorator.ts | 30 +++ v0.2.1/src/Types/State.ts | 9 + v0.2.1/src/Types/__tests__/HttpMethod.test.ts | 40 ++++ .../Types/__tests__/HttpStatusCode.test.ts | 35 +++ v0.2.1/src/Types/mod.ts | 45 ++++ .../__tests__/createEmptyContext.test.ts | 28 +++ .../__tests__/createRouteMatcher.test.ts | 118 +++++++++++ .../Utils/__tests__/normalizeError.test.ts | 35 +++ v0.2.1/src/Utils/createEmptyContext.ts | 30 +++ v0.2.1/src/Utils/createRouteMatcher.ts | 54 +++++ v0.2.1/src/Utils/mod.ts | 5 + v0.2.1/src/Utils/normalizeError.ts | 30 +++ v0.2.1/src/__bench__/HttpKernel.bench.ts | 87 ++++++++ v0.2.1/src/__tests__/HttpKernel.test.ts | 185 ++++++++++++++++ v0.2.1/src/__tests__/RouteBuilder.test.ts | 140 ++++++++++++ v0.2.1/src/mod.ts | 16 ++ 138 files changed, 7472 insertions(+) create mode 100644 v0.1.0/CHANGELOG.md create mode 100644 v0.1.0/VERSION create mode 100644 v0.1.0/cliff.toml create mode 100644 v0.1.0/deno.jsonc create mode 100644 v0.1.0/deno.lock create mode 100644 v0.1.0/src/Errors/InvalidHttpMethodError.ts create mode 100644 v0.1.0/src/Errors/mod.ts create mode 100644 v0.1.0/src/HttpKernel.ts create mode 100644 v0.1.0/src/Interfaces/IContext.ts create mode 100644 v0.1.0/src/Interfaces/IHttpErrorHandlers.ts create mode 100644 v0.1.0/src/Interfaces/IHttpKernel.ts create mode 100644 v0.1.0/src/Interfaces/IHttpKernelConfig.ts create mode 100644 v0.1.0/src/Interfaces/IInternalRoute.ts create mode 100644 v0.1.0/src/Interfaces/IRouteBuilder.ts create mode 100644 v0.1.0/src/Interfaces/IRouteDefinition.ts create mode 100644 v0.1.0/src/Interfaces/IRouteMatch.ts create mode 100644 v0.1.0/src/Interfaces/IRouteMatcher.ts create mode 100644 v0.1.0/src/Interfaces/__tests__/routeDefinitionGuards.test.ts create mode 100644 v0.1.0/src/Interfaces/mod.ts create mode 100644 v0.1.0/src/RouteBuilder.ts create mode 100644 v0.1.0/src/Types/DeepPartial.ts create mode 100644 v0.1.0/src/Types/Handler.ts create mode 100644 v0.1.0/src/Types/HttpErrorHandler.ts create mode 100644 v0.1.0/src/Types/HttpMethod.ts create mode 100644 v0.1.0/src/Types/HttpStatusCode.ts create mode 100644 v0.1.0/src/Types/Middleware.ts create mode 100644 v0.1.0/src/Types/Params.ts create mode 100644 v0.1.0/src/Types/Query.ts create mode 100644 v0.1.0/src/Types/RegisterRoute.ts create mode 100644 v0.1.0/src/Types/ResponseDecorator.ts create mode 100644 v0.1.0/src/Types/State.ts create mode 100644 v0.1.0/src/Types/__tests__/HttpMethod.test.ts create mode 100644 v0.1.0/src/Types/__tests__/HttpStatusCode.test.ts create mode 100644 v0.1.0/src/Types/mod.ts create mode 100644 v0.1.0/src/Utils/__tests__/createEmptyContext.test.ts create mode 100644 v0.1.0/src/Utils/__tests__/createRouteMatcher.test.ts create mode 100644 v0.1.0/src/Utils/__tests__/normalizeError.test.ts create mode 100644 v0.1.0/src/Utils/createEmptyContext.ts create mode 100644 v0.1.0/src/Utils/createRouteMatcher.ts create mode 100644 v0.1.0/src/Utils/mod.ts create mode 100644 v0.1.0/src/Utils/normalizeError.ts create mode 100644 v0.1.0/src/__tests__/HttpKernel.test.ts create mode 100644 v0.1.0/src/__tests__/RouteBuilder.test.ts create mode 100644 v0.1.0/src/mod.ts create mode 100644 v0.2.0/CHANGELOG.md create mode 100644 v0.2.0/LICENSE create mode 100644 v0.2.0/README.md create mode 100644 v0.2.0/VERSION create mode 100644 v0.2.0/cliff.toml create mode 100644 v0.2.0/deno.jsonc create mode 100644 v0.2.0/deno.lock create mode 100644 v0.2.0/src/Errors/InvalidHttpMethodError.ts create mode 100644 v0.2.0/src/Errors/mod.ts create mode 100644 v0.2.0/src/HttpKernel.ts create mode 100644 v0.2.0/src/Interfaces/IContext.ts create mode 100644 v0.2.0/src/Interfaces/IHttpErrorHandlers.ts create mode 100644 v0.2.0/src/Interfaces/IHttpKernel.ts create mode 100644 v0.2.0/src/Interfaces/IHttpKernelConfig.ts create mode 100644 v0.2.0/src/Interfaces/IInternalRoute.ts create mode 100644 v0.2.0/src/Interfaces/IRouteBuilder.ts create mode 100644 v0.2.0/src/Interfaces/IRouteDefinition.ts create mode 100644 v0.2.0/src/Interfaces/IRouteMatch.ts create mode 100644 v0.2.0/src/Interfaces/IRouteMatcher.ts create mode 100644 v0.2.0/src/Interfaces/__tests__/routeDefinitionGuards.test.ts create mode 100644 v0.2.0/src/Interfaces/mod.ts create mode 100644 v0.2.0/src/RouteBuilder.ts create mode 100644 v0.2.0/src/Types/DeepPartial.ts create mode 100644 v0.2.0/src/Types/Handler.ts create mode 100644 v0.2.0/src/Types/HttpErrorHandler.ts create mode 100644 v0.2.0/src/Types/HttpMethod.ts create mode 100644 v0.2.0/src/Types/HttpStatusCode.ts create mode 100644 v0.2.0/src/Types/Middleware.ts create mode 100644 v0.2.0/src/Types/Params.ts create mode 100644 v0.2.0/src/Types/Query.ts create mode 100644 v0.2.0/src/Types/RegisterRoute.ts create mode 100644 v0.2.0/src/Types/ResponseDecorator.ts create mode 100644 v0.2.0/src/Types/State.ts create mode 100644 v0.2.0/src/Types/__tests__/HttpMethod.test.ts create mode 100644 v0.2.0/src/Types/__tests__/HttpStatusCode.test.ts create mode 100644 v0.2.0/src/Types/mod.ts create mode 100644 v0.2.0/src/Utils/__tests__/createEmptyContext.test.ts create mode 100644 v0.2.0/src/Utils/__tests__/createRouteMatcher.test.ts create mode 100644 v0.2.0/src/Utils/__tests__/normalizeError.test.ts create mode 100644 v0.2.0/src/Utils/createEmptyContext.ts create mode 100644 v0.2.0/src/Utils/createRouteMatcher.ts create mode 100644 v0.2.0/src/Utils/mod.ts create mode 100644 v0.2.0/src/Utils/normalizeError.ts create mode 100644 v0.2.0/src/__bench__/HttpKernel.bench.ts create mode 100644 v0.2.0/src/__tests__/HttpKernel.test.ts create mode 100644 v0.2.0/src/__tests__/RouteBuilder.test.ts create mode 100644 v0.2.0/src/mod.ts create mode 100644 v0.2.1/CHANGELOG.md create mode 100644 v0.2.1/LICENSE create mode 100644 v0.2.1/README.md create mode 100644 v0.2.1/VERSION create mode 100644 v0.2.1/cliff.toml create mode 100644 v0.2.1/deno.jsonc create mode 100644 v0.2.1/deno.lock create mode 100644 v0.2.1/src/Errors/InvalidHttpMethodError.ts create mode 100644 v0.2.1/src/Errors/mod.ts create mode 100644 v0.2.1/src/HttpKernel.ts create mode 100644 v0.2.1/src/Interfaces/IContext.ts create mode 100644 v0.2.1/src/Interfaces/IHttpErrorHandlers.ts create mode 100644 v0.2.1/src/Interfaces/IHttpKernel.ts create mode 100644 v0.2.1/src/Interfaces/IHttpKernelConfig.ts create mode 100644 v0.2.1/src/Interfaces/IInternalRoute.ts create mode 100644 v0.2.1/src/Interfaces/IRouteBuilder.ts create mode 100644 v0.2.1/src/Interfaces/IRouteDefinition.ts create mode 100644 v0.2.1/src/Interfaces/IRouteMatch.ts create mode 100644 v0.2.1/src/Interfaces/IRouteMatcher.ts create mode 100644 v0.2.1/src/Interfaces/__tests__/routeDefinitionGuards.test.ts create mode 100644 v0.2.1/src/Interfaces/mod.ts create mode 100644 v0.2.1/src/RouteBuilder.ts create mode 100644 v0.2.1/src/Types/DeepPartial.ts create mode 100644 v0.2.1/src/Types/Handler.ts create mode 100644 v0.2.1/src/Types/HttpErrorHandler.ts create mode 100644 v0.2.1/src/Types/HttpMethod.ts create mode 100644 v0.2.1/src/Types/HttpStatusCode.ts create mode 100644 v0.2.1/src/Types/Middleware.ts create mode 100644 v0.2.1/src/Types/Params.ts create mode 100644 v0.2.1/src/Types/Query.ts create mode 100644 v0.2.1/src/Types/RegisterRoute.ts create mode 100644 v0.2.1/src/Types/ResponseDecorator.ts create mode 100644 v0.2.1/src/Types/State.ts create mode 100644 v0.2.1/src/Types/__tests__/HttpMethod.test.ts create mode 100644 v0.2.1/src/Types/__tests__/HttpStatusCode.test.ts create mode 100644 v0.2.1/src/Types/mod.ts create mode 100644 v0.2.1/src/Utils/__tests__/createEmptyContext.test.ts create mode 100644 v0.2.1/src/Utils/__tests__/createRouteMatcher.test.ts create mode 100644 v0.2.1/src/Utils/__tests__/normalizeError.test.ts create mode 100644 v0.2.1/src/Utils/createEmptyContext.ts create mode 100644 v0.2.1/src/Utils/createRouteMatcher.ts create mode 100644 v0.2.1/src/Utils/mod.ts create mode 100644 v0.2.1/src/Utils/normalizeError.ts create mode 100644 v0.2.1/src/__bench__/HttpKernel.bench.ts create mode 100644 v0.2.1/src/__tests__/HttpKernel.test.ts create mode 100644 v0.2.1/src/__tests__/RouteBuilder.test.ts create mode 100644 v0.2.1/src/mod.ts diff --git a/v0.1.0/CHANGELOG.md b/v0.1.0/CHANGELOG.md new file mode 100644 index 0000000..d98d941 --- /dev/null +++ b/v0.1.0/CHANGELOG.md @@ -0,0 +1,43 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [0.1.0] - 2025-05-08 + +### πŸš€ Features + +- *(workflows)* Add automated changelog and release workflow - ([bbf78cf](https://git.0xmax42.io/maxp/http-kernel/commit/bbf78cff17be0cae651b8abf3e239103b26354bf)) +- *(vscode)* Customize activity bar and peacock colors - ([56633cd](https://git.0xmax42.io/maxp/http-kernel/commit/56633cd95b37a8b2cfd8eb95982d07cd1f9b5126)) +- *(workflows)* Add upload assets template for releases - ([7b6eb2b](https://git.0xmax42.io/maxp/http-kernel/commit/7b6eb2b57470198684a1dfa8b668351b8b9a91ae)) +- *(config)* Add project metadata and test watch task - ([b009b57](https://git.0xmax42.io/maxp/http-kernel/commit/b009b5763d1824fc94fdc1e3d919fe2597158f84)) +- *(http)* Add error handling for invalid HTTP methods - ([ba7aa79](https://git.0xmax42.io/maxp/http-kernel/commit/ba7aa79f56772213bf73b62bc6bf8810f3871127)) +- *(http)* Enhance type safety and extend route context - ([a236fa7](https://git.0xmax42.io/maxp/http-kernel/commit/a236fa7c97ae49e6baf560d4ca92c6e83702b3ec)) + +### πŸ› Bug Fixes + +- *(params)* Enforce non-undefined route parameter values - ([b0c6901](https://git.0xmax42.io/maxp/http-kernel/commit/b0c6901d7d272ec98b3d00ef2dd2848482892a25)) + +### 🚜 Refactor + +- *(types)* Unify handler and middleware definitions - ([8235680](https://git.0xmax42.io/maxp/http-kernel/commit/8235680904c7f30f25b98b835d48376431108e91)) +- *(core)* [**breaking**] Enhance HttpKernel pipeline and matcher system with full context and error handling - ([b7410b4](https://git.0xmax42.io/maxp/http-kernel/commit/b7410b44dd8720e46ee2871aa1727ce5039ebad4)) +- *(httpkernel)* Introduce configuration object for flexibility - ([9059bdd](https://git.0xmax42.io/maxp/http-kernel/commit/9059bdda62081c8e775087cabe4c3406e42065a5)) + +### πŸ“š Documentation + +- *(gitea)* Add release automation guide and scripts - ([5c03cdf](https://git.0xmax42.io/maxp/http-kernel/commit/5c03cdfb031adeb6ee5d0de0889477d6d1efafef)) +- *(httpkernel)* Enhance class and interface documentation - ([6c4420d](https://git.0xmax42.io/maxp/http-kernel/commit/6c4420d32f8e7fe317f7c1b0b45de2dcf8565ef5)) + +### πŸ§ͺ Testing + +- *(utils)* Rename and update import paths in test file - ([82a6877](https://git.0xmax42.io/maxp/http-kernel/commit/82a687748558f15c2023861a0cc3a33095c86731)) +- *(utils)* Add unit tests for parseQuery function - ([94525fc](https://git.0xmax42.io/maxp/http-kernel/commit/94525fce5299f3417801f0152a475892e1edac30)) + +### βš™οΈ Miscellaneous Tasks + +- *(config)* Add default git-cliff configuration - ([661f83d](https://git.0xmax42.io/maxp/http-kernel/commit/661f83d1fd0101aa0d5d06b60f6eeb68efac6ceb)) +- *(gitignore)* Add .gitea/COMMIT_GPT.md to ignored files - ([f083856](https://git.0xmax42.io/maxp/http-kernel/commit/f0838567b46822327fe739d8de099722e405dfa3)) +- *(settings)* Add exportall configuration for barrel name and message - ([0990cac](https://git.0xmax42.io/maxp/http-kernel/commit/0990cacb225e1cbbbbb2a288501df7de9641294f)) +- *(.gitignore)* Add git_log_diff.txt to ignore list - ([fd1c7f4](https://git.0xmax42.io/maxp/http-kernel/commit/fd1c7f4170ffffd55ab276090f8b90ee82b853fc)) + + diff --git a/v0.1.0/VERSION b/v0.1.0/VERSION new file mode 100644 index 0000000..6c6aa7c --- /dev/null +++ b/v0.1.0/VERSION @@ -0,0 +1 @@ +0.1.0 \ No newline at end of file diff --git a/v0.1.0/cliff.toml b/v0.1.0/cliff.toml new file mode 100644 index 0000000..4d5ab28 --- /dev/null +++ b/v0.1.0/cliff.toml @@ -0,0 +1,104 @@ +# CLIFF_VERSION=2.8.0 +# git-cliff ~ default configuration file +# https://git-cliff.org/docs/configuration +# +# Lines starting with "#" are comments. +# Configuration options are organized into tables and keys. +# See documentation for more information on available options. +[remote.gitea] +owner = "maxp" +repo = "http-kernel" + +[changelog] +# postprocessors +postprocessors = [ + { pattern = '', replace = "https://git.0xmax42.io" }, # replace gitea url +] + +# template for the changelog header +header = """ +# Changelog\n +All notable changes to this project will be documented in this file.\n +""" +# template for the changelog body +# https://keats.github.io/tera/docs/#introduction +body = """ +{%- macro remote_url() -%} + /{{ remote.gitea.owner }}/{{ remote.gitea.repo }} +{%- endmacro -%} + +{% if version %}\ + {% if previous.version %}\ + ## [{{ version | trim_start_matches(pat="v") }}]\ + ({{ self::remote_url() }}/compare/{{ previous.version }}..{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }} + {% else %}\ + ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} + {% endif %}\ +{% else %}\ + ## [unreleased] +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | striptags | trim | upper_first }} + {% for commit in commits %} + - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ + {% if commit.breaking %}[**breaking**] {% endif %}\ + {{ commit.message | upper_first }} - \ + ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ + {% endfor %} +{% endfor %}\n +""" +# template for the changelog footer +footer = """ + +""" +# remove the leading and trailing s +trim = true + +# render body even when there are no releases to process +# render_always = true +# output file path +# output = "test.md" + +[git] +# parse the commits based on https://www.conventionalcommits.org +conventional_commits = true +# filter out the commits that are not conventional +filter_unconventional = true +# process each line of a commit as an individual commit +split_commits = false +# regex for preprocessing the commit messages +commit_preprocessors = [ + # Replace issue numbers + #{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))"}, + # Check spelling of the commit with https://github.com/crate-ci/typos + # If the spelling is incorrect, it will be automatically fixed. + #{ pattern = '.*', replace_command = 'typos --write-changes -' }, +] +# regex for parsing and grouping commits +commit_parsers = [ + { message = "^feat", group = "πŸš€ Features" }, + { message = "^fix", group = "πŸ› Bug Fixes" }, + { message = "^doc", group = "πŸ“š Documentation" }, + { message = "^perf", group = "⚑ Performance" }, + { message = "^refactor", group = "🚜 Refactor" }, + { message = "^style", group = "🎨 Styling" }, + { message = "^test", group = "πŸ§ͺ Testing" }, + { message = "^chore\\(changelog\\)", skip = true }, + { message = "^chore\\(version\\)", skip = true }, + { message = "^chore\\(release\\): prepare for", skip = true }, + { message = "^chore\\(deps.*\\)", skip = true }, + { message = "^chore\\(pr\\)", skip = true }, + { message = "^chore\\(pull\\)", skip = true }, + { message = "^chore|^ci", group = "βš™οΈ Miscellaneous Tasks" }, + { body = ".*security", group = "πŸ›‘οΈ Security" }, + { message = "^revert", group = "◀️ Revert" }, + { message = ".*", group = "πŸ’Ό Other" }, +] +# Regex to select git tags that represent releases. +tag_pattern = "v[0-9]+\\.[0-9]+\\.[0-9]+" +# filter out the commits that are not matched by commit parsers +filter_commits = false +# sort the tags topologically +topo_order = false +# sort the commits inside sections by oldest/newest order +sort_commits = "newest" diff --git a/v0.1.0/deno.jsonc b/v0.1.0/deno.jsonc new file mode 100644 index 0000000..6e55ce7 --- /dev/null +++ b/v0.1.0/deno.jsonc @@ -0,0 +1,32 @@ +{ + "name": "@0xmax42/http-kernel", + "description": "A simple HTTP kernel for Deno", + "tasks": { + // "start": "deno run --allow-net --allow-env --unstable-kv --allow-read --allow-write --env-file src/main.ts -- --verbose", + // "watch": "deno run --watch --allow-net --allow-env --unstable-kv --allow-read --allow-write --env-file src/main.ts -- --verbose", + "test": "deno test --allow-net --allow-env --unstable-kv --allow-read --allow-write --coverage **/__tests__/*.test.ts", + "test:watch": "deno test --watch --allow-net --allow-env --unstable-kv --allow-read --allow-write **/__tests__/*.test.ts" + }, + "compilerOptions": { + "lib": [ + "dom", + "dom.iterable", + "esnext", + "deno.ns" + ], + "strict": true + }, + "fmt": { + "useTabs": false, + "lineWidth": 80, + "indentWidth": 4, + "semiColons": true, + "singleQuote": true, + "proseWrap": "preserve", + "include": [ + "src/", + "main.ts" + ] + } + //"importMap": "./import_map.json" +} \ No newline at end of file diff --git a/v0.1.0/deno.lock b/v0.1.0/deno.lock new file mode 100644 index 0000000..aa91fda --- /dev/null +++ b/v0.1.0/deno.lock @@ -0,0 +1,155 @@ +{ + "version": "5", + "redirects": { + "https://deno.land/std/assert/mod.ts": "https://deno.land/std@0.224.0/assert/mod.ts", + "https://deno.land/std/fs/walk.ts": "https://deno.land/std@0.224.0/fs/walk.ts", + "https://deno.land/std/path/mod.ts": "https://deno.land/std@0.224.0/path/mod.ts" + }, + "remote": { + "https://deno.land/std@0.204.0/assert/_constants.ts": "8a9da298c26750b28b326b297316cdde860bc237533b07e1337c021379e6b2a9", + "https://deno.land/std@0.204.0/assert/_diff.ts": "58e1461cc61d8eb1eacbf2a010932bf6a05b79344b02ca38095f9b805795dc48", + "https://deno.land/std@0.204.0/assert/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7", + "https://deno.land/std@0.204.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee", + "https://deno.land/std@0.204.0/assert/assert_almost_equals.ts": "e15ca1f34d0d5e0afae63b3f5d975cbd18335a132e42b0c747d282f62ad2cd6c", + "https://deno.land/std@0.204.0/assert/assert_array_includes.ts": "6856d7f2c3544bc6e62fb4646dfefa3d1df5ff14744d1bca19f0cbaf3b0d66c9", + "https://deno.land/std@0.204.0/assert/assert_equals.ts": "d8ec8a22447fbaf2fc9d7c3ed2e66790fdb74beae3e482855d75782218d68227", + "https://deno.land/std@0.204.0/assert/assert_exists.ts": "407cb6b9fb23a835cd8d5ad804e2e2edbbbf3870e322d53f79e1c7a512e2efd7", + "https://deno.land/std@0.204.0/assert/assert_false.ts": "0ccbcaae910f52c857192ff16ea08bda40fdc79de80846c206bfc061e8c851c6", + "https://deno.land/std@0.204.0/assert/assert_greater.ts": "ae2158a2d19313bf675bf7251d31c6dc52973edb12ac64ac8fc7064152af3e63", + "https://deno.land/std@0.204.0/assert/assert_greater_or_equal.ts": "1439da5ebbe20855446cac50097ac78b9742abe8e9a43e7de1ce1426d556e89c", + "https://deno.land/std@0.204.0/assert/assert_instance_of.ts": "3aedb3d8186e120812d2b3a5dea66a6e42bf8c57a8bd927645770bd21eea554c", + "https://deno.land/std@0.204.0/assert/assert_is_error.ts": "c21113094a51a296ffaf036767d616a78a2ae5f9f7bbd464cd0197476498b94b", + "https://deno.land/std@0.204.0/assert/assert_less.ts": "aec695db57db42ec3e2b62e97e1e93db0063f5a6ec133326cc290ff4b71b47e4", + "https://deno.land/std@0.204.0/assert/assert_less_or_equal.ts": "5fa8b6a3ffa20fd0a05032fe7257bf985d207b85685fdbcd23651b70f928c848", + "https://deno.land/std@0.204.0/assert/assert_match.ts": "c4083f80600bc190309903c95e397a7c9257ff8b5ae5c7ef91e834704e672e9b", + "https://deno.land/std@0.204.0/assert/assert_not_equals.ts": "9f1acab95bd1f5fc9a1b17b8027d894509a745d91bac1718fdab51dc76831754", + "https://deno.land/std@0.204.0/assert/assert_not_instance_of.ts": "0c14d3dfd9ab7a5276ed8ed0b18c703d79a3d106102077ec437bfe7ed912bd22", + "https://deno.land/std@0.204.0/assert/assert_not_match.ts": "3796a5b0c57a1ce6c1c57883dd4286be13a26f715ea662318ab43a8491a13ab0", + "https://deno.land/std@0.204.0/assert/assert_not_strict_equals.ts": "ca6c6d645e95fbc873d25320efeb8c4c6089a9a5e09f92d7c1c4b6e935c2a6ad", + "https://deno.land/std@0.204.0/assert/assert_object_match.ts": "d8fc2867cfd92eeacf9cea621e10336b666de1874a6767b5ec48988838370b54", + "https://deno.land/std@0.204.0/assert/assert_rejects.ts": "45c59724de2701e3b1f67c391d6c71c392363635aad3f68a1b3408f9efca0057", + "https://deno.land/std@0.204.0/assert/assert_strict_equals.ts": "b1f538a7ea5f8348aeca261d4f9ca603127c665e0f2bbfeb91fa272787c87265", + "https://deno.land/std@0.204.0/assert/assert_string_includes.ts": "b821d39ebf5cb0200a348863c86d8c4c4b398e02012ce74ad15666fc4b631b0c", + "https://deno.land/std@0.204.0/assert/assert_throws.ts": "63784e951475cb7bdfd59878cd25a0931e18f6dc32a6077c454b2cd94f4f4bcd", + "https://deno.land/std@0.204.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56", + "https://deno.land/std@0.204.0/assert/equal.ts": "9f1a46d5993966d2596c44e5858eec821859b45f783a5ee2f7a695dfc12d8ece", + "https://deno.land/std@0.204.0/assert/fail.ts": "c36353d7ae6e1f7933d45f8ea51e358c8c4b67d7e7502028598fe1fea062e278", + "https://deno.land/std@0.204.0/assert/mod.ts": "37c49a26aae2b254bbe25723434dc28cd7532e444cf0b481a97c045d110ec085", + "https://deno.land/std@0.204.0/assert/unimplemented.ts": "d56fbeecb1f108331a380f72e3e010a1f161baa6956fd0f7cf3e095ae1a4c75a", + "https://deno.land/std@0.204.0/assert/unreachable.ts": "4600dc0baf7d9c15a7f7d234f00c23bca8f3eba8b140286aaca7aa998cf9a536", + "https://deno.land/std@0.204.0/fmt/colors.ts": "c51c4642678eb690dcf5ffee5918b675bf01a33fba82acf303701ae1a4f8c8d9", + "https://deno.land/std@0.224.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975", + "https://deno.land/std@0.224.0/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834", + "https://deno.land/std@0.224.0/assert/assert_almost_equals.ts": "9e416114322012c9a21fa68e187637ce2d7df25bcbdbfd957cd639e65d3cf293", + "https://deno.land/std@0.224.0/assert/assert_array_includes.ts": "14c5094471bc8e4a7895fc6aa5a184300d8a1879606574cb1cd715ef36a4a3c7", + "https://deno.land/std@0.224.0/assert/assert_equals.ts": "3bbca947d85b9d374a108687b1a8ba3785a7850436b5a8930d81f34a32cb8c74", + "https://deno.land/std@0.224.0/assert/assert_exists.ts": "43420cf7f956748ae6ed1230646567b3593cb7a36c5a5327269279c870c5ddfd", + "https://deno.land/std@0.224.0/assert/assert_false.ts": "3e9be8e33275db00d952e9acb0cd29481a44fa0a4af6d37239ff58d79e8edeff", + "https://deno.land/std@0.224.0/assert/assert_greater.ts": "5e57b201fd51b64ced36c828e3dfd773412c1a6120c1a5a99066c9b261974e46", + "https://deno.land/std@0.224.0/assert/assert_greater_or_equal.ts": "9870030f997a08361b6f63400273c2fb1856f5db86c0c3852aab2a002e425c5b", + "https://deno.land/std@0.224.0/assert/assert_instance_of.ts": "e22343c1fdcacfaea8f37784ad782683ec1cf599ae9b1b618954e9c22f376f2c", + "https://deno.land/std@0.224.0/assert/assert_is_error.ts": "f856b3bc978a7aa6a601f3fec6603491ab6255118afa6baa84b04426dd3cc491", + "https://deno.land/std@0.224.0/assert/assert_less.ts": "60b61e13a1982865a72726a5fa86c24fad7eb27c3c08b13883fb68882b307f68", + "https://deno.land/std@0.224.0/assert/assert_less_or_equal.ts": "d2c84e17faba4afe085e6c9123a63395accf4f9e00150db899c46e67420e0ec3", + "https://deno.land/std@0.224.0/assert/assert_match.ts": "ace1710dd3b2811c391946954234b5da910c5665aed817943d086d4d4871a8b7", + "https://deno.land/std@0.224.0/assert/assert_not_equals.ts": "78d45dd46133d76ce624b2c6c09392f6110f0df9b73f911d20208a68dee2ef29", + "https://deno.land/std@0.224.0/assert/assert_not_instance_of.ts": "3434a669b4d20cdcc5359779301a0588f941ffdc2ad68803c31eabdb4890cf7a", + "https://deno.land/std@0.224.0/assert/assert_not_match.ts": "df30417240aa2d35b1ea44df7e541991348a063d9ee823430e0b58079a72242a", + "https://deno.land/std@0.224.0/assert/assert_not_strict_equals.ts": "37f73880bd672709373d6dc2c5f148691119bed161f3020fff3548a0496f71b8", + "https://deno.land/std@0.224.0/assert/assert_object_match.ts": "411450fd194fdaabc0089ae68f916b545a49d7b7e6d0026e84a54c9e7eed2693", + "https://deno.land/std@0.224.0/assert/assert_rejects.ts": "4bee1d6d565a5b623146a14668da8f9eb1f026a4f338bbf92b37e43e0aa53c31", + "https://deno.land/std@0.224.0/assert/assert_strict_equals.ts": "b4f45f0fd2e54d9029171876bd0b42dd9ed0efd8f853ab92a3f50127acfa54f5", + "https://deno.land/std@0.224.0/assert/assert_string_includes.ts": "496b9ecad84deab72c8718735373feb6cdaa071eb91a98206f6f3cb4285e71b8", + "https://deno.land/std@0.224.0/assert/assert_throws.ts": "c6508b2879d465898dab2798009299867e67c570d7d34c90a2d235e4553906eb", + "https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917", + "https://deno.land/std@0.224.0/assert/equal.ts": "bddf07bb5fc718e10bb72d5dc2c36c1ce5a8bdd3b647069b6319e07af181ac47", + "https://deno.land/std@0.224.0/assert/fail.ts": "0eba674ffb47dff083f02ced76d5130460bff1a9a68c6514ebe0cdea4abadb68", + "https://deno.land/std@0.224.0/assert/mod.ts": "48b8cb8a619ea0b7958ad7ee9376500fe902284bb36f0e32c598c3dc34cbd6f3", + "https://deno.land/std@0.224.0/assert/unimplemented.ts": "8c55a5793e9147b4f1ef68cd66496b7d5ba7a9e7ca30c6da070c1a58da723d73", + "https://deno.land/std@0.224.0/assert/unreachable.ts": "5ae3dbf63ef988615b93eb08d395dda771c96546565f9e521ed86f6510c29e19", + "https://deno.land/std@0.224.0/fmt/colors.ts": "508563c0659dd7198ba4bbf87e97f654af3c34eb56ba790260f252ad8012e1c5", + "https://deno.land/std@0.224.0/fs/_create_walk_entry.ts": "5d9d2aaec05bcf09a06748b1684224d33eba7a4de24cf4cf5599991ca6b5b412", + "https://deno.land/std@0.224.0/fs/_to_path_string.ts": "29bfc9c6c112254961d75cbf6ba814d6de5349767818eb93090cecfa9665591e", + "https://deno.land/std@0.224.0/fs/walk.ts": "cddf87d2705c0163bff5d7767291f05b0f46ba10b8b28f227c3849cace08d303", + "https://deno.land/std@0.224.0/internal/diff.ts": "6234a4b493ebe65dc67a18a0eb97ef683626a1166a1906232ce186ae9f65f4e6", + "https://deno.land/std@0.224.0/internal/format.ts": "0a98ee226fd3d43450245b1844b47003419d34d210fa989900861c79820d21c2", + "https://deno.land/std@0.224.0/internal/mod.ts": "534125398c8e7426183e12dc255bb635d94e06d0f93c60a297723abe69d3b22e", + "https://deno.land/std@0.224.0/path/_common/assert_path.ts": "dbdd757a465b690b2cc72fc5fb7698c51507dec6bfafce4ca500c46b76ff7bd8", + "https://deno.land/std@0.224.0/path/_common/basename.ts": "569744855bc8445f3a56087fd2aed56bdad39da971a8d92b138c9913aecc5fa2", + "https://deno.land/std@0.224.0/path/_common/common.ts": "ef73c2860694775fe8ffcbcdd387f9f97c7a656febf0daa8c73b56f4d8a7bd4c", + "https://deno.land/std@0.224.0/path/_common/constants.ts": "dc5f8057159f4b48cd304eb3027e42f1148cf4df1fb4240774d3492b5d12ac0c", + "https://deno.land/std@0.224.0/path/_common/dirname.ts": "684df4aa71a04bbcc346c692c8485594fc8a90b9408dfbc26ff32cf3e0c98cc8", + "https://deno.land/std@0.224.0/path/_common/format.ts": "92500e91ea5de21c97f5fe91e178bae62af524b72d5fcd246d6d60ae4bcada8b", + "https://deno.land/std@0.224.0/path/_common/from_file_url.ts": "d672bdeebc11bf80e99bf266f886c70963107bdd31134c4e249eef51133ceccf", + "https://deno.land/std@0.224.0/path/_common/glob_to_reg_exp.ts": "6cac16d5c2dc23af7d66348a7ce430e5de4e70b0eede074bdbcf4903f4374d8d", + "https://deno.land/std@0.224.0/path/_common/normalize.ts": "684df4aa71a04bbcc346c692c8485594fc8a90b9408dfbc26ff32cf3e0c98cc8", + "https://deno.land/std@0.224.0/path/_common/normalize_string.ts": "33edef773c2a8e242761f731adeb2bd6d683e9c69e4e3d0092985bede74f4ac3", + "https://deno.land/std@0.224.0/path/_common/relative.ts": "faa2753d9b32320ed4ada0733261e3357c186e5705678d9dd08b97527deae607", + "https://deno.land/std@0.224.0/path/_common/strip_trailing_separators.ts": "7024a93447efcdcfeaa9339a98fa63ef9d53de363f1fbe9858970f1bba02655a", + "https://deno.land/std@0.224.0/path/_common/to_file_url.ts": "7f76adbc83ece1bba173e6e98a27c647712cab773d3f8cbe0398b74afc817883", + "https://deno.land/std@0.224.0/path/_interface.ts": "8dfeb930ca4a772c458a8c7bbe1e33216fe91c253411338ad80c5b6fa93ddba0", + "https://deno.land/std@0.224.0/path/_os.ts": "8fb9b90fb6b753bd8c77cfd8a33c2ff6c5f5bc185f50de8ca4ac6a05710b2c15", + "https://deno.land/std@0.224.0/path/basename.ts": "7ee495c2d1ee516ffff48fb9a93267ba928b5a3486b550be73071bc14f8cc63e", + "https://deno.land/std@0.224.0/path/common.ts": "03e52e22882402c986fe97ca3b5bb4263c2aa811c515ce84584b23bac4cc2643", + "https://deno.land/std@0.224.0/path/constants.ts": "0c206169ca104938ede9da48ac952de288f23343304a1c3cb6ec7625e7325f36", + "https://deno.land/std@0.224.0/path/dirname.ts": "85bd955bf31d62c9aafdd7ff561c4b5fb587d11a9a5a45e2b01aedffa4238a7c", + "https://deno.land/std@0.224.0/path/extname.ts": "593303db8ae8c865cbd9ceec6e55d4b9ac5410c1e276bfd3131916591b954441", + "https://deno.land/std@0.224.0/path/format.ts": "6ce1779b0980296cf2bc20d66436b12792102b831fd281ab9eb08fa8a3e6f6ac", + "https://deno.land/std@0.224.0/path/from_file_url.ts": "911833ae4fd10a1c84f6271f36151ab785955849117dc48c6e43b929504ee069", + "https://deno.land/std@0.224.0/path/glob_to_regexp.ts": "7f30f0a21439cadfdae1be1bf370880b415e676097fda584a63ce319053b5972", + "https://deno.land/std@0.224.0/path/is_absolute.ts": "4791afc8bfd0c87f0526eaa616b0d16e7b3ab6a65b62942e50eac68de4ef67d7", + "https://deno.land/std@0.224.0/path/is_glob.ts": "a65f6195d3058c3050ab905705891b412ff942a292bcbaa1a807a74439a14141", + "https://deno.land/std@0.224.0/path/join.ts": "ae2ec5ca44c7e84a235fd532e4a0116bfb1f2368b394db1c4fb75e3c0f26a33a", + "https://deno.land/std@0.224.0/path/join_globs.ts": "5b3bf248b93247194f94fa6947b612ab9d3abd571ca8386cf7789038545e54a0", + "https://deno.land/std@0.224.0/path/mod.ts": "f6bd79cb08be0e604201bc9de41ac9248582699d1b2ee0ab6bc9190d472cf9cd", + "https://deno.land/std@0.224.0/path/normalize.ts": "4155743ccceeed319b350c1e62e931600272fad8ad00c417b91df093867a8352", + "https://deno.land/std@0.224.0/path/normalize_glob.ts": "cc89a77a7d3b1d01053b9dcd59462b75482b11e9068ae6c754b5cf5d794b374f", + "https://deno.land/std@0.224.0/path/parse.ts": "77ad91dcb235a66c6f504df83087ce2a5471e67d79c402014f6e847389108d5a", + "https://deno.land/std@0.224.0/path/posix/_util.ts": "1e3937da30f080bfc99fe45d7ed23c47dd8585c5e473b2d771380d3a6937cf9d", + "https://deno.land/std@0.224.0/path/posix/basename.ts": "d2fa5fbbb1c5a3ab8b9326458a8d4ceac77580961b3739cd5bfd1d3541a3e5f0", + "https://deno.land/std@0.224.0/path/posix/common.ts": "26f60ccc8b2cac3e1613000c23ac5a7d392715d479e5be413473a37903a2b5d4", + "https://deno.land/std@0.224.0/path/posix/constants.ts": "93481efb98cdffa4c719c22a0182b994e5a6aed3047e1962f6c2c75b7592bef1", + "https://deno.land/std@0.224.0/path/posix/dirname.ts": "76cd348ffe92345711409f88d4d8561d8645353ac215c8e9c80140069bf42f00", + "https://deno.land/std@0.224.0/path/posix/extname.ts": "e398c1d9d1908d3756a7ed94199fcd169e79466dd88feffd2f47ce0abf9d61d2", + "https://deno.land/std@0.224.0/path/posix/format.ts": "185e9ee2091a42dd39e2a3b8e4925370ee8407572cee1ae52838aed96310c5c1", + "https://deno.land/std@0.224.0/path/posix/from_file_url.ts": "951aee3a2c46fd0ed488899d024c6352b59154c70552e90885ed0c2ab699bc40", + "https://deno.land/std@0.224.0/path/posix/glob_to_regexp.ts": "76f012fcdb22c04b633f536c0b9644d100861bea36e9da56a94b9c589a742e8f", + "https://deno.land/std@0.224.0/path/posix/is_absolute.ts": "cebe561ad0ae294f0ce0365a1879dcfca8abd872821519b4fcc8d8967f888ede", + "https://deno.land/std@0.224.0/path/posix/is_glob.ts": "8a8b08c08bf731acf2c1232218f1f45a11131bc01de81e5f803450a5914434b9", + "https://deno.land/std@0.224.0/path/posix/join.ts": "7fc2cb3716aa1b863e990baf30b101d768db479e70b7313b4866a088db016f63", + "https://deno.land/std@0.224.0/path/posix/join_globs.ts": "a9475b44645feddceb484ee0498e456f4add112e181cb94042cdc6d47d1cdd25", + "https://deno.land/std@0.224.0/path/posix/mod.ts": "2301fc1c54a28b349e20656f68a85f75befa0ee9b6cd75bfac3da5aca9c3f604", + "https://deno.land/std@0.224.0/path/posix/normalize.ts": "baeb49816a8299f90a0237d214cef46f00ba3e95c0d2ceb74205a6a584b58a91", + "https://deno.land/std@0.224.0/path/posix/normalize_glob.ts": "9c87a829b6c0f445d03b3ecadc14492e2864c3ebb966f4cea41e98326e4435c6", + "https://deno.land/std@0.224.0/path/posix/parse.ts": "09dfad0cae530f93627202f28c1befa78ea6e751f92f478ca2cc3b56be2cbb6a", + "https://deno.land/std@0.224.0/path/posix/relative.ts": "3907d6eda41f0ff723d336125a1ad4349112cd4d48f693859980314d5b9da31c", + "https://deno.land/std@0.224.0/path/posix/resolve.ts": "08b699cfeee10cb6857ccab38fa4b2ec703b0ea33e8e69964f29d02a2d5257cf", + "https://deno.land/std@0.224.0/path/posix/to_file_url.ts": "7aa752ba66a35049e0e4a4be5a0a31ac6b645257d2e031142abb1854de250aaf", + "https://deno.land/std@0.224.0/path/posix/to_namespaced_path.ts": "28b216b3c76f892a4dca9734ff1cc0045d135532bfd9c435ae4858bfa5a2ebf0", + "https://deno.land/std@0.224.0/path/relative.ts": "ab739d727180ed8727e34ed71d976912461d98e2b76de3d3de834c1066667add", + "https://deno.land/std@0.224.0/path/resolve.ts": "a6f977bdb4272e79d8d0ed4333e3d71367cc3926acf15ac271f1d059c8494d8d", + "https://deno.land/std@0.224.0/path/to_file_url.ts": "88f049b769bce411e2d2db5bd9e6fd9a185a5fbd6b9f5ad8f52bef517c4ece1b", + "https://deno.land/std@0.224.0/path/to_namespaced_path.ts": "b706a4103b104cfadc09600a5f838c2ba94dbcdb642344557122dda444526e40", + "https://deno.land/std@0.224.0/path/windows/_util.ts": "d5f47363e5293fced22c984550d5e70e98e266cc3f31769e1710511803d04808", + "https://deno.land/std@0.224.0/path/windows/basename.ts": "6bbc57bac9df2cec43288c8c5334919418d784243a00bc10de67d392ab36d660", + "https://deno.land/std@0.224.0/path/windows/common.ts": "26f60ccc8b2cac3e1613000c23ac5a7d392715d479e5be413473a37903a2b5d4", + "https://deno.land/std@0.224.0/path/windows/constants.ts": "5afaac0a1f67b68b0a380a4ef391bf59feb55856aa8c60dfc01bd3b6abb813f5", + "https://deno.land/std@0.224.0/path/windows/dirname.ts": "33e421be5a5558a1346a48e74c330b8e560be7424ed7684ea03c12c21b627bc9", + "https://deno.land/std@0.224.0/path/windows/extname.ts": "165a61b00d781257fda1e9606a48c78b06815385e7d703232548dbfc95346bef", + "https://deno.land/std@0.224.0/path/windows/format.ts": "bbb5ecf379305b472b1082cd2fdc010e44a0020030414974d6029be9ad52aeb6", + "https://deno.land/std@0.224.0/path/windows/from_file_url.ts": "ced2d587b6dff18f963f269d745c4a599cf82b0c4007356bd957cb4cb52efc01", + "https://deno.land/std@0.224.0/path/windows/glob_to_regexp.ts": "e45f1f89bf3fc36f94ab7b3b9d0026729829fabc486c77f414caebef3b7304f8", + "https://deno.land/std@0.224.0/path/windows/is_absolute.ts": "4a8f6853f8598cf91a835f41abed42112cebab09478b072e4beb00ec81f8ca8a", + "https://deno.land/std@0.224.0/path/windows/is_glob.ts": "8a8b08c08bf731acf2c1232218f1f45a11131bc01de81e5f803450a5914434b9", + "https://deno.land/std@0.224.0/path/windows/join.ts": "8d03530ab89195185103b7da9dfc6327af13eabdcd44c7c63e42e27808f50ecf", + "https://deno.land/std@0.224.0/path/windows/join_globs.ts": "a9475b44645feddceb484ee0498e456f4add112e181cb94042cdc6d47d1cdd25", + "https://deno.land/std@0.224.0/path/windows/mod.ts": "2301fc1c54a28b349e20656f68a85f75befa0ee9b6cd75bfac3da5aca9c3f604", + "https://deno.land/std@0.224.0/path/windows/normalize.ts": "78126170ab917f0ca355a9af9e65ad6bfa5be14d574c5fb09bb1920f52577780", + "https://deno.land/std@0.224.0/path/windows/normalize_glob.ts": "9c87a829b6c0f445d03b3ecadc14492e2864c3ebb966f4cea41e98326e4435c6", + "https://deno.land/std@0.224.0/path/windows/parse.ts": "08804327b0484d18ab4d6781742bf374976de662f8642e62a67e93346e759707", + "https://deno.land/std@0.224.0/path/windows/relative.ts": "3e1abc7977ee6cc0db2730d1f9cb38be87b0ce4806759d271a70e4997fc638d7", + "https://deno.land/std@0.224.0/path/windows/resolve.ts": "8dae1dadfed9d46ff46cc337c9525c0c7d959fb400a6308f34595c45bdca1972", + "https://deno.land/std@0.224.0/path/windows/to_file_url.ts": "40e560ee4854fe5a3d4d12976cef2f4e8914125c81b11f1108e127934ced502e", + "https://deno.land/std@0.224.0/path/windows/to_namespaced_path.ts": "4ffa4fb6fae321448d5fe810b3ca741d84df4d7897e61ee29be961a6aac89a4c" + } +} diff --git a/v0.1.0/src/Errors/InvalidHttpMethodError.ts b/v0.1.0/src/Errors/InvalidHttpMethodError.ts new file mode 100644 index 0000000..8d9047f --- /dev/null +++ b/v0.1.0/src/Errors/InvalidHttpMethodError.ts @@ -0,0 +1,25 @@ +/** + * Represents an error thrown when an incoming HTTP method + * is not among the recognized set of valid HTTP methods. + * + * This is typically used in routers or request dispatchers + * to enforce allowed methods and produce 405-like behavior. + */ +export class InvalidHttpMethodError extends Error { + /** + * The invalid method that triggered this error. + */ + public readonly method: unknown; + + /** + * A fixed HTTP status code representing "Method Not Allowed". + */ + public readonly status: number = 405; + + constructor(method: unknown) { + const label = typeof method === 'string' ? method : '[non-string]'; + super(`Unsupported HTTP method: ${label}`); + this.name = 'InvalidHttpMethodError'; + this.method = method; + } +} diff --git a/v0.1.0/src/Errors/mod.ts b/v0.1.0/src/Errors/mod.ts new file mode 100644 index 0000000..50e5ba8 --- /dev/null +++ b/v0.1.0/src/Errors/mod.ts @@ -0,0 +1,3 @@ +// deno-coverage-ignore-file + +export { InvalidHttpMethodError } from './InvalidHttpMethodError.ts'; diff --git a/v0.1.0/src/HttpKernel.ts b/v0.1.0/src/HttpKernel.ts new file mode 100644 index 0000000..25ababb --- /dev/null +++ b/v0.1.0/src/HttpKernel.ts @@ -0,0 +1,199 @@ +import { + IContext, + IHttpKernel, + IHttpKernelConfig, + IInternalRoute, + IRouteBuilder, + IRouteDefinition, +} from './Interfaces/mod.ts'; +import { + DeepPartial, + Handler, + HTTP_404_NOT_FOUND, + HTTP_500_INTERNAL_SERVER_ERROR, + HttpStatusTextMap, + isHandler, + isMiddleware, + Middleware, +} from './Types/mod.ts'; +import { RouteBuilder } from './RouteBuilder.ts'; +import { createEmptyContext, normalizeError } from './Utils/mod.ts'; + +/** + * The `HttpKernel` is the central routing engine that manages the full HTTP request lifecycle. + * + * It enables: + * - Dynamic and static route registration via a fluent API + * - Execution of typed middleware chains and final route handlers + * - Injection of response decorators and factory overrides + * - Fine-grained error handling via typed status-code-based handlers + * + * The kernel is designed with generics for flexible context typing, strong type safety, + * and a clear extension point for advanced routing, DI, or tracing logic. + * + * @typeParam TContext - The global context type used for all requests handled by this kernel. + */ +export class HttpKernel + implements IHttpKernel { + private cfg: IHttpKernelConfig; + + /** + * The list of registered route definitions, including method, matcher, + * middleware pipeline, and final handler. + */ + private routes: IInternalRoute[] = []; + + /** + * Initializes the `HttpKernel` with optional configuration overrides. + * + * Default components such as the route builder factory, response decorator, + * and 404/500 error handlers can be replaced by injecting a partial config. + * Any omitted values fall back to sensible defaults. + * + * @param config - Partial kernel configuration. Missing fields are filled with defaults. + */ + public constructor( + config?: DeepPartial>, + ) { + this.cfg = { + decorateResponse: (res) => res, + routeBuilderFactory: RouteBuilder, + httpErrorHandlers: { + [HTTP_404_NOT_FOUND]: () => + new Response(HttpStatusTextMap[HTTP_404_NOT_FOUND], { + status: HTTP_404_NOT_FOUND, + }), + [HTTP_500_INTERNAL_SERVER_ERROR]: () => + new Response( + HttpStatusTextMap[HTTP_500_INTERNAL_SERVER_ERROR], + { + status: HTTP_500_INTERNAL_SERVER_ERROR, + }, + ), + ...(config?.httpErrorHandlers ?? {}), + }, + ...config, + } as IHttpKernelConfig; + + this.handle = this.handle.bind(this); + this.registerRoute = this.registerRoute.bind(this); + } + + /** + * @inheritdoc + */ + public route<_TContext extends IContext = TContext>( + definition: IRouteDefinition, + ): IRouteBuilder<_TContext> { + return new this.cfg.routeBuilderFactory( + this.registerRoute, + definition, + ) as IRouteBuilder<_TContext>; + } + + /** + * @inheritdoc + */ + public async handle(request: Request): Promise { + const url = new URL(request.url); + const method = request.method.toUpperCase(); + + for (const route of this.routes) { + if (route.method !== method) continue; + const match = route.matcher(url, request); + if (match) { + const ctx: TContext = { + req: request, + params: match.params, + query: match.query, + state: {}, + } as TContext; + return await this.executePipeline( + ctx, + route.middlewares, + route.handler, + ); + } + } + + return this.cfg.httpErrorHandlers[HTTP_404_NOT_FOUND]( + createEmptyContext(request), + ); + } + + /** + * Finalizes and registers a route within the kernel. + * + * This method is invoked internally by the route builder once + * `.handle()` is called. It appends the route to the internal list. + * + * @param route - A fully constructed internal route object. + */ + private registerRoute<_TContext extends IContext = TContext>( + route: IInternalRoute<_TContext>, + ): void { + this.routes.push(route as unknown as IInternalRoute); + } + + /** + * Executes the middleware and handler pipeline for a matched route. + * + * This function: + * - Enforces linear middleware execution with `next()` tracking + * - Validates middleware and handler types at runtime + * - Applies the optional response decorator post-processing + * - Handles all runtime errors via the configured 500 handler + * + * @param ctx - The active request context passed to middleware and handler. + * @param middleware - Ordered middleware functions for this route. + * @param handler - The final handler responsible for generating a response. + * @returns The final HTTP `Response`, possibly decorated. + */ + private async executePipeline( + ctx: TContext, + middleware: Middleware[], + handler: Handler, + ): Promise { + const handleInternalError = (ctx: TContext, err?: unknown) => + this.cfg.httpErrorHandlers[HTTP_500_INTERNAL_SERVER_ERROR]( + ctx, + normalizeError(err), + ); + + let lastIndex = -1; + + const dispatch = async (currentIndex: number): Promise => { + if (currentIndex <= lastIndex) { + throw new Error('Middleware called `next()` multiple times'); + } + lastIndex = currentIndex; + + const isWithinMiddleware = currentIndex < middleware.length; + const fn = isWithinMiddleware ? middleware[currentIndex] : handler; + + if (isWithinMiddleware) { + if (!isMiddleware(fn)) { + throw new Error( + 'Expected middleware function, but received invalid value', + ); + } + return await fn(ctx, () => dispatch(currentIndex + 1)); + } + + if (!isHandler(fn)) { + throw new Error( + 'Expected request handler, but received invalid value', + ); + } + + return await fn(ctx); + }; + + try { + const response = await dispatch(0); + return this.cfg.decorateResponse(response, ctx); + } catch (e) { + return handleInternalError(ctx, e); + } + } +} diff --git a/v0.1.0/src/Interfaces/IContext.ts b/v0.1.0/src/Interfaces/IContext.ts new file mode 100644 index 0000000..3b63d33 --- /dev/null +++ b/v0.1.0/src/Interfaces/IContext.ts @@ -0,0 +1,53 @@ +import { Params, Query, State } from '../Types/mod.ts'; + +/** + * Represents the complete context for a single HTTP request, + * passed through the middleware pipeline and to the final route handler. + * + * This context object encapsulates all relevant runtime data for a request, + * including the original request, path parameters, query parameters, + * and a shared, mutable application state. + * + * @template TState Structured per-request state shared across middlewares and handlers. + * @template TParams Parsed URL path parameters, typically derived from route templates. + * @template TQuery Parsed query string parameters, preserving multi-value semantics. + */ +export interface IContext< + TState extends State = State, + TParams extends Params = Params, + TQuery extends Query = Query, +> { + /** + * The original HTTP request object as received by Deno. + * Contains all standard fields like headers, method, body, etc. + */ + req: Request; + + /** + * Route parameters parsed from the URL path, based on route definitions + * that include dynamic segments (e.g., `/users/:id` β†’ `{ id: "123" }`). + * + * These parameters are considered read-only and are set by the router. + */ + params: TParams; + + /** + * Query parameters extracted from the request URL's search string. + * + * Values may occur multiple times (e.g., `?tag=ts&tag=deno`), and are therefore + * represented as either a string or an array of strings, depending on occurrence. + * + * Use this field to access filters, flags, pagination info, or similar modifiers. + */ + query: TQuery; + + /** + * A typed, mutable object used to pass structured data between middlewares and handlers. + * + * This object is ideal for sharing validated input, user identity, trace information, + * or other contextual state throughout the request lifecycle. + * + * Type-safe access to fields is ensured by the generic `TState` type. + */ + state: TState; +} diff --git a/v0.1.0/src/Interfaces/IHttpErrorHandlers.ts b/v0.1.0/src/Interfaces/IHttpErrorHandlers.ts new file mode 100644 index 0000000..fb1a9a4 --- /dev/null +++ b/v0.1.0/src/Interfaces/IHttpErrorHandlers.ts @@ -0,0 +1,40 @@ +import { IContext } from '../Interfaces/mod.ts'; +import { HttpErrorHandler, validHttpErrorCodes } from '../Types/mod.ts'; + +/** + * A mapping of HTTP status codes to their corresponding error handlers. + * + * This interface defines required handlers for common critical status codes (404 and 500) + * and allows optional handlers for all other known error codes defined in `validHttpErrorCodes`. + * + * This hybrid approach ensures predictable handling for key failure cases, + * while remaining flexible for less common codes. + * + * @template TContext - The context type used in all error handlers. + * + * @example + * ```ts + * const errorHandlers: IHttpErrorHandlers = { + * 404: (ctx) => new Response("Not Found", { status: 404 }), + * 500: (ctx, err) => { + * console.error(err); + * return new Response("Internal Server Error", { status: 500 }); + * }, + * 429: (ctx) => new Response("Too Many Requests", { status: 429 }), + * }; + * ``` + */ +export interface IHttpErrorHandlers + extends + Partial< + Record< + Exclude, + HttpErrorHandler + > + > { + /** Required error handler for HTTP 404 (Not Found). */ + 404: HttpErrorHandler; + + /** Required error handler for HTTP 500 (Internal Server Error). */ + 500: HttpErrorHandler; +} diff --git a/v0.1.0/src/Interfaces/IHttpKernel.ts b/v0.1.0/src/Interfaces/IHttpKernel.ts new file mode 100644 index 0000000..3f28eea --- /dev/null +++ b/v0.1.0/src/Interfaces/IHttpKernel.ts @@ -0,0 +1,49 @@ +import { IContext } from './IContext.ts'; +import { IRouteBuilder } from './IRouteBuilder.ts'; +import { IRouteDefinition } from './IRouteDefinition.ts'; + +/** + * The `IHttpKernel` interface defines the public API for a type-safe, middleware-driven HTTP dispatching system. + * + * Implementations of this interface are responsible for: + * - Registering routes with optional per-route context typing + * - Handling incoming requests by matching and dispatching to appropriate handlers + * - Managing the complete middleware pipeline and final response generation + * + * The kernel operates on a customizable `IContext` type to support strongly typed request parameters, state, + * and query values across the entire routing lifecycle. + * + * @typeParam TContext - The default context type used for all routes unless overridden per-route. + */ +export interface IHttpKernel { + /** + * Registers a new HTTP route (static or dynamic) and returns a route builder for middleware/handler chaining. + * + * This method supports contextual polymorphism via the `_TContext` type parameter, enabling fine-grained + * typing of route-specific `params`, `query`, and `state` values. The route is not registered until + * `.handle()` is called on the returned builder. + * + * @typeParam _TContext - An optional override for the context type specific to this route. + * Falls back to the global `TContext` of the kernel if omitted. + * + * @param definition - A route definition specifying the HTTP method and path or custom matcher. + * @returns A fluent builder interface to define middleware and attach a final handler. + */ + route<_TContext extends IContext = TContext>( + definition: IRouteDefinition, + ): IRouteBuilder<_TContext>; + + /** + * Handles an incoming HTTP request and produces a `Response`. + * + * The kernel matches the request against all registered routes by method and matcher, + * constructs a typed context, and executes the middleware/handler pipeline. + * If no route matches, a 404 error handler is invoked. + * + * This method is designed to be passed directly to `Deno.serve()` or similar server frameworks. + * + * @param request - The incoming HTTP request object. + * @returns A `Promise` resolving to a complete HTTP response. + */ + handle(request: Request): Promise; +} diff --git a/v0.1.0/src/Interfaces/IHttpKernelConfig.ts b/v0.1.0/src/Interfaces/IHttpKernelConfig.ts new file mode 100644 index 0000000..cd2cdc3 --- /dev/null +++ b/v0.1.0/src/Interfaces/IHttpKernelConfig.ts @@ -0,0 +1,10 @@ +import { ResponseDecorator } from '../Types/mod.ts'; +import { IContext } from './IContext.ts'; +import { IHttpErrorHandlers } from './IHttpErrorHandlers.ts'; +import { IRouteBuilderFactory } from './IRouteBuilder.ts'; + +export interface IHttpKernelConfig { + decorateResponse: ResponseDecorator; + routeBuilderFactory: IRouteBuilderFactory; + httpErrorHandlers: IHttpErrorHandlers; +} diff --git a/v0.1.0/src/Interfaces/IInternalRoute.ts b/v0.1.0/src/Interfaces/IInternalRoute.ts new file mode 100644 index 0000000..7565a4a --- /dev/null +++ b/v0.1.0/src/Interfaces/IInternalRoute.ts @@ -0,0 +1,39 @@ +import { Handler, HttpMethod, Middleware } from '../Types/mod.ts'; +import { IContext, IRouteMatcher } from './mod.ts'; + +/** + * Represents an internally registered route within the HttpKernel. + * + * Contains all data required to match an incoming request and dispatch it + * through the associated middleware chain and final handler. + */ +export interface IInternalRoute { + /** + * The HTTP method (e.g. 'GET', 'POST') that this route responds to. + * The method should always be in uppercase. + */ + method: HttpMethod; + + /** + * A matcher function used to determine whether this route matches a given request. + * + * If the matcher returns `null`, the route does not apply to the request. + * If it returns a params object, the route is considered matched and the extracted + * parameters are passed into the request context. + * + * @param url - The parsed URL object from the incoming request. + * @param req - The original Request object. + * @returns An object with extracted path parameters, or `null` if not matched. + */ + matcher: IRouteMatcher; + + /** + * An ordered list of middleware functions to be executed before the handler. + */ + middlewares: Middleware[]; + + /** + * The final handler that generates the HTTP response after all middleware has run. + */ + handler: Handler; +} diff --git a/v0.1.0/src/Interfaces/IRouteBuilder.ts b/v0.1.0/src/Interfaces/IRouteBuilder.ts new file mode 100644 index 0000000..24ee1a4 --- /dev/null +++ b/v0.1.0/src/Interfaces/IRouteBuilder.ts @@ -0,0 +1,39 @@ +import { Handler, Middleware } from '../Types/mod.ts'; +import { IInternalRoute } from './IInternalRoute.ts'; +import { IRouteDefinition } from './IRouteDefinition.ts'; +import { IContext } from './mod.ts'; + +export interface IRouteBuilderFactory { + new ( + registerRoute: (route: IInternalRoute) => void, + def: IRouteDefinition, + mws?: Middleware[], + ): IRouteBuilder; +} + +/** + * Provides a fluent API to build a single route configuration by chaining + * middleware and setting the final request handler. + */ +export interface IRouteBuilder { + /** + * Adds a middleware to the current route. + * Middleware will be executed in the order of registration. + * + * @param mw - A middleware function. + * @returns The route builder for further chaining. + */ + middleware( + mw: Middleware, + ): IRouteBuilder; + + /** + * Sets the final request handler for the route. + * Calling this finalizes the route and registers it in the kernel. + * + * @param handler - The function to execute when this route is matched. + */ + handle( + handler: Handler, + ): void; +} diff --git a/v0.1.0/src/Interfaces/IRouteDefinition.ts b/v0.1.0/src/Interfaces/IRouteDefinition.ts new file mode 100644 index 0000000..43146cc --- /dev/null +++ b/v0.1.0/src/Interfaces/IRouteDefinition.ts @@ -0,0 +1,91 @@ +import { HttpMethod, isHttpMethod } from '../Types/mod.ts'; +import { IRouteMatcher } from './IRouteMatcher.ts'; + +/** + * Defines a static route using a path pattern with optional parameters. + * + * Suitable for conventional routes like "/users/:id", which can be parsed + * into named parameters using a path-matching library. + */ +export interface IStaticRouteDefinition { + /** + * The HTTP method this route should match (e.g. "GET", "POST"). + */ + method: HttpMethod; + + /** + * A static path pattern for the route, which may include named parameters + * (e.g. "/caches/:id"). Internally, this can be converted to a regex matcher. + */ + path: string; +} + +/** + * Defines a dynamic route using a custom matcher function instead of a static path. + * + * Useful for complex URL structures that cannot easily be expressed using a static pattern, + * such as routes with variable prefixes or conditional segment logic. + */ +export interface IDynamicRouteDefinition { + /** + * The HTTP method this route should match (e.g. "GET", "POST"). + */ + method: HttpMethod; + + /** + * A custom matcher function that receives the parsed URL and raw request. + * If the function returns `null`, the route does not match. + * If the function returns a params object, the route is considered matched. + */ + matcher: IRouteMatcher; +} + +/** + * A route definition can either be a conventional static route with a path pattern, + * or a dynamic route with a custom matcher function for advanced matching logic. + */ +export type IRouteDefinition = IStaticRouteDefinition | IDynamicRouteDefinition; + +/** + * Type guard to check whether a route definition is a valid static route definition. + * + * Ensures that the object: + * - has a `method` property of type `HttpMethod` + * - has a `path` property of type `string` + * - does NOT have a `matcher` function (to avoid ambiguous mixed types) + */ +export function isStaticRouteDefinition( + def: IRouteDefinition, +): def is IStaticRouteDefinition { + return ( + def && + typeof def === 'object' && + 'method' in def && + isHttpMethod(def.method) && + 'path' in def && + typeof (def as { path?: unknown }).path === 'string' && + !('matcher' in def) + ); +} + +/** + * Type guard to check whether a route definition is a valid dynamic route definition. + * + * Ensures that the object: + * - has a `method` property of type `HttpMethod` + * - has a `matcher` property of type `function` + * - does NOT have a `path` property (to avoid ambiguous mixed types) + */ +export function isDynamicRouteDefinition( + def: IRouteDefinition, +): def is IDynamicRouteDefinition { + return ( + def && + typeof def === 'object' && + 'method' in def && + isHttpMethod(def.method) && + 'matcher' in def && + typeof (def as { matcher?: unknown }).matcher === 'function' && + !('path' in def) + ); +} diff --git a/v0.1.0/src/Interfaces/IRouteMatch.ts b/v0.1.0/src/Interfaces/IRouteMatch.ts new file mode 100644 index 0000000..06e1d7c --- /dev/null +++ b/v0.1.0/src/Interfaces/IRouteMatch.ts @@ -0,0 +1,6 @@ +import { Params, Query } from '../Types/mod.ts'; + +export interface IRouteMatch { + params?: Params; + query?: Query; +} diff --git a/v0.1.0/src/Interfaces/IRouteMatcher.ts b/v0.1.0/src/Interfaces/IRouteMatcher.ts new file mode 100644 index 0000000..1cbd3a3 --- /dev/null +++ b/v0.1.0/src/Interfaces/IRouteMatcher.ts @@ -0,0 +1,36 @@ +import { Params } from '../Types/mod.ts'; +import { IRouteDefinition } from './IRouteDefinition.ts'; +import { IRouteMatch } from './IRouteMatch.ts'; + +/** + * Defines a route matcher function that evaluates whether a route applies to a given request. + * + * If the route matches, the matcher returns an object containing extracted route parameters. + * Otherwise, it returns `null`. + */ +export interface IRouteMatcher { + /** + * Evaluates whether the given URL and request match a defined route. + * + * @param url - The full URL of the incoming request. + * @param req - The raw Request object (may be used for context or headers). + * @returns An object containing path parameters if matched, or `null` if not matched. + */ + (url: URL, req: Request): null | IRouteMatch; +} + +/** + * Represents a factory for creating route matcher functions from route definitions. + * + * This allows the matcher logic to be injected or replaced (e.g. for testing, + * pattern libraries, or advanced routing scenarios). + */ +export interface IRouteMatcherFactory { + /** + * Creates a matcher function based on a given route definition. + * + * @param def - The route definition (static or dynamic). + * @returns A matcher function that checks if a request matches and extracts parameters. + */ + (def: IRouteDefinition): IRouteMatcher; +} diff --git a/v0.1.0/src/Interfaces/__tests__/routeDefinitionGuards.test.ts b/v0.1.0/src/Interfaces/__tests__/routeDefinitionGuards.test.ts new file mode 100644 index 0000000..c00b7c6 --- /dev/null +++ b/v0.1.0/src/Interfaces/__tests__/routeDefinitionGuards.test.ts @@ -0,0 +1,43 @@ +import { assertEquals } from 'https://deno.land/std@0.204.0/assert/mod.ts'; +import { + IRouteDefinition, + isDynamicRouteDefinition, + isStaticRouteDefinition, +} from '../IRouteDefinition.ts'; + +Deno.test('isStaticRouteDefinition returns true for static route', () => { + const staticDef: IRouteDefinition = { + method: 'GET', + path: '/users/:id', + }; + + assertEquals(isStaticRouteDefinition(staticDef), true); + assertEquals(isDynamicRouteDefinition(staticDef), false); +}); + +Deno.test('isDynamicRouteDefinition returns true for dynamic route', () => { + const dynamicDef: IRouteDefinition = { + method: 'POST', + matcher: (_url, _req) => ({ params: {} }), + }; + + assertEquals(isDynamicRouteDefinition(dynamicDef), true); + assertEquals(isStaticRouteDefinition(dynamicDef), false); +}); + +Deno.test('isStaticRouteDefinition returns false for invalid object', () => { + const invalidDef = { + method: 'GET', + } as unknown as IRouteDefinition; + + assertEquals(isStaticRouteDefinition(invalidDef), false); +}); + +Deno.test('isDynamicRouteDefinition returns false for object with no matcher', () => { + const def = { + method: 'DELETE', + path: '/something', + }; + + assertEquals(isDynamicRouteDefinition(def as IRouteDefinition), false); +}); diff --git a/v0.1.0/src/Interfaces/mod.ts b/v0.1.0/src/Interfaces/mod.ts new file mode 100644 index 0000000..7c235d9 --- /dev/null +++ b/v0.1.0/src/Interfaces/mod.ts @@ -0,0 +1,19 @@ +// deno-coverage-ignore-file + +export type { IContext } from './IContext.ts'; +export type { IHttpErrorHandlers } from './IHttpErrorHandlers.ts'; +export type { IHttpKernel } from './IHttpKernel.ts'; +export type { IHttpKernelConfig } from './IHttpKernelConfig.ts'; +export type { IInternalRoute } from './IInternalRoute.ts'; +export type { IRouteBuilder, IRouteBuilderFactory } from './IRouteBuilder.ts'; +export { + isDynamicRouteDefinition, + isStaticRouteDefinition, +} from './IRouteDefinition.ts'; +export type { + IDynamicRouteDefinition, + IRouteDefinition, + IStaticRouteDefinition, +} from './IRouteDefinition.ts'; +export type { IRouteMatch } from './IRouteMatch.ts'; +export type { IRouteMatcher, IRouteMatcherFactory } from './IRouteMatcher.ts'; diff --git a/v0.1.0/src/RouteBuilder.ts b/v0.1.0/src/RouteBuilder.ts new file mode 100644 index 0000000..c6e6e8d --- /dev/null +++ b/v0.1.0/src/RouteBuilder.ts @@ -0,0 +1,67 @@ +import { IRouteMatcherFactory } from './Interfaces/IRouteMatcher.ts'; +import { IContext, IRouteBuilder, IRouteDefinition } from './Interfaces/mod.ts'; +import { Handler, Middleware, RegisterRoute } from './Types/mod.ts'; +import { createRouteMatcher } from './Utils/createRouteMatcher.ts'; + +/** + * Provides a fluent builder interface for defining a single route, + * including HTTP method, path or matcher, middleware chain and final handler. + * + * This builder is stateless and immutable; each chained call returns a new instance. + */ +export class RouteBuilder + implements IRouteBuilder { + /** + * Constructs a new instance of the route builder. + * + * @param registerRoute - A delegate used to register the finalized route definition. + * @param def - The route definition (static path or dynamic matcher). + * @param mws - The list of middleware functions collected so far (default: empty). + */ + constructor( + private readonly registerRoute: RegisterRoute, + private readonly def: IRouteDefinition, + private readonly mws: Middleware[] = [], + private readonly matcherFactory: IRouteMatcherFactory = + createRouteMatcher, + ) {} + + /** + * Adds a middleware function to the current route definition. + * + * Middleware is executed in the order it is added. + * Returns a new builder instance with the additional middleware appended. + * + * @param mw - A middleware function to be executed before the handler. + * @returns A new `RouteBuilder` instance for continued chaining. + */ + middleware( + mw: Middleware, + ): IRouteBuilder { + return new RouteBuilder( + this.registerRoute, + this.def, + [...this.mws, mw], + ); + } + + /** + * Finalizes the route by assigning the handler and registering the route. + * + * Internally constructs a matcher function from the route definition + * and passes all route data to the registration delegate. + * + * @param handler - The final request handler for this route. + */ + handle( + handler: Handler, + ): void { + const matcher = this.matcherFactory(this.def); + this.registerRoute({ + method: this.def.method, + matcher, + middlewares: this.mws, + handler: handler, + }); + } +} diff --git a/v0.1.0/src/Types/DeepPartial.ts b/v0.1.0/src/Types/DeepPartial.ts new file mode 100644 index 0000000..07c5132 --- /dev/null +++ b/v0.1.0/src/Types/DeepPartial.ts @@ -0,0 +1,4 @@ +export type DeepPartial = { + [P in keyof T]?: T[P] extends object ? DeepPartial + : T[P]; +}; diff --git a/v0.1.0/src/Types/Handler.ts b/v0.1.0/src/Types/Handler.ts new file mode 100644 index 0000000..0a67a6e --- /dev/null +++ b/v0.1.0/src/Types/Handler.ts @@ -0,0 +1,57 @@ +import { IContext } from '../Interfaces/mod.ts'; + +/** + * Represents a final request handler responsible for producing an HTTP response. + * + * The handler is the terminal stage of the middleware pipeline and is responsible + * for processing the incoming request and generating the final `Response`. + * + * It receives the fully-typed request context, which includes the original request, + * parsed route parameters, query parameters, and any shared state populated by prior middleware. + * + * @template TContext The specific context type for this handler, including typed `state`, `params`, and `query`. + */ +type Handler = ( + ctx: TContext, +) => Promise; + +/** + * Represents a handler function with an associated name. + * + * This is useful for debugging, logging, or when you need to reference + * the handler by name in your application. + * + * @template TContext The specific context type for this handler, including typed `state`, `params`, and `query`. + */ +type NamedHandler = + & Handler + & { name?: string }; + +export type { NamedHandler as Handler }; + +/** + * Type guard to determine whether a given value is a valid `IHandler` function. + * + * This function checks whether the input is a function and whether it returns + * a `Promise` when called. Due to TypeScript's structural typing and + * the lack of runtime type information, only minimal runtime validation is possible. + * + * @param value - The value to test. + * @returns `true` if the value is a function that appears to conform to `IHandler`. + * + * @example + * ```ts + * const candidate = async (ctx: IContext) => new Response("ok"); + * if (isHandler(candidate)) { + * // candidate is now typed as IHandler + * } + * ``` + */ +export function isHandler( + value: unknown, +): value is Handler { + return ( + typeof value === 'function' && + value.length === 1 // ctx + ); +} diff --git a/v0.1.0/src/Types/HttpErrorHandler.ts b/v0.1.0/src/Types/HttpErrorHandler.ts new file mode 100644 index 0000000..5a7667f --- /dev/null +++ b/v0.1.0/src/Types/HttpErrorHandler.ts @@ -0,0 +1,28 @@ +import { IContext } from '../Interfaces/mod.ts'; + +/** + * Defines a handler function for errors that occur during the execution + * of middleware or route handlers within the HTTP kernel. + * + * This function receives both the request context and the thrown error, + * and is responsible for producing an appropriate HTTP `Response`. + * + * Typical use cases include: + * - Mapping known error types to specific HTTP status codes. + * - Generating structured error responses (e.g. JSON error payloads). + * - Logging errors centrally with request metadata. + * + * The handler may return the response synchronously or asynchronously. + * + * @template TContext - The specific request context type, allowing typed access to route parameters, + * query parameters, and per-request state when formatting error responses. + * + * @param context - The active request context at the time the error occurred. + * @param error - The exception or error that was thrown during request processing. + * + * @returns A `Response` object or a `Promise` resolving to one, to be sent to the client. + */ +export type HttpErrorHandler = ( + context?: Partial, + error?: Error, +) => Promise | Response; diff --git a/v0.1.0/src/Types/HttpMethod.ts b/v0.1.0/src/Types/HttpMethod.ts new file mode 100644 index 0000000..d076523 --- /dev/null +++ b/v0.1.0/src/Types/HttpMethod.ts @@ -0,0 +1,52 @@ +/** + * A constant list of all supported HTTP methods according to RFC 7231 and RFC 5789. + * + * This array serves both as a runtime value list for validation + * and as the basis for deriving the `HttpMethod` union type. + * + * Note: The list is immutable and should not be modified at runtime. + */ +export const validHttpMethods = [ + 'GET', + 'POST', + 'PUT', + 'PATCH', + 'DELETE', + 'HEAD', + 'OPTIONS', +] as const; + +/** + * A union type representing all valid HTTP methods recognized by this application. + * + * This type is derived directly from the `validHttpMethods` constant, + * ensuring type safety and consistency between type system and runtime checks. + * + * Example: + * ```ts + * const method: HttpMethod = 'POST'; // βœ… valid + * const method: HttpMethod = 'FOO'; // ❌ Type error + * ``` + */ +export type HttpMethod = typeof validHttpMethods[number]; + +/** + * Type guard to verify whether a given value is a valid HTTP method. + * + * This function checks both the type and content of the value + * and is suitable for runtime validation of inputs (e.g., from HTTP requests). + * + * Example: + * ```ts + * if (isHttpMethod(input)) { + * // input is now typed as HttpMethod + * } + * ``` + * + * @param value - The value to test (typically a string from a request). + * @returns `true` if the value is a valid `HttpMethod`, otherwise `false`. + */ +export function isHttpMethod(value: unknown): value is HttpMethod { + return typeof value === 'string' && + validHttpMethods.includes(value as HttpMethod); +} diff --git a/v0.1.0/src/Types/HttpStatusCode.ts b/v0.1.0/src/Types/HttpStatusCode.ts new file mode 100644 index 0000000..2230d6f --- /dev/null +++ b/v0.1.0/src/Types/HttpStatusCode.ts @@ -0,0 +1,189 @@ +// Informational responses +/** Indicates that the request was received and the client can continue. */ +export const HTTP_100_CONTINUE = 100; +/** The server is switching protocols as requested by the client. */ +export const HTTP_101_SWITCHING_PROTOCOLS = 101; +/** The server has received and is processing the request, but no response is available yet. */ +export const HTTP_102_PROCESSING = 102; + +// Successful responses +/** The request has succeeded. */ +export const HTTP_200_OK = 200; +/** The request has succeeded and a new resource has been created as a result. */ +export const HTTP_201_CREATED = 201; +/** The request has been accepted for processing, but the processing is not complete. */ +export const HTTP_202_ACCEPTED = 202; +/** The server has successfully fulfilled the request and there is no content to send. */ +export const HTTP_204_NO_CONTENT = 204; + +// Redirection messages +/** The resource has been moved permanently to a new URI. */ +export const HTTP_301_MOVED_PERMANENTLY = 301; +/** The resource resides temporarily under a different URI. */ +export const HTTP_302_FOUND = 302; +/** Indicates that the resource has not been modified since the last request. */ +export const HTTP_304_NOT_MODIFIED = 304; + +// Client error responses +/** The server could not understand the request due to invalid syntax. */ +export const HTTP_400_BAD_REQUEST = 400; +/** The request requires user authentication. */ +export const HTTP_401_UNAUTHORIZED = 401; +/** The server understood the request but refuses to authorize it. */ +export const HTTP_403_FORBIDDEN = 403; +/** The server cannot find the requested resource. */ +export const HTTP_404_NOT_FOUND = 404; +/** The request method is known by the server but is not supported by the target resource. */ +export const HTTP_405_METHOD_NOT_ALLOWED = 405; +/** The request could not be completed due to a conflict with the current state of the resource. */ +export const HTTP_409_CONFLICT = 409; +/** The server understands the content type but was unable to process the contained instructions. */ +export const HTTP_422_UNPROCESSABLE_ENTITY = 422; +/** The user has sent too many requests in a given amount of time. */ +export const HTTP_429_TOO_MANY_REQUESTS = 429; + +// Server error responses +/** The server encountered an unexpected condition that prevented it from fulfilling the request. */ +export const HTTP_500_INTERNAL_SERVER_ERROR = 500; +/** The server does not support the functionality required to fulfill the request. */ +export const HTTP_501_NOT_IMPLEMENTED = 501; +/** The server, while acting as a gateway or proxy, received an invalid response from the upstream server. */ +export const HTTP_502_BAD_GATEWAY = 502; +/** The server is not ready to handle the request, often due to maintenance or overload. */ +export const HTTP_503_SERVICE_UNAVAILABLE = 503; +/** The server is acting as a gateway and cannot get a response in time. */ +export const HTTP_504_GATEWAY_TIMEOUT = 504; + +/** + * A constant list of supported HTTP status codes used by this application. + * + * These constants are grouped by category and used to construct the union type `HttpStatusCode`. + */ +export const validHttpStatusCodes = [ + // Informational + HTTP_100_CONTINUE, + HTTP_101_SWITCHING_PROTOCOLS, + HTTP_102_PROCESSING, + + // Successful + HTTP_200_OK, + HTTP_201_CREATED, + HTTP_202_ACCEPTED, + HTTP_204_NO_CONTENT, + + // Redirection + HTTP_301_MOVED_PERMANENTLY, + HTTP_302_FOUND, + HTTP_304_NOT_MODIFIED, + + // Client Errors + HTTP_400_BAD_REQUEST, + HTTP_401_UNAUTHORIZED, + HTTP_403_FORBIDDEN, + HTTP_404_NOT_FOUND, + HTTP_405_METHOD_NOT_ALLOWED, + HTTP_409_CONFLICT, + HTTP_422_UNPROCESSABLE_ENTITY, + HTTP_429_TOO_MANY_REQUESTS, + + // Server Errors + HTTP_500_INTERNAL_SERVER_ERROR, + HTTP_501_NOT_IMPLEMENTED, + HTTP_502_BAD_GATEWAY, + HTTP_503_SERVICE_UNAVAILABLE, + HTTP_504_GATEWAY_TIMEOUT, +] as const; + +/** + * A constant list of HTTP error codes that are commonly used in the application. + */ +export const validHttpErrorCodes = [ + // Client Errors + HTTP_400_BAD_REQUEST, + HTTP_401_UNAUTHORIZED, + HTTP_403_FORBIDDEN, + HTTP_404_NOT_FOUND, + HTTP_405_METHOD_NOT_ALLOWED, + HTTP_409_CONFLICT, + HTTP_422_UNPROCESSABLE_ENTITY, + HTTP_429_TOO_MANY_REQUESTS, + + // Server Errors + HTTP_500_INTERNAL_SERVER_ERROR, + HTTP_501_NOT_IMPLEMENTED, + HTTP_502_BAD_GATEWAY, + HTTP_503_SERVICE_UNAVAILABLE, + HTTP_504_GATEWAY_TIMEOUT, +] as const; + +/** + * Maps each supported HTTP status code to its standard status message. + * + * Useful for logging, diagnostics, or building custom error responses. + */ +export const HttpStatusTextMap: Record< + typeof validHttpStatusCodes[number], + string +> = { + [HTTP_100_CONTINUE]: 'Continue', + [HTTP_101_SWITCHING_PROTOCOLS]: 'Switching Protocols', + [HTTP_102_PROCESSING]: 'Processing', + + [HTTP_200_OK]: 'OK', + [HTTP_201_CREATED]: 'Created', + [HTTP_202_ACCEPTED]: 'Accepted', + [HTTP_204_NO_CONTENT]: 'No Content', + + [HTTP_301_MOVED_PERMANENTLY]: 'Moved Permanently', + [HTTP_302_FOUND]: 'Found', + [HTTP_304_NOT_MODIFIED]: 'Not Modified', + + [HTTP_400_BAD_REQUEST]: 'Bad Request', + [HTTP_401_UNAUTHORIZED]: 'Unauthorized', + [HTTP_403_FORBIDDEN]: 'Forbidden', + [HTTP_404_NOT_FOUND]: 'Not Found', + [HTTP_405_METHOD_NOT_ALLOWED]: 'Method Not Allowed', + [HTTP_409_CONFLICT]: 'Conflict', + [HTTP_422_UNPROCESSABLE_ENTITY]: 'Unprocessable Entity', + [HTTP_429_TOO_MANY_REQUESTS]: 'Too Many Requests', + + [HTTP_500_INTERNAL_SERVER_ERROR]: 'Internal Server Error', + [HTTP_501_NOT_IMPLEMENTED]: 'Not Implemented', + [HTTP_502_BAD_GATEWAY]: 'Bad Gateway', + [HTTP_503_SERVICE_UNAVAILABLE]: 'Service Unavailable', + [HTTP_504_GATEWAY_TIMEOUT]: 'Gateway Timeout', +}; + +/** + * A union type representing commonly used HTTP status codes. + * + * This type ensures consistency between runtime and type-level status code handling. + * + * Example: + * ```ts + * const status: HttpStatusCode = 404; // βœ… valid + * const status: HttpStatusCode = 418; // ❌ Type error (unless added to list) + * ``` + */ +export type HttpStatusCode = typeof validHttpStatusCodes[number]; + +/** + * Type guard to check whether a given value is a valid HTTP status code. + * + * This is useful for validating numeric values received from external input, + * ensuring they conform to known HTTP semantics. + * + * Example: + * ```ts + * if (isHttpStatusCode(value)) { + * // value is now typed as HttpStatusCode + * } + * ``` + * + * @param value - The numeric value to check. + * @returns `true` if the value is a recognized HTTP status code, otherwise `false`. + */ +export function isHttpStatusCode(value: unknown): value is HttpStatusCode { + return typeof value === 'number' && + validHttpStatusCodes.includes(value as HttpStatusCode); +} diff --git a/v0.1.0/src/Types/Middleware.ts b/v0.1.0/src/Types/Middleware.ts new file mode 100644 index 0000000..0001af4 --- /dev/null +++ b/v0.1.0/src/Types/Middleware.ts @@ -0,0 +1,51 @@ +import { IContext } from '../Interfaces/IContext.ts'; + +/** + * Represents a middleware function in the HTTP request pipeline. + * + * Middleware is a core mechanism to intercept, observe, or modify the request lifecycle. + * It can be used for tasks such as logging, authentication, input validation, + * metrics collection, or response transformation. + * + * Each middleware receives a fully-typed request context and a `next()` function + * to invoke the next stage of the pipeline. Middleware may choose to short-circuit + * the pipeline by returning a `Response` early. + * + * @template TContext The specific context type for this middleware, including state, params, and query information. + */ +type Middleware = ( + ctx: TContext, + next: () => Promise, +) => Promise; + +/** + * Represents a middleware function with an associated name. + * + * This is useful for debugging, logging, or when you need to reference + * the middleware by name in your application. + * + * @template TContext The specific context type for this middleware, including state, params, and query information. + */ +type NamedMiddleware = + & Middleware + & { name?: string }; + +export type { NamedMiddleware as Middleware }; + +/** + * Type guard to verify whether a given value is a valid `IMiddleware` function. + * + * This guard checks whether the input is a function that accepts exactly two arguments. + * Note: This is a structural check and cannot fully guarantee the semantics of a middleware. + * + * @param value - The value to test. + * @returns `true` if the value is structurally a valid middleware function. + */ +export function isMiddleware( + value: unknown, +): value is Middleware { + return ( + typeof value === 'function' && + value.length === 2 // ctx, next + ); +} diff --git a/v0.1.0/src/Types/Params.ts b/v0.1.0/src/Types/Params.ts new file mode 100644 index 0000000..045e056 --- /dev/null +++ b/v0.1.0/src/Types/Params.ts @@ -0,0 +1,10 @@ +/** + * Represents route parameters parsed from dynamic segments in the URL path. + * + * This type is typically derived from route definitions with placeholders, + * such as `/users/:id`, which would yield `{ id: "123" }`. + * + * All values are strings and should be considered read-only, as they are + * extracted by the router and should not be modified by application code. + */ +export type Params = Record; diff --git a/v0.1.0/src/Types/Query.ts b/v0.1.0/src/Types/Query.ts new file mode 100644 index 0000000..b9453fe --- /dev/null +++ b/v0.1.0/src/Types/Query.ts @@ -0,0 +1,12 @@ +/** + * Represents the parsed query parameters from the request URL. + * + * Query parameters originate from the URL search string (e.g. `?filter=active&tags=ts&tags=deno`) + * and may contain single or multiple values per key. + * + * All values are expressed as strings or arrays of strings, depending on how often + * the key occurs. This structure preserves the raw semantics of the query. + * + * For normalized single-value access, prefer custom DTOs or wrapper utilities. + */ +export type Query = Record; diff --git a/v0.1.0/src/Types/RegisterRoute.ts b/v0.1.0/src/Types/RegisterRoute.ts new file mode 100644 index 0000000..21af1b3 --- /dev/null +++ b/v0.1.0/src/Types/RegisterRoute.ts @@ -0,0 +1,16 @@ +import { IContext } from '../Interfaces/IContext.ts'; +import { IInternalRoute } from '../Interfaces/mod.ts'; + +/** + * A type alias for the internal route registration function used by the `HttpKernel`. + * + * This function accepts a fully constructed internal route, including method, matcher, + * middleware chain, and final handler, and registers it for dispatching. + * + * Typically passed into `RouteBuilder` instances to enable fluent API chaining. + * + * @template TContext The context type associated with the route being registered. + */ +export type RegisterRoute = ( + route: IInternalRoute, +) => void; diff --git a/v0.1.0/src/Types/ResponseDecorator.ts b/v0.1.0/src/Types/ResponseDecorator.ts new file mode 100644 index 0000000..a027f4f --- /dev/null +++ b/v0.1.0/src/Types/ResponseDecorator.ts @@ -0,0 +1,30 @@ +import { IContext } from '../Interfaces/mod.ts'; + +/** + * A function that modifies or enriches an outgoing HTTP response before it is returned to the client. + * + * This decorator can be used to inject headers (e.g., CORS, security), apply global transformations, + * or wrap responses for logging, analytics, or debugging purposes. + * + * It is called exactly once at the end of the middleware/handler pipeline, + * allowing central response customization without interfering with business logic. + * + * @param res - The original `Response` object produced by the route handler or middleware chain. + * @returns A modified or wrapped `Response` object to be sent back to the client. + * + * @example + * ```ts + * const addCors: ResponseDecorator = (res) => { + * const headers = new Headers(res.headers); + * headers.set("Access-Control-Allow-Origin", "*"); + * return new Response(res.body, { + * status: res.status, + * headers, + * }); + * }; + * ``` + */ +export type ResponseDecorator = ( + res: Response, + ctx: TContext, +) => Response; diff --git a/v0.1.0/src/Types/State.ts b/v0.1.0/src/Types/State.ts new file mode 100644 index 0000000..b512d9e --- /dev/null +++ b/v0.1.0/src/Types/State.ts @@ -0,0 +1,9 @@ +/** + * Represents the per-request state object shared across the middleware pipeline. + * + * This type defines the base structure for custom state definitions, + * which can be extended with concrete fields like user data, request metadata, etc. + * + * Custom `TState` types must extend this base to ensure compatibility. + */ +export type State = Record; diff --git a/v0.1.0/src/Types/__tests__/HttpMethod.test.ts b/v0.1.0/src/Types/__tests__/HttpMethod.test.ts new file mode 100644 index 0000000..9559d79 --- /dev/null +++ b/v0.1.0/src/Types/__tests__/HttpMethod.test.ts @@ -0,0 +1,40 @@ +import { assertEquals } from 'https://deno.land/std/assert/mod.ts'; +import { isHttpMethod, validHttpMethods } from '../HttpMethod.ts'; + +Deno.test('isHttpMethod: returns true for all valid methods', () => { + for (const method of validHttpMethods) { + const result = isHttpMethod(method); + assertEquals(result, true, `Expected "${method}" to be valid`); + } +}); + +Deno.test('isHttpMethod: returns false for lowercase or unknown strings', () => { + const invalid = [ + 'get', + 'post', + 'FETCH', + 'TRACE', + 'CONNECT', + 'INVALID', + '', + ' ', + ]; + + for (const method of invalid) { + const result = isHttpMethod(method); + assertEquals(result, false, `Expected "${method}" to be invalid`); + } +}); + +Deno.test('isHttpMethod: returns false for non-string inputs', () => { + const invalidInputs = [null, undefined, 123, {}, [], true, Symbol('GET')]; + + for (const input of invalidInputs) { + const result = isHttpMethod(input); + assertEquals( + result, + false, + `Expected non-string input to be invalid: ${String(input)}`, + ); + } +}); diff --git a/v0.1.0/src/Types/__tests__/HttpStatusCode.test.ts b/v0.1.0/src/Types/__tests__/HttpStatusCode.test.ts new file mode 100644 index 0000000..3042ff1 --- /dev/null +++ b/v0.1.0/src/Types/__tests__/HttpStatusCode.test.ts @@ -0,0 +1,35 @@ +// src/Types/__tests__/HttpStatusCode.test.ts +import { assertEquals } from 'https://deno.land/std@0.204.0/assert/mod.ts'; +import { isHttpStatusCode, validHttpStatusCodes } from '../HttpStatusCode.ts'; + +Deno.test('isHttpStatusCode: returns true for all valid status codes', () => { + for (const code of validHttpStatusCodes) { + assertEquals( + isHttpStatusCode(code), + true, + `Expected ${code} to be valid`, + ); + } +}); + +Deno.test('isHttpStatusCode: returns false for invalid status codes', () => { + const invalidInputs = [99, 600, 1234, -1, 0, 999]; + for (const val of invalidInputs) { + assertEquals( + isHttpStatusCode(val), + false, + `Expected ${val} to be invalid`, + ); + } +}); + +Deno.test('isHttpStatusCode: returns false for non-numeric values', () => { + const invalid = ['200', null, undefined, {}, [], true]; + for (const val of invalid) { + assertEquals( + isHttpStatusCode(val), + false, + `Expected ${val} to be invalid`, + ); + } +}); diff --git a/v0.1.0/src/Types/mod.ts b/v0.1.0/src/Types/mod.ts new file mode 100644 index 0000000..c159495 --- /dev/null +++ b/v0.1.0/src/Types/mod.ts @@ -0,0 +1,45 @@ +// deno-coverage-ignore-file + +export type { DeepPartial } from './DeepPartial.ts'; +export { isHandler } from './Handler.ts'; +export type { Handler } from './Handler.ts'; +export type { HttpErrorHandler } from './HttpErrorHandler.ts'; +export { isHttpMethod, validHttpMethods } from './HttpMethod.ts'; +export type { HttpMethod } from './HttpMethod.ts'; +export { + HTTP_100_CONTINUE, + HTTP_101_SWITCHING_PROTOCOLS, + HTTP_102_PROCESSING, + HTTP_200_OK, + HTTP_201_CREATED, + HTTP_202_ACCEPTED, + HTTP_204_NO_CONTENT, + HTTP_301_MOVED_PERMANENTLY, + HTTP_302_FOUND, + HTTP_304_NOT_MODIFIED, + HTTP_400_BAD_REQUEST, + HTTP_401_UNAUTHORIZED, + HTTP_403_FORBIDDEN, + HTTP_404_NOT_FOUND, + HTTP_405_METHOD_NOT_ALLOWED, + HTTP_409_CONFLICT, + HTTP_422_UNPROCESSABLE_ENTITY, + HTTP_429_TOO_MANY_REQUESTS, + HTTP_500_INTERNAL_SERVER_ERROR, + HTTP_501_NOT_IMPLEMENTED, + HTTP_502_BAD_GATEWAY, + HTTP_503_SERVICE_UNAVAILABLE, + HTTP_504_GATEWAY_TIMEOUT, + HttpStatusTextMap, + isHttpStatusCode, + validHttpErrorCodes, + validHttpStatusCodes, +} from './HttpStatusCode.ts'; +export type { HttpStatusCode } from './HttpStatusCode.ts'; +export { isMiddleware } from './Middleware.ts'; +export type { Middleware } from './Middleware.ts'; +export type { Params } from './Params.ts'; +export type { Query } from './Query.ts'; +export type { RegisterRoute } from './RegisterRoute.ts'; +export type { ResponseDecorator } from './ResponseDecorator.ts'; +export type { State } from './State.ts'; diff --git a/v0.1.0/src/Utils/__tests__/createEmptyContext.test.ts b/v0.1.0/src/Utils/__tests__/createEmptyContext.test.ts new file mode 100644 index 0000000..fe58fb2 --- /dev/null +++ b/v0.1.0/src/Utils/__tests__/createEmptyContext.test.ts @@ -0,0 +1,28 @@ +import { assertEquals } from 'https://deno.land/std/assert/mod.ts'; +import { createEmptyContext } from '../createEmptyContext.ts'; +import { IContext } from '../../Interfaces/mod.ts'; + +Deno.test('createEmptyContext: returns default-initialized context', () => { + const request = new Request('http://localhost'); + const ctx = createEmptyContext(request); + + assertEquals(ctx.req, request); + assertEquals(ctx.params, {}); + assertEquals(ctx.query, {}); + assertEquals(ctx.state, {}); +}); + +Deno.test('createEmptyContext: preserves generic type compatibility', () => { + interface MyContext + extends + IContext<{ userId: string }, { id: string }, { verbose: string }> {} + + const req = new Request('http://localhost'); + const ctx = createEmptyContext(req); + + // All properties exist and are empty + assertEquals(ctx.params, {} as MyContext['params']); + assertEquals(ctx.query, {} as MyContext['query']); + assertEquals(ctx.state, {} as MyContext['state']); + assertEquals(ctx.req, req); +}); diff --git a/v0.1.0/src/Utils/__tests__/createRouteMatcher.test.ts b/v0.1.0/src/Utils/__tests__/createRouteMatcher.test.ts new file mode 100644 index 0000000..13a1eb7 --- /dev/null +++ b/v0.1.0/src/Utils/__tests__/createRouteMatcher.test.ts @@ -0,0 +1,118 @@ +import { + assert, + assertEquals, + assertStrictEquals, +} from 'https://deno.land/std/assert/mod.ts'; +import { IRouteDefinition } from '../../Interfaces/mod.ts'; +import { createRouteMatcher } from '../../mod.ts'; + +// Dummy request +const dummyRequest = new Request('http://localhost'); + +Deno.test('createRouteMatcher: static route matches and extracts params', () => { + const def: IRouteDefinition = { method: 'GET', path: '/users/:id' }; + const matcher = createRouteMatcher(def); + + const result = matcher(new URL('http://localhost/users/42'), dummyRequest); + + assert(result); + assertEquals(result.params, { id: '42' }); +}); + +Deno.test('createRouteMatcher: static route with multiple params', () => { + const def: IRouteDefinition = { method: 'GET', path: '/repo/:owner/:name' }; + const matcher = createRouteMatcher(def); + + const result = matcher( + new URL('http://localhost/repo/max/wiki'), + dummyRequest, + ); + + assert(result); + assertEquals(result.params, { owner: 'max', name: 'wiki' }); +}); + +Deno.test('createRouteMatcher: static route does not match wrong path', () => { + const def: IRouteDefinition = { method: 'GET', path: '/users/:id' }; + const matcher = createRouteMatcher(def); + + const result = matcher(new URL('http://localhost/posts/42'), dummyRequest); + + assertStrictEquals(result, null); +}); + +Deno.test('createRouteMatcher: uses custom matcher if provided', () => { + const def: IRouteDefinition = { + method: 'GET', + matcher: (url) => url.pathname === '/ping' ? { params: {} } : null, + }; + const matcher = createRouteMatcher(def); + + const result = matcher(new URL('http://localhost/ping'), dummyRequest); + assert(result); + assertEquals(result.params, {}); +}); + +Deno.test('createRouteMatcher: extracts single query param', () => { + const def: IRouteDefinition = { method: 'GET', path: '/search' }; + const matcher = createRouteMatcher(def); + + const url = new URL('http://localhost/search?q=deno'); + const result = matcher(url, dummyRequest); + + assert(result); + assertEquals(result.params, {}); // no path params + assertEquals(result.query, { q: 'deno' }); // single key β†’ string +}); + +Deno.test('createRouteMatcher: duplicate query keys become array', () => { + const def: IRouteDefinition = { method: 'GET', path: '/tags' }; + const matcher = createRouteMatcher(def); + + const url = new URL('http://localhost/tags?tag=js&tag=ts&tag=deno'); + const result = matcher(url, dummyRequest); + + assert(result); + assertEquals(result.params, {}); + assertEquals(result.query, { tag: ['js', 'ts', 'deno'] }); // multi β†’ string[] +}); + +Deno.test('createRouteMatcher: mix of single and duplicate keys', () => { + const def: IRouteDefinition = { method: 'GET', path: '/filter/:type' }; + const matcher = createRouteMatcher(def); + + const url = new URL('http://localhost/filter/repo?lang=ts&lang=js&page=2'); + const result = matcher(url, dummyRequest); + + assert(result); + assertEquals(result.params, { type: 'repo' }); + assertEquals(result.query, { + lang: ['ts', 'js'], // duplicated + page: '2', // single + }); +}); + +Deno.test('createRouteMatcher: no query parameters returns empty object', () => { + const def: IRouteDefinition = { method: 'GET', path: '/info' }; + const matcher = createRouteMatcher(def); + + const url = new URL('http://localhost/info'); + const result = matcher(url, dummyRequest); + + assert(result); + assertEquals(result.params, {}); + assertEquals(result.query, {}); // empty +}); + +Deno.test('createRouteMatcher: retains array order of duplicate keys', () => { + const def: IRouteDefinition = { method: 'GET', path: '/order' }; + const matcher = createRouteMatcher(def); + + const url = new URL( + 'http://localhost/order?item=first&item=second&item=third', + ); + const result = matcher(url, dummyRequest); + + assert(result); + assertEquals(result.query?.item, ['first', 'second', 'third']); +}); diff --git a/v0.1.0/src/Utils/__tests__/normalizeError.test.ts b/v0.1.0/src/Utils/__tests__/normalizeError.test.ts new file mode 100644 index 0000000..0dbb8d9 --- /dev/null +++ b/v0.1.0/src/Utils/__tests__/normalizeError.test.ts @@ -0,0 +1,35 @@ +import { + assertEquals, + assertInstanceOf, +} from 'https://deno.land/std/assert/mod.ts'; +import { normalizeError } from '../normalizeError.ts'; + +Deno.test('normalizeError: preserves Error instances', () => { + const original = new Error('original'); + const result = normalizeError(original); + + assertInstanceOf(result, Error); + assertEquals(result, original); +}); + +Deno.test('normalizeError: converts string to Error', () => { + const result = normalizeError('something went wrong'); + + assertInstanceOf(result, Error); + assertEquals(result.message, 'something went wrong'); +}); + +Deno.test('normalizeError: converts number to Error', () => { + const result = normalizeError(404); + + assertInstanceOf(result, Error); + assertEquals(result.message, '404'); +}); + +Deno.test('normalizeError: converts plain object to Error', () => { + const input = { error: true, msg: 'Invalid' }; + const result = normalizeError(input); + + assertInstanceOf(result, Error); + assertEquals(result.message, JSON.stringify(input)); +}); diff --git a/v0.1.0/src/Utils/createEmptyContext.ts b/v0.1.0/src/Utils/createEmptyContext.ts new file mode 100644 index 0000000..e5d115a --- /dev/null +++ b/v0.1.0/src/Utils/createEmptyContext.ts @@ -0,0 +1,30 @@ +import { IContext } from '../Interfaces/mod.ts'; +import { Params, Query, State } from '../Types/mod.ts'; + +/** + * Creates an empty request context suitable for fallback handlers (e.g., 404 or 500 errors). + * + * This function is primarily intended for cases where no route matched, but a context-compatible + * object is still needed to invoke a generic error handler. All context fields are initialized + * to their default empty values (`{}` for params, query, and state). + * + * @template TContext - The expected context type, typically extending `IContext`. + * @param req - The original HTTP request object from `Deno.serve()`. + * @returns A minimal context object compatible with `TContext`. + * + * @example + * ```ts + * const ctx = createEmptyContext(request); + * return httpErrorHandlers[404](ctx); + * ``` + */ +export function createEmptyContext( + req: Request, +): TContext { + return { + req, + params: {} as Params, + query: {} as Query, + state: {} as State, + } as TContext; +} diff --git a/v0.1.0/src/Utils/createRouteMatcher.ts b/v0.1.0/src/Utils/createRouteMatcher.ts new file mode 100644 index 0000000..ea3775f --- /dev/null +++ b/v0.1.0/src/Utils/createRouteMatcher.ts @@ -0,0 +1,54 @@ +// createRouteMatcher.ts + +import { + IRouteDefinition, + IRouteMatch, + IRouteMatcher, + isDynamicRouteDefinition, +} from '../Interfaces/mod.ts'; +import { Params, Query } from '../Types/mod.ts'; + +/** + * Transforms a route definition into a matcher using Deno's URLPattern API. + * + * @param def - Static path pattern or custom matcher. + * @returns IRouteMatcher that returns `{ params, query }` or `null`. + */ +export function createRouteMatcher( + def: IRouteDefinition, +): IRouteMatcher { + // 1. Allow users to provide their own matcher + if (isDynamicRouteDefinition(def)) { + return def.matcher; + } + + // 2. Build URLPattern; supports :id, *wildcards, regex groups, etc. + const pattern = new URLPattern({ pathname: def.path }); + + // 3. The actual matcher closure + return (url: URL): IRouteMatch | null => { + const result = pattern.exec(url); + + // 3a. Path did not match + if (!result) return null; + + // 3b. Extract route params + const params: Params = {}; + for (const [key, value] of Object.entries(result.pathname.groups)) { + if (value) { + params[key] = value; + } + } + + // 3c. Extract query parameters – keep duplicates as arrays + const query: Query = {}; + for (const key of url.searchParams.keys()) { + const values = url.searchParams.getAll(key); // β†’ string[] + query[key] = values.length === 1 + ? values[0] // single β†’ "foo" + : values; // multi β†’ ["foo","bar"] + } + + return { params, query }; + }; +} diff --git a/v0.1.0/src/Utils/mod.ts b/v0.1.0/src/Utils/mod.ts new file mode 100644 index 0000000..2e21a1d --- /dev/null +++ b/v0.1.0/src/Utils/mod.ts @@ -0,0 +1,5 @@ +// deno-coverage-ignore-file + +export { createEmptyContext } from './createEmptyContext.ts'; +export { createRouteMatcher } from './createRouteMatcher.ts'; +export { normalizeError } from './normalizeError.ts'; diff --git a/v0.1.0/src/Utils/normalizeError.ts b/v0.1.0/src/Utils/normalizeError.ts new file mode 100644 index 0000000..e058ee7 --- /dev/null +++ b/v0.1.0/src/Utils/normalizeError.ts @@ -0,0 +1,32 @@ +/** + * Normalizes any thrown value to a proper `Error` instance. + * + * This is useful when handling unknown thrown values that may be: + * - strings (e.g. `throw "oops"`) + * - numbers (e.g. `throw 404`) + * - objects that are not instances of `Error` + * + * Ensures that downstream error handling logic always receives a consistent `Error` object. + * + * @param unknownError - Any value that might have been thrown. + * @returns A valid `Error` instance wrapping the original input. + * + * @example + * ```ts + * try { + * throw "something went wrong"; + * } catch (e) { + * const err = normalizeError(e); + * console.error(err.message); // "something went wrong" + * } + * ``` + */ +export function normalizeError(unknownError: unknown): Error { + return unknownError instanceof Error + ? unknownError + : new Error( + typeof unknownError === 'string' + ? unknownError + : JSON.stringify(unknownError), + ); +} diff --git a/v0.1.0/src/__tests__/HttpKernel.test.ts b/v0.1.0/src/__tests__/HttpKernel.test.ts new file mode 100644 index 0000000..214ce51 --- /dev/null +++ b/v0.1.0/src/__tests__/HttpKernel.test.ts @@ -0,0 +1,180 @@ +import { assertEquals } from 'https://deno.land/std@0.204.0/assert/mod.ts'; +import { HttpKernel } from '../HttpKernel.ts'; +import { IRouteDefinition } from '../Interfaces/mod.ts'; + +Deno.test('HttpKernel: matches static route and executes handler', async () => { + const kernel = new HttpKernel(); + + const def: IRouteDefinition = { method: 'GET', path: '/hello' }; + let called = false; + + kernel.route(def).handle((_ctx) => { + called = true; + return Promise.resolve(new Response('OK', { status: 200 })); + }); + + const res = await kernel.handle( + new Request('http://localhost/hello', { method: 'GET' }), + ); + assertEquals(res.status, 200); + assertEquals(await res.text(), 'OK'); + assertEquals(called, true); +}); + +Deno.test('HttpKernel: supports dynamic matcher', async () => { + const kernel = new HttpKernel(); + const def: IRouteDefinition = { + method: 'GET', + matcher: (url) => url.pathname === '/dyn' ? { params: {} } : null, + }; + + kernel.route(def).handle((_ctx) => + Promise.resolve(new Response('Dyn', { status: 200 })) + ); + + const res = await kernel.handle(new Request('http://localhost/dyn')); + assertEquals(res.status, 200); + assertEquals(await res.text(), 'Dyn'); +}); + +Deno.test('HttpKernel: calls middleware in order and passes to handler', async () => { + const kernel = new HttpKernel(); + const calls: string[] = []; + + kernel.route({ method: 'GET', path: '/test' }) + .middleware(async (_ctx, next) => { + calls.push('mw1'); + return await next(); + }) + .middleware(async (_ctx, next) => { + calls.push('mw2'); + return await next(); + }) + .handle((_ctx) => { + calls.push('handler'); + return Promise.resolve(new Response('done')); + }); + + const res = await kernel.handle( + new Request('http://localhost/test', { method: 'GET' }), + ); + assertEquals(await res.text(), 'done'); + assertEquals(calls, ['mw1', 'mw2', 'handler']); +}); + +Deno.test('HttpKernel: middleware short-circuits pipeline', async () => { + const kernel = new HttpKernel(); + const calls: string[] = []; + + kernel.route({ method: 'GET', path: '/stop' }) + .middleware((_ctx, _next) => { + calls.push('mw1'); + return Promise.resolve(new Response('blocked', { status: 403 })); + }) + .middleware((_ctx, _next) => { + calls.push('mw2'); + return Promise.resolve(new Response('should-not-call')); + }) + .handle((_ctx) => { + calls.push('handler'); + return Promise.resolve(new Response('ok')); + }); + + const res = await kernel.handle( + new Request('http://localhost/stop', { method: 'GET' }), + ); + assertEquals(res.status, 403); + assertEquals(await res.text(), 'blocked'); + assertEquals(calls, ['mw1']); +}); + +Deno.test('HttpKernel: invalid middleware or handler signature triggers 500', async () => { + const kernel = new HttpKernel(); + + // Middleware with wrong signature (missing ctx, next) + kernel.route({ method: 'GET', path: '/bad-mw' }) + // @ts-expect-error invalid middleware + .middleware(() => new Response('invalid')) + .handle((_ctx) => Promise.resolve(new Response('ok'))); + + const res1 = await kernel.handle(new Request('http://localhost/bad-mw')); + assertEquals(res1.status, 500); + assertEquals(await res1.text(), 'Internal Server Error'); + + // Handler with wrong signature (no ctx) + kernel.route({ method: 'GET', path: '/bad-handler' }) + .middleware(async (_ctx, next) => await next()) + // @ts-expect-error invalid handler + .handle(() => new Response('invalid')); + + const res2 = await kernel.handle( + new Request('http://localhost/bad-handler'), + ); + assertEquals(res2.status, 500); + assertEquals(await res2.text(), 'Internal Server Error'); +}); + +Deno.test('HttpKernel: 404 for unmatched route', async () => { + const kernel = new HttpKernel(); + const res = await kernel.handle(new Request('http://localhost/nothing')); + assertEquals(res.status, 404); +}); + +Deno.test('HttpKernel: skips route with wrong method', async () => { + const kernel = new HttpKernel(); + + kernel.route({ method: 'POST', path: '/only-post' }) + .handle(() => Promise.resolve(new Response('nope'))); + + const res = await kernel.handle( + new Request('http://localhost/only-post', { method: 'GET' }), + ); + assertEquals(res.status, 404); +}); + +Deno.test('HttpKernel: throws on next() called twice', async () => { + const kernel = new HttpKernel(); + + kernel.route({ method: 'GET', path: '/bad' }) + .middleware(async (_ctx, next) => { + await next(); + await next(); // ❌ + return new Response('should never reach'); + }) + .handle((_ctx) => Promise.resolve(new Response('OK'))); + + const res = await kernel.handle(new Request('http://localhost/bad')); + assertEquals(res.status, 500); + assertEquals(await res.text(), 'Internal Server Error'); +}); + +Deno.test('HttpKernel: handler throws β†’ error propagates', async () => { + const kernel = new HttpKernel(); + + kernel.route({ method: 'GET', path: '/throw' }) + .handle(() => { + throw new Error('fail!'); + }); + + const res = await kernel.handle(new Request('http://localhost/throw')); + assertEquals(res.status, 500); + assertEquals(await res.text(), 'Internal Server Error'); +}); + +Deno.test('HttpKernel: returns 500 if no handler or middleware defined', async () => { + const kernel = new HttpKernel(); + + // Force-manual Registrierung mit `handler: undefined` + // Umgehen des Builders zur Simulation dieses Edge-Cases + kernel['routes'].push({ + method: 'GET', + matcher: (url) => url.pathname === '/fail' ? { params: {} } : null, + middlewares: [], + // @ts-expect-error absichtlich ungΓΌltiger Handler + handler: undefined, + }); + + const res = await kernel.handle(new Request('http://localhost/fail')); + assertEquals(res.status, 500); + assertEquals(await res.text(), 'Internal Server Error'); +}); diff --git a/v0.1.0/src/__tests__/RouteBuilder.test.ts b/v0.1.0/src/__tests__/RouteBuilder.test.ts new file mode 100644 index 0000000..1e9cc77 --- /dev/null +++ b/v0.1.0/src/__tests__/RouteBuilder.test.ts @@ -0,0 +1,118 @@ +import { + assert, + assertEquals, + assertNotEquals, + assertThrows, +} from 'https://deno.land/std@0.204.0/assert/mod.ts'; +import { IInternalRoute, IRouteDefinition } from '../Interfaces/mod.ts'; +import { RouteBuilder } from '../mod.ts'; +import { Handler, Middleware } from '../Types/mod.ts'; + +// Dummy objects +// deno-lint-ignore require-await +const dummyHandler: Handler = async () => new Response('ok'); +const dummyMiddleware: Middleware = async (_, next) => await next(); +const dummyDef: IRouteDefinition = { method: 'GET', path: '/hello' }; +const dummyMatcher = () => ({ params: {} }); + +Deno.test('middleware: single middleware is registered correctly', () => { + let registered: IInternalRoute | null = null as IInternalRoute | null; + + const builder = new RouteBuilder((r) => registered = r, dummyDef) + .middleware(dummyMiddleware); + + builder.handle(dummyHandler); + + assert(registered); + assertEquals(registered?.middlewares.length, 1); + assertEquals(registered?.middlewares[0], dummyMiddleware); +}); + +Deno.test('middleware: middleware is chained immutably', () => { + const builder1 = new RouteBuilder(() => {}, dummyDef); + const builder2 = builder1.middleware(dummyMiddleware); + + assertNotEquals(builder1, builder2); +}); + +Deno.test('middleware: preserves order of middleware', () => { + const mw1: Middleware = async (_, next) => await next(); + const mw2: Middleware = async (_, next) => await next(); + + let result: IInternalRoute | null = null as IInternalRoute | null; + + const builder = new RouteBuilder((r) => result = r, dummyDef) + .middleware(mw1) + .middleware(mw2); + + builder.handle(dummyHandler); + + assert(result); + assertEquals(result!.middlewares, [mw1, mw2]); +}); + +Deno.test('handle: uppercases method', () => { + let result: IInternalRoute | null = null as IInternalRoute | null; + + new RouteBuilder((r) => result = r, { method: 'POST', path: '/x' }) + .handle(dummyHandler); + + assertEquals(result?.method, 'POST'); +}); + +Deno.test('handle: works with no middleware', async () => { + let route: IInternalRoute | null = null as IInternalRoute | null; + + const builder = new RouteBuilder((r) => route = r, dummyDef); + builder.handle(dummyHandler); + + assert(route); + assertEquals(route?.middlewares.length, 0); + + const request = new Request('http://localhost'); + + const res1 = await route?.handler({ + req: request, + params: {}, + state: {}, + query: {}, + }); + const res2 = await dummyHandler({ + req: request, + params: {}, + state: {}, + query: {}, + }); + + assertEquals(res1?.status, res2?.status); + assertEquals(await res1?.text(), await res2?.text()); +}); + +Deno.test('handle: uses custom matcher factory', () => { + let called = false; + + const factory = (def: IRouteDefinition) => { + called = true; + return dummyMatcher; + }; + + let route: IInternalRoute | null = null as IInternalRoute | null; + + new RouteBuilder((r) => route = r, dummyDef, [], factory).handle( + dummyHandler, + ); + + assert(called); + assert(route); + assertEquals(route!.matcher, dummyMatcher); +}); + +Deno.test('handle: throws if matcher factory throws', () => { + const faultyFactory = () => { + throw new Error('matcher fail'); + }; + + const builder = new RouteBuilder(() => {}, dummyDef, [], faultyFactory); + + assertThrows(() => builder.handle(dummyHandler), Error, 'matcher fail'); +}); diff --git a/v0.1.0/src/mod.ts b/v0.1.0/src/mod.ts new file mode 100644 index 0000000..0754746 --- /dev/null +++ b/v0.1.0/src/mod.ts @@ -0,0 +1,4 @@ +// deno-coverage-ignore-file +export { HttpKernel } from './HttpKernel.ts'; +export { RouteBuilder } from './RouteBuilder.ts'; +export { createRouteMatcher } from './Utils/createRouteMatcher.ts'; diff --git a/v0.2.0/CHANGELOG.md b/v0.2.0/CHANGELOG.md new file mode 100644 index 0000000..0972bf9 --- /dev/null +++ b/v0.2.0/CHANGELOG.md @@ -0,0 +1,93 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [0.2.0](https://git.0xmax42.io/maxp/http-kernel/compare/v0.1.0..v0.2.0) - 2025-05-27 + +### πŸš€ Features + +- *(workflows)* Add GitHub release sync workflow - ([de6d3ee](https://git.0xmax42.io/maxp/http-kernel/commit/de6d3ee389b0d92c5056e47be85da1d0c41f62af)) +- *(ci)* Enhance CI workflow with granular steps and error handling - ([54cfa18](https://git.0xmax42.io/maxp/http-kernel/commit/54cfa1888e13d0872b5411e83d3d45925f2687ee)) +- *(route-builder)* Add middleware chain compilation - ([35d83c0](https://git.0xmax42.io/maxp/http-kernel/commit/35d83c073ef8644d657195c332b463d18e856e18)) +- *(interfaces)* Add runRoute method to IInternalRoute - ([67ebb43](https://git.0xmax42.io/maxp/http-kernel/commit/67ebb4307a2a1c588b78f8f0c498d1a4276ad09b)) +- *(workflows)* Conditionally generate changelog - ([b44bb2d](https://git.0xmax42.io/maxp/http-kernel/commit/b44bb2ddafe99c85b25229d2c4a0dfeacf750052)) +- *(workflows)* Add CI for Deno project tests - ([9d5db4f](https://git.0xmax42.io/maxp/http-kernel/commit/9d5db4f414cf961248f2b879f2b132b81a32cb92)) + +### πŸ› Bug Fixes + +- *(workflows)* Ensure version detection output is always set - ([3707242](https://git.0xmax42.io/maxp/http-kernel/commit/3707242d278e15c55a41056bb64810f6824d24b3)) + +### 🚜 Refactor + +- *(kernel)* Simplify middleware and handler execution - ([aea3fb4](https://git.0xmax42.io/maxp/http-kernel/commit/aea3fb45e7c099a38440c85783747e80fca54ba6)) +- *(imports)* Use explicit type-only imports across codebase - ([b83aa33](https://git.0xmax42.io/maxp/http-kernel/commit/b83aa330b34523e5102ab98ee61dedbbd62d4656)) +- *(workflows)* Rename changelog file for consistency - ([b9d25f2](https://git.0xmax42.io/maxp/http-kernel/commit/b9d25f23fc6ad7696deee319024aa5b1af4d98c0)) + +### πŸ“š Documentation + +- *(release)* Update guidelines for handling changelog - ([6a0f1c7](https://git.0xmax42.io/maxp/http-kernel/commit/6a0f1c774bc01ab976090612bbc361576feb3942)) +- Add README for HttpKernel project - ([a1ce306](https://git.0xmax42.io/maxp/http-kernel/commit/a1ce30627c68a3f869eb6a104308322af8596dc1)) +- Add MIT license file - ([5118a19](https://git.0xmax42.io/maxp/http-kernel/commit/5118a19aeaa1102591aa7fe093fdec1aa19dc7f5)) + +### πŸ§ͺ Testing + +- *(routebuilder)* Add validation tests for handler and middleware - ([b14e9ac](https://git.0xmax42.io/maxp/http-kernel/commit/b14e9acc5f9617a01886e7734b2ae717b86de03e)) +- *(httpkernel)* Enforce compile-time validation for signatures - ([731bba2](https://git.0xmax42.io/maxp/http-kernel/commit/731bba22d88df077b0a39293ddd1a3eec3bf96e8)) +- *(bench)* Add parallel benchmarks for HTTP kernel - ([3da34e2](https://git.0xmax42.io/maxp/http-kernel/commit/3da34e268426b92510c7f9b730a2fa297dca6fbf)) + +### βš™οΈ Miscellaneous Tasks + +- *(config)* Add CI task for local checks - ([c207dc7](https://git.0xmax42.io/maxp/http-kernel/commit/c207dc7392d9f40e7b7c736eadf6c9c7bbf9b7d4)) +- *(gitignore)* Add .local directory to ignored files - ([1b447f5](https://git.0xmax42.io/maxp/http-kernel/commit/1b447f51900b3a1a7f1be9d5192fd5aba37bdbc4)) +- *(ci)* Update deno tasks in CI workflow - ([38c00b0](https://git.0xmax42.io/maxp/http-kernel/commit/38c00b035bfd05c83d5898c97c9423a653db0840)) +- *(tasks)* Add benchmark, format, and lint commands - ([6e6e616](https://git.0xmax42.io/maxp/http-kernel/commit/6e6e61693fef3b11a81ce260d80bc93edae1e718)) +- *(workflows)* Consolidate and update CI configuration - ([ec1697d](https://git.0xmax42.io/maxp/http-kernel/commit/ec1697df94b5378f1766663e278a41d403a64336)) +- *(config)* Add exports field to module metadata - ([c28eb7f](https://git.0xmax42.io/maxp/http-kernel/commit/c28eb7f28dfaa8d3fdc540c4bcc306a3a8b9d6f8)) +- *(git)* Ignore merge conflicts for CHANGELOG.md - ([6399113](https://git.0xmax42.io/maxp/http-kernel/commit/6399113e122e1207ebf4113aebd250358e31f461)) +- *(workflows)* Refine branch handling in release process - ([71ea424](https://git.0xmax42.io/maxp/http-kernel/commit/71ea4247b35dc4afe5090d3c6502bfa936b5a947)) +- *(workflows)* Update changelog file extension to .md and revert b9d25f23fc - ([a88b4d1](https://git.0xmax42.io/maxp/http-kernel/commit/a88b4d112f5c07664d41f6e9d03246307551f25d)) +- Rename changelog and readme files to use .md extension - ([4f2b650](https://git.0xmax42.io/maxp/http-kernel/commit/4f2b65049f461ef377e7231905fd066cbc3c7fe0)) +- *(workflows)* Update test workflow for http-kernel project - ([0311546](https://git.0xmax42.io/maxp/http-kernel/commit/03115464e0fb01b8ca00a2fdabde013d004ae8a2)) +- *(workflows)* Update Deno setup action to v2 - ([1233a0b](https://git.0xmax42.io/maxp/http-kernel/commit/1233a0b7204d12a60f4b7bd1199242a4cb7c4579)) +- *(workflows)* Remove unused workflow_dispatch trigger - ([16c0053](https://git.0xmax42.io/maxp/http-kernel/commit/16c0053964c72d01e5f555ec8f33c9eead160e69)) +- *(tasks)* Remove commented-out start and watch scripts - ([04029f8](https://git.0xmax42.io/maxp/http-kernel/commit/04029f87a3b9dd24e8792b852ead9097e18d23c7)) + +## [0.1.0] - 2025-05-08 + +### πŸš€ Features + +- *(workflows)* Add automated changelog and release workflow - ([bbf78cf](https://git.0xmax42.io/maxp/http-kernel/commit/bbf78cff17be0cae651b8abf3e239103b26354bf)) +- *(vscode)* Customize activity bar and peacock colors - ([56633cd](https://git.0xmax42.io/maxp/http-kernel/commit/56633cd95b37a8b2cfd8eb95982d07cd1f9b5126)) +- *(workflows)* Add upload assets template for releases - ([7b6eb2b](https://git.0xmax42.io/maxp/http-kernel/commit/7b6eb2b57470198684a1dfa8b668351b8b9a91ae)) +- *(config)* Add project metadata and test watch task - ([b009b57](https://git.0xmax42.io/maxp/http-kernel/commit/b009b5763d1824fc94fdc1e3d919fe2597158f84)) +- *(http)* Add error handling for invalid HTTP methods - ([ba7aa79](https://git.0xmax42.io/maxp/http-kernel/commit/ba7aa79f56772213bf73b62bc6bf8810f3871127)) +- *(http)* Enhance type safety and extend route context - ([a236fa7](https://git.0xmax42.io/maxp/http-kernel/commit/a236fa7c97ae49e6baf560d4ca92c6e83702b3ec)) + +### πŸ› Bug Fixes + +- *(params)* Enforce non-undefined route parameter values - ([b0c6901](https://git.0xmax42.io/maxp/http-kernel/commit/b0c6901d7d272ec98b3d00ef2dd2848482892a25)) + +### 🚜 Refactor + +- *(types)* Unify handler and middleware definitions - ([8235680](https://git.0xmax42.io/maxp/http-kernel/commit/8235680904c7f30f25b98b835d48376431108e91)) +- *(core)* [**breaking**] Enhance HttpKernel pipeline and matcher system with full context and error handling - ([b7410b4](https://git.0xmax42.io/maxp/http-kernel/commit/b7410b44dd8720e46ee2871aa1727ce5039ebad4)) +- *(httpkernel)* Introduce configuration object for flexibility - ([9059bdd](https://git.0xmax42.io/maxp/http-kernel/commit/9059bdda62081c8e775087cabe4c3406e42065a5)) + +### πŸ“š Documentation + +- *(gitea)* Add release automation guide and scripts - ([5c03cdf](https://git.0xmax42.io/maxp/http-kernel/commit/5c03cdfb031adeb6ee5d0de0889477d6d1efafef)) +- *(httpkernel)* Enhance class and interface documentation - ([6c4420d](https://git.0xmax42.io/maxp/http-kernel/commit/6c4420d32f8e7fe317f7c1b0b45de2dcf8565ef5)) + +### πŸ§ͺ Testing + +- *(utils)* Rename and update import paths in test file - ([82a6877](https://git.0xmax42.io/maxp/http-kernel/commit/82a687748558f15c2023861a0cc3a33095c86731)) +- *(utils)* Add unit tests for parseQuery function - ([94525fc](https://git.0xmax42.io/maxp/http-kernel/commit/94525fce5299f3417801f0152a475892e1edac30)) + +### βš™οΈ Miscellaneous Tasks + +- *(config)* Add default git-cliff configuration - ([661f83d](https://git.0xmax42.io/maxp/http-kernel/commit/661f83d1fd0101aa0d5d06b60f6eeb68efac6ceb)) +- *(gitignore)* Add .gitea/COMMIT_GPT.md to ignored files - ([f083856](https://git.0xmax42.io/maxp/http-kernel/commit/f0838567b46822327fe739d8de099722e405dfa3)) +- *(settings)* Add exportall configuration for barrel name and message - ([0990cac](https://git.0xmax42.io/maxp/http-kernel/commit/0990cacb225e1cbbbbb2a288501df7de9641294f)) +- *(.gitignore)* Add git_log_diff.txt to ignore list - ([fd1c7f4](https://git.0xmax42.io/maxp/http-kernel/commit/fd1c7f4170ffffd55ab276090f8b90ee82b853fc)) + + diff --git a/v0.2.0/LICENSE b/v0.2.0/LICENSE new file mode 100644 index 0000000..8941059 --- /dev/null +++ b/v0.2.0/LICENSE @@ -0,0 +1,18 @@ +MIT License + +Copyright (c) 2025 0xMax42 + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/v0.2.0/README.md b/v0.2.0/README.md new file mode 100644 index 0000000..14159bf --- /dev/null +++ b/v0.2.0/README.md @@ -0,0 +1,135 @@ +# HttpKernel – A Type-Safe Router & Middleware Kernel for Deno + +> Fluent routing β€’ Zero-dependency core β€’ 100 % TypeScript + +HttpKernel is a small but powerful dispatching engine that turns an ordinary +`Deno.serve()` loop into a structured, middleware-driven HTTP server. +It focuses on **type safety**, **immutability**, and an **expressive builder API** +while staying framework-agnostic and dependency‑free. + +--- + +## ✨ Key Features + +* **Fluent Route Builder** – chain middleware and handlers without side effects +* **Static *and* Dynamic Matching** – use URL patterns *or* custom matcher functions +* **First-Class Generics** – strongly‑typed `ctx.params`, `ctx.query`, and `ctx.state` +* **Pluggable Error Handling** – override 404/500 (and any other status) per kernel +* **Response Decorators** – inject CORS headers, security headers, logging, … in one place +* **100 % Test Coverage** – built‑in unit tests ensure every edge case is covered + +--- + +## πŸš€ Quick Start + +```ts +// Import directly from your repo or deno.land/x +import { HttpKernel } from "https://deno.land/x/httpkernel/mod.ts"; + +// 1) Create a kernel (optionally pass overrides) +const kernel = new HttpKernel(); + +// 2) Register a route with fluent chaining +kernel + .route({ method: "GET", path: "/hello/:name" }) + .middleware(async (ctx, next) => { + console.log("Incoming request for", ctx.params.name); + return await next(); // continue pipeline + }) + .handle(async (ctx) => + new Response(`Hello ${ctx.params.name}!`, { status: 200 }) + ); + +// 3) Let Deno serve the kernel +Deno.serve(kernel.handle); +``` + +Run it: + +```bash +deno run --allow-net main.ts +# β†’ GET http://localhost:8000/hello/Isaac +``` + +--- + +## 🧩 API Overview + +| Method / Type | Purpose | Hints | +| --------------------- | ---------------------------------------------- | ------------------------------------------------------------- | +| `kernel.route(def)` | Begin defining a new route. Returns `RouteBuilder`. | `def` can be `{ method, path }` **or** `{ method, matcher }`. | +| `.middleware(fn)` | Add a middleware to the current builder. | Each call returns a *new* builder (immutability). | +| `.handle(fn)` | Finalise the route and register the handler. | Must be called exactly once per route. | +| `kernel.handle(req)` | Kernel entry point you pass to `Deno.serve()`. | Resolves to a `Response`. | + +### Context Shape + +```ts +interface Context> { + req: Request; // original request + params: Record; // route params e.g. { id: "42" } + query: Record; // parsed query string + state: S; // per‑request mutable storage +} +``` + +Generics let you supply your own param / query / state types for full IntelliSense. + +--- + +## πŸ› οΈ Configuration + +```ts +new HttpKernel({ + decorateResponse: (res, ctx) => { + // add CORS header globally + const headers = new Headers(res.headers); + headers.set("Access-Control-Allow-Origin", "*"); + return new Response(res.body, { ...res, headers }); + }, + httpErrorHandlers: { + 404: () => new Response("Nothing here ☹️", { status: 404 }), + 500: (_ctx, err) => { + console.error(err); + return new Response("Custom 500", { status: 500 }); + }, + }, +}); +``` + +Everything is optional – omit what you do not override. + +--- + +## πŸ§ͺ Testing + +All logic is covered by unit tests using `std@0.204.0/testing`. +Run them with: + +```bash +deno test -A +``` + +The CI suite checks: + +* Route guards (`isStaticRouteDefinition`, `isDynamicRouteDefinition`) +* Builder immutability & middleware order +* 404 / 500 fall-backs and error propagation +* Middleware mis-use (double `next()`, wrong signatures, …) + +--- + +## πŸ“¦ Roadmap + +* πŸ”Œ Adapter helpers for Oak / Fresh / any framework that can delegate to `kernel.handle` +* πŸ” Built‑in logger & timing middleware +* πŸ”’ CSRF & auth middleware presets +* πŸ“ OpenAPI route generator + +Contributions & ideas are welcome – feel free to open an issue or PR. + +--- + +## πŸ“„ License + +[MIT](LICENSE) diff --git a/v0.2.0/VERSION b/v0.2.0/VERSION new file mode 100644 index 0000000..341cf11 --- /dev/null +++ b/v0.2.0/VERSION @@ -0,0 +1 @@ +0.2.0 \ No newline at end of file diff --git a/v0.2.0/cliff.toml b/v0.2.0/cliff.toml new file mode 100644 index 0000000..4d5ab28 --- /dev/null +++ b/v0.2.0/cliff.toml @@ -0,0 +1,104 @@ +# CLIFF_VERSION=2.8.0 +# git-cliff ~ default configuration file +# https://git-cliff.org/docs/configuration +# +# Lines starting with "#" are comments. +# Configuration options are organized into tables and keys. +# See documentation for more information on available options. +[remote.gitea] +owner = "maxp" +repo = "http-kernel" + +[changelog] +# postprocessors +postprocessors = [ + { pattern = '', replace = "https://git.0xmax42.io" }, # replace gitea url +] + +# template for the changelog header +header = """ +# Changelog\n +All notable changes to this project will be documented in this file.\n +""" +# template for the changelog body +# https://keats.github.io/tera/docs/#introduction +body = """ +{%- macro remote_url() -%} + /{{ remote.gitea.owner }}/{{ remote.gitea.repo }} +{%- endmacro -%} + +{% if version %}\ + {% if previous.version %}\ + ## [{{ version | trim_start_matches(pat="v") }}]\ + ({{ self::remote_url() }}/compare/{{ previous.version }}..{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }} + {% else %}\ + ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} + {% endif %}\ +{% else %}\ + ## [unreleased] +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | striptags | trim | upper_first }} + {% for commit in commits %} + - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ + {% if commit.breaking %}[**breaking**] {% endif %}\ + {{ commit.message | upper_first }} - \ + ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ + {% endfor %} +{% endfor %}\n +""" +# template for the changelog footer +footer = """ + +""" +# remove the leading and trailing s +trim = true + +# render body even when there are no releases to process +# render_always = true +# output file path +# output = "test.md" + +[git] +# parse the commits based on https://www.conventionalcommits.org +conventional_commits = true +# filter out the commits that are not conventional +filter_unconventional = true +# process each line of a commit as an individual commit +split_commits = false +# regex for preprocessing the commit messages +commit_preprocessors = [ + # Replace issue numbers + #{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))"}, + # Check spelling of the commit with https://github.com/crate-ci/typos + # If the spelling is incorrect, it will be automatically fixed. + #{ pattern = '.*', replace_command = 'typos --write-changes -' }, +] +# regex for parsing and grouping commits +commit_parsers = [ + { message = "^feat", group = "πŸš€ Features" }, + { message = "^fix", group = "πŸ› Bug Fixes" }, + { message = "^doc", group = "πŸ“š Documentation" }, + { message = "^perf", group = "⚑ Performance" }, + { message = "^refactor", group = "🚜 Refactor" }, + { message = "^style", group = "🎨 Styling" }, + { message = "^test", group = "πŸ§ͺ Testing" }, + { message = "^chore\\(changelog\\)", skip = true }, + { message = "^chore\\(version\\)", skip = true }, + { message = "^chore\\(release\\): prepare for", skip = true }, + { message = "^chore\\(deps.*\\)", skip = true }, + { message = "^chore\\(pr\\)", skip = true }, + { message = "^chore\\(pull\\)", skip = true }, + { message = "^chore|^ci", group = "βš™οΈ Miscellaneous Tasks" }, + { body = ".*security", group = "πŸ›‘οΈ Security" }, + { message = "^revert", group = "◀️ Revert" }, + { message = ".*", group = "πŸ’Ό Other" }, +] +# Regex to select git tags that represent releases. +tag_pattern = "v[0-9]+\\.[0-9]+\\.[0-9]+" +# filter out the commits that are not matched by commit parsers +filter_commits = false +# sort the tags topologically +topo_order = false +# sort the commits inside sections by oldest/newest order +sort_commits = "newest" diff --git a/v0.2.0/deno.jsonc b/v0.2.0/deno.jsonc new file mode 100644 index 0000000..e13a592 --- /dev/null +++ b/v0.2.0/deno.jsonc @@ -0,0 +1,36 @@ +{ + "name": "@0xmax42/http-kernel", + "description": "A simple HTTP kernel for Deno", + "exports": { + "./mod.ts": "./src/mod.ts" + }, + "tasks": { + "test": "deno test --allow-net --allow-env --unstable-kv --allow-read --allow-write --coverage **/__tests__/*.test.ts", + "test:watch": "deno test --watch --allow-net --allow-env --unstable-kv --allow-read --allow-write **/__tests__/*.test.ts", + "benchmark": "deno bench --allow-net --allow-env --unstable-kv --allow-read --allow-write **/__bench__/*.bench.ts", + "fmt": "deno fmt --check", + "lint": "deno lint", + "ci": "deno task fmt && deno task lint && deno task test && deno task benchmark" // For local CI checks + }, + "compilerOptions": { + "lib": [ + "dom", + "dom.iterable", + "esnext", + "deno.ns" + ], + "strict": true + }, + "fmt": { + "useTabs": false, + "lineWidth": 80, + "indentWidth": 4, + "semiColons": true, + "singleQuote": true, + "proseWrap": "preserve", + "include": [ + "src/", + "main.ts" + ] + } +} \ No newline at end of file diff --git a/v0.2.0/deno.lock b/v0.2.0/deno.lock new file mode 100644 index 0000000..aa91fda --- /dev/null +++ b/v0.2.0/deno.lock @@ -0,0 +1,155 @@ +{ + "version": "5", + "redirects": { + "https://deno.land/std/assert/mod.ts": "https://deno.land/std@0.224.0/assert/mod.ts", + "https://deno.land/std/fs/walk.ts": "https://deno.land/std@0.224.0/fs/walk.ts", + "https://deno.land/std/path/mod.ts": "https://deno.land/std@0.224.0/path/mod.ts" + }, + "remote": { + "https://deno.land/std@0.204.0/assert/_constants.ts": "8a9da298c26750b28b326b297316cdde860bc237533b07e1337c021379e6b2a9", + "https://deno.land/std@0.204.0/assert/_diff.ts": "58e1461cc61d8eb1eacbf2a010932bf6a05b79344b02ca38095f9b805795dc48", + "https://deno.land/std@0.204.0/assert/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7", + "https://deno.land/std@0.204.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee", + "https://deno.land/std@0.204.0/assert/assert_almost_equals.ts": "e15ca1f34d0d5e0afae63b3f5d975cbd18335a132e42b0c747d282f62ad2cd6c", + "https://deno.land/std@0.204.0/assert/assert_array_includes.ts": "6856d7f2c3544bc6e62fb4646dfefa3d1df5ff14744d1bca19f0cbaf3b0d66c9", + "https://deno.land/std@0.204.0/assert/assert_equals.ts": "d8ec8a22447fbaf2fc9d7c3ed2e66790fdb74beae3e482855d75782218d68227", + "https://deno.land/std@0.204.0/assert/assert_exists.ts": "407cb6b9fb23a835cd8d5ad804e2e2edbbbf3870e322d53f79e1c7a512e2efd7", + "https://deno.land/std@0.204.0/assert/assert_false.ts": "0ccbcaae910f52c857192ff16ea08bda40fdc79de80846c206bfc061e8c851c6", + "https://deno.land/std@0.204.0/assert/assert_greater.ts": "ae2158a2d19313bf675bf7251d31c6dc52973edb12ac64ac8fc7064152af3e63", + "https://deno.land/std@0.204.0/assert/assert_greater_or_equal.ts": "1439da5ebbe20855446cac50097ac78b9742abe8e9a43e7de1ce1426d556e89c", + "https://deno.land/std@0.204.0/assert/assert_instance_of.ts": "3aedb3d8186e120812d2b3a5dea66a6e42bf8c57a8bd927645770bd21eea554c", + "https://deno.land/std@0.204.0/assert/assert_is_error.ts": "c21113094a51a296ffaf036767d616a78a2ae5f9f7bbd464cd0197476498b94b", + "https://deno.land/std@0.204.0/assert/assert_less.ts": "aec695db57db42ec3e2b62e97e1e93db0063f5a6ec133326cc290ff4b71b47e4", + "https://deno.land/std@0.204.0/assert/assert_less_or_equal.ts": "5fa8b6a3ffa20fd0a05032fe7257bf985d207b85685fdbcd23651b70f928c848", + "https://deno.land/std@0.204.0/assert/assert_match.ts": "c4083f80600bc190309903c95e397a7c9257ff8b5ae5c7ef91e834704e672e9b", + "https://deno.land/std@0.204.0/assert/assert_not_equals.ts": "9f1acab95bd1f5fc9a1b17b8027d894509a745d91bac1718fdab51dc76831754", + "https://deno.land/std@0.204.0/assert/assert_not_instance_of.ts": "0c14d3dfd9ab7a5276ed8ed0b18c703d79a3d106102077ec437bfe7ed912bd22", + "https://deno.land/std@0.204.0/assert/assert_not_match.ts": "3796a5b0c57a1ce6c1c57883dd4286be13a26f715ea662318ab43a8491a13ab0", + "https://deno.land/std@0.204.0/assert/assert_not_strict_equals.ts": "ca6c6d645e95fbc873d25320efeb8c4c6089a9a5e09f92d7c1c4b6e935c2a6ad", + "https://deno.land/std@0.204.0/assert/assert_object_match.ts": "d8fc2867cfd92eeacf9cea621e10336b666de1874a6767b5ec48988838370b54", + "https://deno.land/std@0.204.0/assert/assert_rejects.ts": "45c59724de2701e3b1f67c391d6c71c392363635aad3f68a1b3408f9efca0057", + "https://deno.land/std@0.204.0/assert/assert_strict_equals.ts": "b1f538a7ea5f8348aeca261d4f9ca603127c665e0f2bbfeb91fa272787c87265", + "https://deno.land/std@0.204.0/assert/assert_string_includes.ts": "b821d39ebf5cb0200a348863c86d8c4c4b398e02012ce74ad15666fc4b631b0c", + "https://deno.land/std@0.204.0/assert/assert_throws.ts": "63784e951475cb7bdfd59878cd25a0931e18f6dc32a6077c454b2cd94f4f4bcd", + "https://deno.land/std@0.204.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56", + "https://deno.land/std@0.204.0/assert/equal.ts": "9f1a46d5993966d2596c44e5858eec821859b45f783a5ee2f7a695dfc12d8ece", + "https://deno.land/std@0.204.0/assert/fail.ts": "c36353d7ae6e1f7933d45f8ea51e358c8c4b67d7e7502028598fe1fea062e278", + "https://deno.land/std@0.204.0/assert/mod.ts": "37c49a26aae2b254bbe25723434dc28cd7532e444cf0b481a97c045d110ec085", + "https://deno.land/std@0.204.0/assert/unimplemented.ts": "d56fbeecb1f108331a380f72e3e010a1f161baa6956fd0f7cf3e095ae1a4c75a", + "https://deno.land/std@0.204.0/assert/unreachable.ts": "4600dc0baf7d9c15a7f7d234f00c23bca8f3eba8b140286aaca7aa998cf9a536", + "https://deno.land/std@0.204.0/fmt/colors.ts": "c51c4642678eb690dcf5ffee5918b675bf01a33fba82acf303701ae1a4f8c8d9", + "https://deno.land/std@0.224.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975", + "https://deno.land/std@0.224.0/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834", + "https://deno.land/std@0.224.0/assert/assert_almost_equals.ts": "9e416114322012c9a21fa68e187637ce2d7df25bcbdbfd957cd639e65d3cf293", + "https://deno.land/std@0.224.0/assert/assert_array_includes.ts": "14c5094471bc8e4a7895fc6aa5a184300d8a1879606574cb1cd715ef36a4a3c7", + "https://deno.land/std@0.224.0/assert/assert_equals.ts": "3bbca947d85b9d374a108687b1a8ba3785a7850436b5a8930d81f34a32cb8c74", + "https://deno.land/std@0.224.0/assert/assert_exists.ts": "43420cf7f956748ae6ed1230646567b3593cb7a36c5a5327269279c870c5ddfd", + "https://deno.land/std@0.224.0/assert/assert_false.ts": "3e9be8e33275db00d952e9acb0cd29481a44fa0a4af6d37239ff58d79e8edeff", + "https://deno.land/std@0.224.0/assert/assert_greater.ts": "5e57b201fd51b64ced36c828e3dfd773412c1a6120c1a5a99066c9b261974e46", + "https://deno.land/std@0.224.0/assert/assert_greater_or_equal.ts": "9870030f997a08361b6f63400273c2fb1856f5db86c0c3852aab2a002e425c5b", + "https://deno.land/std@0.224.0/assert/assert_instance_of.ts": "e22343c1fdcacfaea8f37784ad782683ec1cf599ae9b1b618954e9c22f376f2c", + "https://deno.land/std@0.224.0/assert/assert_is_error.ts": "f856b3bc978a7aa6a601f3fec6603491ab6255118afa6baa84b04426dd3cc491", + "https://deno.land/std@0.224.0/assert/assert_less.ts": "60b61e13a1982865a72726a5fa86c24fad7eb27c3c08b13883fb68882b307f68", + "https://deno.land/std@0.224.0/assert/assert_less_or_equal.ts": "d2c84e17faba4afe085e6c9123a63395accf4f9e00150db899c46e67420e0ec3", + "https://deno.land/std@0.224.0/assert/assert_match.ts": "ace1710dd3b2811c391946954234b5da910c5665aed817943d086d4d4871a8b7", + "https://deno.land/std@0.224.0/assert/assert_not_equals.ts": "78d45dd46133d76ce624b2c6c09392f6110f0df9b73f911d20208a68dee2ef29", + "https://deno.land/std@0.224.0/assert/assert_not_instance_of.ts": "3434a669b4d20cdcc5359779301a0588f941ffdc2ad68803c31eabdb4890cf7a", + "https://deno.land/std@0.224.0/assert/assert_not_match.ts": "df30417240aa2d35b1ea44df7e541991348a063d9ee823430e0b58079a72242a", + "https://deno.land/std@0.224.0/assert/assert_not_strict_equals.ts": "37f73880bd672709373d6dc2c5f148691119bed161f3020fff3548a0496f71b8", + "https://deno.land/std@0.224.0/assert/assert_object_match.ts": "411450fd194fdaabc0089ae68f916b545a49d7b7e6d0026e84a54c9e7eed2693", + "https://deno.land/std@0.224.0/assert/assert_rejects.ts": "4bee1d6d565a5b623146a14668da8f9eb1f026a4f338bbf92b37e43e0aa53c31", + "https://deno.land/std@0.224.0/assert/assert_strict_equals.ts": "b4f45f0fd2e54d9029171876bd0b42dd9ed0efd8f853ab92a3f50127acfa54f5", + "https://deno.land/std@0.224.0/assert/assert_string_includes.ts": "496b9ecad84deab72c8718735373feb6cdaa071eb91a98206f6f3cb4285e71b8", + "https://deno.land/std@0.224.0/assert/assert_throws.ts": "c6508b2879d465898dab2798009299867e67c570d7d34c90a2d235e4553906eb", + "https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917", + "https://deno.land/std@0.224.0/assert/equal.ts": "bddf07bb5fc718e10bb72d5dc2c36c1ce5a8bdd3b647069b6319e07af181ac47", + "https://deno.land/std@0.224.0/assert/fail.ts": "0eba674ffb47dff083f02ced76d5130460bff1a9a68c6514ebe0cdea4abadb68", + "https://deno.land/std@0.224.0/assert/mod.ts": "48b8cb8a619ea0b7958ad7ee9376500fe902284bb36f0e32c598c3dc34cbd6f3", + "https://deno.land/std@0.224.0/assert/unimplemented.ts": "8c55a5793e9147b4f1ef68cd66496b7d5ba7a9e7ca30c6da070c1a58da723d73", + "https://deno.land/std@0.224.0/assert/unreachable.ts": "5ae3dbf63ef988615b93eb08d395dda771c96546565f9e521ed86f6510c29e19", + "https://deno.land/std@0.224.0/fmt/colors.ts": "508563c0659dd7198ba4bbf87e97f654af3c34eb56ba790260f252ad8012e1c5", + "https://deno.land/std@0.224.0/fs/_create_walk_entry.ts": "5d9d2aaec05bcf09a06748b1684224d33eba7a4de24cf4cf5599991ca6b5b412", + "https://deno.land/std@0.224.0/fs/_to_path_string.ts": "29bfc9c6c112254961d75cbf6ba814d6de5349767818eb93090cecfa9665591e", + "https://deno.land/std@0.224.0/fs/walk.ts": "cddf87d2705c0163bff5d7767291f05b0f46ba10b8b28f227c3849cace08d303", + "https://deno.land/std@0.224.0/internal/diff.ts": "6234a4b493ebe65dc67a18a0eb97ef683626a1166a1906232ce186ae9f65f4e6", + "https://deno.land/std@0.224.0/internal/format.ts": "0a98ee226fd3d43450245b1844b47003419d34d210fa989900861c79820d21c2", + "https://deno.land/std@0.224.0/internal/mod.ts": "534125398c8e7426183e12dc255bb635d94e06d0f93c60a297723abe69d3b22e", + "https://deno.land/std@0.224.0/path/_common/assert_path.ts": "dbdd757a465b690b2cc72fc5fb7698c51507dec6bfafce4ca500c46b76ff7bd8", + "https://deno.land/std@0.224.0/path/_common/basename.ts": "569744855bc8445f3a56087fd2aed56bdad39da971a8d92b138c9913aecc5fa2", + "https://deno.land/std@0.224.0/path/_common/common.ts": "ef73c2860694775fe8ffcbcdd387f9f97c7a656febf0daa8c73b56f4d8a7bd4c", + "https://deno.land/std@0.224.0/path/_common/constants.ts": "dc5f8057159f4b48cd304eb3027e42f1148cf4df1fb4240774d3492b5d12ac0c", + "https://deno.land/std@0.224.0/path/_common/dirname.ts": "684df4aa71a04bbcc346c692c8485594fc8a90b9408dfbc26ff32cf3e0c98cc8", + "https://deno.land/std@0.224.0/path/_common/format.ts": "92500e91ea5de21c97f5fe91e178bae62af524b72d5fcd246d6d60ae4bcada8b", + "https://deno.land/std@0.224.0/path/_common/from_file_url.ts": "d672bdeebc11bf80e99bf266f886c70963107bdd31134c4e249eef51133ceccf", + "https://deno.land/std@0.224.0/path/_common/glob_to_reg_exp.ts": "6cac16d5c2dc23af7d66348a7ce430e5de4e70b0eede074bdbcf4903f4374d8d", + "https://deno.land/std@0.224.0/path/_common/normalize.ts": "684df4aa71a04bbcc346c692c8485594fc8a90b9408dfbc26ff32cf3e0c98cc8", + "https://deno.land/std@0.224.0/path/_common/normalize_string.ts": "33edef773c2a8e242761f731adeb2bd6d683e9c69e4e3d0092985bede74f4ac3", + "https://deno.land/std@0.224.0/path/_common/relative.ts": "faa2753d9b32320ed4ada0733261e3357c186e5705678d9dd08b97527deae607", + "https://deno.land/std@0.224.0/path/_common/strip_trailing_separators.ts": "7024a93447efcdcfeaa9339a98fa63ef9d53de363f1fbe9858970f1bba02655a", + "https://deno.land/std@0.224.0/path/_common/to_file_url.ts": "7f76adbc83ece1bba173e6e98a27c647712cab773d3f8cbe0398b74afc817883", + "https://deno.land/std@0.224.0/path/_interface.ts": "8dfeb930ca4a772c458a8c7bbe1e33216fe91c253411338ad80c5b6fa93ddba0", + "https://deno.land/std@0.224.0/path/_os.ts": "8fb9b90fb6b753bd8c77cfd8a33c2ff6c5f5bc185f50de8ca4ac6a05710b2c15", + "https://deno.land/std@0.224.0/path/basename.ts": "7ee495c2d1ee516ffff48fb9a93267ba928b5a3486b550be73071bc14f8cc63e", + "https://deno.land/std@0.224.0/path/common.ts": "03e52e22882402c986fe97ca3b5bb4263c2aa811c515ce84584b23bac4cc2643", + "https://deno.land/std@0.224.0/path/constants.ts": "0c206169ca104938ede9da48ac952de288f23343304a1c3cb6ec7625e7325f36", + "https://deno.land/std@0.224.0/path/dirname.ts": "85bd955bf31d62c9aafdd7ff561c4b5fb587d11a9a5a45e2b01aedffa4238a7c", + "https://deno.land/std@0.224.0/path/extname.ts": "593303db8ae8c865cbd9ceec6e55d4b9ac5410c1e276bfd3131916591b954441", + "https://deno.land/std@0.224.0/path/format.ts": "6ce1779b0980296cf2bc20d66436b12792102b831fd281ab9eb08fa8a3e6f6ac", + "https://deno.land/std@0.224.0/path/from_file_url.ts": "911833ae4fd10a1c84f6271f36151ab785955849117dc48c6e43b929504ee069", + "https://deno.land/std@0.224.0/path/glob_to_regexp.ts": "7f30f0a21439cadfdae1be1bf370880b415e676097fda584a63ce319053b5972", + "https://deno.land/std@0.224.0/path/is_absolute.ts": "4791afc8bfd0c87f0526eaa616b0d16e7b3ab6a65b62942e50eac68de4ef67d7", + "https://deno.land/std@0.224.0/path/is_glob.ts": "a65f6195d3058c3050ab905705891b412ff942a292bcbaa1a807a74439a14141", + "https://deno.land/std@0.224.0/path/join.ts": "ae2ec5ca44c7e84a235fd532e4a0116bfb1f2368b394db1c4fb75e3c0f26a33a", + "https://deno.land/std@0.224.0/path/join_globs.ts": "5b3bf248b93247194f94fa6947b612ab9d3abd571ca8386cf7789038545e54a0", + "https://deno.land/std@0.224.0/path/mod.ts": "f6bd79cb08be0e604201bc9de41ac9248582699d1b2ee0ab6bc9190d472cf9cd", + "https://deno.land/std@0.224.0/path/normalize.ts": "4155743ccceeed319b350c1e62e931600272fad8ad00c417b91df093867a8352", + "https://deno.land/std@0.224.0/path/normalize_glob.ts": "cc89a77a7d3b1d01053b9dcd59462b75482b11e9068ae6c754b5cf5d794b374f", + "https://deno.land/std@0.224.0/path/parse.ts": "77ad91dcb235a66c6f504df83087ce2a5471e67d79c402014f6e847389108d5a", + "https://deno.land/std@0.224.0/path/posix/_util.ts": "1e3937da30f080bfc99fe45d7ed23c47dd8585c5e473b2d771380d3a6937cf9d", + "https://deno.land/std@0.224.0/path/posix/basename.ts": "d2fa5fbbb1c5a3ab8b9326458a8d4ceac77580961b3739cd5bfd1d3541a3e5f0", + "https://deno.land/std@0.224.0/path/posix/common.ts": "26f60ccc8b2cac3e1613000c23ac5a7d392715d479e5be413473a37903a2b5d4", + "https://deno.land/std@0.224.0/path/posix/constants.ts": "93481efb98cdffa4c719c22a0182b994e5a6aed3047e1962f6c2c75b7592bef1", + "https://deno.land/std@0.224.0/path/posix/dirname.ts": "76cd348ffe92345711409f88d4d8561d8645353ac215c8e9c80140069bf42f00", + "https://deno.land/std@0.224.0/path/posix/extname.ts": "e398c1d9d1908d3756a7ed94199fcd169e79466dd88feffd2f47ce0abf9d61d2", + "https://deno.land/std@0.224.0/path/posix/format.ts": "185e9ee2091a42dd39e2a3b8e4925370ee8407572cee1ae52838aed96310c5c1", + "https://deno.land/std@0.224.0/path/posix/from_file_url.ts": "951aee3a2c46fd0ed488899d024c6352b59154c70552e90885ed0c2ab699bc40", + "https://deno.land/std@0.224.0/path/posix/glob_to_regexp.ts": "76f012fcdb22c04b633f536c0b9644d100861bea36e9da56a94b9c589a742e8f", + "https://deno.land/std@0.224.0/path/posix/is_absolute.ts": "cebe561ad0ae294f0ce0365a1879dcfca8abd872821519b4fcc8d8967f888ede", + "https://deno.land/std@0.224.0/path/posix/is_glob.ts": "8a8b08c08bf731acf2c1232218f1f45a11131bc01de81e5f803450a5914434b9", + "https://deno.land/std@0.224.0/path/posix/join.ts": "7fc2cb3716aa1b863e990baf30b101d768db479e70b7313b4866a088db016f63", + "https://deno.land/std@0.224.0/path/posix/join_globs.ts": "a9475b44645feddceb484ee0498e456f4add112e181cb94042cdc6d47d1cdd25", + "https://deno.land/std@0.224.0/path/posix/mod.ts": "2301fc1c54a28b349e20656f68a85f75befa0ee9b6cd75bfac3da5aca9c3f604", + "https://deno.land/std@0.224.0/path/posix/normalize.ts": "baeb49816a8299f90a0237d214cef46f00ba3e95c0d2ceb74205a6a584b58a91", + "https://deno.land/std@0.224.0/path/posix/normalize_glob.ts": "9c87a829b6c0f445d03b3ecadc14492e2864c3ebb966f4cea41e98326e4435c6", + "https://deno.land/std@0.224.0/path/posix/parse.ts": "09dfad0cae530f93627202f28c1befa78ea6e751f92f478ca2cc3b56be2cbb6a", + "https://deno.land/std@0.224.0/path/posix/relative.ts": "3907d6eda41f0ff723d336125a1ad4349112cd4d48f693859980314d5b9da31c", + "https://deno.land/std@0.224.0/path/posix/resolve.ts": "08b699cfeee10cb6857ccab38fa4b2ec703b0ea33e8e69964f29d02a2d5257cf", + "https://deno.land/std@0.224.0/path/posix/to_file_url.ts": "7aa752ba66a35049e0e4a4be5a0a31ac6b645257d2e031142abb1854de250aaf", + "https://deno.land/std@0.224.0/path/posix/to_namespaced_path.ts": "28b216b3c76f892a4dca9734ff1cc0045d135532bfd9c435ae4858bfa5a2ebf0", + "https://deno.land/std@0.224.0/path/relative.ts": "ab739d727180ed8727e34ed71d976912461d98e2b76de3d3de834c1066667add", + "https://deno.land/std@0.224.0/path/resolve.ts": "a6f977bdb4272e79d8d0ed4333e3d71367cc3926acf15ac271f1d059c8494d8d", + "https://deno.land/std@0.224.0/path/to_file_url.ts": "88f049b769bce411e2d2db5bd9e6fd9a185a5fbd6b9f5ad8f52bef517c4ece1b", + "https://deno.land/std@0.224.0/path/to_namespaced_path.ts": "b706a4103b104cfadc09600a5f838c2ba94dbcdb642344557122dda444526e40", + "https://deno.land/std@0.224.0/path/windows/_util.ts": "d5f47363e5293fced22c984550d5e70e98e266cc3f31769e1710511803d04808", + "https://deno.land/std@0.224.0/path/windows/basename.ts": "6bbc57bac9df2cec43288c8c5334919418d784243a00bc10de67d392ab36d660", + "https://deno.land/std@0.224.0/path/windows/common.ts": "26f60ccc8b2cac3e1613000c23ac5a7d392715d479e5be413473a37903a2b5d4", + "https://deno.land/std@0.224.0/path/windows/constants.ts": "5afaac0a1f67b68b0a380a4ef391bf59feb55856aa8c60dfc01bd3b6abb813f5", + "https://deno.land/std@0.224.0/path/windows/dirname.ts": "33e421be5a5558a1346a48e74c330b8e560be7424ed7684ea03c12c21b627bc9", + "https://deno.land/std@0.224.0/path/windows/extname.ts": "165a61b00d781257fda1e9606a48c78b06815385e7d703232548dbfc95346bef", + "https://deno.land/std@0.224.0/path/windows/format.ts": "bbb5ecf379305b472b1082cd2fdc010e44a0020030414974d6029be9ad52aeb6", + "https://deno.land/std@0.224.0/path/windows/from_file_url.ts": "ced2d587b6dff18f963f269d745c4a599cf82b0c4007356bd957cb4cb52efc01", + "https://deno.land/std@0.224.0/path/windows/glob_to_regexp.ts": "e45f1f89bf3fc36f94ab7b3b9d0026729829fabc486c77f414caebef3b7304f8", + "https://deno.land/std@0.224.0/path/windows/is_absolute.ts": "4a8f6853f8598cf91a835f41abed42112cebab09478b072e4beb00ec81f8ca8a", + "https://deno.land/std@0.224.0/path/windows/is_glob.ts": "8a8b08c08bf731acf2c1232218f1f45a11131bc01de81e5f803450a5914434b9", + "https://deno.land/std@0.224.0/path/windows/join.ts": "8d03530ab89195185103b7da9dfc6327af13eabdcd44c7c63e42e27808f50ecf", + "https://deno.land/std@0.224.0/path/windows/join_globs.ts": "a9475b44645feddceb484ee0498e456f4add112e181cb94042cdc6d47d1cdd25", + "https://deno.land/std@0.224.0/path/windows/mod.ts": "2301fc1c54a28b349e20656f68a85f75befa0ee9b6cd75bfac3da5aca9c3f604", + "https://deno.land/std@0.224.0/path/windows/normalize.ts": "78126170ab917f0ca355a9af9e65ad6bfa5be14d574c5fb09bb1920f52577780", + "https://deno.land/std@0.224.0/path/windows/normalize_glob.ts": "9c87a829b6c0f445d03b3ecadc14492e2864c3ebb966f4cea41e98326e4435c6", + "https://deno.land/std@0.224.0/path/windows/parse.ts": "08804327b0484d18ab4d6781742bf374976de662f8642e62a67e93346e759707", + "https://deno.land/std@0.224.0/path/windows/relative.ts": "3e1abc7977ee6cc0db2730d1f9cb38be87b0ce4806759d271a70e4997fc638d7", + "https://deno.land/std@0.224.0/path/windows/resolve.ts": "8dae1dadfed9d46ff46cc337c9525c0c7d959fb400a6308f34595c45bdca1972", + "https://deno.land/std@0.224.0/path/windows/to_file_url.ts": "40e560ee4854fe5a3d4d12976cef2f4e8914125c81b11f1108e127934ced502e", + "https://deno.land/std@0.224.0/path/windows/to_namespaced_path.ts": "4ffa4fb6fae321448d5fe810b3ca741d84df4d7897e61ee29be961a6aac89a4c" + } +} diff --git a/v0.2.0/src/Errors/InvalidHttpMethodError.ts b/v0.2.0/src/Errors/InvalidHttpMethodError.ts new file mode 100644 index 0000000..8d9047f --- /dev/null +++ b/v0.2.0/src/Errors/InvalidHttpMethodError.ts @@ -0,0 +1,25 @@ +/** + * Represents an error thrown when an incoming HTTP method + * is not among the recognized set of valid HTTP methods. + * + * This is typically used in routers or request dispatchers + * to enforce allowed methods and produce 405-like behavior. + */ +export class InvalidHttpMethodError extends Error { + /** + * The invalid method that triggered this error. + */ + public readonly method: unknown; + + /** + * A fixed HTTP status code representing "Method Not Allowed". + */ + public readonly status: number = 405; + + constructor(method: unknown) { + const label = typeof method === 'string' ? method : '[non-string]'; + super(`Unsupported HTTP method: ${label}`); + this.name = 'InvalidHttpMethodError'; + this.method = method; + } +} diff --git a/v0.2.0/src/Errors/mod.ts b/v0.2.0/src/Errors/mod.ts new file mode 100644 index 0000000..50e5ba8 --- /dev/null +++ b/v0.2.0/src/Errors/mod.ts @@ -0,0 +1,3 @@ +// deno-coverage-ignore-file + +export { InvalidHttpMethodError } from './InvalidHttpMethodError.ts'; diff --git a/v0.2.0/src/HttpKernel.ts b/v0.2.0/src/HttpKernel.ts new file mode 100644 index 0000000..fa6c778 --- /dev/null +++ b/v0.2.0/src/HttpKernel.ts @@ -0,0 +1,144 @@ +import type { + IContext, + IHttpKernel, + IHttpKernelConfig, + IInternalRoute, + IRouteBuilder, + IRouteDefinition, +} from './Interfaces/mod.ts'; +import { + type DeepPartial, + HTTP_404_NOT_FOUND, + HTTP_500_INTERNAL_SERVER_ERROR, + HttpStatusTextMap, +} from './Types/mod.ts'; +import { RouteBuilder } from './RouteBuilder.ts'; +import { createEmptyContext, normalizeError } from './Utils/mod.ts'; + +/** + * The `HttpKernel` is the central routing engine that manages the full HTTP request lifecycle. + * + * It enables: + * - Dynamic and static route registration via a fluent API + * - Execution of typed middleware chains and final route handlers + * - Injection of response decorators and factory overrides + * - Fine-grained error handling via typed status-code-based handlers + * + * The kernel is designed with generics for flexible context typing, strong type safety, + * and a clear extension point for advanced routing, DI, or tracing logic. + * + * @typeParam TContext - The global context type used for all requests handled by this kernel. + */ +export class HttpKernel + implements IHttpKernel { + private cfg: IHttpKernelConfig; + + /** + * The list of registered route definitions, including method, matcher, + * middleware pipeline, and final handler. + */ + private routes: IInternalRoute[] = []; + + /** + * Initializes the `HttpKernel` with optional configuration overrides. + * + * Default components such as the route builder factory, response decorator, + * and 404/500 error handlers can be replaced by injecting a partial config. + * Any omitted values fall back to sensible defaults. + * + * @param config - Partial kernel configuration. Missing fields are filled with defaults. + */ + public constructor( + config?: DeepPartial>, + ) { + this.cfg = { + decorateResponse: (res) => res, + routeBuilderFactory: RouteBuilder, + httpErrorHandlers: { + [HTTP_404_NOT_FOUND]: () => + new Response(HttpStatusTextMap[HTTP_404_NOT_FOUND], { + status: HTTP_404_NOT_FOUND, + }), + [HTTP_500_INTERNAL_SERVER_ERROR]: () => + new Response( + HttpStatusTextMap[HTTP_500_INTERNAL_SERVER_ERROR], + { + status: HTTP_500_INTERNAL_SERVER_ERROR, + }, + ), + ...(config?.httpErrorHandlers ?? {}), + }, + ...config, + } as IHttpKernelConfig; + + this.handle = this.handle.bind(this); + this.registerRoute = this.registerRoute.bind(this); + } + + /** + * @inheritdoc + */ + public route<_TContext extends IContext = TContext>( + definition: IRouteDefinition, + ): IRouteBuilder<_TContext> { + return new this.cfg.routeBuilderFactory( + this.registerRoute, + definition, + ) as IRouteBuilder<_TContext>; + } + + /** + * @inheritdoc + */ + public async handle(request: Request): Promise { + const url = new URL(request.url); + const method = request.method.toUpperCase(); + + for (const route of this.routes) { + if (route.method !== method) continue; + const match = route.matcher(url, request); + if (match) { + const ctx: TContext = { + req: request, + params: match.params, + query: match.query, + state: {}, + } as TContext; + try { + const response = await route.runRoute(ctx); + return this.cfg.decorateResponse(response, ctx); + } catch (e) { + return await this.handleInternalError(ctx, e); + } + } + } + + return this.cfg.httpErrorHandlers[HTTP_404_NOT_FOUND]( + createEmptyContext(request), + ); + } + + /** + * Finalizes and registers a route within the kernel. + * + * This method is invoked internally by the route builder once + * `.handle()` is called. It appends the route to the internal list. + * + * @param route - A fully constructed internal route object. + */ + private registerRoute<_TContext extends IContext = TContext>( + route: IInternalRoute<_TContext>, + ): void { + this.routes.push(route as unknown as IInternalRoute); + } + + private handleInternalError = ( + ctx: TContext, + err?: unknown, + ): Response | Promise => { + return this.cfg.httpErrorHandlers[HTTP_500_INTERNAL_SERVER_ERROR]( + ctx, + normalizeError(err), + ); + }; +} diff --git a/v0.2.0/src/Interfaces/IContext.ts b/v0.2.0/src/Interfaces/IContext.ts new file mode 100644 index 0000000..564fcd3 --- /dev/null +++ b/v0.2.0/src/Interfaces/IContext.ts @@ -0,0 +1,53 @@ +import type { Params, Query, State } from '../Types/mod.ts'; + +/** + * Represents the complete context for a single HTTP request, + * passed through the middleware pipeline and to the final route handler. + * + * This context object encapsulates all relevant runtime data for a request, + * including the original request, path parameters, query parameters, + * and a shared, mutable application state. + * + * @template TState Structured per-request state shared across middlewares and handlers. + * @template TParams Parsed URL path parameters, typically derived from route templates. + * @template TQuery Parsed query string parameters, preserving multi-value semantics. + */ +export interface IContext< + TState extends State = State, + TParams extends Params = Params, + TQuery extends Query = Query, +> { + /** + * The original HTTP request object as received by Deno. + * Contains all standard fields like headers, method, body, etc. + */ + req: Request; + + /** + * Route parameters parsed from the URL path, based on route definitions + * that include dynamic segments (e.g., `/users/:id` β†’ `{ id: "123" }`). + * + * These parameters are considered read-only and are set by the router. + */ + params: TParams; + + /** + * Query parameters extracted from the request URL's search string. + * + * Values may occur multiple times (e.g., `?tag=ts&tag=deno`), and are therefore + * represented as either a string or an array of strings, depending on occurrence. + * + * Use this field to access filters, flags, pagination info, or similar modifiers. + */ + query: TQuery; + + /** + * A typed, mutable object used to pass structured data between middlewares and handlers. + * + * This object is ideal for sharing validated input, user identity, trace information, + * or other contextual state throughout the request lifecycle. + * + * Type-safe access to fields is ensured by the generic `TState` type. + */ + state: TState; +} diff --git a/v0.2.0/src/Interfaces/IHttpErrorHandlers.ts b/v0.2.0/src/Interfaces/IHttpErrorHandlers.ts new file mode 100644 index 0000000..935223d --- /dev/null +++ b/v0.2.0/src/Interfaces/IHttpErrorHandlers.ts @@ -0,0 +1,40 @@ +import type { IContext } from '../Interfaces/mod.ts'; +import type { HttpErrorHandler, validHttpErrorCodes } from '../Types/mod.ts'; + +/** + * A mapping of HTTP status codes to their corresponding error handlers. + * + * This interface defines required handlers for common critical status codes (404 and 500) + * and allows optional handlers for all other known error codes defined in `validHttpErrorCodes`. + * + * This hybrid approach ensures predictable handling for key failure cases, + * while remaining flexible for less common codes. + * + * @template TContext - The context type used in all error handlers. + * + * @example + * ```ts + * const errorHandlers: IHttpErrorHandlers = { + * 404: (ctx) => new Response("Not Found", { status: 404 }), + * 500: (ctx, err) => { + * console.error(err); + * return new Response("Internal Server Error", { status: 500 }); + * }, + * 429: (ctx) => new Response("Too Many Requests", { status: 429 }), + * }; + * ``` + */ +export interface IHttpErrorHandlers + extends + Partial< + Record< + Exclude, + HttpErrorHandler + > + > { + /** Required error handler for HTTP 404 (Not Found). */ + 404: HttpErrorHandler; + + /** Required error handler for HTTP 500 (Internal Server Error). */ + 500: HttpErrorHandler; +} diff --git a/v0.2.0/src/Interfaces/IHttpKernel.ts b/v0.2.0/src/Interfaces/IHttpKernel.ts new file mode 100644 index 0000000..371486a --- /dev/null +++ b/v0.2.0/src/Interfaces/IHttpKernel.ts @@ -0,0 +1,49 @@ +import type { IContext } from './IContext.ts'; +import type { IRouteBuilder } from './IRouteBuilder.ts'; +import type { IRouteDefinition } from './IRouteDefinition.ts'; + +/** + * The `IHttpKernel` interface defines the public API for a type-safe, middleware-driven HTTP dispatching system. + * + * Implementations of this interface are responsible for: + * - Registering routes with optional per-route context typing + * - Handling incoming requests by matching and dispatching to appropriate handlers + * - Managing the complete middleware pipeline and final response generation + * + * The kernel operates on a customizable `IContext` type to support strongly typed request parameters, state, + * and query values across the entire routing lifecycle. + * + * @typeParam TContext - The default context type used for all routes unless overridden per-route. + */ +export interface IHttpKernel { + /** + * Registers a new HTTP route (static or dynamic) and returns a route builder for middleware/handler chaining. + * + * This method supports contextual polymorphism via the `_TContext` type parameter, enabling fine-grained + * typing of route-specific `params`, `query`, and `state` values. The route is not registered until + * `.handle()` is called on the returned builder. + * + * @typeParam _TContext - An optional override for the context type specific to this route. + * Falls back to the global `TContext` of the kernel if omitted. + * + * @param definition - A route definition specifying the HTTP method and path or custom matcher. + * @returns A fluent builder interface to define middleware and attach a final handler. + */ + route<_TContext extends IContext = TContext>( + definition: IRouteDefinition, + ): IRouteBuilder<_TContext>; + + /** + * Handles an incoming HTTP request and produces a `Response`. + * + * The kernel matches the request against all registered routes by method and matcher, + * constructs a typed context, and executes the middleware/handler pipeline. + * If no route matches, a 404 error handler is invoked. + * + * This method is designed to be passed directly to `Deno.serve()` or similar server frameworks. + * + * @param request - The incoming HTTP request object. + * @returns A `Promise` resolving to a complete HTTP response. + */ + handle(request: Request): Promise; +} diff --git a/v0.2.0/src/Interfaces/IHttpKernelConfig.ts b/v0.2.0/src/Interfaces/IHttpKernelConfig.ts new file mode 100644 index 0000000..8a91e98 --- /dev/null +++ b/v0.2.0/src/Interfaces/IHttpKernelConfig.ts @@ -0,0 +1,10 @@ +import type { ResponseDecorator } from '../Types/mod.ts'; +import type { IContext } from './IContext.ts'; +import type { IHttpErrorHandlers } from './IHttpErrorHandlers.ts'; +import type { IRouteBuilderFactory } from './IRouteBuilder.ts'; + +export interface IHttpKernelConfig { + decorateResponse: ResponseDecorator; + routeBuilderFactory: IRouteBuilderFactory; + httpErrorHandlers: IHttpErrorHandlers; +} diff --git a/v0.2.0/src/Interfaces/IInternalRoute.ts b/v0.2.0/src/Interfaces/IInternalRoute.ts new file mode 100644 index 0000000..fd3e33f --- /dev/null +++ b/v0.2.0/src/Interfaces/IInternalRoute.ts @@ -0,0 +1,64 @@ +import type { Handler, HttpMethod, Middleware } from '../Types/mod.ts'; +import type { IContext, IRouteMatcher } from './mod.ts'; + +/** + * Represents an internally registered route within the HttpKernel. + * + * Contains all data required to match an incoming request and dispatch it + * through the associated middleware chain and final handler. + */ +export interface IInternalRoute { + /** + * The HTTP method (e.g. 'GET', 'POST') that this route responds to. + * The method should always be in uppercase. + */ + method: HttpMethod; + + /** + * A matcher function used to determine whether this route matches a given request. + * + * If the matcher returns `null`, the route does not apply to the request. + * If it returns a params object, the route is considered matched and the extracted + * parameters are passed into the request context. + * + * @param url - The parsed URL object from the incoming request. + * @param req - The original Request object. + * @returns An object with extracted path parameters, or `null` if not matched. + */ + matcher: IRouteMatcher; + + /** + * An ordered list of middleware functions to be executed before the handler. + */ + middlewares: Middleware[]; + + /** + * The final handler that generates the HTTP response after all middleware has run. + */ + handler: Handler; + + /** + * The fully compiled execution pipeline for this route. + * + * This function is generated at route registration time and encapsulates the + * entire middleware chain as well as the final handler. It is called by the + * HttpKernel during request dispatch when a route has been matched. + * + * Internally, `runRoute` ensures that each middleware is invoked in the correct order + * and receives a `next()` callback to pass control downstream. The final handler is + * invoked once all middleware has completed or short-circuited the pipeline. + * + * It is guaranteed that: + * - The function is statically compiled and does not perform dynamic dispatching. + * - Each middleware can only call `next()` once; repeated invocations will throw. + * - The return value is either a `Response` or a Promise resolving to one. + * + * @param ctx - The context object carrying route, request, response and other scoped data. + * @returns A `Response` object or a Promise resolving to a `Response`. + * + * @throws {Error} If a middleware calls `next()` more than once. + */ + runRoute: ( + ctx: TContext, + ) => Promise | Response; +} diff --git a/v0.2.0/src/Interfaces/IRouteBuilder.ts b/v0.2.0/src/Interfaces/IRouteBuilder.ts new file mode 100644 index 0000000..9737ae0 --- /dev/null +++ b/v0.2.0/src/Interfaces/IRouteBuilder.ts @@ -0,0 +1,39 @@ +import type { Handler, Middleware } from '../Types/mod.ts'; +import type { IInternalRoute } from './IInternalRoute.ts'; +import type { IRouteDefinition } from './IRouteDefinition.ts'; +import type { IContext } from './mod.ts'; + +export interface IRouteBuilderFactory { + new ( + registerRoute: (route: IInternalRoute) => void, + def: IRouteDefinition, + mws?: Middleware[], + ): IRouteBuilder; +} + +/** + * Provides a fluent API to build a single route configuration by chaining + * middleware and setting the final request handler. + */ +export interface IRouteBuilder { + /** + * Adds a middleware to the current route. + * Middleware will be executed in the order of registration. + * + * @param mw - A middleware function. + * @returns The route builder for further chaining. + */ + middleware( + mw: Middleware, + ): IRouteBuilder; + + /** + * Sets the final request handler for the route. + * Calling this finalizes the route and registers it in the kernel. + * + * @param handler - The function to execute when this route is matched. + */ + handle( + handler: Handler, + ): void; +} diff --git a/v0.2.0/src/Interfaces/IRouteDefinition.ts b/v0.2.0/src/Interfaces/IRouteDefinition.ts new file mode 100644 index 0000000..c2c41ff --- /dev/null +++ b/v0.2.0/src/Interfaces/IRouteDefinition.ts @@ -0,0 +1,91 @@ +import { type HttpMethod, isHttpMethod } from '../Types/mod.ts'; +import type { IRouteMatcher } from './IRouteMatcher.ts'; + +/** + * Defines a static route using a path pattern with optional parameters. + * + * Suitable for conventional routes like "/users/:id", which can be parsed + * into named parameters using a path-matching library. + */ +export interface IStaticRouteDefinition { + /** + * The HTTP method this route should match (e.g. "GET", "POST"). + */ + method: HttpMethod; + + /** + * A static path pattern for the route, which may include named parameters + * (e.g. "/caches/:id"). Internally, this can be converted to a regex matcher. + */ + path: string; +} + +/** + * Defines a dynamic route using a custom matcher function instead of a static path. + * + * Useful for complex URL structures that cannot easily be expressed using a static pattern, + * such as routes with variable prefixes or conditional segment logic. + */ +export interface IDynamicRouteDefinition { + /** + * The HTTP method this route should match (e.g. "GET", "POST"). + */ + method: HttpMethod; + + /** + * A custom matcher function that receives the parsed URL and raw request. + * If the function returns `null`, the route does not match. + * If the function returns a params object, the route is considered matched. + */ + matcher: IRouteMatcher; +} + +/** + * A route definition can either be a conventional static route with a path pattern, + * or a dynamic route with a custom matcher function for advanced matching logic. + */ +export type IRouteDefinition = IStaticRouteDefinition | IDynamicRouteDefinition; + +/** + * Type guard to check whether a route definition is a valid static route definition. + * + * Ensures that the object: + * - has a `method` property of type `HttpMethod` + * - has a `path` property of type `string` + * - does NOT have a `matcher` function (to avoid ambiguous mixed types) + */ +export function isStaticRouteDefinition( + def: IRouteDefinition, +): def is IStaticRouteDefinition { + return ( + def && + typeof def === 'object' && + 'method' in def && + isHttpMethod(def.method) && + 'path' in def && + typeof (def as { path?: unknown }).path === 'string' && + !('matcher' in def) + ); +} + +/** + * Type guard to check whether a route definition is a valid dynamic route definition. + * + * Ensures that the object: + * - has a `method` property of type `HttpMethod` + * - has a `matcher` property of type `function` + * - does NOT have a `path` property (to avoid ambiguous mixed types) + */ +export function isDynamicRouteDefinition( + def: IRouteDefinition, +): def is IDynamicRouteDefinition { + return ( + def && + typeof def === 'object' && + 'method' in def && + isHttpMethod(def.method) && + 'matcher' in def && + typeof (def as { matcher?: unknown }).matcher === 'function' && + !('path' in def) + ); +} diff --git a/v0.2.0/src/Interfaces/IRouteMatch.ts b/v0.2.0/src/Interfaces/IRouteMatch.ts new file mode 100644 index 0000000..9ed1067 --- /dev/null +++ b/v0.2.0/src/Interfaces/IRouteMatch.ts @@ -0,0 +1,6 @@ +import type { Params, Query } from '../Types/mod.ts'; + +export interface IRouteMatch { + params?: Params; + query?: Query; +} diff --git a/v0.2.0/src/Interfaces/IRouteMatcher.ts b/v0.2.0/src/Interfaces/IRouteMatcher.ts new file mode 100644 index 0000000..1dee7eb --- /dev/null +++ b/v0.2.0/src/Interfaces/IRouteMatcher.ts @@ -0,0 +1,35 @@ +import type { IRouteDefinition } from './IRouteDefinition.ts'; +import type { IRouteMatch } from './IRouteMatch.ts'; + +/** + * Defines a route matcher function that evaluates whether a route applies to a given request. + * + * If the route matches, the matcher returns an object containing extracted route parameters. + * Otherwise, it returns `null`. + */ +export interface IRouteMatcher { + /** + * Evaluates whether the given URL and request match a defined route. + * + * @param url - The full URL of the incoming request. + * @param req - The raw Request object (may be used for context or headers). + * @returns An object containing path parameters if matched, or `null` if not matched. + */ + (url: URL, req: Request): null | IRouteMatch; +} + +/** + * Represents a factory for creating route matcher functions from route definitions. + * + * This allows the matcher logic to be injected or replaced (e.g. for testing, + * pattern libraries, or advanced routing scenarios). + */ +export interface IRouteMatcherFactory { + /** + * Creates a matcher function based on a given route definition. + * + * @param def - The route definition (static or dynamic). + * @returns A matcher function that checks if a request matches and extracts parameters. + */ + (def: IRouteDefinition): IRouteMatcher; +} diff --git a/v0.2.0/src/Interfaces/__tests__/routeDefinitionGuards.test.ts b/v0.2.0/src/Interfaces/__tests__/routeDefinitionGuards.test.ts new file mode 100644 index 0000000..98cc0a0 --- /dev/null +++ b/v0.2.0/src/Interfaces/__tests__/routeDefinitionGuards.test.ts @@ -0,0 +1,43 @@ +import { assertEquals } from 'https://deno.land/std@0.204.0/assert/mod.ts'; +import { + type IRouteDefinition, + isDynamicRouteDefinition, + isStaticRouteDefinition, +} from '../IRouteDefinition.ts'; + +Deno.test('isStaticRouteDefinition returns true for static route', () => { + const staticDef: IRouteDefinition = { + method: 'GET', + path: '/users/:id', + }; + + assertEquals(isStaticRouteDefinition(staticDef), true); + assertEquals(isDynamicRouteDefinition(staticDef), false); +}); + +Deno.test('isDynamicRouteDefinition returns true for dynamic route', () => { + const dynamicDef: IRouteDefinition = { + method: 'POST', + matcher: (_url, _req) => ({ params: {} }), + }; + + assertEquals(isDynamicRouteDefinition(dynamicDef), true); + assertEquals(isStaticRouteDefinition(dynamicDef), false); +}); + +Deno.test('isStaticRouteDefinition returns false for invalid object', () => { + const invalidDef = { + method: 'GET', + } as unknown as IRouteDefinition; + + assertEquals(isStaticRouteDefinition(invalidDef), false); +}); + +Deno.test('isDynamicRouteDefinition returns false for object with no matcher', () => { + const def = { + method: 'DELETE', + path: '/something', + }; + + assertEquals(isDynamicRouteDefinition(def as IRouteDefinition), false); +}); diff --git a/v0.2.0/src/Interfaces/mod.ts b/v0.2.0/src/Interfaces/mod.ts new file mode 100644 index 0000000..7c235d9 --- /dev/null +++ b/v0.2.0/src/Interfaces/mod.ts @@ -0,0 +1,19 @@ +// deno-coverage-ignore-file + +export type { IContext } from './IContext.ts'; +export type { IHttpErrorHandlers } from './IHttpErrorHandlers.ts'; +export type { IHttpKernel } from './IHttpKernel.ts'; +export type { IHttpKernelConfig } from './IHttpKernelConfig.ts'; +export type { IInternalRoute } from './IInternalRoute.ts'; +export type { IRouteBuilder, IRouteBuilderFactory } from './IRouteBuilder.ts'; +export { + isDynamicRouteDefinition, + isStaticRouteDefinition, +} from './IRouteDefinition.ts'; +export type { + IDynamicRouteDefinition, + IRouteDefinition, + IStaticRouteDefinition, +} from './IRouteDefinition.ts'; +export type { IRouteMatch } from './IRouteMatch.ts'; +export type { IRouteMatcher, IRouteMatcherFactory } from './IRouteMatcher.ts'; diff --git a/v0.2.0/src/RouteBuilder.ts b/v0.2.0/src/RouteBuilder.ts new file mode 100644 index 0000000..7635058 --- /dev/null +++ b/v0.2.0/src/RouteBuilder.ts @@ -0,0 +1,148 @@ +import type { IRouteMatcherFactory } from './Interfaces/IRouteMatcher.ts'; +import type { + IContext, + IInternalRoute, + IRouteBuilder, + IRouteDefinition, +} from './Interfaces/mod.ts'; +import { isHandler } from './Types/Handler.ts'; +import { + type Handler, + isMiddleware, + type Middleware, + type RegisterRoute, +} from './Types/mod.ts'; +import { createRouteMatcher } from './Utils/createRouteMatcher.ts'; + +/** + * Provides a fluent builder interface for defining a single route, + * including HTTP method, path or matcher, middleware chain and final handler. + * + * This builder is stateless and immutable; each chained call returns a new instance. + */ +export class RouteBuilder + implements IRouteBuilder { + /** + * Constructs a new instance of the route builder. + * + * @param registerRoute - A delegate used to register the finalized route definition. + * @param def - The route definition (static path or dynamic matcher). + * @param mws - The list of middleware functions collected so far (default: empty). + */ + constructor( + private readonly registerRoute: RegisterRoute, + private readonly def: IRouteDefinition, + private readonly mws: Middleware[] = [], + private readonly matcherFactory: IRouteMatcherFactory = + createRouteMatcher, + ) {} + + /** + * Adds a middleware function to the current route definition. + * + * Middleware is executed in the order it is added. + * Returns a new builder instance with the additional middleware appended. + * + * @param mw - A middleware function to be executed before the handler. + * @returns A new `RouteBuilder` instance for continued chaining. + */ + middleware( + mw: Middleware, + ): IRouteBuilder { + return new RouteBuilder( + this.registerRoute, + this.def, + [...this.mws, mw], + ); + } + + /** + * Finalizes the route by assigning the handler and registering the route. + * + * Internally constructs a matcher function from the route definition + * and passes all route data to the registration delegate. + * + * @param handler - The final request handler for this route. + */ + handle( + handler: Handler, + ): void { + const matcher = this.matcherFactory(this.def); + this.registerRoute({ + method: this.def.method, + matcher, + middlewares: this.mws, + handler: handler, + runRoute: this.compile({ + middlewares: this.mws, + handler: handler, + }), + }); + } + + /** + * Compiles the middleware chain and handler into a single executable function. + * + * This method constructs a statically linked function chain by reducing all middleware + * and the final handler into one composed `runRoute` function. Each middleware receives + * a `next()` callback that invokes the next function in the chain. + * + * Additionally, the returned function ensures that `next()` can only be called once + * per middleware. If `next()` is invoked multiple times within the same middleware, + * a runtime `Error` is thrown, preventing unintended double-processing. + * + * Type safety is enforced at compile time: + * - If the final handler does not match the expected signature, a `TypeError` is thrown. + * - If any middleware does not conform to the middleware interface, a `TypeError` is thrown. + * + * @param route - A partial route object containing middleware and handler, + * excluding `matcher`, `method`, and `runRoute`. + * @returns A composed route execution function that takes a context object + * and returns a `Promise`. + * + * @throws {TypeError} If the handler or any middleware function is invalid. + * @throws {Error} If a middleware calls `next()` more than once during execution. + */ + private compile( + route: Omit< + IInternalRoute, + 'runRoute' | 'matcher' | 'method' + >, + ): ( + ctx: TContext, + ) => Promise { + if (!isHandler(route.handler)) { + throw new TypeError( + 'Route handler must be a function returning a Promise.', + ); + } + let composed = route.handler; + + for (let i = route.middlewares.length - 1; i >= 0; i--) { + if (!isMiddleware(route.middlewares[i])) { + throw new TypeError( + `Middleware at index ${i} is not a valid function.`, + ); + } + + const current = route.middlewares[i]; + const next = composed; + + composed = async (ctx: TContext): Promise => { + let called = false; + + return await current(ctx, async () => { + if (called) { + throw new Error( + `next() called multiple times in middleware at index ${i}`, + ); + } + called = true; + return await next(ctx); + }); + }; + } + + return composed; + } +} diff --git a/v0.2.0/src/Types/DeepPartial.ts b/v0.2.0/src/Types/DeepPartial.ts new file mode 100644 index 0000000..07c5132 --- /dev/null +++ b/v0.2.0/src/Types/DeepPartial.ts @@ -0,0 +1,4 @@ +export type DeepPartial = { + [P in keyof T]?: T[P] extends object ? DeepPartial + : T[P]; +}; diff --git a/v0.2.0/src/Types/Handler.ts b/v0.2.0/src/Types/Handler.ts new file mode 100644 index 0000000..11f28f8 --- /dev/null +++ b/v0.2.0/src/Types/Handler.ts @@ -0,0 +1,57 @@ +import type { IContext } from '../Interfaces/mod.ts'; + +/** + * Represents a final request handler responsible for producing an HTTP response. + * + * The handler is the terminal stage of the middleware pipeline and is responsible + * for processing the incoming request and generating the final `Response`. + * + * It receives the fully-typed request context, which includes the original request, + * parsed route parameters, query parameters, and any shared state populated by prior middleware. + * + * @template TContext The specific context type for this handler, including typed `state`, `params`, and `query`. + */ +type Handler = ( + ctx: TContext, +) => Promise; + +/** + * Represents a handler function with an associated name. + * + * This is useful for debugging, logging, or when you need to reference + * the handler by name in your application. + * + * @template TContext The specific context type for this handler, including typed `state`, `params`, and `query`. + */ +type NamedHandler = + & Handler + & { name?: string }; + +export type { NamedHandler as Handler }; + +/** + * Type guard to determine whether a given value is a valid `IHandler` function. + * + * This function checks whether the input is a function and whether it returns + * a `Promise` when called. Due to TypeScript's structural typing and + * the lack of runtime type information, only minimal runtime validation is possible. + * + * @param value - The value to test. + * @returns `true` if the value is a function that appears to conform to `IHandler`. + * + * @example + * ```ts + * const candidate = async (ctx: IContext) => new Response("ok"); + * if (isHandler(candidate)) { + * // candidate is now typed as IHandler + * } + * ``` + */ +export function isHandler( + value: unknown, +): value is Handler { + return ( + typeof value === 'function' && + value.length === 1 // ctx + ); +} diff --git a/v0.2.0/src/Types/HttpErrorHandler.ts b/v0.2.0/src/Types/HttpErrorHandler.ts new file mode 100644 index 0000000..8c62fae --- /dev/null +++ b/v0.2.0/src/Types/HttpErrorHandler.ts @@ -0,0 +1,28 @@ +import type { IContext } from '../Interfaces/mod.ts'; + +/** + * Defines a handler function for errors that occur during the execution + * of middleware or route handlers within the HTTP kernel. + * + * This function receives both the request context and the thrown error, + * and is responsible for producing an appropriate HTTP `Response`. + * + * Typical use cases include: + * - Mapping known error types to specific HTTP status codes. + * - Generating structured error responses (e.g. JSON error payloads). + * - Logging errors centrally with request metadata. + * + * The handler may return the response synchronously or asynchronously. + * + * @template TContext - The specific request context type, allowing typed access to route parameters, + * query parameters, and per-request state when formatting error responses. + * + * @param context - The active request context at the time the error occurred. + * @param error - The exception or error that was thrown during request processing. + * + * @returns A `Response` object or a `Promise` resolving to one, to be sent to the client. + */ +export type HttpErrorHandler = ( + context?: Partial, + error?: Error, +) => Promise | Response; diff --git a/v0.2.0/src/Types/HttpMethod.ts b/v0.2.0/src/Types/HttpMethod.ts new file mode 100644 index 0000000..d076523 --- /dev/null +++ b/v0.2.0/src/Types/HttpMethod.ts @@ -0,0 +1,52 @@ +/** + * A constant list of all supported HTTP methods according to RFC 7231 and RFC 5789. + * + * This array serves both as a runtime value list for validation + * and as the basis for deriving the `HttpMethod` union type. + * + * Note: The list is immutable and should not be modified at runtime. + */ +export const validHttpMethods = [ + 'GET', + 'POST', + 'PUT', + 'PATCH', + 'DELETE', + 'HEAD', + 'OPTIONS', +] as const; + +/** + * A union type representing all valid HTTP methods recognized by this application. + * + * This type is derived directly from the `validHttpMethods` constant, + * ensuring type safety and consistency between type system and runtime checks. + * + * Example: + * ```ts + * const method: HttpMethod = 'POST'; // βœ… valid + * const method: HttpMethod = 'FOO'; // ❌ Type error + * ``` + */ +export type HttpMethod = typeof validHttpMethods[number]; + +/** + * Type guard to verify whether a given value is a valid HTTP method. + * + * This function checks both the type and content of the value + * and is suitable for runtime validation of inputs (e.g., from HTTP requests). + * + * Example: + * ```ts + * if (isHttpMethod(input)) { + * // input is now typed as HttpMethod + * } + * ``` + * + * @param value - The value to test (typically a string from a request). + * @returns `true` if the value is a valid `HttpMethod`, otherwise `false`. + */ +export function isHttpMethod(value: unknown): value is HttpMethod { + return typeof value === 'string' && + validHttpMethods.includes(value as HttpMethod); +} diff --git a/v0.2.0/src/Types/HttpStatusCode.ts b/v0.2.0/src/Types/HttpStatusCode.ts new file mode 100644 index 0000000..2230d6f --- /dev/null +++ b/v0.2.0/src/Types/HttpStatusCode.ts @@ -0,0 +1,189 @@ +// Informational responses +/** Indicates that the request was received and the client can continue. */ +export const HTTP_100_CONTINUE = 100; +/** The server is switching protocols as requested by the client. */ +export const HTTP_101_SWITCHING_PROTOCOLS = 101; +/** The server has received and is processing the request, but no response is available yet. */ +export const HTTP_102_PROCESSING = 102; + +// Successful responses +/** The request has succeeded. */ +export const HTTP_200_OK = 200; +/** The request has succeeded and a new resource has been created as a result. */ +export const HTTP_201_CREATED = 201; +/** The request has been accepted for processing, but the processing is not complete. */ +export const HTTP_202_ACCEPTED = 202; +/** The server has successfully fulfilled the request and there is no content to send. */ +export const HTTP_204_NO_CONTENT = 204; + +// Redirection messages +/** The resource has been moved permanently to a new URI. */ +export const HTTP_301_MOVED_PERMANENTLY = 301; +/** The resource resides temporarily under a different URI. */ +export const HTTP_302_FOUND = 302; +/** Indicates that the resource has not been modified since the last request. */ +export const HTTP_304_NOT_MODIFIED = 304; + +// Client error responses +/** The server could not understand the request due to invalid syntax. */ +export const HTTP_400_BAD_REQUEST = 400; +/** The request requires user authentication. */ +export const HTTP_401_UNAUTHORIZED = 401; +/** The server understood the request but refuses to authorize it. */ +export const HTTP_403_FORBIDDEN = 403; +/** The server cannot find the requested resource. */ +export const HTTP_404_NOT_FOUND = 404; +/** The request method is known by the server but is not supported by the target resource. */ +export const HTTP_405_METHOD_NOT_ALLOWED = 405; +/** The request could not be completed due to a conflict with the current state of the resource. */ +export const HTTP_409_CONFLICT = 409; +/** The server understands the content type but was unable to process the contained instructions. */ +export const HTTP_422_UNPROCESSABLE_ENTITY = 422; +/** The user has sent too many requests in a given amount of time. */ +export const HTTP_429_TOO_MANY_REQUESTS = 429; + +// Server error responses +/** The server encountered an unexpected condition that prevented it from fulfilling the request. */ +export const HTTP_500_INTERNAL_SERVER_ERROR = 500; +/** The server does not support the functionality required to fulfill the request. */ +export const HTTP_501_NOT_IMPLEMENTED = 501; +/** The server, while acting as a gateway or proxy, received an invalid response from the upstream server. */ +export const HTTP_502_BAD_GATEWAY = 502; +/** The server is not ready to handle the request, often due to maintenance or overload. */ +export const HTTP_503_SERVICE_UNAVAILABLE = 503; +/** The server is acting as a gateway and cannot get a response in time. */ +export const HTTP_504_GATEWAY_TIMEOUT = 504; + +/** + * A constant list of supported HTTP status codes used by this application. + * + * These constants are grouped by category and used to construct the union type `HttpStatusCode`. + */ +export const validHttpStatusCodes = [ + // Informational + HTTP_100_CONTINUE, + HTTP_101_SWITCHING_PROTOCOLS, + HTTP_102_PROCESSING, + + // Successful + HTTP_200_OK, + HTTP_201_CREATED, + HTTP_202_ACCEPTED, + HTTP_204_NO_CONTENT, + + // Redirection + HTTP_301_MOVED_PERMANENTLY, + HTTP_302_FOUND, + HTTP_304_NOT_MODIFIED, + + // Client Errors + HTTP_400_BAD_REQUEST, + HTTP_401_UNAUTHORIZED, + HTTP_403_FORBIDDEN, + HTTP_404_NOT_FOUND, + HTTP_405_METHOD_NOT_ALLOWED, + HTTP_409_CONFLICT, + HTTP_422_UNPROCESSABLE_ENTITY, + HTTP_429_TOO_MANY_REQUESTS, + + // Server Errors + HTTP_500_INTERNAL_SERVER_ERROR, + HTTP_501_NOT_IMPLEMENTED, + HTTP_502_BAD_GATEWAY, + HTTP_503_SERVICE_UNAVAILABLE, + HTTP_504_GATEWAY_TIMEOUT, +] as const; + +/** + * A constant list of HTTP error codes that are commonly used in the application. + */ +export const validHttpErrorCodes = [ + // Client Errors + HTTP_400_BAD_REQUEST, + HTTP_401_UNAUTHORIZED, + HTTP_403_FORBIDDEN, + HTTP_404_NOT_FOUND, + HTTP_405_METHOD_NOT_ALLOWED, + HTTP_409_CONFLICT, + HTTP_422_UNPROCESSABLE_ENTITY, + HTTP_429_TOO_MANY_REQUESTS, + + // Server Errors + HTTP_500_INTERNAL_SERVER_ERROR, + HTTP_501_NOT_IMPLEMENTED, + HTTP_502_BAD_GATEWAY, + HTTP_503_SERVICE_UNAVAILABLE, + HTTP_504_GATEWAY_TIMEOUT, +] as const; + +/** + * Maps each supported HTTP status code to its standard status message. + * + * Useful for logging, diagnostics, or building custom error responses. + */ +export const HttpStatusTextMap: Record< + typeof validHttpStatusCodes[number], + string +> = { + [HTTP_100_CONTINUE]: 'Continue', + [HTTP_101_SWITCHING_PROTOCOLS]: 'Switching Protocols', + [HTTP_102_PROCESSING]: 'Processing', + + [HTTP_200_OK]: 'OK', + [HTTP_201_CREATED]: 'Created', + [HTTP_202_ACCEPTED]: 'Accepted', + [HTTP_204_NO_CONTENT]: 'No Content', + + [HTTP_301_MOVED_PERMANENTLY]: 'Moved Permanently', + [HTTP_302_FOUND]: 'Found', + [HTTP_304_NOT_MODIFIED]: 'Not Modified', + + [HTTP_400_BAD_REQUEST]: 'Bad Request', + [HTTP_401_UNAUTHORIZED]: 'Unauthorized', + [HTTP_403_FORBIDDEN]: 'Forbidden', + [HTTP_404_NOT_FOUND]: 'Not Found', + [HTTP_405_METHOD_NOT_ALLOWED]: 'Method Not Allowed', + [HTTP_409_CONFLICT]: 'Conflict', + [HTTP_422_UNPROCESSABLE_ENTITY]: 'Unprocessable Entity', + [HTTP_429_TOO_MANY_REQUESTS]: 'Too Many Requests', + + [HTTP_500_INTERNAL_SERVER_ERROR]: 'Internal Server Error', + [HTTP_501_NOT_IMPLEMENTED]: 'Not Implemented', + [HTTP_502_BAD_GATEWAY]: 'Bad Gateway', + [HTTP_503_SERVICE_UNAVAILABLE]: 'Service Unavailable', + [HTTP_504_GATEWAY_TIMEOUT]: 'Gateway Timeout', +}; + +/** + * A union type representing commonly used HTTP status codes. + * + * This type ensures consistency between runtime and type-level status code handling. + * + * Example: + * ```ts + * const status: HttpStatusCode = 404; // βœ… valid + * const status: HttpStatusCode = 418; // ❌ Type error (unless added to list) + * ``` + */ +export type HttpStatusCode = typeof validHttpStatusCodes[number]; + +/** + * Type guard to check whether a given value is a valid HTTP status code. + * + * This is useful for validating numeric values received from external input, + * ensuring they conform to known HTTP semantics. + * + * Example: + * ```ts + * if (isHttpStatusCode(value)) { + * // value is now typed as HttpStatusCode + * } + * ``` + * + * @param value - The numeric value to check. + * @returns `true` if the value is a recognized HTTP status code, otherwise `false`. + */ +export function isHttpStatusCode(value: unknown): value is HttpStatusCode { + return typeof value === 'number' && + validHttpStatusCodes.includes(value as HttpStatusCode); +} diff --git a/v0.2.0/src/Types/Middleware.ts b/v0.2.0/src/Types/Middleware.ts new file mode 100644 index 0000000..08e13bb --- /dev/null +++ b/v0.2.0/src/Types/Middleware.ts @@ -0,0 +1,51 @@ +import type { IContext } from '../Interfaces/IContext.ts'; + +/** + * Represents a middleware function in the HTTP request pipeline. + * + * Middleware is a core mechanism to intercept, observe, or modify the request lifecycle. + * It can be used for tasks such as logging, authentication, input validation, + * metrics collection, or response transformation. + * + * Each middleware receives a fully-typed request context and a `next()` function + * to invoke the next stage of the pipeline. Middleware may choose to short-circuit + * the pipeline by returning a `Response` early. + * + * @template TContext The specific context type for this middleware, including state, params, and query information. + */ +type Middleware = ( + ctx: TContext, + next: () => Promise, +) => Promise; + +/** + * Represents a middleware function with an associated name. + * + * This is useful for debugging, logging, or when you need to reference + * the middleware by name in your application. + * + * @template TContext The specific context type for this middleware, including state, params, and query information. + */ +type NamedMiddleware = + & Middleware + & { name?: string }; + +export type { NamedMiddleware as Middleware }; + +/** + * Type guard to verify whether a given value is a valid `IMiddleware` function. + * + * This guard checks whether the input is a function that accepts exactly two arguments. + * Note: This is a structural check and cannot fully guarantee the semantics of a middleware. + * + * @param value - The value to test. + * @returns `true` if the value is structurally a valid middleware function. + */ +export function isMiddleware( + value: unknown, +): value is Middleware { + return ( + typeof value === 'function' && + value.length === 2 // ctx, next + ); +} diff --git a/v0.2.0/src/Types/Params.ts b/v0.2.0/src/Types/Params.ts new file mode 100644 index 0000000..045e056 --- /dev/null +++ b/v0.2.0/src/Types/Params.ts @@ -0,0 +1,10 @@ +/** + * Represents route parameters parsed from dynamic segments in the URL path. + * + * This type is typically derived from route definitions with placeholders, + * such as `/users/:id`, which would yield `{ id: "123" }`. + * + * All values are strings and should be considered read-only, as they are + * extracted by the router and should not be modified by application code. + */ +export type Params = Record; diff --git a/v0.2.0/src/Types/Query.ts b/v0.2.0/src/Types/Query.ts new file mode 100644 index 0000000..b9453fe --- /dev/null +++ b/v0.2.0/src/Types/Query.ts @@ -0,0 +1,12 @@ +/** + * Represents the parsed query parameters from the request URL. + * + * Query parameters originate from the URL search string (e.g. `?filter=active&tags=ts&tags=deno`) + * and may contain single or multiple values per key. + * + * All values are expressed as strings or arrays of strings, depending on how often + * the key occurs. This structure preserves the raw semantics of the query. + * + * For normalized single-value access, prefer custom DTOs or wrapper utilities. + */ +export type Query = Record; diff --git a/v0.2.0/src/Types/RegisterRoute.ts b/v0.2.0/src/Types/RegisterRoute.ts new file mode 100644 index 0000000..c661e93 --- /dev/null +++ b/v0.2.0/src/Types/RegisterRoute.ts @@ -0,0 +1,16 @@ +import type { IContext } from '../Interfaces/IContext.ts'; +import type { IInternalRoute } from '../Interfaces/mod.ts'; + +/** + * A type alias for the internal route registration function used by the `HttpKernel`. + * + * This function accepts a fully constructed internal route, including method, matcher, + * middleware chain, and final handler, and registers it for dispatching. + * + * Typically passed into `RouteBuilder` instances to enable fluent API chaining. + * + * @template TContext The context type associated with the route being registered. + */ +export type RegisterRoute = ( + route: IInternalRoute, +) => void; diff --git a/v0.2.0/src/Types/ResponseDecorator.ts b/v0.2.0/src/Types/ResponseDecorator.ts new file mode 100644 index 0000000..4291a6d --- /dev/null +++ b/v0.2.0/src/Types/ResponseDecorator.ts @@ -0,0 +1,30 @@ +import type { IContext } from '../Interfaces/mod.ts'; + +/** + * A function that modifies or enriches an outgoing HTTP response before it is returned to the client. + * + * This decorator can be used to inject headers (e.g., CORS, security), apply global transformations, + * or wrap responses for logging, analytics, or debugging purposes. + * + * It is called exactly once at the end of the middleware/handler pipeline, + * allowing central response customization without interfering with business logic. + * + * @param res - The original `Response` object produced by the route handler or middleware chain. + * @returns A modified or wrapped `Response` object to be sent back to the client. + * + * @example + * ```ts + * const addCors: ResponseDecorator = (res) => { + * const headers = new Headers(res.headers); + * headers.set("Access-Control-Allow-Origin", "*"); + * return new Response(res.body, { + * status: res.status, + * headers, + * }); + * }; + * ``` + */ +export type ResponseDecorator = ( + res: Response, + ctx: TContext, +) => Response; diff --git a/v0.2.0/src/Types/State.ts b/v0.2.0/src/Types/State.ts new file mode 100644 index 0000000..b512d9e --- /dev/null +++ b/v0.2.0/src/Types/State.ts @@ -0,0 +1,9 @@ +/** + * Represents the per-request state object shared across the middleware pipeline. + * + * This type defines the base structure for custom state definitions, + * which can be extended with concrete fields like user data, request metadata, etc. + * + * Custom `TState` types must extend this base to ensure compatibility. + */ +export type State = Record; diff --git a/v0.2.0/src/Types/__tests__/HttpMethod.test.ts b/v0.2.0/src/Types/__tests__/HttpMethod.test.ts new file mode 100644 index 0000000..9559d79 --- /dev/null +++ b/v0.2.0/src/Types/__tests__/HttpMethod.test.ts @@ -0,0 +1,40 @@ +import { assertEquals } from 'https://deno.land/std/assert/mod.ts'; +import { isHttpMethod, validHttpMethods } from '../HttpMethod.ts'; + +Deno.test('isHttpMethod: returns true for all valid methods', () => { + for (const method of validHttpMethods) { + const result = isHttpMethod(method); + assertEquals(result, true, `Expected "${method}" to be valid`); + } +}); + +Deno.test('isHttpMethod: returns false for lowercase or unknown strings', () => { + const invalid = [ + 'get', + 'post', + 'FETCH', + 'TRACE', + 'CONNECT', + 'INVALID', + '', + ' ', + ]; + + for (const method of invalid) { + const result = isHttpMethod(method); + assertEquals(result, false, `Expected "${method}" to be invalid`); + } +}); + +Deno.test('isHttpMethod: returns false for non-string inputs', () => { + const invalidInputs = [null, undefined, 123, {}, [], true, Symbol('GET')]; + + for (const input of invalidInputs) { + const result = isHttpMethod(input); + assertEquals( + result, + false, + `Expected non-string input to be invalid: ${String(input)}`, + ); + } +}); diff --git a/v0.2.0/src/Types/__tests__/HttpStatusCode.test.ts b/v0.2.0/src/Types/__tests__/HttpStatusCode.test.ts new file mode 100644 index 0000000..3042ff1 --- /dev/null +++ b/v0.2.0/src/Types/__tests__/HttpStatusCode.test.ts @@ -0,0 +1,35 @@ +// src/Types/__tests__/HttpStatusCode.test.ts +import { assertEquals } from 'https://deno.land/std@0.204.0/assert/mod.ts'; +import { isHttpStatusCode, validHttpStatusCodes } from '../HttpStatusCode.ts'; + +Deno.test('isHttpStatusCode: returns true for all valid status codes', () => { + for (const code of validHttpStatusCodes) { + assertEquals( + isHttpStatusCode(code), + true, + `Expected ${code} to be valid`, + ); + } +}); + +Deno.test('isHttpStatusCode: returns false for invalid status codes', () => { + const invalidInputs = [99, 600, 1234, -1, 0, 999]; + for (const val of invalidInputs) { + assertEquals( + isHttpStatusCode(val), + false, + `Expected ${val} to be invalid`, + ); + } +}); + +Deno.test('isHttpStatusCode: returns false for non-numeric values', () => { + const invalid = ['200', null, undefined, {}, [], true]; + for (const val of invalid) { + assertEquals( + isHttpStatusCode(val), + false, + `Expected ${val} to be invalid`, + ); + } +}); diff --git a/v0.2.0/src/Types/mod.ts b/v0.2.0/src/Types/mod.ts new file mode 100644 index 0000000..c159495 --- /dev/null +++ b/v0.2.0/src/Types/mod.ts @@ -0,0 +1,45 @@ +// deno-coverage-ignore-file + +export type { DeepPartial } from './DeepPartial.ts'; +export { isHandler } from './Handler.ts'; +export type { Handler } from './Handler.ts'; +export type { HttpErrorHandler } from './HttpErrorHandler.ts'; +export { isHttpMethod, validHttpMethods } from './HttpMethod.ts'; +export type { HttpMethod } from './HttpMethod.ts'; +export { + HTTP_100_CONTINUE, + HTTP_101_SWITCHING_PROTOCOLS, + HTTP_102_PROCESSING, + HTTP_200_OK, + HTTP_201_CREATED, + HTTP_202_ACCEPTED, + HTTP_204_NO_CONTENT, + HTTP_301_MOVED_PERMANENTLY, + HTTP_302_FOUND, + HTTP_304_NOT_MODIFIED, + HTTP_400_BAD_REQUEST, + HTTP_401_UNAUTHORIZED, + HTTP_403_FORBIDDEN, + HTTP_404_NOT_FOUND, + HTTP_405_METHOD_NOT_ALLOWED, + HTTP_409_CONFLICT, + HTTP_422_UNPROCESSABLE_ENTITY, + HTTP_429_TOO_MANY_REQUESTS, + HTTP_500_INTERNAL_SERVER_ERROR, + HTTP_501_NOT_IMPLEMENTED, + HTTP_502_BAD_GATEWAY, + HTTP_503_SERVICE_UNAVAILABLE, + HTTP_504_GATEWAY_TIMEOUT, + HttpStatusTextMap, + isHttpStatusCode, + validHttpErrorCodes, + validHttpStatusCodes, +} from './HttpStatusCode.ts'; +export type { HttpStatusCode } from './HttpStatusCode.ts'; +export { isMiddleware } from './Middleware.ts'; +export type { Middleware } from './Middleware.ts'; +export type { Params } from './Params.ts'; +export type { Query } from './Query.ts'; +export type { RegisterRoute } from './RegisterRoute.ts'; +export type { ResponseDecorator } from './ResponseDecorator.ts'; +export type { State } from './State.ts'; diff --git a/v0.2.0/src/Utils/__tests__/createEmptyContext.test.ts b/v0.2.0/src/Utils/__tests__/createEmptyContext.test.ts new file mode 100644 index 0000000..2b3acfe --- /dev/null +++ b/v0.2.0/src/Utils/__tests__/createEmptyContext.test.ts @@ -0,0 +1,28 @@ +import { assertEquals } from 'https://deno.land/std/assert/mod.ts'; +import { createEmptyContext } from '../createEmptyContext.ts'; +import type { IContext } from '../../Interfaces/mod.ts'; + +Deno.test('createEmptyContext: returns default-initialized context', () => { + const request = new Request('http://localhost'); + const ctx = createEmptyContext(request); + + assertEquals(ctx.req, request); + assertEquals(ctx.params, {}); + assertEquals(ctx.query, {}); + assertEquals(ctx.state, {}); +}); + +Deno.test('createEmptyContext: preserves generic type compatibility', () => { + interface MyContext + extends + IContext<{ userId: string }, { id: string }, { verbose: string }> {} + + const req = new Request('http://localhost'); + const ctx = createEmptyContext(req); + + // All properties exist and are empty + assertEquals(ctx.params, {} as MyContext['params']); + assertEquals(ctx.query, {} as MyContext['query']); + assertEquals(ctx.state, {} as MyContext['state']); + assertEquals(ctx.req, req); +}); diff --git a/v0.2.0/src/Utils/__tests__/createRouteMatcher.test.ts b/v0.2.0/src/Utils/__tests__/createRouteMatcher.test.ts new file mode 100644 index 0000000..8338014 --- /dev/null +++ b/v0.2.0/src/Utils/__tests__/createRouteMatcher.test.ts @@ -0,0 +1,118 @@ +import { + assert, + assertEquals, + assertStrictEquals, +} from 'https://deno.land/std/assert/mod.ts'; +import type { IRouteDefinition } from '../../Interfaces/mod.ts'; +import { createRouteMatcher } from '../../mod.ts'; + +// Dummy request +const dummyRequest = new Request('http://localhost'); + +Deno.test('createRouteMatcher: static route matches and extracts params', () => { + const def: IRouteDefinition = { method: 'GET', path: '/users/:id' }; + const matcher = createRouteMatcher(def); + + const result = matcher(new URL('http://localhost/users/42'), dummyRequest); + + assert(result); + assertEquals(result.params, { id: '42' }); +}); + +Deno.test('createRouteMatcher: static route with multiple params', () => { + const def: IRouteDefinition = { method: 'GET', path: '/repo/:owner/:name' }; + const matcher = createRouteMatcher(def); + + const result = matcher( + new URL('http://localhost/repo/max/wiki'), + dummyRequest, + ); + + assert(result); + assertEquals(result.params, { owner: 'max', name: 'wiki' }); +}); + +Deno.test('createRouteMatcher: static route does not match wrong path', () => { + const def: IRouteDefinition = { method: 'GET', path: '/users/:id' }; + const matcher = createRouteMatcher(def); + + const result = matcher(new URL('http://localhost/posts/42'), dummyRequest); + + assertStrictEquals(result, null); +}); + +Deno.test('createRouteMatcher: uses custom matcher if provided', () => { + const def: IRouteDefinition = { + method: 'GET', + matcher: (url) => url.pathname === '/ping' ? { params: {} } : null, + }; + const matcher = createRouteMatcher(def); + + const result = matcher(new URL('http://localhost/ping'), dummyRequest); + assert(result); + assertEquals(result.params, {}); +}); + +Deno.test('createRouteMatcher: extracts single query param', () => { + const def: IRouteDefinition = { method: 'GET', path: '/search' }; + const matcher = createRouteMatcher(def); + + const url = new URL('http://localhost/search?q=deno'); + const result = matcher(url, dummyRequest); + + assert(result); + assertEquals(result.params, {}); // no path params + assertEquals(result.query, { q: 'deno' }); // single key β†’ string +}); + +Deno.test('createRouteMatcher: duplicate query keys become array', () => { + const def: IRouteDefinition = { method: 'GET', path: '/tags' }; + const matcher = createRouteMatcher(def); + + const url = new URL('http://localhost/tags?tag=js&tag=ts&tag=deno'); + const result = matcher(url, dummyRequest); + + assert(result); + assertEquals(result.params, {}); + assertEquals(result.query, { tag: ['js', 'ts', 'deno'] }); // multi β†’ string[] +}); + +Deno.test('createRouteMatcher: mix of single and duplicate keys', () => { + const def: IRouteDefinition = { method: 'GET', path: '/filter/:type' }; + const matcher = createRouteMatcher(def); + + const url = new URL('http://localhost/filter/repo?lang=ts&lang=js&page=2'); + const result = matcher(url, dummyRequest); + + assert(result); + assertEquals(result.params, { type: 'repo' }); + assertEquals(result.query, { + lang: ['ts', 'js'], // duplicated + page: '2', // single + }); +}); + +Deno.test('createRouteMatcher: no query parameters returns empty object', () => { + const def: IRouteDefinition = { method: 'GET', path: '/info' }; + const matcher = createRouteMatcher(def); + + const url = new URL('http://localhost/info'); + const result = matcher(url, dummyRequest); + + assert(result); + assertEquals(result.params, {}); + assertEquals(result.query, {}); // empty +}); + +Deno.test('createRouteMatcher: retains array order of duplicate keys', () => { + const def: IRouteDefinition = { method: 'GET', path: '/order' }; + const matcher = createRouteMatcher(def); + + const url = new URL( + 'http://localhost/order?item=first&item=second&item=third', + ); + const result = matcher(url, dummyRequest); + + assert(result); + assertEquals(result.query?.item, ['first', 'second', 'third']); +}); diff --git a/v0.2.0/src/Utils/__tests__/normalizeError.test.ts b/v0.2.0/src/Utils/__tests__/normalizeError.test.ts new file mode 100644 index 0000000..0dbb8d9 --- /dev/null +++ b/v0.2.0/src/Utils/__tests__/normalizeError.test.ts @@ -0,0 +1,35 @@ +import { + assertEquals, + assertInstanceOf, +} from 'https://deno.land/std/assert/mod.ts'; +import { normalizeError } from '../normalizeError.ts'; + +Deno.test('normalizeError: preserves Error instances', () => { + const original = new Error('original'); + const result = normalizeError(original); + + assertInstanceOf(result, Error); + assertEquals(result, original); +}); + +Deno.test('normalizeError: converts string to Error', () => { + const result = normalizeError('something went wrong'); + + assertInstanceOf(result, Error); + assertEquals(result.message, 'something went wrong'); +}); + +Deno.test('normalizeError: converts number to Error', () => { + const result = normalizeError(404); + + assertInstanceOf(result, Error); + assertEquals(result.message, '404'); +}); + +Deno.test('normalizeError: converts plain object to Error', () => { + const input = { error: true, msg: 'Invalid' }; + const result = normalizeError(input); + + assertInstanceOf(result, Error); + assertEquals(result.message, JSON.stringify(input)); +}); diff --git a/v0.2.0/src/Utils/createEmptyContext.ts b/v0.2.0/src/Utils/createEmptyContext.ts new file mode 100644 index 0000000..18113dd --- /dev/null +++ b/v0.2.0/src/Utils/createEmptyContext.ts @@ -0,0 +1,30 @@ +import type { IContext } from '../Interfaces/mod.ts'; +import type { Params, Query, State } from '../Types/mod.ts'; + +/** + * Creates an empty request context suitable for fallback handlers (e.g., 404 or 500 errors). + * + * This function is primarily intended for cases where no route matched, but a context-compatible + * object is still needed to invoke a generic error handler. All context fields are initialized + * to their default empty values (`{}` for params, query, and state). + * + * @template TContext - The expected context type, typically extending `IContext`. + * @param req - The original HTTP request object from `Deno.serve()`. + * @returns A minimal context object compatible with `TContext`. + * + * @example + * ```ts + * const ctx = createEmptyContext(request); + * return httpErrorHandlers[404](ctx); + * ``` + */ +export function createEmptyContext( + req: Request, +): TContext { + return { + req, + params: {} as Params, + query: {} as Query, + state: {} as State, + } as TContext; +} diff --git a/v0.2.0/src/Utils/createRouteMatcher.ts b/v0.2.0/src/Utils/createRouteMatcher.ts new file mode 100644 index 0000000..dce9185 --- /dev/null +++ b/v0.2.0/src/Utils/createRouteMatcher.ts @@ -0,0 +1,54 @@ +// createRouteMatcher.ts + +import { + type IRouteDefinition, + type IRouteMatch, + type IRouteMatcher, + isDynamicRouteDefinition, +} from '../Interfaces/mod.ts'; +import type { Params, Query } from '../Types/mod.ts'; + +/** + * Transforms a route definition into a matcher using Deno's URLPattern API. + * + * @param def - Static path pattern or custom matcher. + * @returns IRouteMatcher that returns `{ params, query }` or `null`. + */ +export function createRouteMatcher( + def: IRouteDefinition, +): IRouteMatcher { + // 1. Allow users to provide their own matcher + if (isDynamicRouteDefinition(def)) { + return def.matcher; + } + + // 2. Build URLPattern; supports :id, *wildcards, regex groups, etc. + const pattern = new URLPattern({ pathname: def.path }); + + // 3. The actual matcher closure + return (url: URL): IRouteMatch | null => { + const result = pattern.exec(url); + + // 3a. Path did not match + if (!result) return null; + + // 3b. Extract route params + const params: Params = {}; + for (const [key, value] of Object.entries(result.pathname.groups)) { + if (value) { + params[key] = value; + } + } + + // 3c. Extract query parameters – keep duplicates as arrays + const query: Query = {}; + for (const key of url.searchParams.keys()) { + const values = url.searchParams.getAll(key); // β†’ string[] + query[key] = values.length === 1 + ? values[0] // single β†’ "foo" + : values; // multi β†’ ["foo","bar"] + } + + return { params, query }; + }; +} diff --git a/v0.2.0/src/Utils/mod.ts b/v0.2.0/src/Utils/mod.ts new file mode 100644 index 0000000..2e21a1d --- /dev/null +++ b/v0.2.0/src/Utils/mod.ts @@ -0,0 +1,5 @@ +// deno-coverage-ignore-file + +export { createEmptyContext } from './createEmptyContext.ts'; +export { createRouteMatcher } from './createRouteMatcher.ts'; +export { normalizeError } from './normalizeError.ts'; diff --git a/v0.2.0/src/Utils/normalizeError.ts b/v0.2.0/src/Utils/normalizeError.ts new file mode 100644 index 0000000..71fac86 --- /dev/null +++ b/v0.2.0/src/Utils/normalizeError.ts @@ -0,0 +1,30 @@ +/** + * Normalizes any thrown value to a proper `Error` instance. + * + * This is useful when handling unknown thrown values that may be: + * - strings (e.g. `throw "oops"`) + * - numbers (e.g. `throw 404`) + * - objects that are not instances of `Error` + * + * Ensures that downstream error handling logic always receives a consistent `Error` object. + * + * @param unknownError - Any value that might have been thrown. + * @returns A valid `Error` instance wrapping the original input. + * + * @example + * ```ts + * try { + * throw "something went wrong"; + * } catch (e) { + * const err = normalizeError(e); + * console.error(err.message); // "something went wrong" + * } + * ``` + */ +export function normalizeError(unknownError: unknown): Error { + return unknownError instanceof Error ? unknownError : new Error( + typeof unknownError === 'string' + ? unknownError + : JSON.stringify(unknownError), + ); +} diff --git a/v0.2.0/src/__bench__/HttpKernel.bench.ts b/v0.2.0/src/__bench__/HttpKernel.bench.ts new file mode 100644 index 0000000..d08a4bf --- /dev/null +++ b/v0.2.0/src/__bench__/HttpKernel.bench.ts @@ -0,0 +1,87 @@ +import type { IRouteDefinition } from '../Interfaces/mod.ts'; +import { HttpKernel } from '../mod.ts'; + +const CONCURRENT_REQUESTS = 10000; + +// Deno.bench('Simple request', async (b) => { +// const kernel = new HttpKernel(); + +// const def: IRouteDefinition = { method: 'GET', path: '/hello' }; +// kernel.route(def).handle((_ctx) => { +// return Promise.resolve(new Response('OK', { status: 200 })); +// }); +// b.start(); +// await kernel.handle( +// new Request('http://localhost/hello', { method: 'GET' }), +// ); +// b.end(); +// }); + +Deno.bench('Simple request (parallel)', async (b) => { + const kernel = new HttpKernel(); + + const def: IRouteDefinition = { method: 'GET', path: '/hello' }; + kernel.route(def).handle((_ctx) => { + return Promise.resolve(new Response('OK', { status: 200 })); + }); + + const requests = Array.from( + { length: CONCURRENT_REQUESTS }, + () => + kernel.handle( + new Request('http://localhost/hello', { method: 'GET' }), + ), + ); + + b.start(); + await Promise.all(requests); + b.end(); +}); + +// Deno.bench('Complex request', async (b) => { +// const kernel = new HttpKernel(); + +// kernel.route({ method: 'GET', path: '/test' }) +// .middleware(async (_ctx, next) => { +// return await next(); +// }) +// .middleware(async (_ctx, next) => { +// return await next(); +// }) +// .handle((_ctx) => { +// return Promise.resolve(new Response('done')); +// }); + +// b.start(); +// await kernel.handle( +// new Request('http://localhost/test', { method: 'GET' }), +// ); +// b.end(); +// }); + +Deno.bench('Complex request (parallel)', async (b) => { + const kernel = new HttpKernel(); + + kernel.route({ method: 'GET', path: '/test' }) + .middleware(async (_ctx, next) => { + return await next(); + }) + .middleware(async (_ctx, next) => { + return await next(); + }) + .handle((_ctx) => { + return Promise.resolve(new Response('done')); + }); + + const requests = Array.from( + { length: CONCURRENT_REQUESTS }, + () => + kernel.handle( + new Request('http://localhost/test', { method: 'GET' }), + ), + ); + + b.start(); + await Promise.all(requests); + b.end(); +}); diff --git a/v0.2.0/src/__tests__/HttpKernel.test.ts b/v0.2.0/src/__tests__/HttpKernel.test.ts new file mode 100644 index 0000000..2348046 --- /dev/null +++ b/v0.2.0/src/__tests__/HttpKernel.test.ts @@ -0,0 +1,185 @@ +import { + assertEquals, + assertThrows, +} from 'https://deno.land/std@0.204.0/assert/mod.ts'; +import { HttpKernel } from '../HttpKernel.ts'; +import type { IRouteDefinition } from '../Interfaces/mod.ts'; + +Deno.test('HttpKernel: matches static route and executes handler', async () => { + const kernel = new HttpKernel(); + + const def: IRouteDefinition = { method: 'GET', path: '/hello' }; + let called = false; + + kernel.route(def).handle((_ctx) => { + called = true; + return Promise.resolve(new Response('OK', { status: 200 })); + }); + + const res = await kernel.handle( + new Request('http://localhost/hello', { method: 'GET' }), + ); + assertEquals(res.status, 200); + assertEquals(await res.text(), 'OK'); + assertEquals(called, true); +}); + +Deno.test('HttpKernel: supports dynamic matcher', async () => { + const kernel = new HttpKernel(); + const def: IRouteDefinition = { + method: 'GET', + matcher: (url) => url.pathname === '/dyn' ? { params: {} } : null, + }; + + kernel.route(def).handle((_ctx) => + Promise.resolve(new Response('Dyn', { status: 200 })) + ); + + const res = await kernel.handle(new Request('http://localhost/dyn')); + assertEquals(res.status, 200); + assertEquals(await res.text(), 'Dyn'); +}); + +Deno.test('HttpKernel: calls middleware in order and passes to handler', async () => { + const kernel = new HttpKernel(); + const calls: string[] = []; + + kernel.route({ method: 'GET', path: '/test' }) + .middleware(async (_ctx, next) => { + calls.push('mw1'); + return await next(); + }) + .middleware(async (_ctx, next) => { + calls.push('mw2'); + return await next(); + }) + .handle((_ctx) => { + calls.push('handler'); + return Promise.resolve(new Response('done')); + }); + + const res = await kernel.handle( + new Request('http://localhost/test', { method: 'GET' }), + ); + assertEquals(await res.text(), 'done'); + assertEquals(calls, ['mw1', 'mw2', 'handler']); +}); + +Deno.test('HttpKernel: middleware short-circuits pipeline', async () => { + const kernel = new HttpKernel(); + const calls: string[] = []; + + kernel.route({ method: 'GET', path: '/stop' }) + .middleware((_ctx, _next) => { + calls.push('mw1'); + return Promise.resolve(new Response('blocked', { status: 403 })); + }) + .middleware((_ctx, _next) => { + calls.push('mw2'); + return Promise.resolve(new Response('should-not-call')); + }) + .handle((_ctx) => { + calls.push('handler'); + return Promise.resolve(new Response('ok')); + }); + + const res = await kernel.handle( + new Request('http://localhost/stop', { method: 'GET' }), + ); + assertEquals(res.status, 403); + assertEquals(await res.text(), 'blocked'); + assertEquals(calls, ['mw1']); +}); + +Deno.test('HttpKernel: invalid middleware or handler signature throws at compile time', () => { + const kernel = new HttpKernel(); + + // Middleware with wrong signature (missing ctx, next) + assertThrows( + () => { + kernel.route({ method: 'GET', path: '/bad-mw' }) + // @ts-expect-error invalid middleware + .middleware(() => new Response('invalid')) + .handle((_ctx) => Promise.resolve(new Response('ok'))); + }, + TypeError, + 'Middleware at index 0 is not a valid function.', + ); + + // Handler with wrong signature (no ctx) + assertThrows( + () => { + kernel.route({ method: 'GET', path: '/bad-handler' }) + .middleware(async (_ctx, next) => await next()) + // @ts-expect-error invalid handler + .handle(() => new Response('invalid')); + }, + TypeError, + 'Route handler must be a function returning a Promise.', + ); +}); + +Deno.test('HttpKernel: 404 for unmatched route', async () => { + const kernel = new HttpKernel(); + const res = await kernel.handle(new Request('http://localhost/nothing')); + assertEquals(res.status, 404); +}); + +Deno.test('HttpKernel: skips route with wrong method', async () => { + const kernel = new HttpKernel(); + + kernel.route({ method: 'POST', path: '/only-post' }) + .handle((_ctx) => Promise.resolve(new Response('nope'))); + + const res = await kernel.handle( + new Request('http://localhost/only-post', { method: 'GET' }), + ); + assertEquals(res.status, 404); +}); + +Deno.test('HttpKernel: throws on next() called twice', async () => { + const kernel = new HttpKernel(); + + kernel.route({ method: 'GET', path: '/bad' }) + .middleware(async (_ctx, next) => { + await next(); + await next(); // ❌ + return new Response('should never reach'); + }) + .handle((_ctx) => Promise.resolve(new Response('OK'))); + + const res = await kernel.handle(new Request('http://localhost/bad')); + assertEquals(res.status, 500); + assertEquals(await res.text(), 'Internal Server Error'); +}); + +Deno.test('HttpKernel: handler throws β†’ error propagates', async () => { + const kernel = new HttpKernel(); + + kernel.route({ method: 'GET', path: '/throw' }) + .handle((_ctx) => { + throw new Error('fail!'); + }); + + const res = await kernel.handle(new Request('http://localhost/throw')); + assertEquals(res.status, 500); + assertEquals(await res.text(), 'Internal Server Error'); +}); + +Deno.test('HttpKernel: returns 500 if no handler or middleware defined', async () => { + const kernel = new HttpKernel(); + + // Force-manual Registrierung mit `handler: undefined` + // Umgehen des Builders zur Simulation dieses Edge-Cases + kernel['routes'].push({ + method: 'GET', + matcher: (url) => url.pathname === '/fail' ? { params: {} } : null, + middlewares: [], + // @ts-expect-error absichtlich ungΓΌltiger Handler + handler: undefined, + }); + + const res = await kernel.handle(new Request('http://localhost/fail')); + assertEquals(res.status, 500); + assertEquals(await res.text(), 'Internal Server Error'); +}); diff --git a/v0.2.0/src/__tests__/RouteBuilder.test.ts b/v0.2.0/src/__tests__/RouteBuilder.test.ts new file mode 100644 index 0000000..d54fdbe --- /dev/null +++ b/v0.2.0/src/__tests__/RouteBuilder.test.ts @@ -0,0 +1,140 @@ +import { + assert, + assertEquals, + assertNotEquals, + assertThrows, +} from 'https://deno.land/std@0.204.0/assert/mod.ts'; +import type { IInternalRoute, IRouteDefinition } from '../Interfaces/mod.ts'; +import { RouteBuilder } from '../mod.ts'; +import type { Handler, Middleware } from '../Types/mod.ts'; + +// Dummy objects +// deno-lint-ignore require-await +const dummyHandler: Handler = async (_) => new Response('ok'); +// deno-lint-ignore require-await +const wrongHandler: Handler = async () => new Response('ok'); // Wrong signature, no ctx +const dummyMiddleware: Middleware = async (_, next) => await next(); +// deno-lint-ignore require-await +const wrongMiddleware: Middleware = async () => new Response('ok'); // Wrong signature, no ctx, next +const dummyDef: IRouteDefinition = { method: 'GET', path: '/hello' }; +const dummyMatcher = () => ({ params: {} }); + +Deno.test('middleware: throws if middleware signature is wrong', () => { + const builder = new RouteBuilder(() => {}, dummyDef); + assertThrows( + () => builder.middleware(wrongMiddleware).handle(dummyHandler), + TypeError, + 'Middleware at index 0 is not a valid function.', + ); +}); + +Deno.test('middleware: single middleware is registered correctly', () => { + let registered: IInternalRoute | null = null as IInternalRoute | null; + + const builder = new RouteBuilder((r) => registered = r, dummyDef) + .middleware(dummyMiddleware); + + builder.handle(dummyHandler); + + assert(registered); + assertEquals(registered?.middlewares.length, 1); + assertEquals(registered?.middlewares[0], dummyMiddleware); +}); + +Deno.test('middleware: middleware is chained immutably', () => { + const builder1 = new RouteBuilder(() => {}, dummyDef); + const builder2 = builder1.middleware(dummyMiddleware); + + assertNotEquals(builder1, builder2); +}); + +Deno.test('middleware: preserves order of middleware', () => { + const mw1: Middleware = async (_, next) => await next(); + const mw2: Middleware = async (_, next) => await next(); + + let result: IInternalRoute | null = null as IInternalRoute | null; + + const builder = new RouteBuilder((r) => result = r, dummyDef) + .middleware(mw1) + .middleware(mw2); + + builder.handle(dummyHandler); + + assert(result); + assertEquals(result!.middlewares, [mw1, mw2]); +}); + +Deno.test('handle: throws if handler signature is wrong', () => { + const builder = new RouteBuilder(() => {}, dummyDef); + assertThrows( + () => builder.handle(wrongHandler), + TypeError, + 'Route handler must be a function returning a Promise.', + ); +}); + +Deno.test('handle: uppercases method', () => { + let result: IInternalRoute | null = null as IInternalRoute | null; + + new RouteBuilder((r) => result = r, { method: 'POST', path: '/x' }) + .handle(dummyHandler); + + assertEquals(result?.method, 'POST'); +}); + +Deno.test('handle: works with no middleware', async () => { + let route: IInternalRoute | null = null as IInternalRoute | null; + + const builder = new RouteBuilder((r) => route = r, dummyDef); + builder.handle(dummyHandler); + + assert(route); + assertEquals(route?.middlewares.length, 0); + + const request = new Request('http://localhost'); + + const res1 = await route?.handler({ + req: request, + params: {}, + state: {}, + query: {}, + }); + const res2 = await dummyHandler({ + req: request, + params: {}, + state: {}, + query: {}, + }); + + assertEquals(res1?.status, res2?.status); + assertEquals(await res1?.text(), await res2?.text()); +}); + +Deno.test('handle: uses custom matcher factory', () => { + let called = false; + + const factory = (_def: IRouteDefinition) => { + called = true; + return dummyMatcher; + }; + + let route: IInternalRoute | null = null as IInternalRoute | null; + + new RouteBuilder((r) => route = r, dummyDef, [], factory).handle( + dummyHandler, + ); + + assert(called); + assert(route); + assertEquals(route!.matcher, dummyMatcher); +}); + +Deno.test('handle: throws if matcher factory throws', () => { + const faultyFactory = () => { + throw new Error('matcher fail'); + }; + + const builder = new RouteBuilder(() => {}, dummyDef, [], faultyFactory); + + assertThrows(() => builder.handle(dummyHandler), Error, 'matcher fail'); +}); diff --git a/v0.2.0/src/mod.ts b/v0.2.0/src/mod.ts new file mode 100644 index 0000000..0754746 --- /dev/null +++ b/v0.2.0/src/mod.ts @@ -0,0 +1,4 @@ +// deno-coverage-ignore-file +export { HttpKernel } from './HttpKernel.ts'; +export { RouteBuilder } from './RouteBuilder.ts'; +export { createRouteMatcher } from './Utils/createRouteMatcher.ts'; diff --git a/v0.2.1/CHANGELOG.md b/v0.2.1/CHANGELOG.md new file mode 100644 index 0000000..9dcd577 --- /dev/null +++ b/v0.2.1/CHANGELOG.md @@ -0,0 +1,107 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [0.2.1](https://git.0xmax42.io/maxp/http-kernel/compare/v0.2.0..v0.2.1) - 2025-11-12 + +### πŸš€ Features + +- Export errors, interfaces, types, and utils from main module - ([6d7127a](https://git.0xmax42.io/maxp/http-kernel/commit/6d7127a52f4aecfd178523c8a873ab0b558550f1)) + +### πŸ› Bug Fixes + +- *(workflows)* Remove redundant tag fallback in sync job - ([5686940](https://git.0xmax42.io/maxp/http-kernel/commit/5686940fe26b699bffa62af7fb0efc42cc85a6b3)) + +### βš™οΈ Miscellaneous Tasks + +- *(workflows)* Update release workflow for consistency - ([f177746](https://git.0xmax42.io/maxp/http-kernel/commit/f1777467607874f6bc83e1d7e37433298e25607c)) + +## [0.2.0](https://git.0xmax42.io/maxp/http-kernel/compare/v0.1.0..v0.2.0) - 2025-05-27 + +### πŸš€ Features + +- *(workflows)* Add GitHub release sync workflow - ([de6d3ee](https://git.0xmax42.io/maxp/http-kernel/commit/de6d3ee389b0d92c5056e47be85da1d0c41f62af)) +- *(ci)* Enhance CI workflow with granular steps and error handling - ([54cfa18](https://git.0xmax42.io/maxp/http-kernel/commit/54cfa1888e13d0872b5411e83d3d45925f2687ee)) +- *(route-builder)* Add middleware chain compilation - ([35d83c0](https://git.0xmax42.io/maxp/http-kernel/commit/35d83c073ef8644d657195c332b463d18e856e18)) +- *(interfaces)* Add runRoute method to IInternalRoute - ([67ebb43](https://git.0xmax42.io/maxp/http-kernel/commit/67ebb4307a2a1c588b78f8f0c498d1a4276ad09b)) +- *(workflows)* Conditionally generate changelog - ([b44bb2d](https://git.0xmax42.io/maxp/http-kernel/commit/b44bb2ddafe99c85b25229d2c4a0dfeacf750052)) +- *(workflows)* Add CI for Deno project tests - ([9d5db4f](https://git.0xmax42.io/maxp/http-kernel/commit/9d5db4f414cf961248f2b879f2b132b81a32cb92)) + +### πŸ› Bug Fixes + +- *(workflows)* Ensure version detection output is always set - ([3707242](https://git.0xmax42.io/maxp/http-kernel/commit/3707242d278e15c55a41056bb64810f6824d24b3)) + +### 🚜 Refactor + +- *(kernel)* Simplify middleware and handler execution - ([aea3fb4](https://git.0xmax42.io/maxp/http-kernel/commit/aea3fb45e7c099a38440c85783747e80fca54ba6)) +- *(imports)* Use explicit type-only imports across codebase - ([b83aa33](https://git.0xmax42.io/maxp/http-kernel/commit/b83aa330b34523e5102ab98ee61dedbbd62d4656)) +- *(workflows)* Rename changelog file for consistency - ([b9d25f2](https://git.0xmax42.io/maxp/http-kernel/commit/b9d25f23fc6ad7696deee319024aa5b1af4d98c0)) + +### πŸ“š Documentation + +- *(release)* Update guidelines for handling changelog - ([6a0f1c7](https://git.0xmax42.io/maxp/http-kernel/commit/6a0f1c774bc01ab976090612bbc361576feb3942)) +- Add README for HttpKernel project - ([a1ce306](https://git.0xmax42.io/maxp/http-kernel/commit/a1ce30627c68a3f869eb6a104308322af8596dc1)) +- Add MIT license file - ([5118a19](https://git.0xmax42.io/maxp/http-kernel/commit/5118a19aeaa1102591aa7fe093fdec1aa19dc7f5)) + +### πŸ§ͺ Testing + +- *(routebuilder)* Add validation tests for handler and middleware - ([b14e9ac](https://git.0xmax42.io/maxp/http-kernel/commit/b14e9acc5f9617a01886e7734b2ae717b86de03e)) +- *(httpkernel)* Enforce compile-time validation for signatures - ([731bba2](https://git.0xmax42.io/maxp/http-kernel/commit/731bba22d88df077b0a39293ddd1a3eec3bf96e8)) +- *(bench)* Add parallel benchmarks for HTTP kernel - ([3da34e2](https://git.0xmax42.io/maxp/http-kernel/commit/3da34e268426b92510c7f9b730a2fa297dca6fbf)) + +### βš™οΈ Miscellaneous Tasks + +- *(config)* Add CI task for local checks - ([c207dc7](https://git.0xmax42.io/maxp/http-kernel/commit/c207dc7392d9f40e7b7c736eadf6c9c7bbf9b7d4)) +- *(gitignore)* Add .local directory to ignored files - ([1b447f5](https://git.0xmax42.io/maxp/http-kernel/commit/1b447f51900b3a1a7f1be9d5192fd5aba37bdbc4)) +- *(ci)* Update deno tasks in CI workflow - ([38c00b0](https://git.0xmax42.io/maxp/http-kernel/commit/38c00b035bfd05c83d5898c97c9423a653db0840)) +- *(tasks)* Add benchmark, format, and lint commands - ([6e6e616](https://git.0xmax42.io/maxp/http-kernel/commit/6e6e61693fef3b11a81ce260d80bc93edae1e718)) +- *(workflows)* Consolidate and update CI configuration - ([ec1697d](https://git.0xmax42.io/maxp/http-kernel/commit/ec1697df94b5378f1766663e278a41d403a64336)) +- *(config)* Add exports field to module metadata - ([c28eb7f](https://git.0xmax42.io/maxp/http-kernel/commit/c28eb7f28dfaa8d3fdc540c4bcc306a3a8b9d6f8)) +- *(git)* Ignore merge conflicts for CHANGELOG.md - ([6399113](https://git.0xmax42.io/maxp/http-kernel/commit/6399113e122e1207ebf4113aebd250358e31f461)) +- *(workflows)* Refine branch handling in release process - ([71ea424](https://git.0xmax42.io/maxp/http-kernel/commit/71ea4247b35dc4afe5090d3c6502bfa936b5a947)) +- *(workflows)* Update changelog file extension to .md and revert b9d25f23fc - ([a88b4d1](https://git.0xmax42.io/maxp/http-kernel/commit/a88b4d112f5c07664d41f6e9d03246307551f25d)) +- Rename changelog and readme files to use .md extension - ([4f2b650](https://git.0xmax42.io/maxp/http-kernel/commit/4f2b65049f461ef377e7231905fd066cbc3c7fe0)) +- *(workflows)* Update test workflow for http-kernel project - ([0311546](https://git.0xmax42.io/maxp/http-kernel/commit/03115464e0fb01b8ca00a2fdabde013d004ae8a2)) +- *(workflows)* Update Deno setup action to v2 - ([1233a0b](https://git.0xmax42.io/maxp/http-kernel/commit/1233a0b7204d12a60f4b7bd1199242a4cb7c4579)) +- *(workflows)* Remove unused workflow_dispatch trigger - ([16c0053](https://git.0xmax42.io/maxp/http-kernel/commit/16c0053964c72d01e5f555ec8f33c9eead160e69)) +- *(tasks)* Remove commented-out start and watch scripts - ([04029f8](https://git.0xmax42.io/maxp/http-kernel/commit/04029f87a3b9dd24e8792b852ead9097e18d23c7)) + +## [0.1.0] - 2025-05-08 + +### πŸš€ Features + +- *(workflows)* Add automated changelog and release workflow - ([bbf78cf](https://git.0xmax42.io/maxp/http-kernel/commit/bbf78cff17be0cae651b8abf3e239103b26354bf)) +- *(vscode)* Customize activity bar and peacock colors - ([56633cd](https://git.0xmax42.io/maxp/http-kernel/commit/56633cd95b37a8b2cfd8eb95982d07cd1f9b5126)) +- *(workflows)* Add upload assets template for releases - ([7b6eb2b](https://git.0xmax42.io/maxp/http-kernel/commit/7b6eb2b57470198684a1dfa8b668351b8b9a91ae)) +- *(config)* Add project metadata and test watch task - ([b009b57](https://git.0xmax42.io/maxp/http-kernel/commit/b009b5763d1824fc94fdc1e3d919fe2597158f84)) +- *(http)* Add error handling for invalid HTTP methods - ([ba7aa79](https://git.0xmax42.io/maxp/http-kernel/commit/ba7aa79f56772213bf73b62bc6bf8810f3871127)) +- *(http)* Enhance type safety and extend route context - ([a236fa7](https://git.0xmax42.io/maxp/http-kernel/commit/a236fa7c97ae49e6baf560d4ca92c6e83702b3ec)) + +### πŸ› Bug Fixes + +- *(params)* Enforce non-undefined route parameter values - ([b0c6901](https://git.0xmax42.io/maxp/http-kernel/commit/b0c6901d7d272ec98b3d00ef2dd2848482892a25)) + +### 🚜 Refactor + +- *(types)* Unify handler and middleware definitions - ([8235680](https://git.0xmax42.io/maxp/http-kernel/commit/8235680904c7f30f25b98b835d48376431108e91)) +- *(core)* [**breaking**] Enhance HttpKernel pipeline and matcher system with full context and error handling - ([b7410b4](https://git.0xmax42.io/maxp/http-kernel/commit/b7410b44dd8720e46ee2871aa1727ce5039ebad4)) +- *(httpkernel)* Introduce configuration object for flexibility - ([9059bdd](https://git.0xmax42.io/maxp/http-kernel/commit/9059bdda62081c8e775087cabe4c3406e42065a5)) + +### πŸ“š Documentation + +- *(gitea)* Add release automation guide and scripts - ([5c03cdf](https://git.0xmax42.io/maxp/http-kernel/commit/5c03cdfb031adeb6ee5d0de0889477d6d1efafef)) +- *(httpkernel)* Enhance class and interface documentation - ([6c4420d](https://git.0xmax42.io/maxp/http-kernel/commit/6c4420d32f8e7fe317f7c1b0b45de2dcf8565ef5)) + +### πŸ§ͺ Testing + +- *(utils)* Rename and update import paths in test file - ([82a6877](https://git.0xmax42.io/maxp/http-kernel/commit/82a687748558f15c2023861a0cc3a33095c86731)) +- *(utils)* Add unit tests for parseQuery function - ([94525fc](https://git.0xmax42.io/maxp/http-kernel/commit/94525fce5299f3417801f0152a475892e1edac30)) + +### βš™οΈ Miscellaneous Tasks + +- *(config)* Add default git-cliff configuration - ([661f83d](https://git.0xmax42.io/maxp/http-kernel/commit/661f83d1fd0101aa0d5d06b60f6eeb68efac6ceb)) +- *(gitignore)* Add .gitea/COMMIT_GPT.md to ignored files - ([f083856](https://git.0xmax42.io/maxp/http-kernel/commit/f0838567b46822327fe739d8de099722e405dfa3)) +- *(settings)* Add exportall configuration for barrel name and message - ([0990cac](https://git.0xmax42.io/maxp/http-kernel/commit/0990cacb225e1cbbbbb2a288501df7de9641294f)) +- *(.gitignore)* Add git_log_diff.txt to ignore list - ([fd1c7f4](https://git.0xmax42.io/maxp/http-kernel/commit/fd1c7f4170ffffd55ab276090f8b90ee82b853fc)) + + diff --git a/v0.2.1/LICENSE b/v0.2.1/LICENSE new file mode 100644 index 0000000..8941059 --- /dev/null +++ b/v0.2.1/LICENSE @@ -0,0 +1,18 @@ +MIT License + +Copyright (c) 2025 0xMax42 + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/v0.2.1/README.md b/v0.2.1/README.md new file mode 100644 index 0000000..14159bf --- /dev/null +++ b/v0.2.1/README.md @@ -0,0 +1,135 @@ +# HttpKernel – A Type-Safe Router & Middleware Kernel for Deno + +> Fluent routing β€’ Zero-dependency core β€’ 100 % TypeScript + +HttpKernel is a small but powerful dispatching engine that turns an ordinary +`Deno.serve()` loop into a structured, middleware-driven HTTP server. +It focuses on **type safety**, **immutability**, and an **expressive builder API** +while staying framework-agnostic and dependency‑free. + +--- + +## ✨ Key Features + +* **Fluent Route Builder** – chain middleware and handlers without side effects +* **Static *and* Dynamic Matching** – use URL patterns *or* custom matcher functions +* **First-Class Generics** – strongly‑typed `ctx.params`, `ctx.query`, and `ctx.state` +* **Pluggable Error Handling** – override 404/500 (and any other status) per kernel +* **Response Decorators** – inject CORS headers, security headers, logging, … in one place +* **100 % Test Coverage** – built‑in unit tests ensure every edge case is covered + +--- + +## πŸš€ Quick Start + +```ts +// Import directly from your repo or deno.land/x +import { HttpKernel } from "https://deno.land/x/httpkernel/mod.ts"; + +// 1) Create a kernel (optionally pass overrides) +const kernel = new HttpKernel(); + +// 2) Register a route with fluent chaining +kernel + .route({ method: "GET", path: "/hello/:name" }) + .middleware(async (ctx, next) => { + console.log("Incoming request for", ctx.params.name); + return await next(); // continue pipeline + }) + .handle(async (ctx) => + new Response(`Hello ${ctx.params.name}!`, { status: 200 }) + ); + +// 3) Let Deno serve the kernel +Deno.serve(kernel.handle); +``` + +Run it: + +```bash +deno run --allow-net main.ts +# β†’ GET http://localhost:8000/hello/Isaac +``` + +--- + +## 🧩 API Overview + +| Method / Type | Purpose | Hints | +| --------------------- | ---------------------------------------------- | ------------------------------------------------------------- | +| `kernel.route(def)` | Begin defining a new route. Returns `RouteBuilder`. | `def` can be `{ method, path }` **or** `{ method, matcher }`. | +| `.middleware(fn)` | Add a middleware to the current builder. | Each call returns a *new* builder (immutability). | +| `.handle(fn)` | Finalise the route and register the handler. | Must be called exactly once per route. | +| `kernel.handle(req)` | Kernel entry point you pass to `Deno.serve()`. | Resolves to a `Response`. | + +### Context Shape + +```ts +interface Context> { + req: Request; // original request + params: Record; // route params e.g. { id: "42" } + query: Record; // parsed query string + state: S; // per‑request mutable storage +} +``` + +Generics let you supply your own param / query / state types for full IntelliSense. + +--- + +## πŸ› οΈ Configuration + +```ts +new HttpKernel({ + decorateResponse: (res, ctx) => { + // add CORS header globally + const headers = new Headers(res.headers); + headers.set("Access-Control-Allow-Origin", "*"); + return new Response(res.body, { ...res, headers }); + }, + httpErrorHandlers: { + 404: () => new Response("Nothing here ☹️", { status: 404 }), + 500: (_ctx, err) => { + console.error(err); + return new Response("Custom 500", { status: 500 }); + }, + }, +}); +``` + +Everything is optional – omit what you do not override. + +--- + +## πŸ§ͺ Testing + +All logic is covered by unit tests using `std@0.204.0/testing`. +Run them with: + +```bash +deno test -A +``` + +The CI suite checks: + +* Route guards (`isStaticRouteDefinition`, `isDynamicRouteDefinition`) +* Builder immutability & middleware order +* 404 / 500 fall-backs and error propagation +* Middleware mis-use (double `next()`, wrong signatures, …) + +--- + +## πŸ“¦ Roadmap + +* πŸ”Œ Adapter helpers for Oak / Fresh / any framework that can delegate to `kernel.handle` +* πŸ” Built‑in logger & timing middleware +* πŸ”’ CSRF & auth middleware presets +* πŸ“ OpenAPI route generator + +Contributions & ideas are welcome – feel free to open an issue or PR. + +--- + +## πŸ“„ License + +[MIT](LICENSE) diff --git a/v0.2.1/VERSION b/v0.2.1/VERSION new file mode 100644 index 0000000..7dff5b8 --- /dev/null +++ b/v0.2.1/VERSION @@ -0,0 +1 @@ +0.2.1 \ No newline at end of file diff --git a/v0.2.1/cliff.toml b/v0.2.1/cliff.toml new file mode 100644 index 0000000..4d5ab28 --- /dev/null +++ b/v0.2.1/cliff.toml @@ -0,0 +1,104 @@ +# CLIFF_VERSION=2.8.0 +# git-cliff ~ default configuration file +# https://git-cliff.org/docs/configuration +# +# Lines starting with "#" are comments. +# Configuration options are organized into tables and keys. +# See documentation for more information on available options. +[remote.gitea] +owner = "maxp" +repo = "http-kernel" + +[changelog] +# postprocessors +postprocessors = [ + { pattern = '', replace = "https://git.0xmax42.io" }, # replace gitea url +] + +# template for the changelog header +header = """ +# Changelog\n +All notable changes to this project will be documented in this file.\n +""" +# template for the changelog body +# https://keats.github.io/tera/docs/#introduction +body = """ +{%- macro remote_url() -%} + /{{ remote.gitea.owner }}/{{ remote.gitea.repo }} +{%- endmacro -%} + +{% if version %}\ + {% if previous.version %}\ + ## [{{ version | trim_start_matches(pat="v") }}]\ + ({{ self::remote_url() }}/compare/{{ previous.version }}..{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }} + {% else %}\ + ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} + {% endif %}\ +{% else %}\ + ## [unreleased] +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | striptags | trim | upper_first }} + {% for commit in commits %} + - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ + {% if commit.breaking %}[**breaking**] {% endif %}\ + {{ commit.message | upper_first }} - \ + ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ + {% endfor %} +{% endfor %}\n +""" +# template for the changelog footer +footer = """ + +""" +# remove the leading and trailing s +trim = true + +# render body even when there are no releases to process +# render_always = true +# output file path +# output = "test.md" + +[git] +# parse the commits based on https://www.conventionalcommits.org +conventional_commits = true +# filter out the commits that are not conventional +filter_unconventional = true +# process each line of a commit as an individual commit +split_commits = false +# regex for preprocessing the commit messages +commit_preprocessors = [ + # Replace issue numbers + #{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))"}, + # Check spelling of the commit with https://github.com/crate-ci/typos + # If the spelling is incorrect, it will be automatically fixed. + #{ pattern = '.*', replace_command = 'typos --write-changes -' }, +] +# regex for parsing and grouping commits +commit_parsers = [ + { message = "^feat", group = "πŸš€ Features" }, + { message = "^fix", group = "πŸ› Bug Fixes" }, + { message = "^doc", group = "πŸ“š Documentation" }, + { message = "^perf", group = "⚑ Performance" }, + { message = "^refactor", group = "🚜 Refactor" }, + { message = "^style", group = "🎨 Styling" }, + { message = "^test", group = "πŸ§ͺ Testing" }, + { message = "^chore\\(changelog\\)", skip = true }, + { message = "^chore\\(version\\)", skip = true }, + { message = "^chore\\(release\\): prepare for", skip = true }, + { message = "^chore\\(deps.*\\)", skip = true }, + { message = "^chore\\(pr\\)", skip = true }, + { message = "^chore\\(pull\\)", skip = true }, + { message = "^chore|^ci", group = "βš™οΈ Miscellaneous Tasks" }, + { body = ".*security", group = "πŸ›‘οΈ Security" }, + { message = "^revert", group = "◀️ Revert" }, + { message = ".*", group = "πŸ’Ό Other" }, +] +# Regex to select git tags that represent releases. +tag_pattern = "v[0-9]+\\.[0-9]+\\.[0-9]+" +# filter out the commits that are not matched by commit parsers +filter_commits = false +# sort the tags topologically +topo_order = false +# sort the commits inside sections by oldest/newest order +sort_commits = "newest" diff --git a/v0.2.1/deno.jsonc b/v0.2.1/deno.jsonc new file mode 100644 index 0000000..e13a592 --- /dev/null +++ b/v0.2.1/deno.jsonc @@ -0,0 +1,36 @@ +{ + "name": "@0xmax42/http-kernel", + "description": "A simple HTTP kernel for Deno", + "exports": { + "./mod.ts": "./src/mod.ts" + }, + "tasks": { + "test": "deno test --allow-net --allow-env --unstable-kv --allow-read --allow-write --coverage **/__tests__/*.test.ts", + "test:watch": "deno test --watch --allow-net --allow-env --unstable-kv --allow-read --allow-write **/__tests__/*.test.ts", + "benchmark": "deno bench --allow-net --allow-env --unstable-kv --allow-read --allow-write **/__bench__/*.bench.ts", + "fmt": "deno fmt --check", + "lint": "deno lint", + "ci": "deno task fmt && deno task lint && deno task test && deno task benchmark" // For local CI checks + }, + "compilerOptions": { + "lib": [ + "dom", + "dom.iterable", + "esnext", + "deno.ns" + ], + "strict": true + }, + "fmt": { + "useTabs": false, + "lineWidth": 80, + "indentWidth": 4, + "semiColons": true, + "singleQuote": true, + "proseWrap": "preserve", + "include": [ + "src/", + "main.ts" + ] + } +} \ No newline at end of file diff --git a/v0.2.1/deno.lock b/v0.2.1/deno.lock new file mode 100644 index 0000000..aa91fda --- /dev/null +++ b/v0.2.1/deno.lock @@ -0,0 +1,155 @@ +{ + "version": "5", + "redirects": { + "https://deno.land/std/assert/mod.ts": "https://deno.land/std@0.224.0/assert/mod.ts", + "https://deno.land/std/fs/walk.ts": "https://deno.land/std@0.224.0/fs/walk.ts", + "https://deno.land/std/path/mod.ts": "https://deno.land/std@0.224.0/path/mod.ts" + }, + "remote": { + "https://deno.land/std@0.204.0/assert/_constants.ts": "8a9da298c26750b28b326b297316cdde860bc237533b07e1337c021379e6b2a9", + "https://deno.land/std@0.204.0/assert/_diff.ts": "58e1461cc61d8eb1eacbf2a010932bf6a05b79344b02ca38095f9b805795dc48", + "https://deno.land/std@0.204.0/assert/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7", + "https://deno.land/std@0.204.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee", + "https://deno.land/std@0.204.0/assert/assert_almost_equals.ts": "e15ca1f34d0d5e0afae63b3f5d975cbd18335a132e42b0c747d282f62ad2cd6c", + "https://deno.land/std@0.204.0/assert/assert_array_includes.ts": "6856d7f2c3544bc6e62fb4646dfefa3d1df5ff14744d1bca19f0cbaf3b0d66c9", + "https://deno.land/std@0.204.0/assert/assert_equals.ts": "d8ec8a22447fbaf2fc9d7c3ed2e66790fdb74beae3e482855d75782218d68227", + "https://deno.land/std@0.204.0/assert/assert_exists.ts": "407cb6b9fb23a835cd8d5ad804e2e2edbbbf3870e322d53f79e1c7a512e2efd7", + "https://deno.land/std@0.204.0/assert/assert_false.ts": "0ccbcaae910f52c857192ff16ea08bda40fdc79de80846c206bfc061e8c851c6", + "https://deno.land/std@0.204.0/assert/assert_greater.ts": "ae2158a2d19313bf675bf7251d31c6dc52973edb12ac64ac8fc7064152af3e63", + "https://deno.land/std@0.204.0/assert/assert_greater_or_equal.ts": "1439da5ebbe20855446cac50097ac78b9742abe8e9a43e7de1ce1426d556e89c", + "https://deno.land/std@0.204.0/assert/assert_instance_of.ts": "3aedb3d8186e120812d2b3a5dea66a6e42bf8c57a8bd927645770bd21eea554c", + "https://deno.land/std@0.204.0/assert/assert_is_error.ts": "c21113094a51a296ffaf036767d616a78a2ae5f9f7bbd464cd0197476498b94b", + "https://deno.land/std@0.204.0/assert/assert_less.ts": "aec695db57db42ec3e2b62e97e1e93db0063f5a6ec133326cc290ff4b71b47e4", + "https://deno.land/std@0.204.0/assert/assert_less_or_equal.ts": "5fa8b6a3ffa20fd0a05032fe7257bf985d207b85685fdbcd23651b70f928c848", + "https://deno.land/std@0.204.0/assert/assert_match.ts": "c4083f80600bc190309903c95e397a7c9257ff8b5ae5c7ef91e834704e672e9b", + "https://deno.land/std@0.204.0/assert/assert_not_equals.ts": "9f1acab95bd1f5fc9a1b17b8027d894509a745d91bac1718fdab51dc76831754", + "https://deno.land/std@0.204.0/assert/assert_not_instance_of.ts": "0c14d3dfd9ab7a5276ed8ed0b18c703d79a3d106102077ec437bfe7ed912bd22", + "https://deno.land/std@0.204.0/assert/assert_not_match.ts": "3796a5b0c57a1ce6c1c57883dd4286be13a26f715ea662318ab43a8491a13ab0", + "https://deno.land/std@0.204.0/assert/assert_not_strict_equals.ts": "ca6c6d645e95fbc873d25320efeb8c4c6089a9a5e09f92d7c1c4b6e935c2a6ad", + "https://deno.land/std@0.204.0/assert/assert_object_match.ts": "d8fc2867cfd92eeacf9cea621e10336b666de1874a6767b5ec48988838370b54", + "https://deno.land/std@0.204.0/assert/assert_rejects.ts": "45c59724de2701e3b1f67c391d6c71c392363635aad3f68a1b3408f9efca0057", + "https://deno.land/std@0.204.0/assert/assert_strict_equals.ts": "b1f538a7ea5f8348aeca261d4f9ca603127c665e0f2bbfeb91fa272787c87265", + "https://deno.land/std@0.204.0/assert/assert_string_includes.ts": "b821d39ebf5cb0200a348863c86d8c4c4b398e02012ce74ad15666fc4b631b0c", + "https://deno.land/std@0.204.0/assert/assert_throws.ts": "63784e951475cb7bdfd59878cd25a0931e18f6dc32a6077c454b2cd94f4f4bcd", + "https://deno.land/std@0.204.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56", + "https://deno.land/std@0.204.0/assert/equal.ts": "9f1a46d5993966d2596c44e5858eec821859b45f783a5ee2f7a695dfc12d8ece", + "https://deno.land/std@0.204.0/assert/fail.ts": "c36353d7ae6e1f7933d45f8ea51e358c8c4b67d7e7502028598fe1fea062e278", + "https://deno.land/std@0.204.0/assert/mod.ts": "37c49a26aae2b254bbe25723434dc28cd7532e444cf0b481a97c045d110ec085", + "https://deno.land/std@0.204.0/assert/unimplemented.ts": "d56fbeecb1f108331a380f72e3e010a1f161baa6956fd0f7cf3e095ae1a4c75a", + "https://deno.land/std@0.204.0/assert/unreachable.ts": "4600dc0baf7d9c15a7f7d234f00c23bca8f3eba8b140286aaca7aa998cf9a536", + "https://deno.land/std@0.204.0/fmt/colors.ts": "c51c4642678eb690dcf5ffee5918b675bf01a33fba82acf303701ae1a4f8c8d9", + "https://deno.land/std@0.224.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975", + "https://deno.land/std@0.224.0/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834", + "https://deno.land/std@0.224.0/assert/assert_almost_equals.ts": "9e416114322012c9a21fa68e187637ce2d7df25bcbdbfd957cd639e65d3cf293", + "https://deno.land/std@0.224.0/assert/assert_array_includes.ts": "14c5094471bc8e4a7895fc6aa5a184300d8a1879606574cb1cd715ef36a4a3c7", + "https://deno.land/std@0.224.0/assert/assert_equals.ts": "3bbca947d85b9d374a108687b1a8ba3785a7850436b5a8930d81f34a32cb8c74", + "https://deno.land/std@0.224.0/assert/assert_exists.ts": "43420cf7f956748ae6ed1230646567b3593cb7a36c5a5327269279c870c5ddfd", + "https://deno.land/std@0.224.0/assert/assert_false.ts": "3e9be8e33275db00d952e9acb0cd29481a44fa0a4af6d37239ff58d79e8edeff", + "https://deno.land/std@0.224.0/assert/assert_greater.ts": "5e57b201fd51b64ced36c828e3dfd773412c1a6120c1a5a99066c9b261974e46", + "https://deno.land/std@0.224.0/assert/assert_greater_or_equal.ts": "9870030f997a08361b6f63400273c2fb1856f5db86c0c3852aab2a002e425c5b", + "https://deno.land/std@0.224.0/assert/assert_instance_of.ts": "e22343c1fdcacfaea8f37784ad782683ec1cf599ae9b1b618954e9c22f376f2c", + "https://deno.land/std@0.224.0/assert/assert_is_error.ts": "f856b3bc978a7aa6a601f3fec6603491ab6255118afa6baa84b04426dd3cc491", + "https://deno.land/std@0.224.0/assert/assert_less.ts": "60b61e13a1982865a72726a5fa86c24fad7eb27c3c08b13883fb68882b307f68", + "https://deno.land/std@0.224.0/assert/assert_less_or_equal.ts": "d2c84e17faba4afe085e6c9123a63395accf4f9e00150db899c46e67420e0ec3", + "https://deno.land/std@0.224.0/assert/assert_match.ts": "ace1710dd3b2811c391946954234b5da910c5665aed817943d086d4d4871a8b7", + "https://deno.land/std@0.224.0/assert/assert_not_equals.ts": "78d45dd46133d76ce624b2c6c09392f6110f0df9b73f911d20208a68dee2ef29", + "https://deno.land/std@0.224.0/assert/assert_not_instance_of.ts": "3434a669b4d20cdcc5359779301a0588f941ffdc2ad68803c31eabdb4890cf7a", + "https://deno.land/std@0.224.0/assert/assert_not_match.ts": "df30417240aa2d35b1ea44df7e541991348a063d9ee823430e0b58079a72242a", + "https://deno.land/std@0.224.0/assert/assert_not_strict_equals.ts": "37f73880bd672709373d6dc2c5f148691119bed161f3020fff3548a0496f71b8", + "https://deno.land/std@0.224.0/assert/assert_object_match.ts": "411450fd194fdaabc0089ae68f916b545a49d7b7e6d0026e84a54c9e7eed2693", + "https://deno.land/std@0.224.0/assert/assert_rejects.ts": "4bee1d6d565a5b623146a14668da8f9eb1f026a4f338bbf92b37e43e0aa53c31", + "https://deno.land/std@0.224.0/assert/assert_strict_equals.ts": "b4f45f0fd2e54d9029171876bd0b42dd9ed0efd8f853ab92a3f50127acfa54f5", + "https://deno.land/std@0.224.0/assert/assert_string_includes.ts": "496b9ecad84deab72c8718735373feb6cdaa071eb91a98206f6f3cb4285e71b8", + "https://deno.land/std@0.224.0/assert/assert_throws.ts": "c6508b2879d465898dab2798009299867e67c570d7d34c90a2d235e4553906eb", + "https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917", + "https://deno.land/std@0.224.0/assert/equal.ts": "bddf07bb5fc718e10bb72d5dc2c36c1ce5a8bdd3b647069b6319e07af181ac47", + "https://deno.land/std@0.224.0/assert/fail.ts": "0eba674ffb47dff083f02ced76d5130460bff1a9a68c6514ebe0cdea4abadb68", + "https://deno.land/std@0.224.0/assert/mod.ts": "48b8cb8a619ea0b7958ad7ee9376500fe902284bb36f0e32c598c3dc34cbd6f3", + "https://deno.land/std@0.224.0/assert/unimplemented.ts": "8c55a5793e9147b4f1ef68cd66496b7d5ba7a9e7ca30c6da070c1a58da723d73", + "https://deno.land/std@0.224.0/assert/unreachable.ts": "5ae3dbf63ef988615b93eb08d395dda771c96546565f9e521ed86f6510c29e19", + "https://deno.land/std@0.224.0/fmt/colors.ts": "508563c0659dd7198ba4bbf87e97f654af3c34eb56ba790260f252ad8012e1c5", + "https://deno.land/std@0.224.0/fs/_create_walk_entry.ts": "5d9d2aaec05bcf09a06748b1684224d33eba7a4de24cf4cf5599991ca6b5b412", + "https://deno.land/std@0.224.0/fs/_to_path_string.ts": "29bfc9c6c112254961d75cbf6ba814d6de5349767818eb93090cecfa9665591e", + "https://deno.land/std@0.224.0/fs/walk.ts": "cddf87d2705c0163bff5d7767291f05b0f46ba10b8b28f227c3849cace08d303", + "https://deno.land/std@0.224.0/internal/diff.ts": "6234a4b493ebe65dc67a18a0eb97ef683626a1166a1906232ce186ae9f65f4e6", + "https://deno.land/std@0.224.0/internal/format.ts": "0a98ee226fd3d43450245b1844b47003419d34d210fa989900861c79820d21c2", + "https://deno.land/std@0.224.0/internal/mod.ts": "534125398c8e7426183e12dc255bb635d94e06d0f93c60a297723abe69d3b22e", + "https://deno.land/std@0.224.0/path/_common/assert_path.ts": "dbdd757a465b690b2cc72fc5fb7698c51507dec6bfafce4ca500c46b76ff7bd8", + "https://deno.land/std@0.224.0/path/_common/basename.ts": "569744855bc8445f3a56087fd2aed56bdad39da971a8d92b138c9913aecc5fa2", + "https://deno.land/std@0.224.0/path/_common/common.ts": "ef73c2860694775fe8ffcbcdd387f9f97c7a656febf0daa8c73b56f4d8a7bd4c", + "https://deno.land/std@0.224.0/path/_common/constants.ts": "dc5f8057159f4b48cd304eb3027e42f1148cf4df1fb4240774d3492b5d12ac0c", + "https://deno.land/std@0.224.0/path/_common/dirname.ts": "684df4aa71a04bbcc346c692c8485594fc8a90b9408dfbc26ff32cf3e0c98cc8", + "https://deno.land/std@0.224.0/path/_common/format.ts": "92500e91ea5de21c97f5fe91e178bae62af524b72d5fcd246d6d60ae4bcada8b", + "https://deno.land/std@0.224.0/path/_common/from_file_url.ts": "d672bdeebc11bf80e99bf266f886c70963107bdd31134c4e249eef51133ceccf", + "https://deno.land/std@0.224.0/path/_common/glob_to_reg_exp.ts": "6cac16d5c2dc23af7d66348a7ce430e5de4e70b0eede074bdbcf4903f4374d8d", + "https://deno.land/std@0.224.0/path/_common/normalize.ts": "684df4aa71a04bbcc346c692c8485594fc8a90b9408dfbc26ff32cf3e0c98cc8", + "https://deno.land/std@0.224.0/path/_common/normalize_string.ts": "33edef773c2a8e242761f731adeb2bd6d683e9c69e4e3d0092985bede74f4ac3", + "https://deno.land/std@0.224.0/path/_common/relative.ts": "faa2753d9b32320ed4ada0733261e3357c186e5705678d9dd08b97527deae607", + "https://deno.land/std@0.224.0/path/_common/strip_trailing_separators.ts": "7024a93447efcdcfeaa9339a98fa63ef9d53de363f1fbe9858970f1bba02655a", + "https://deno.land/std@0.224.0/path/_common/to_file_url.ts": "7f76adbc83ece1bba173e6e98a27c647712cab773d3f8cbe0398b74afc817883", + "https://deno.land/std@0.224.0/path/_interface.ts": "8dfeb930ca4a772c458a8c7bbe1e33216fe91c253411338ad80c5b6fa93ddba0", + "https://deno.land/std@0.224.0/path/_os.ts": "8fb9b90fb6b753bd8c77cfd8a33c2ff6c5f5bc185f50de8ca4ac6a05710b2c15", + "https://deno.land/std@0.224.0/path/basename.ts": "7ee495c2d1ee516ffff48fb9a93267ba928b5a3486b550be73071bc14f8cc63e", + "https://deno.land/std@0.224.0/path/common.ts": "03e52e22882402c986fe97ca3b5bb4263c2aa811c515ce84584b23bac4cc2643", + "https://deno.land/std@0.224.0/path/constants.ts": "0c206169ca104938ede9da48ac952de288f23343304a1c3cb6ec7625e7325f36", + "https://deno.land/std@0.224.0/path/dirname.ts": "85bd955bf31d62c9aafdd7ff561c4b5fb587d11a9a5a45e2b01aedffa4238a7c", + "https://deno.land/std@0.224.0/path/extname.ts": "593303db8ae8c865cbd9ceec6e55d4b9ac5410c1e276bfd3131916591b954441", + "https://deno.land/std@0.224.0/path/format.ts": "6ce1779b0980296cf2bc20d66436b12792102b831fd281ab9eb08fa8a3e6f6ac", + "https://deno.land/std@0.224.0/path/from_file_url.ts": "911833ae4fd10a1c84f6271f36151ab785955849117dc48c6e43b929504ee069", + "https://deno.land/std@0.224.0/path/glob_to_regexp.ts": "7f30f0a21439cadfdae1be1bf370880b415e676097fda584a63ce319053b5972", + "https://deno.land/std@0.224.0/path/is_absolute.ts": "4791afc8bfd0c87f0526eaa616b0d16e7b3ab6a65b62942e50eac68de4ef67d7", + "https://deno.land/std@0.224.0/path/is_glob.ts": "a65f6195d3058c3050ab905705891b412ff942a292bcbaa1a807a74439a14141", + "https://deno.land/std@0.224.0/path/join.ts": "ae2ec5ca44c7e84a235fd532e4a0116bfb1f2368b394db1c4fb75e3c0f26a33a", + "https://deno.land/std@0.224.0/path/join_globs.ts": "5b3bf248b93247194f94fa6947b612ab9d3abd571ca8386cf7789038545e54a0", + "https://deno.land/std@0.224.0/path/mod.ts": "f6bd79cb08be0e604201bc9de41ac9248582699d1b2ee0ab6bc9190d472cf9cd", + "https://deno.land/std@0.224.0/path/normalize.ts": "4155743ccceeed319b350c1e62e931600272fad8ad00c417b91df093867a8352", + "https://deno.land/std@0.224.0/path/normalize_glob.ts": "cc89a77a7d3b1d01053b9dcd59462b75482b11e9068ae6c754b5cf5d794b374f", + "https://deno.land/std@0.224.0/path/parse.ts": "77ad91dcb235a66c6f504df83087ce2a5471e67d79c402014f6e847389108d5a", + "https://deno.land/std@0.224.0/path/posix/_util.ts": "1e3937da30f080bfc99fe45d7ed23c47dd8585c5e473b2d771380d3a6937cf9d", + "https://deno.land/std@0.224.0/path/posix/basename.ts": "d2fa5fbbb1c5a3ab8b9326458a8d4ceac77580961b3739cd5bfd1d3541a3e5f0", + "https://deno.land/std@0.224.0/path/posix/common.ts": "26f60ccc8b2cac3e1613000c23ac5a7d392715d479e5be413473a37903a2b5d4", + "https://deno.land/std@0.224.0/path/posix/constants.ts": "93481efb98cdffa4c719c22a0182b994e5a6aed3047e1962f6c2c75b7592bef1", + "https://deno.land/std@0.224.0/path/posix/dirname.ts": "76cd348ffe92345711409f88d4d8561d8645353ac215c8e9c80140069bf42f00", + "https://deno.land/std@0.224.0/path/posix/extname.ts": "e398c1d9d1908d3756a7ed94199fcd169e79466dd88feffd2f47ce0abf9d61d2", + "https://deno.land/std@0.224.0/path/posix/format.ts": "185e9ee2091a42dd39e2a3b8e4925370ee8407572cee1ae52838aed96310c5c1", + "https://deno.land/std@0.224.0/path/posix/from_file_url.ts": "951aee3a2c46fd0ed488899d024c6352b59154c70552e90885ed0c2ab699bc40", + "https://deno.land/std@0.224.0/path/posix/glob_to_regexp.ts": "76f012fcdb22c04b633f536c0b9644d100861bea36e9da56a94b9c589a742e8f", + "https://deno.land/std@0.224.0/path/posix/is_absolute.ts": "cebe561ad0ae294f0ce0365a1879dcfca8abd872821519b4fcc8d8967f888ede", + "https://deno.land/std@0.224.0/path/posix/is_glob.ts": "8a8b08c08bf731acf2c1232218f1f45a11131bc01de81e5f803450a5914434b9", + "https://deno.land/std@0.224.0/path/posix/join.ts": "7fc2cb3716aa1b863e990baf30b101d768db479e70b7313b4866a088db016f63", + "https://deno.land/std@0.224.0/path/posix/join_globs.ts": "a9475b44645feddceb484ee0498e456f4add112e181cb94042cdc6d47d1cdd25", + "https://deno.land/std@0.224.0/path/posix/mod.ts": "2301fc1c54a28b349e20656f68a85f75befa0ee9b6cd75bfac3da5aca9c3f604", + "https://deno.land/std@0.224.0/path/posix/normalize.ts": "baeb49816a8299f90a0237d214cef46f00ba3e95c0d2ceb74205a6a584b58a91", + "https://deno.land/std@0.224.0/path/posix/normalize_glob.ts": "9c87a829b6c0f445d03b3ecadc14492e2864c3ebb966f4cea41e98326e4435c6", + "https://deno.land/std@0.224.0/path/posix/parse.ts": "09dfad0cae530f93627202f28c1befa78ea6e751f92f478ca2cc3b56be2cbb6a", + "https://deno.land/std@0.224.0/path/posix/relative.ts": "3907d6eda41f0ff723d336125a1ad4349112cd4d48f693859980314d5b9da31c", + "https://deno.land/std@0.224.0/path/posix/resolve.ts": "08b699cfeee10cb6857ccab38fa4b2ec703b0ea33e8e69964f29d02a2d5257cf", + "https://deno.land/std@0.224.0/path/posix/to_file_url.ts": "7aa752ba66a35049e0e4a4be5a0a31ac6b645257d2e031142abb1854de250aaf", + "https://deno.land/std@0.224.0/path/posix/to_namespaced_path.ts": "28b216b3c76f892a4dca9734ff1cc0045d135532bfd9c435ae4858bfa5a2ebf0", + "https://deno.land/std@0.224.0/path/relative.ts": "ab739d727180ed8727e34ed71d976912461d98e2b76de3d3de834c1066667add", + "https://deno.land/std@0.224.0/path/resolve.ts": "a6f977bdb4272e79d8d0ed4333e3d71367cc3926acf15ac271f1d059c8494d8d", + "https://deno.land/std@0.224.0/path/to_file_url.ts": "88f049b769bce411e2d2db5bd9e6fd9a185a5fbd6b9f5ad8f52bef517c4ece1b", + "https://deno.land/std@0.224.0/path/to_namespaced_path.ts": "b706a4103b104cfadc09600a5f838c2ba94dbcdb642344557122dda444526e40", + "https://deno.land/std@0.224.0/path/windows/_util.ts": "d5f47363e5293fced22c984550d5e70e98e266cc3f31769e1710511803d04808", + "https://deno.land/std@0.224.0/path/windows/basename.ts": "6bbc57bac9df2cec43288c8c5334919418d784243a00bc10de67d392ab36d660", + "https://deno.land/std@0.224.0/path/windows/common.ts": "26f60ccc8b2cac3e1613000c23ac5a7d392715d479e5be413473a37903a2b5d4", + "https://deno.land/std@0.224.0/path/windows/constants.ts": "5afaac0a1f67b68b0a380a4ef391bf59feb55856aa8c60dfc01bd3b6abb813f5", + "https://deno.land/std@0.224.0/path/windows/dirname.ts": "33e421be5a5558a1346a48e74c330b8e560be7424ed7684ea03c12c21b627bc9", + "https://deno.land/std@0.224.0/path/windows/extname.ts": "165a61b00d781257fda1e9606a48c78b06815385e7d703232548dbfc95346bef", + "https://deno.land/std@0.224.0/path/windows/format.ts": "bbb5ecf379305b472b1082cd2fdc010e44a0020030414974d6029be9ad52aeb6", + "https://deno.land/std@0.224.0/path/windows/from_file_url.ts": "ced2d587b6dff18f963f269d745c4a599cf82b0c4007356bd957cb4cb52efc01", + "https://deno.land/std@0.224.0/path/windows/glob_to_regexp.ts": "e45f1f89bf3fc36f94ab7b3b9d0026729829fabc486c77f414caebef3b7304f8", + "https://deno.land/std@0.224.0/path/windows/is_absolute.ts": "4a8f6853f8598cf91a835f41abed42112cebab09478b072e4beb00ec81f8ca8a", + "https://deno.land/std@0.224.0/path/windows/is_glob.ts": "8a8b08c08bf731acf2c1232218f1f45a11131bc01de81e5f803450a5914434b9", + "https://deno.land/std@0.224.0/path/windows/join.ts": "8d03530ab89195185103b7da9dfc6327af13eabdcd44c7c63e42e27808f50ecf", + "https://deno.land/std@0.224.0/path/windows/join_globs.ts": "a9475b44645feddceb484ee0498e456f4add112e181cb94042cdc6d47d1cdd25", + "https://deno.land/std@0.224.0/path/windows/mod.ts": "2301fc1c54a28b349e20656f68a85f75befa0ee9b6cd75bfac3da5aca9c3f604", + "https://deno.land/std@0.224.0/path/windows/normalize.ts": "78126170ab917f0ca355a9af9e65ad6bfa5be14d574c5fb09bb1920f52577780", + "https://deno.land/std@0.224.0/path/windows/normalize_glob.ts": "9c87a829b6c0f445d03b3ecadc14492e2864c3ebb966f4cea41e98326e4435c6", + "https://deno.land/std@0.224.0/path/windows/parse.ts": "08804327b0484d18ab4d6781742bf374976de662f8642e62a67e93346e759707", + "https://deno.land/std@0.224.0/path/windows/relative.ts": "3e1abc7977ee6cc0db2730d1f9cb38be87b0ce4806759d271a70e4997fc638d7", + "https://deno.land/std@0.224.0/path/windows/resolve.ts": "8dae1dadfed9d46ff46cc337c9525c0c7d959fb400a6308f34595c45bdca1972", + "https://deno.land/std@0.224.0/path/windows/to_file_url.ts": "40e560ee4854fe5a3d4d12976cef2f4e8914125c81b11f1108e127934ced502e", + "https://deno.land/std@0.224.0/path/windows/to_namespaced_path.ts": "4ffa4fb6fae321448d5fe810b3ca741d84df4d7897e61ee29be961a6aac89a4c" + } +} diff --git a/v0.2.1/src/Errors/InvalidHttpMethodError.ts b/v0.2.1/src/Errors/InvalidHttpMethodError.ts new file mode 100644 index 0000000..8d9047f --- /dev/null +++ b/v0.2.1/src/Errors/InvalidHttpMethodError.ts @@ -0,0 +1,25 @@ +/** + * Represents an error thrown when an incoming HTTP method + * is not among the recognized set of valid HTTP methods. + * + * This is typically used in routers or request dispatchers + * to enforce allowed methods and produce 405-like behavior. + */ +export class InvalidHttpMethodError extends Error { + /** + * The invalid method that triggered this error. + */ + public readonly method: unknown; + + /** + * A fixed HTTP status code representing "Method Not Allowed". + */ + public readonly status: number = 405; + + constructor(method: unknown) { + const label = typeof method === 'string' ? method : '[non-string]'; + super(`Unsupported HTTP method: ${label}`); + this.name = 'InvalidHttpMethodError'; + this.method = method; + } +} diff --git a/v0.2.1/src/Errors/mod.ts b/v0.2.1/src/Errors/mod.ts new file mode 100644 index 0000000..50e5ba8 --- /dev/null +++ b/v0.2.1/src/Errors/mod.ts @@ -0,0 +1,3 @@ +// deno-coverage-ignore-file + +export { InvalidHttpMethodError } from './InvalidHttpMethodError.ts'; diff --git a/v0.2.1/src/HttpKernel.ts b/v0.2.1/src/HttpKernel.ts new file mode 100644 index 0000000..fa6c778 --- /dev/null +++ b/v0.2.1/src/HttpKernel.ts @@ -0,0 +1,144 @@ +import type { + IContext, + IHttpKernel, + IHttpKernelConfig, + IInternalRoute, + IRouteBuilder, + IRouteDefinition, +} from './Interfaces/mod.ts'; +import { + type DeepPartial, + HTTP_404_NOT_FOUND, + HTTP_500_INTERNAL_SERVER_ERROR, + HttpStatusTextMap, +} from './Types/mod.ts'; +import { RouteBuilder } from './RouteBuilder.ts'; +import { createEmptyContext, normalizeError } from './Utils/mod.ts'; + +/** + * The `HttpKernel` is the central routing engine that manages the full HTTP request lifecycle. + * + * It enables: + * - Dynamic and static route registration via a fluent API + * - Execution of typed middleware chains and final route handlers + * - Injection of response decorators and factory overrides + * - Fine-grained error handling via typed status-code-based handlers + * + * The kernel is designed with generics for flexible context typing, strong type safety, + * and a clear extension point for advanced routing, DI, or tracing logic. + * + * @typeParam TContext - The global context type used for all requests handled by this kernel. + */ +export class HttpKernel + implements IHttpKernel { + private cfg: IHttpKernelConfig; + + /** + * The list of registered route definitions, including method, matcher, + * middleware pipeline, and final handler. + */ + private routes: IInternalRoute[] = []; + + /** + * Initializes the `HttpKernel` with optional configuration overrides. + * + * Default components such as the route builder factory, response decorator, + * and 404/500 error handlers can be replaced by injecting a partial config. + * Any omitted values fall back to sensible defaults. + * + * @param config - Partial kernel configuration. Missing fields are filled with defaults. + */ + public constructor( + config?: DeepPartial>, + ) { + this.cfg = { + decorateResponse: (res) => res, + routeBuilderFactory: RouteBuilder, + httpErrorHandlers: { + [HTTP_404_NOT_FOUND]: () => + new Response(HttpStatusTextMap[HTTP_404_NOT_FOUND], { + status: HTTP_404_NOT_FOUND, + }), + [HTTP_500_INTERNAL_SERVER_ERROR]: () => + new Response( + HttpStatusTextMap[HTTP_500_INTERNAL_SERVER_ERROR], + { + status: HTTP_500_INTERNAL_SERVER_ERROR, + }, + ), + ...(config?.httpErrorHandlers ?? {}), + }, + ...config, + } as IHttpKernelConfig; + + this.handle = this.handle.bind(this); + this.registerRoute = this.registerRoute.bind(this); + } + + /** + * @inheritdoc + */ + public route<_TContext extends IContext = TContext>( + definition: IRouteDefinition, + ): IRouteBuilder<_TContext> { + return new this.cfg.routeBuilderFactory( + this.registerRoute, + definition, + ) as IRouteBuilder<_TContext>; + } + + /** + * @inheritdoc + */ + public async handle(request: Request): Promise { + const url = new URL(request.url); + const method = request.method.toUpperCase(); + + for (const route of this.routes) { + if (route.method !== method) continue; + const match = route.matcher(url, request); + if (match) { + const ctx: TContext = { + req: request, + params: match.params, + query: match.query, + state: {}, + } as TContext; + try { + const response = await route.runRoute(ctx); + return this.cfg.decorateResponse(response, ctx); + } catch (e) { + return await this.handleInternalError(ctx, e); + } + } + } + + return this.cfg.httpErrorHandlers[HTTP_404_NOT_FOUND]( + createEmptyContext(request), + ); + } + + /** + * Finalizes and registers a route within the kernel. + * + * This method is invoked internally by the route builder once + * `.handle()` is called. It appends the route to the internal list. + * + * @param route - A fully constructed internal route object. + */ + private registerRoute<_TContext extends IContext = TContext>( + route: IInternalRoute<_TContext>, + ): void { + this.routes.push(route as unknown as IInternalRoute); + } + + private handleInternalError = ( + ctx: TContext, + err?: unknown, + ): Response | Promise => { + return this.cfg.httpErrorHandlers[HTTP_500_INTERNAL_SERVER_ERROR]( + ctx, + normalizeError(err), + ); + }; +} diff --git a/v0.2.1/src/Interfaces/IContext.ts b/v0.2.1/src/Interfaces/IContext.ts new file mode 100644 index 0000000..564fcd3 --- /dev/null +++ b/v0.2.1/src/Interfaces/IContext.ts @@ -0,0 +1,53 @@ +import type { Params, Query, State } from '../Types/mod.ts'; + +/** + * Represents the complete context for a single HTTP request, + * passed through the middleware pipeline and to the final route handler. + * + * This context object encapsulates all relevant runtime data for a request, + * including the original request, path parameters, query parameters, + * and a shared, mutable application state. + * + * @template TState Structured per-request state shared across middlewares and handlers. + * @template TParams Parsed URL path parameters, typically derived from route templates. + * @template TQuery Parsed query string parameters, preserving multi-value semantics. + */ +export interface IContext< + TState extends State = State, + TParams extends Params = Params, + TQuery extends Query = Query, +> { + /** + * The original HTTP request object as received by Deno. + * Contains all standard fields like headers, method, body, etc. + */ + req: Request; + + /** + * Route parameters parsed from the URL path, based on route definitions + * that include dynamic segments (e.g., `/users/:id` β†’ `{ id: "123" }`). + * + * These parameters are considered read-only and are set by the router. + */ + params: TParams; + + /** + * Query parameters extracted from the request URL's search string. + * + * Values may occur multiple times (e.g., `?tag=ts&tag=deno`), and are therefore + * represented as either a string or an array of strings, depending on occurrence. + * + * Use this field to access filters, flags, pagination info, or similar modifiers. + */ + query: TQuery; + + /** + * A typed, mutable object used to pass structured data between middlewares and handlers. + * + * This object is ideal for sharing validated input, user identity, trace information, + * or other contextual state throughout the request lifecycle. + * + * Type-safe access to fields is ensured by the generic `TState` type. + */ + state: TState; +} diff --git a/v0.2.1/src/Interfaces/IHttpErrorHandlers.ts b/v0.2.1/src/Interfaces/IHttpErrorHandlers.ts new file mode 100644 index 0000000..935223d --- /dev/null +++ b/v0.2.1/src/Interfaces/IHttpErrorHandlers.ts @@ -0,0 +1,40 @@ +import type { IContext } from '../Interfaces/mod.ts'; +import type { HttpErrorHandler, validHttpErrorCodes } from '../Types/mod.ts'; + +/** + * A mapping of HTTP status codes to their corresponding error handlers. + * + * This interface defines required handlers for common critical status codes (404 and 500) + * and allows optional handlers for all other known error codes defined in `validHttpErrorCodes`. + * + * This hybrid approach ensures predictable handling for key failure cases, + * while remaining flexible for less common codes. + * + * @template TContext - The context type used in all error handlers. + * + * @example + * ```ts + * const errorHandlers: IHttpErrorHandlers = { + * 404: (ctx) => new Response("Not Found", { status: 404 }), + * 500: (ctx, err) => { + * console.error(err); + * return new Response("Internal Server Error", { status: 500 }); + * }, + * 429: (ctx) => new Response("Too Many Requests", { status: 429 }), + * }; + * ``` + */ +export interface IHttpErrorHandlers + extends + Partial< + Record< + Exclude, + HttpErrorHandler + > + > { + /** Required error handler for HTTP 404 (Not Found). */ + 404: HttpErrorHandler; + + /** Required error handler for HTTP 500 (Internal Server Error). */ + 500: HttpErrorHandler; +} diff --git a/v0.2.1/src/Interfaces/IHttpKernel.ts b/v0.2.1/src/Interfaces/IHttpKernel.ts new file mode 100644 index 0000000..371486a --- /dev/null +++ b/v0.2.1/src/Interfaces/IHttpKernel.ts @@ -0,0 +1,49 @@ +import type { IContext } from './IContext.ts'; +import type { IRouteBuilder } from './IRouteBuilder.ts'; +import type { IRouteDefinition } from './IRouteDefinition.ts'; + +/** + * The `IHttpKernel` interface defines the public API for a type-safe, middleware-driven HTTP dispatching system. + * + * Implementations of this interface are responsible for: + * - Registering routes with optional per-route context typing + * - Handling incoming requests by matching and dispatching to appropriate handlers + * - Managing the complete middleware pipeline and final response generation + * + * The kernel operates on a customizable `IContext` type to support strongly typed request parameters, state, + * and query values across the entire routing lifecycle. + * + * @typeParam TContext - The default context type used for all routes unless overridden per-route. + */ +export interface IHttpKernel { + /** + * Registers a new HTTP route (static or dynamic) and returns a route builder for middleware/handler chaining. + * + * This method supports contextual polymorphism via the `_TContext` type parameter, enabling fine-grained + * typing of route-specific `params`, `query`, and `state` values. The route is not registered until + * `.handle()` is called on the returned builder. + * + * @typeParam _TContext - An optional override for the context type specific to this route. + * Falls back to the global `TContext` of the kernel if omitted. + * + * @param definition - A route definition specifying the HTTP method and path or custom matcher. + * @returns A fluent builder interface to define middleware and attach a final handler. + */ + route<_TContext extends IContext = TContext>( + definition: IRouteDefinition, + ): IRouteBuilder<_TContext>; + + /** + * Handles an incoming HTTP request and produces a `Response`. + * + * The kernel matches the request against all registered routes by method and matcher, + * constructs a typed context, and executes the middleware/handler pipeline. + * If no route matches, a 404 error handler is invoked. + * + * This method is designed to be passed directly to `Deno.serve()` or similar server frameworks. + * + * @param request - The incoming HTTP request object. + * @returns A `Promise` resolving to a complete HTTP response. + */ + handle(request: Request): Promise; +} diff --git a/v0.2.1/src/Interfaces/IHttpKernelConfig.ts b/v0.2.1/src/Interfaces/IHttpKernelConfig.ts new file mode 100644 index 0000000..8a91e98 --- /dev/null +++ b/v0.2.1/src/Interfaces/IHttpKernelConfig.ts @@ -0,0 +1,10 @@ +import type { ResponseDecorator } from '../Types/mod.ts'; +import type { IContext } from './IContext.ts'; +import type { IHttpErrorHandlers } from './IHttpErrorHandlers.ts'; +import type { IRouteBuilderFactory } from './IRouteBuilder.ts'; + +export interface IHttpKernelConfig { + decorateResponse: ResponseDecorator; + routeBuilderFactory: IRouteBuilderFactory; + httpErrorHandlers: IHttpErrorHandlers; +} diff --git a/v0.2.1/src/Interfaces/IInternalRoute.ts b/v0.2.1/src/Interfaces/IInternalRoute.ts new file mode 100644 index 0000000..fd3e33f --- /dev/null +++ b/v0.2.1/src/Interfaces/IInternalRoute.ts @@ -0,0 +1,64 @@ +import type { Handler, HttpMethod, Middleware } from '../Types/mod.ts'; +import type { IContext, IRouteMatcher } from './mod.ts'; + +/** + * Represents an internally registered route within the HttpKernel. + * + * Contains all data required to match an incoming request and dispatch it + * through the associated middleware chain and final handler. + */ +export interface IInternalRoute { + /** + * The HTTP method (e.g. 'GET', 'POST') that this route responds to. + * The method should always be in uppercase. + */ + method: HttpMethod; + + /** + * A matcher function used to determine whether this route matches a given request. + * + * If the matcher returns `null`, the route does not apply to the request. + * If it returns a params object, the route is considered matched and the extracted + * parameters are passed into the request context. + * + * @param url - The parsed URL object from the incoming request. + * @param req - The original Request object. + * @returns An object with extracted path parameters, or `null` if not matched. + */ + matcher: IRouteMatcher; + + /** + * An ordered list of middleware functions to be executed before the handler. + */ + middlewares: Middleware[]; + + /** + * The final handler that generates the HTTP response after all middleware has run. + */ + handler: Handler; + + /** + * The fully compiled execution pipeline for this route. + * + * This function is generated at route registration time and encapsulates the + * entire middleware chain as well as the final handler. It is called by the + * HttpKernel during request dispatch when a route has been matched. + * + * Internally, `runRoute` ensures that each middleware is invoked in the correct order + * and receives a `next()` callback to pass control downstream. The final handler is + * invoked once all middleware has completed or short-circuited the pipeline. + * + * It is guaranteed that: + * - The function is statically compiled and does not perform dynamic dispatching. + * - Each middleware can only call `next()` once; repeated invocations will throw. + * - The return value is either a `Response` or a Promise resolving to one. + * + * @param ctx - The context object carrying route, request, response and other scoped data. + * @returns A `Response` object or a Promise resolving to a `Response`. + * + * @throws {Error} If a middleware calls `next()` more than once. + */ + runRoute: ( + ctx: TContext, + ) => Promise | Response; +} diff --git a/v0.2.1/src/Interfaces/IRouteBuilder.ts b/v0.2.1/src/Interfaces/IRouteBuilder.ts new file mode 100644 index 0000000..9737ae0 --- /dev/null +++ b/v0.2.1/src/Interfaces/IRouteBuilder.ts @@ -0,0 +1,39 @@ +import type { Handler, Middleware } from '../Types/mod.ts'; +import type { IInternalRoute } from './IInternalRoute.ts'; +import type { IRouteDefinition } from './IRouteDefinition.ts'; +import type { IContext } from './mod.ts'; + +export interface IRouteBuilderFactory { + new ( + registerRoute: (route: IInternalRoute) => void, + def: IRouteDefinition, + mws?: Middleware[], + ): IRouteBuilder; +} + +/** + * Provides a fluent API to build a single route configuration by chaining + * middleware and setting the final request handler. + */ +export interface IRouteBuilder { + /** + * Adds a middleware to the current route. + * Middleware will be executed in the order of registration. + * + * @param mw - A middleware function. + * @returns The route builder for further chaining. + */ + middleware( + mw: Middleware, + ): IRouteBuilder; + + /** + * Sets the final request handler for the route. + * Calling this finalizes the route and registers it in the kernel. + * + * @param handler - The function to execute when this route is matched. + */ + handle( + handler: Handler, + ): void; +} diff --git a/v0.2.1/src/Interfaces/IRouteDefinition.ts b/v0.2.1/src/Interfaces/IRouteDefinition.ts new file mode 100644 index 0000000..c2c41ff --- /dev/null +++ b/v0.2.1/src/Interfaces/IRouteDefinition.ts @@ -0,0 +1,91 @@ +import { type HttpMethod, isHttpMethod } from '../Types/mod.ts'; +import type { IRouteMatcher } from './IRouteMatcher.ts'; + +/** + * Defines a static route using a path pattern with optional parameters. + * + * Suitable for conventional routes like "/users/:id", which can be parsed + * into named parameters using a path-matching library. + */ +export interface IStaticRouteDefinition { + /** + * The HTTP method this route should match (e.g. "GET", "POST"). + */ + method: HttpMethod; + + /** + * A static path pattern for the route, which may include named parameters + * (e.g. "/caches/:id"). Internally, this can be converted to a regex matcher. + */ + path: string; +} + +/** + * Defines a dynamic route using a custom matcher function instead of a static path. + * + * Useful for complex URL structures that cannot easily be expressed using a static pattern, + * such as routes with variable prefixes or conditional segment logic. + */ +export interface IDynamicRouteDefinition { + /** + * The HTTP method this route should match (e.g. "GET", "POST"). + */ + method: HttpMethod; + + /** + * A custom matcher function that receives the parsed URL and raw request. + * If the function returns `null`, the route does not match. + * If the function returns a params object, the route is considered matched. + */ + matcher: IRouteMatcher; +} + +/** + * A route definition can either be a conventional static route with a path pattern, + * or a dynamic route with a custom matcher function for advanced matching logic. + */ +export type IRouteDefinition = IStaticRouteDefinition | IDynamicRouteDefinition; + +/** + * Type guard to check whether a route definition is a valid static route definition. + * + * Ensures that the object: + * - has a `method` property of type `HttpMethod` + * - has a `path` property of type `string` + * - does NOT have a `matcher` function (to avoid ambiguous mixed types) + */ +export function isStaticRouteDefinition( + def: IRouteDefinition, +): def is IStaticRouteDefinition { + return ( + def && + typeof def === 'object' && + 'method' in def && + isHttpMethod(def.method) && + 'path' in def && + typeof (def as { path?: unknown }).path === 'string' && + !('matcher' in def) + ); +} + +/** + * Type guard to check whether a route definition is a valid dynamic route definition. + * + * Ensures that the object: + * - has a `method` property of type `HttpMethod` + * - has a `matcher` property of type `function` + * - does NOT have a `path` property (to avoid ambiguous mixed types) + */ +export function isDynamicRouteDefinition( + def: IRouteDefinition, +): def is IDynamicRouteDefinition { + return ( + def && + typeof def === 'object' && + 'method' in def && + isHttpMethod(def.method) && + 'matcher' in def && + typeof (def as { matcher?: unknown }).matcher === 'function' && + !('path' in def) + ); +} diff --git a/v0.2.1/src/Interfaces/IRouteMatch.ts b/v0.2.1/src/Interfaces/IRouteMatch.ts new file mode 100644 index 0000000..9ed1067 --- /dev/null +++ b/v0.2.1/src/Interfaces/IRouteMatch.ts @@ -0,0 +1,6 @@ +import type { Params, Query } from '../Types/mod.ts'; + +export interface IRouteMatch { + params?: Params; + query?: Query; +} diff --git a/v0.2.1/src/Interfaces/IRouteMatcher.ts b/v0.2.1/src/Interfaces/IRouteMatcher.ts new file mode 100644 index 0000000..1dee7eb --- /dev/null +++ b/v0.2.1/src/Interfaces/IRouteMatcher.ts @@ -0,0 +1,35 @@ +import type { IRouteDefinition } from './IRouteDefinition.ts'; +import type { IRouteMatch } from './IRouteMatch.ts'; + +/** + * Defines a route matcher function that evaluates whether a route applies to a given request. + * + * If the route matches, the matcher returns an object containing extracted route parameters. + * Otherwise, it returns `null`. + */ +export interface IRouteMatcher { + /** + * Evaluates whether the given URL and request match a defined route. + * + * @param url - The full URL of the incoming request. + * @param req - The raw Request object (may be used for context or headers). + * @returns An object containing path parameters if matched, or `null` if not matched. + */ + (url: URL, req: Request): null | IRouteMatch; +} + +/** + * Represents a factory for creating route matcher functions from route definitions. + * + * This allows the matcher logic to be injected or replaced (e.g. for testing, + * pattern libraries, or advanced routing scenarios). + */ +export interface IRouteMatcherFactory { + /** + * Creates a matcher function based on a given route definition. + * + * @param def - The route definition (static or dynamic). + * @returns A matcher function that checks if a request matches and extracts parameters. + */ + (def: IRouteDefinition): IRouteMatcher; +} diff --git a/v0.2.1/src/Interfaces/__tests__/routeDefinitionGuards.test.ts b/v0.2.1/src/Interfaces/__tests__/routeDefinitionGuards.test.ts new file mode 100644 index 0000000..98cc0a0 --- /dev/null +++ b/v0.2.1/src/Interfaces/__tests__/routeDefinitionGuards.test.ts @@ -0,0 +1,43 @@ +import { assertEquals } from 'https://deno.land/std@0.204.0/assert/mod.ts'; +import { + type IRouteDefinition, + isDynamicRouteDefinition, + isStaticRouteDefinition, +} from '../IRouteDefinition.ts'; + +Deno.test('isStaticRouteDefinition returns true for static route', () => { + const staticDef: IRouteDefinition = { + method: 'GET', + path: '/users/:id', + }; + + assertEquals(isStaticRouteDefinition(staticDef), true); + assertEquals(isDynamicRouteDefinition(staticDef), false); +}); + +Deno.test('isDynamicRouteDefinition returns true for dynamic route', () => { + const dynamicDef: IRouteDefinition = { + method: 'POST', + matcher: (_url, _req) => ({ params: {} }), + }; + + assertEquals(isDynamicRouteDefinition(dynamicDef), true); + assertEquals(isStaticRouteDefinition(dynamicDef), false); +}); + +Deno.test('isStaticRouteDefinition returns false for invalid object', () => { + const invalidDef = { + method: 'GET', + } as unknown as IRouteDefinition; + + assertEquals(isStaticRouteDefinition(invalidDef), false); +}); + +Deno.test('isDynamicRouteDefinition returns false for object with no matcher', () => { + const def = { + method: 'DELETE', + path: '/something', + }; + + assertEquals(isDynamicRouteDefinition(def as IRouteDefinition), false); +}); diff --git a/v0.2.1/src/Interfaces/mod.ts b/v0.2.1/src/Interfaces/mod.ts new file mode 100644 index 0000000..7c235d9 --- /dev/null +++ b/v0.2.1/src/Interfaces/mod.ts @@ -0,0 +1,19 @@ +// deno-coverage-ignore-file + +export type { IContext } from './IContext.ts'; +export type { IHttpErrorHandlers } from './IHttpErrorHandlers.ts'; +export type { IHttpKernel } from './IHttpKernel.ts'; +export type { IHttpKernelConfig } from './IHttpKernelConfig.ts'; +export type { IInternalRoute } from './IInternalRoute.ts'; +export type { IRouteBuilder, IRouteBuilderFactory } from './IRouteBuilder.ts'; +export { + isDynamicRouteDefinition, + isStaticRouteDefinition, +} from './IRouteDefinition.ts'; +export type { + IDynamicRouteDefinition, + IRouteDefinition, + IStaticRouteDefinition, +} from './IRouteDefinition.ts'; +export type { IRouteMatch } from './IRouteMatch.ts'; +export type { IRouteMatcher, IRouteMatcherFactory } from './IRouteMatcher.ts'; diff --git a/v0.2.1/src/RouteBuilder.ts b/v0.2.1/src/RouteBuilder.ts new file mode 100644 index 0000000..7635058 --- /dev/null +++ b/v0.2.1/src/RouteBuilder.ts @@ -0,0 +1,148 @@ +import type { IRouteMatcherFactory } from './Interfaces/IRouteMatcher.ts'; +import type { + IContext, + IInternalRoute, + IRouteBuilder, + IRouteDefinition, +} from './Interfaces/mod.ts'; +import { isHandler } from './Types/Handler.ts'; +import { + type Handler, + isMiddleware, + type Middleware, + type RegisterRoute, +} from './Types/mod.ts'; +import { createRouteMatcher } from './Utils/createRouteMatcher.ts'; + +/** + * Provides a fluent builder interface for defining a single route, + * including HTTP method, path or matcher, middleware chain and final handler. + * + * This builder is stateless and immutable; each chained call returns a new instance. + */ +export class RouteBuilder + implements IRouteBuilder { + /** + * Constructs a new instance of the route builder. + * + * @param registerRoute - A delegate used to register the finalized route definition. + * @param def - The route definition (static path or dynamic matcher). + * @param mws - The list of middleware functions collected so far (default: empty). + */ + constructor( + private readonly registerRoute: RegisterRoute, + private readonly def: IRouteDefinition, + private readonly mws: Middleware[] = [], + private readonly matcherFactory: IRouteMatcherFactory = + createRouteMatcher, + ) {} + + /** + * Adds a middleware function to the current route definition. + * + * Middleware is executed in the order it is added. + * Returns a new builder instance with the additional middleware appended. + * + * @param mw - A middleware function to be executed before the handler. + * @returns A new `RouteBuilder` instance for continued chaining. + */ + middleware( + mw: Middleware, + ): IRouteBuilder { + return new RouteBuilder( + this.registerRoute, + this.def, + [...this.mws, mw], + ); + } + + /** + * Finalizes the route by assigning the handler and registering the route. + * + * Internally constructs a matcher function from the route definition + * and passes all route data to the registration delegate. + * + * @param handler - The final request handler for this route. + */ + handle( + handler: Handler, + ): void { + const matcher = this.matcherFactory(this.def); + this.registerRoute({ + method: this.def.method, + matcher, + middlewares: this.mws, + handler: handler, + runRoute: this.compile({ + middlewares: this.mws, + handler: handler, + }), + }); + } + + /** + * Compiles the middleware chain and handler into a single executable function. + * + * This method constructs a statically linked function chain by reducing all middleware + * and the final handler into one composed `runRoute` function. Each middleware receives + * a `next()` callback that invokes the next function in the chain. + * + * Additionally, the returned function ensures that `next()` can only be called once + * per middleware. If `next()` is invoked multiple times within the same middleware, + * a runtime `Error` is thrown, preventing unintended double-processing. + * + * Type safety is enforced at compile time: + * - If the final handler does not match the expected signature, a `TypeError` is thrown. + * - If any middleware does not conform to the middleware interface, a `TypeError` is thrown. + * + * @param route - A partial route object containing middleware and handler, + * excluding `matcher`, `method`, and `runRoute`. + * @returns A composed route execution function that takes a context object + * and returns a `Promise`. + * + * @throws {TypeError} If the handler or any middleware function is invalid. + * @throws {Error} If a middleware calls `next()` more than once during execution. + */ + private compile( + route: Omit< + IInternalRoute, + 'runRoute' | 'matcher' | 'method' + >, + ): ( + ctx: TContext, + ) => Promise { + if (!isHandler(route.handler)) { + throw new TypeError( + 'Route handler must be a function returning a Promise.', + ); + } + let composed = route.handler; + + for (let i = route.middlewares.length - 1; i >= 0; i--) { + if (!isMiddleware(route.middlewares[i])) { + throw new TypeError( + `Middleware at index ${i} is not a valid function.`, + ); + } + + const current = route.middlewares[i]; + const next = composed; + + composed = async (ctx: TContext): Promise => { + let called = false; + + return await current(ctx, async () => { + if (called) { + throw new Error( + `next() called multiple times in middleware at index ${i}`, + ); + } + called = true; + return await next(ctx); + }); + }; + } + + return composed; + } +} diff --git a/v0.2.1/src/Types/DeepPartial.ts b/v0.2.1/src/Types/DeepPartial.ts new file mode 100644 index 0000000..07c5132 --- /dev/null +++ b/v0.2.1/src/Types/DeepPartial.ts @@ -0,0 +1,4 @@ +export type DeepPartial = { + [P in keyof T]?: T[P] extends object ? DeepPartial + : T[P]; +}; diff --git a/v0.2.1/src/Types/Handler.ts b/v0.2.1/src/Types/Handler.ts new file mode 100644 index 0000000..11f28f8 --- /dev/null +++ b/v0.2.1/src/Types/Handler.ts @@ -0,0 +1,57 @@ +import type { IContext } from '../Interfaces/mod.ts'; + +/** + * Represents a final request handler responsible for producing an HTTP response. + * + * The handler is the terminal stage of the middleware pipeline and is responsible + * for processing the incoming request and generating the final `Response`. + * + * It receives the fully-typed request context, which includes the original request, + * parsed route parameters, query parameters, and any shared state populated by prior middleware. + * + * @template TContext The specific context type for this handler, including typed `state`, `params`, and `query`. + */ +type Handler = ( + ctx: TContext, +) => Promise; + +/** + * Represents a handler function with an associated name. + * + * This is useful for debugging, logging, or when you need to reference + * the handler by name in your application. + * + * @template TContext The specific context type for this handler, including typed `state`, `params`, and `query`. + */ +type NamedHandler = + & Handler + & { name?: string }; + +export type { NamedHandler as Handler }; + +/** + * Type guard to determine whether a given value is a valid `IHandler` function. + * + * This function checks whether the input is a function and whether it returns + * a `Promise` when called. Due to TypeScript's structural typing and + * the lack of runtime type information, only minimal runtime validation is possible. + * + * @param value - The value to test. + * @returns `true` if the value is a function that appears to conform to `IHandler`. + * + * @example + * ```ts + * const candidate = async (ctx: IContext) => new Response("ok"); + * if (isHandler(candidate)) { + * // candidate is now typed as IHandler + * } + * ``` + */ +export function isHandler( + value: unknown, +): value is Handler { + return ( + typeof value === 'function' && + value.length === 1 // ctx + ); +} diff --git a/v0.2.1/src/Types/HttpErrorHandler.ts b/v0.2.1/src/Types/HttpErrorHandler.ts new file mode 100644 index 0000000..8c62fae --- /dev/null +++ b/v0.2.1/src/Types/HttpErrorHandler.ts @@ -0,0 +1,28 @@ +import type { IContext } from '../Interfaces/mod.ts'; + +/** + * Defines a handler function for errors that occur during the execution + * of middleware or route handlers within the HTTP kernel. + * + * This function receives both the request context and the thrown error, + * and is responsible for producing an appropriate HTTP `Response`. + * + * Typical use cases include: + * - Mapping known error types to specific HTTP status codes. + * - Generating structured error responses (e.g. JSON error payloads). + * - Logging errors centrally with request metadata. + * + * The handler may return the response synchronously or asynchronously. + * + * @template TContext - The specific request context type, allowing typed access to route parameters, + * query parameters, and per-request state when formatting error responses. + * + * @param context - The active request context at the time the error occurred. + * @param error - The exception or error that was thrown during request processing. + * + * @returns A `Response` object or a `Promise` resolving to one, to be sent to the client. + */ +export type HttpErrorHandler = ( + context?: Partial, + error?: Error, +) => Promise | Response; diff --git a/v0.2.1/src/Types/HttpMethod.ts b/v0.2.1/src/Types/HttpMethod.ts new file mode 100644 index 0000000..d076523 --- /dev/null +++ b/v0.2.1/src/Types/HttpMethod.ts @@ -0,0 +1,52 @@ +/** + * A constant list of all supported HTTP methods according to RFC 7231 and RFC 5789. + * + * This array serves both as a runtime value list for validation + * and as the basis for deriving the `HttpMethod` union type. + * + * Note: The list is immutable and should not be modified at runtime. + */ +export const validHttpMethods = [ + 'GET', + 'POST', + 'PUT', + 'PATCH', + 'DELETE', + 'HEAD', + 'OPTIONS', +] as const; + +/** + * A union type representing all valid HTTP methods recognized by this application. + * + * This type is derived directly from the `validHttpMethods` constant, + * ensuring type safety and consistency between type system and runtime checks. + * + * Example: + * ```ts + * const method: HttpMethod = 'POST'; // βœ… valid + * const method: HttpMethod = 'FOO'; // ❌ Type error + * ``` + */ +export type HttpMethod = typeof validHttpMethods[number]; + +/** + * Type guard to verify whether a given value is a valid HTTP method. + * + * This function checks both the type and content of the value + * and is suitable for runtime validation of inputs (e.g., from HTTP requests). + * + * Example: + * ```ts + * if (isHttpMethod(input)) { + * // input is now typed as HttpMethod + * } + * ``` + * + * @param value - The value to test (typically a string from a request). + * @returns `true` if the value is a valid `HttpMethod`, otherwise `false`. + */ +export function isHttpMethod(value: unknown): value is HttpMethod { + return typeof value === 'string' && + validHttpMethods.includes(value as HttpMethod); +} diff --git a/v0.2.1/src/Types/HttpStatusCode.ts b/v0.2.1/src/Types/HttpStatusCode.ts new file mode 100644 index 0000000..2230d6f --- /dev/null +++ b/v0.2.1/src/Types/HttpStatusCode.ts @@ -0,0 +1,189 @@ +// Informational responses +/** Indicates that the request was received and the client can continue. */ +export const HTTP_100_CONTINUE = 100; +/** The server is switching protocols as requested by the client. */ +export const HTTP_101_SWITCHING_PROTOCOLS = 101; +/** The server has received and is processing the request, but no response is available yet. */ +export const HTTP_102_PROCESSING = 102; + +// Successful responses +/** The request has succeeded. */ +export const HTTP_200_OK = 200; +/** The request has succeeded and a new resource has been created as a result. */ +export const HTTP_201_CREATED = 201; +/** The request has been accepted for processing, but the processing is not complete. */ +export const HTTP_202_ACCEPTED = 202; +/** The server has successfully fulfilled the request and there is no content to send. */ +export const HTTP_204_NO_CONTENT = 204; + +// Redirection messages +/** The resource has been moved permanently to a new URI. */ +export const HTTP_301_MOVED_PERMANENTLY = 301; +/** The resource resides temporarily under a different URI. */ +export const HTTP_302_FOUND = 302; +/** Indicates that the resource has not been modified since the last request. */ +export const HTTP_304_NOT_MODIFIED = 304; + +// Client error responses +/** The server could not understand the request due to invalid syntax. */ +export const HTTP_400_BAD_REQUEST = 400; +/** The request requires user authentication. */ +export const HTTP_401_UNAUTHORIZED = 401; +/** The server understood the request but refuses to authorize it. */ +export const HTTP_403_FORBIDDEN = 403; +/** The server cannot find the requested resource. */ +export const HTTP_404_NOT_FOUND = 404; +/** The request method is known by the server but is not supported by the target resource. */ +export const HTTP_405_METHOD_NOT_ALLOWED = 405; +/** The request could not be completed due to a conflict with the current state of the resource. */ +export const HTTP_409_CONFLICT = 409; +/** The server understands the content type but was unable to process the contained instructions. */ +export const HTTP_422_UNPROCESSABLE_ENTITY = 422; +/** The user has sent too many requests in a given amount of time. */ +export const HTTP_429_TOO_MANY_REQUESTS = 429; + +// Server error responses +/** The server encountered an unexpected condition that prevented it from fulfilling the request. */ +export const HTTP_500_INTERNAL_SERVER_ERROR = 500; +/** The server does not support the functionality required to fulfill the request. */ +export const HTTP_501_NOT_IMPLEMENTED = 501; +/** The server, while acting as a gateway or proxy, received an invalid response from the upstream server. */ +export const HTTP_502_BAD_GATEWAY = 502; +/** The server is not ready to handle the request, often due to maintenance or overload. */ +export const HTTP_503_SERVICE_UNAVAILABLE = 503; +/** The server is acting as a gateway and cannot get a response in time. */ +export const HTTP_504_GATEWAY_TIMEOUT = 504; + +/** + * A constant list of supported HTTP status codes used by this application. + * + * These constants are grouped by category and used to construct the union type `HttpStatusCode`. + */ +export const validHttpStatusCodes = [ + // Informational + HTTP_100_CONTINUE, + HTTP_101_SWITCHING_PROTOCOLS, + HTTP_102_PROCESSING, + + // Successful + HTTP_200_OK, + HTTP_201_CREATED, + HTTP_202_ACCEPTED, + HTTP_204_NO_CONTENT, + + // Redirection + HTTP_301_MOVED_PERMANENTLY, + HTTP_302_FOUND, + HTTP_304_NOT_MODIFIED, + + // Client Errors + HTTP_400_BAD_REQUEST, + HTTP_401_UNAUTHORIZED, + HTTP_403_FORBIDDEN, + HTTP_404_NOT_FOUND, + HTTP_405_METHOD_NOT_ALLOWED, + HTTP_409_CONFLICT, + HTTP_422_UNPROCESSABLE_ENTITY, + HTTP_429_TOO_MANY_REQUESTS, + + // Server Errors + HTTP_500_INTERNAL_SERVER_ERROR, + HTTP_501_NOT_IMPLEMENTED, + HTTP_502_BAD_GATEWAY, + HTTP_503_SERVICE_UNAVAILABLE, + HTTP_504_GATEWAY_TIMEOUT, +] as const; + +/** + * A constant list of HTTP error codes that are commonly used in the application. + */ +export const validHttpErrorCodes = [ + // Client Errors + HTTP_400_BAD_REQUEST, + HTTP_401_UNAUTHORIZED, + HTTP_403_FORBIDDEN, + HTTP_404_NOT_FOUND, + HTTP_405_METHOD_NOT_ALLOWED, + HTTP_409_CONFLICT, + HTTP_422_UNPROCESSABLE_ENTITY, + HTTP_429_TOO_MANY_REQUESTS, + + // Server Errors + HTTP_500_INTERNAL_SERVER_ERROR, + HTTP_501_NOT_IMPLEMENTED, + HTTP_502_BAD_GATEWAY, + HTTP_503_SERVICE_UNAVAILABLE, + HTTP_504_GATEWAY_TIMEOUT, +] as const; + +/** + * Maps each supported HTTP status code to its standard status message. + * + * Useful for logging, diagnostics, or building custom error responses. + */ +export const HttpStatusTextMap: Record< + typeof validHttpStatusCodes[number], + string +> = { + [HTTP_100_CONTINUE]: 'Continue', + [HTTP_101_SWITCHING_PROTOCOLS]: 'Switching Protocols', + [HTTP_102_PROCESSING]: 'Processing', + + [HTTP_200_OK]: 'OK', + [HTTP_201_CREATED]: 'Created', + [HTTP_202_ACCEPTED]: 'Accepted', + [HTTP_204_NO_CONTENT]: 'No Content', + + [HTTP_301_MOVED_PERMANENTLY]: 'Moved Permanently', + [HTTP_302_FOUND]: 'Found', + [HTTP_304_NOT_MODIFIED]: 'Not Modified', + + [HTTP_400_BAD_REQUEST]: 'Bad Request', + [HTTP_401_UNAUTHORIZED]: 'Unauthorized', + [HTTP_403_FORBIDDEN]: 'Forbidden', + [HTTP_404_NOT_FOUND]: 'Not Found', + [HTTP_405_METHOD_NOT_ALLOWED]: 'Method Not Allowed', + [HTTP_409_CONFLICT]: 'Conflict', + [HTTP_422_UNPROCESSABLE_ENTITY]: 'Unprocessable Entity', + [HTTP_429_TOO_MANY_REQUESTS]: 'Too Many Requests', + + [HTTP_500_INTERNAL_SERVER_ERROR]: 'Internal Server Error', + [HTTP_501_NOT_IMPLEMENTED]: 'Not Implemented', + [HTTP_502_BAD_GATEWAY]: 'Bad Gateway', + [HTTP_503_SERVICE_UNAVAILABLE]: 'Service Unavailable', + [HTTP_504_GATEWAY_TIMEOUT]: 'Gateway Timeout', +}; + +/** + * A union type representing commonly used HTTP status codes. + * + * This type ensures consistency between runtime and type-level status code handling. + * + * Example: + * ```ts + * const status: HttpStatusCode = 404; // βœ… valid + * const status: HttpStatusCode = 418; // ❌ Type error (unless added to list) + * ``` + */ +export type HttpStatusCode = typeof validHttpStatusCodes[number]; + +/** + * Type guard to check whether a given value is a valid HTTP status code. + * + * This is useful for validating numeric values received from external input, + * ensuring they conform to known HTTP semantics. + * + * Example: + * ```ts + * if (isHttpStatusCode(value)) { + * // value is now typed as HttpStatusCode + * } + * ``` + * + * @param value - The numeric value to check. + * @returns `true` if the value is a recognized HTTP status code, otherwise `false`. + */ +export function isHttpStatusCode(value: unknown): value is HttpStatusCode { + return typeof value === 'number' && + validHttpStatusCodes.includes(value as HttpStatusCode); +} diff --git a/v0.2.1/src/Types/Middleware.ts b/v0.2.1/src/Types/Middleware.ts new file mode 100644 index 0000000..08e13bb --- /dev/null +++ b/v0.2.1/src/Types/Middleware.ts @@ -0,0 +1,51 @@ +import type { IContext } from '../Interfaces/IContext.ts'; + +/** + * Represents a middleware function in the HTTP request pipeline. + * + * Middleware is a core mechanism to intercept, observe, or modify the request lifecycle. + * It can be used for tasks such as logging, authentication, input validation, + * metrics collection, or response transformation. + * + * Each middleware receives a fully-typed request context and a `next()` function + * to invoke the next stage of the pipeline. Middleware may choose to short-circuit + * the pipeline by returning a `Response` early. + * + * @template TContext The specific context type for this middleware, including state, params, and query information. + */ +type Middleware = ( + ctx: TContext, + next: () => Promise, +) => Promise; + +/** + * Represents a middleware function with an associated name. + * + * This is useful for debugging, logging, or when you need to reference + * the middleware by name in your application. + * + * @template TContext The specific context type for this middleware, including state, params, and query information. + */ +type NamedMiddleware = + & Middleware + & { name?: string }; + +export type { NamedMiddleware as Middleware }; + +/** + * Type guard to verify whether a given value is a valid `IMiddleware` function. + * + * This guard checks whether the input is a function that accepts exactly two arguments. + * Note: This is a structural check and cannot fully guarantee the semantics of a middleware. + * + * @param value - The value to test. + * @returns `true` if the value is structurally a valid middleware function. + */ +export function isMiddleware( + value: unknown, +): value is Middleware { + return ( + typeof value === 'function' && + value.length === 2 // ctx, next + ); +} diff --git a/v0.2.1/src/Types/Params.ts b/v0.2.1/src/Types/Params.ts new file mode 100644 index 0000000..045e056 --- /dev/null +++ b/v0.2.1/src/Types/Params.ts @@ -0,0 +1,10 @@ +/** + * Represents route parameters parsed from dynamic segments in the URL path. + * + * This type is typically derived from route definitions with placeholders, + * such as `/users/:id`, which would yield `{ id: "123" }`. + * + * All values are strings and should be considered read-only, as they are + * extracted by the router and should not be modified by application code. + */ +export type Params = Record; diff --git a/v0.2.1/src/Types/Query.ts b/v0.2.1/src/Types/Query.ts new file mode 100644 index 0000000..b9453fe --- /dev/null +++ b/v0.2.1/src/Types/Query.ts @@ -0,0 +1,12 @@ +/** + * Represents the parsed query parameters from the request URL. + * + * Query parameters originate from the URL search string (e.g. `?filter=active&tags=ts&tags=deno`) + * and may contain single or multiple values per key. + * + * All values are expressed as strings or arrays of strings, depending on how often + * the key occurs. This structure preserves the raw semantics of the query. + * + * For normalized single-value access, prefer custom DTOs or wrapper utilities. + */ +export type Query = Record; diff --git a/v0.2.1/src/Types/RegisterRoute.ts b/v0.2.1/src/Types/RegisterRoute.ts new file mode 100644 index 0000000..c661e93 --- /dev/null +++ b/v0.2.1/src/Types/RegisterRoute.ts @@ -0,0 +1,16 @@ +import type { IContext } from '../Interfaces/IContext.ts'; +import type { IInternalRoute } from '../Interfaces/mod.ts'; + +/** + * A type alias for the internal route registration function used by the `HttpKernel`. + * + * This function accepts a fully constructed internal route, including method, matcher, + * middleware chain, and final handler, and registers it for dispatching. + * + * Typically passed into `RouteBuilder` instances to enable fluent API chaining. + * + * @template TContext The context type associated with the route being registered. + */ +export type RegisterRoute = ( + route: IInternalRoute, +) => void; diff --git a/v0.2.1/src/Types/ResponseDecorator.ts b/v0.2.1/src/Types/ResponseDecorator.ts new file mode 100644 index 0000000..4291a6d --- /dev/null +++ b/v0.2.1/src/Types/ResponseDecorator.ts @@ -0,0 +1,30 @@ +import type { IContext } from '../Interfaces/mod.ts'; + +/** + * A function that modifies or enriches an outgoing HTTP response before it is returned to the client. + * + * This decorator can be used to inject headers (e.g., CORS, security), apply global transformations, + * or wrap responses for logging, analytics, or debugging purposes. + * + * It is called exactly once at the end of the middleware/handler pipeline, + * allowing central response customization without interfering with business logic. + * + * @param res - The original `Response` object produced by the route handler or middleware chain. + * @returns A modified or wrapped `Response` object to be sent back to the client. + * + * @example + * ```ts + * const addCors: ResponseDecorator = (res) => { + * const headers = new Headers(res.headers); + * headers.set("Access-Control-Allow-Origin", "*"); + * return new Response(res.body, { + * status: res.status, + * headers, + * }); + * }; + * ``` + */ +export type ResponseDecorator = ( + res: Response, + ctx: TContext, +) => Response; diff --git a/v0.2.1/src/Types/State.ts b/v0.2.1/src/Types/State.ts new file mode 100644 index 0000000..b512d9e --- /dev/null +++ b/v0.2.1/src/Types/State.ts @@ -0,0 +1,9 @@ +/** + * Represents the per-request state object shared across the middleware pipeline. + * + * This type defines the base structure for custom state definitions, + * which can be extended with concrete fields like user data, request metadata, etc. + * + * Custom `TState` types must extend this base to ensure compatibility. + */ +export type State = Record; diff --git a/v0.2.1/src/Types/__tests__/HttpMethod.test.ts b/v0.2.1/src/Types/__tests__/HttpMethod.test.ts new file mode 100644 index 0000000..9559d79 --- /dev/null +++ b/v0.2.1/src/Types/__tests__/HttpMethod.test.ts @@ -0,0 +1,40 @@ +import { assertEquals } from 'https://deno.land/std/assert/mod.ts'; +import { isHttpMethod, validHttpMethods } from '../HttpMethod.ts'; + +Deno.test('isHttpMethod: returns true for all valid methods', () => { + for (const method of validHttpMethods) { + const result = isHttpMethod(method); + assertEquals(result, true, `Expected "${method}" to be valid`); + } +}); + +Deno.test('isHttpMethod: returns false for lowercase or unknown strings', () => { + const invalid = [ + 'get', + 'post', + 'FETCH', + 'TRACE', + 'CONNECT', + 'INVALID', + '', + ' ', + ]; + + for (const method of invalid) { + const result = isHttpMethod(method); + assertEquals(result, false, `Expected "${method}" to be invalid`); + } +}); + +Deno.test('isHttpMethod: returns false for non-string inputs', () => { + const invalidInputs = [null, undefined, 123, {}, [], true, Symbol('GET')]; + + for (const input of invalidInputs) { + const result = isHttpMethod(input); + assertEquals( + result, + false, + `Expected non-string input to be invalid: ${String(input)}`, + ); + } +}); diff --git a/v0.2.1/src/Types/__tests__/HttpStatusCode.test.ts b/v0.2.1/src/Types/__tests__/HttpStatusCode.test.ts new file mode 100644 index 0000000..3042ff1 --- /dev/null +++ b/v0.2.1/src/Types/__tests__/HttpStatusCode.test.ts @@ -0,0 +1,35 @@ +// src/Types/__tests__/HttpStatusCode.test.ts +import { assertEquals } from 'https://deno.land/std@0.204.0/assert/mod.ts'; +import { isHttpStatusCode, validHttpStatusCodes } from '../HttpStatusCode.ts'; + +Deno.test('isHttpStatusCode: returns true for all valid status codes', () => { + for (const code of validHttpStatusCodes) { + assertEquals( + isHttpStatusCode(code), + true, + `Expected ${code} to be valid`, + ); + } +}); + +Deno.test('isHttpStatusCode: returns false for invalid status codes', () => { + const invalidInputs = [99, 600, 1234, -1, 0, 999]; + for (const val of invalidInputs) { + assertEquals( + isHttpStatusCode(val), + false, + `Expected ${val} to be invalid`, + ); + } +}); + +Deno.test('isHttpStatusCode: returns false for non-numeric values', () => { + const invalid = ['200', null, undefined, {}, [], true]; + for (const val of invalid) { + assertEquals( + isHttpStatusCode(val), + false, + `Expected ${val} to be invalid`, + ); + } +}); diff --git a/v0.2.1/src/Types/mod.ts b/v0.2.1/src/Types/mod.ts new file mode 100644 index 0000000..c159495 --- /dev/null +++ b/v0.2.1/src/Types/mod.ts @@ -0,0 +1,45 @@ +// deno-coverage-ignore-file + +export type { DeepPartial } from './DeepPartial.ts'; +export { isHandler } from './Handler.ts'; +export type { Handler } from './Handler.ts'; +export type { HttpErrorHandler } from './HttpErrorHandler.ts'; +export { isHttpMethod, validHttpMethods } from './HttpMethod.ts'; +export type { HttpMethod } from './HttpMethod.ts'; +export { + HTTP_100_CONTINUE, + HTTP_101_SWITCHING_PROTOCOLS, + HTTP_102_PROCESSING, + HTTP_200_OK, + HTTP_201_CREATED, + HTTP_202_ACCEPTED, + HTTP_204_NO_CONTENT, + HTTP_301_MOVED_PERMANENTLY, + HTTP_302_FOUND, + HTTP_304_NOT_MODIFIED, + HTTP_400_BAD_REQUEST, + HTTP_401_UNAUTHORIZED, + HTTP_403_FORBIDDEN, + HTTP_404_NOT_FOUND, + HTTP_405_METHOD_NOT_ALLOWED, + HTTP_409_CONFLICT, + HTTP_422_UNPROCESSABLE_ENTITY, + HTTP_429_TOO_MANY_REQUESTS, + HTTP_500_INTERNAL_SERVER_ERROR, + HTTP_501_NOT_IMPLEMENTED, + HTTP_502_BAD_GATEWAY, + HTTP_503_SERVICE_UNAVAILABLE, + HTTP_504_GATEWAY_TIMEOUT, + HttpStatusTextMap, + isHttpStatusCode, + validHttpErrorCodes, + validHttpStatusCodes, +} from './HttpStatusCode.ts'; +export type { HttpStatusCode } from './HttpStatusCode.ts'; +export { isMiddleware } from './Middleware.ts'; +export type { Middleware } from './Middleware.ts'; +export type { Params } from './Params.ts'; +export type { Query } from './Query.ts'; +export type { RegisterRoute } from './RegisterRoute.ts'; +export type { ResponseDecorator } from './ResponseDecorator.ts'; +export type { State } from './State.ts'; diff --git a/v0.2.1/src/Utils/__tests__/createEmptyContext.test.ts b/v0.2.1/src/Utils/__tests__/createEmptyContext.test.ts new file mode 100644 index 0000000..2b3acfe --- /dev/null +++ b/v0.2.1/src/Utils/__tests__/createEmptyContext.test.ts @@ -0,0 +1,28 @@ +import { assertEquals } from 'https://deno.land/std/assert/mod.ts'; +import { createEmptyContext } from '../createEmptyContext.ts'; +import type { IContext } from '../../Interfaces/mod.ts'; + +Deno.test('createEmptyContext: returns default-initialized context', () => { + const request = new Request('http://localhost'); + const ctx = createEmptyContext(request); + + assertEquals(ctx.req, request); + assertEquals(ctx.params, {}); + assertEquals(ctx.query, {}); + assertEquals(ctx.state, {}); +}); + +Deno.test('createEmptyContext: preserves generic type compatibility', () => { + interface MyContext + extends + IContext<{ userId: string }, { id: string }, { verbose: string }> {} + + const req = new Request('http://localhost'); + const ctx = createEmptyContext(req); + + // All properties exist and are empty + assertEquals(ctx.params, {} as MyContext['params']); + assertEquals(ctx.query, {} as MyContext['query']); + assertEquals(ctx.state, {} as MyContext['state']); + assertEquals(ctx.req, req); +}); diff --git a/v0.2.1/src/Utils/__tests__/createRouteMatcher.test.ts b/v0.2.1/src/Utils/__tests__/createRouteMatcher.test.ts new file mode 100644 index 0000000..8338014 --- /dev/null +++ b/v0.2.1/src/Utils/__tests__/createRouteMatcher.test.ts @@ -0,0 +1,118 @@ +import { + assert, + assertEquals, + assertStrictEquals, +} from 'https://deno.land/std/assert/mod.ts'; +import type { IRouteDefinition } from '../../Interfaces/mod.ts'; +import { createRouteMatcher } from '../../mod.ts'; + +// Dummy request +const dummyRequest = new Request('http://localhost'); + +Deno.test('createRouteMatcher: static route matches and extracts params', () => { + const def: IRouteDefinition = { method: 'GET', path: '/users/:id' }; + const matcher = createRouteMatcher(def); + + const result = matcher(new URL('http://localhost/users/42'), dummyRequest); + + assert(result); + assertEquals(result.params, { id: '42' }); +}); + +Deno.test('createRouteMatcher: static route with multiple params', () => { + const def: IRouteDefinition = { method: 'GET', path: '/repo/:owner/:name' }; + const matcher = createRouteMatcher(def); + + const result = matcher( + new URL('http://localhost/repo/max/wiki'), + dummyRequest, + ); + + assert(result); + assertEquals(result.params, { owner: 'max', name: 'wiki' }); +}); + +Deno.test('createRouteMatcher: static route does not match wrong path', () => { + const def: IRouteDefinition = { method: 'GET', path: '/users/:id' }; + const matcher = createRouteMatcher(def); + + const result = matcher(new URL('http://localhost/posts/42'), dummyRequest); + + assertStrictEquals(result, null); +}); + +Deno.test('createRouteMatcher: uses custom matcher if provided', () => { + const def: IRouteDefinition = { + method: 'GET', + matcher: (url) => url.pathname === '/ping' ? { params: {} } : null, + }; + const matcher = createRouteMatcher(def); + + const result = matcher(new URL('http://localhost/ping'), dummyRequest); + assert(result); + assertEquals(result.params, {}); +}); + +Deno.test('createRouteMatcher: extracts single query param', () => { + const def: IRouteDefinition = { method: 'GET', path: '/search' }; + const matcher = createRouteMatcher(def); + + const url = new URL('http://localhost/search?q=deno'); + const result = matcher(url, dummyRequest); + + assert(result); + assertEquals(result.params, {}); // no path params + assertEquals(result.query, { q: 'deno' }); // single key β†’ string +}); + +Deno.test('createRouteMatcher: duplicate query keys become array', () => { + const def: IRouteDefinition = { method: 'GET', path: '/tags' }; + const matcher = createRouteMatcher(def); + + const url = new URL('http://localhost/tags?tag=js&tag=ts&tag=deno'); + const result = matcher(url, dummyRequest); + + assert(result); + assertEquals(result.params, {}); + assertEquals(result.query, { tag: ['js', 'ts', 'deno'] }); // multi β†’ string[] +}); + +Deno.test('createRouteMatcher: mix of single and duplicate keys', () => { + const def: IRouteDefinition = { method: 'GET', path: '/filter/:type' }; + const matcher = createRouteMatcher(def); + + const url = new URL('http://localhost/filter/repo?lang=ts&lang=js&page=2'); + const result = matcher(url, dummyRequest); + + assert(result); + assertEquals(result.params, { type: 'repo' }); + assertEquals(result.query, { + lang: ['ts', 'js'], // duplicated + page: '2', // single + }); +}); + +Deno.test('createRouteMatcher: no query parameters returns empty object', () => { + const def: IRouteDefinition = { method: 'GET', path: '/info' }; + const matcher = createRouteMatcher(def); + + const url = new URL('http://localhost/info'); + const result = matcher(url, dummyRequest); + + assert(result); + assertEquals(result.params, {}); + assertEquals(result.query, {}); // empty +}); + +Deno.test('createRouteMatcher: retains array order of duplicate keys', () => { + const def: IRouteDefinition = { method: 'GET', path: '/order' }; + const matcher = createRouteMatcher(def); + + const url = new URL( + 'http://localhost/order?item=first&item=second&item=third', + ); + const result = matcher(url, dummyRequest); + + assert(result); + assertEquals(result.query?.item, ['first', 'second', 'third']); +}); diff --git a/v0.2.1/src/Utils/__tests__/normalizeError.test.ts b/v0.2.1/src/Utils/__tests__/normalizeError.test.ts new file mode 100644 index 0000000..0dbb8d9 --- /dev/null +++ b/v0.2.1/src/Utils/__tests__/normalizeError.test.ts @@ -0,0 +1,35 @@ +import { + assertEquals, + assertInstanceOf, +} from 'https://deno.land/std/assert/mod.ts'; +import { normalizeError } from '../normalizeError.ts'; + +Deno.test('normalizeError: preserves Error instances', () => { + const original = new Error('original'); + const result = normalizeError(original); + + assertInstanceOf(result, Error); + assertEquals(result, original); +}); + +Deno.test('normalizeError: converts string to Error', () => { + const result = normalizeError('something went wrong'); + + assertInstanceOf(result, Error); + assertEquals(result.message, 'something went wrong'); +}); + +Deno.test('normalizeError: converts number to Error', () => { + const result = normalizeError(404); + + assertInstanceOf(result, Error); + assertEquals(result.message, '404'); +}); + +Deno.test('normalizeError: converts plain object to Error', () => { + const input = { error: true, msg: 'Invalid' }; + const result = normalizeError(input); + + assertInstanceOf(result, Error); + assertEquals(result.message, JSON.stringify(input)); +}); diff --git a/v0.2.1/src/Utils/createEmptyContext.ts b/v0.2.1/src/Utils/createEmptyContext.ts new file mode 100644 index 0000000..18113dd --- /dev/null +++ b/v0.2.1/src/Utils/createEmptyContext.ts @@ -0,0 +1,30 @@ +import type { IContext } from '../Interfaces/mod.ts'; +import type { Params, Query, State } from '../Types/mod.ts'; + +/** + * Creates an empty request context suitable for fallback handlers (e.g., 404 or 500 errors). + * + * This function is primarily intended for cases where no route matched, but a context-compatible + * object is still needed to invoke a generic error handler. All context fields are initialized + * to their default empty values (`{}` for params, query, and state). + * + * @template TContext - The expected context type, typically extending `IContext`. + * @param req - The original HTTP request object from `Deno.serve()`. + * @returns A minimal context object compatible with `TContext`. + * + * @example + * ```ts + * const ctx = createEmptyContext(request); + * return httpErrorHandlers[404](ctx); + * ``` + */ +export function createEmptyContext( + req: Request, +): TContext { + return { + req, + params: {} as Params, + query: {} as Query, + state: {} as State, + } as TContext; +} diff --git a/v0.2.1/src/Utils/createRouteMatcher.ts b/v0.2.1/src/Utils/createRouteMatcher.ts new file mode 100644 index 0000000..dce9185 --- /dev/null +++ b/v0.2.1/src/Utils/createRouteMatcher.ts @@ -0,0 +1,54 @@ +// createRouteMatcher.ts + +import { + type IRouteDefinition, + type IRouteMatch, + type IRouteMatcher, + isDynamicRouteDefinition, +} from '../Interfaces/mod.ts'; +import type { Params, Query } from '../Types/mod.ts'; + +/** + * Transforms a route definition into a matcher using Deno's URLPattern API. + * + * @param def - Static path pattern or custom matcher. + * @returns IRouteMatcher that returns `{ params, query }` or `null`. + */ +export function createRouteMatcher( + def: IRouteDefinition, +): IRouteMatcher { + // 1. Allow users to provide their own matcher + if (isDynamicRouteDefinition(def)) { + return def.matcher; + } + + // 2. Build URLPattern; supports :id, *wildcards, regex groups, etc. + const pattern = new URLPattern({ pathname: def.path }); + + // 3. The actual matcher closure + return (url: URL): IRouteMatch | null => { + const result = pattern.exec(url); + + // 3a. Path did not match + if (!result) return null; + + // 3b. Extract route params + const params: Params = {}; + for (const [key, value] of Object.entries(result.pathname.groups)) { + if (value) { + params[key] = value; + } + } + + // 3c. Extract query parameters – keep duplicates as arrays + const query: Query = {}; + for (const key of url.searchParams.keys()) { + const values = url.searchParams.getAll(key); // β†’ string[] + query[key] = values.length === 1 + ? values[0] // single β†’ "foo" + : values; // multi β†’ ["foo","bar"] + } + + return { params, query }; + }; +} diff --git a/v0.2.1/src/Utils/mod.ts b/v0.2.1/src/Utils/mod.ts new file mode 100644 index 0000000..2e21a1d --- /dev/null +++ b/v0.2.1/src/Utils/mod.ts @@ -0,0 +1,5 @@ +// deno-coverage-ignore-file + +export { createEmptyContext } from './createEmptyContext.ts'; +export { createRouteMatcher } from './createRouteMatcher.ts'; +export { normalizeError } from './normalizeError.ts'; diff --git a/v0.2.1/src/Utils/normalizeError.ts b/v0.2.1/src/Utils/normalizeError.ts new file mode 100644 index 0000000..71fac86 --- /dev/null +++ b/v0.2.1/src/Utils/normalizeError.ts @@ -0,0 +1,30 @@ +/** + * Normalizes any thrown value to a proper `Error` instance. + * + * This is useful when handling unknown thrown values that may be: + * - strings (e.g. `throw "oops"`) + * - numbers (e.g. `throw 404`) + * - objects that are not instances of `Error` + * + * Ensures that downstream error handling logic always receives a consistent `Error` object. + * + * @param unknownError - Any value that might have been thrown. + * @returns A valid `Error` instance wrapping the original input. + * + * @example + * ```ts + * try { + * throw "something went wrong"; + * } catch (e) { + * const err = normalizeError(e); + * console.error(err.message); // "something went wrong" + * } + * ``` + */ +export function normalizeError(unknownError: unknown): Error { + return unknownError instanceof Error ? unknownError : new Error( + typeof unknownError === 'string' + ? unknownError + : JSON.stringify(unknownError), + ); +} diff --git a/v0.2.1/src/__bench__/HttpKernel.bench.ts b/v0.2.1/src/__bench__/HttpKernel.bench.ts new file mode 100644 index 0000000..d08a4bf --- /dev/null +++ b/v0.2.1/src/__bench__/HttpKernel.bench.ts @@ -0,0 +1,87 @@ +import type { IRouteDefinition } from '../Interfaces/mod.ts'; +import { HttpKernel } from '../mod.ts'; + +const CONCURRENT_REQUESTS = 10000; + +// Deno.bench('Simple request', async (b) => { +// const kernel = new HttpKernel(); + +// const def: IRouteDefinition = { method: 'GET', path: '/hello' }; +// kernel.route(def).handle((_ctx) => { +// return Promise.resolve(new Response('OK', { status: 200 })); +// }); +// b.start(); +// await kernel.handle( +// new Request('http://localhost/hello', { method: 'GET' }), +// ); +// b.end(); +// }); + +Deno.bench('Simple request (parallel)', async (b) => { + const kernel = new HttpKernel(); + + const def: IRouteDefinition = { method: 'GET', path: '/hello' }; + kernel.route(def).handle((_ctx) => { + return Promise.resolve(new Response('OK', { status: 200 })); + }); + + const requests = Array.from( + { length: CONCURRENT_REQUESTS }, + () => + kernel.handle( + new Request('http://localhost/hello', { method: 'GET' }), + ), + ); + + b.start(); + await Promise.all(requests); + b.end(); +}); + +// Deno.bench('Complex request', async (b) => { +// const kernel = new HttpKernel(); + +// kernel.route({ method: 'GET', path: '/test' }) +// .middleware(async (_ctx, next) => { +// return await next(); +// }) +// .middleware(async (_ctx, next) => { +// return await next(); +// }) +// .handle((_ctx) => { +// return Promise.resolve(new Response('done')); +// }); + +// b.start(); +// await kernel.handle( +// new Request('http://localhost/test', { method: 'GET' }), +// ); +// b.end(); +// }); + +Deno.bench('Complex request (parallel)', async (b) => { + const kernel = new HttpKernel(); + + kernel.route({ method: 'GET', path: '/test' }) + .middleware(async (_ctx, next) => { + return await next(); + }) + .middleware(async (_ctx, next) => { + return await next(); + }) + .handle((_ctx) => { + return Promise.resolve(new Response('done')); + }); + + const requests = Array.from( + { length: CONCURRENT_REQUESTS }, + () => + kernel.handle( + new Request('http://localhost/test', { method: 'GET' }), + ), + ); + + b.start(); + await Promise.all(requests); + b.end(); +}); diff --git a/v0.2.1/src/__tests__/HttpKernel.test.ts b/v0.2.1/src/__tests__/HttpKernel.test.ts new file mode 100644 index 0000000..2348046 --- /dev/null +++ b/v0.2.1/src/__tests__/HttpKernel.test.ts @@ -0,0 +1,185 @@ +import { + assertEquals, + assertThrows, +} from 'https://deno.land/std@0.204.0/assert/mod.ts'; +import { HttpKernel } from '../HttpKernel.ts'; +import type { IRouteDefinition } from '../Interfaces/mod.ts'; + +Deno.test('HttpKernel: matches static route and executes handler', async () => { + const kernel = new HttpKernel(); + + const def: IRouteDefinition = { method: 'GET', path: '/hello' }; + let called = false; + + kernel.route(def).handle((_ctx) => { + called = true; + return Promise.resolve(new Response('OK', { status: 200 })); + }); + + const res = await kernel.handle( + new Request('http://localhost/hello', { method: 'GET' }), + ); + assertEquals(res.status, 200); + assertEquals(await res.text(), 'OK'); + assertEquals(called, true); +}); + +Deno.test('HttpKernel: supports dynamic matcher', async () => { + const kernel = new HttpKernel(); + const def: IRouteDefinition = { + method: 'GET', + matcher: (url) => url.pathname === '/dyn' ? { params: {} } : null, + }; + + kernel.route(def).handle((_ctx) => + Promise.resolve(new Response('Dyn', { status: 200 })) + ); + + const res = await kernel.handle(new Request('http://localhost/dyn')); + assertEquals(res.status, 200); + assertEquals(await res.text(), 'Dyn'); +}); + +Deno.test('HttpKernel: calls middleware in order and passes to handler', async () => { + const kernel = new HttpKernel(); + const calls: string[] = []; + + kernel.route({ method: 'GET', path: '/test' }) + .middleware(async (_ctx, next) => { + calls.push('mw1'); + return await next(); + }) + .middleware(async (_ctx, next) => { + calls.push('mw2'); + return await next(); + }) + .handle((_ctx) => { + calls.push('handler'); + return Promise.resolve(new Response('done')); + }); + + const res = await kernel.handle( + new Request('http://localhost/test', { method: 'GET' }), + ); + assertEquals(await res.text(), 'done'); + assertEquals(calls, ['mw1', 'mw2', 'handler']); +}); + +Deno.test('HttpKernel: middleware short-circuits pipeline', async () => { + const kernel = new HttpKernel(); + const calls: string[] = []; + + kernel.route({ method: 'GET', path: '/stop' }) + .middleware((_ctx, _next) => { + calls.push('mw1'); + return Promise.resolve(new Response('blocked', { status: 403 })); + }) + .middleware((_ctx, _next) => { + calls.push('mw2'); + return Promise.resolve(new Response('should-not-call')); + }) + .handle((_ctx) => { + calls.push('handler'); + return Promise.resolve(new Response('ok')); + }); + + const res = await kernel.handle( + new Request('http://localhost/stop', { method: 'GET' }), + ); + assertEquals(res.status, 403); + assertEquals(await res.text(), 'blocked'); + assertEquals(calls, ['mw1']); +}); + +Deno.test('HttpKernel: invalid middleware or handler signature throws at compile time', () => { + const kernel = new HttpKernel(); + + // Middleware with wrong signature (missing ctx, next) + assertThrows( + () => { + kernel.route({ method: 'GET', path: '/bad-mw' }) + // @ts-expect-error invalid middleware + .middleware(() => new Response('invalid')) + .handle((_ctx) => Promise.resolve(new Response('ok'))); + }, + TypeError, + 'Middleware at index 0 is not a valid function.', + ); + + // Handler with wrong signature (no ctx) + assertThrows( + () => { + kernel.route({ method: 'GET', path: '/bad-handler' }) + .middleware(async (_ctx, next) => await next()) + // @ts-expect-error invalid handler + .handle(() => new Response('invalid')); + }, + TypeError, + 'Route handler must be a function returning a Promise.', + ); +}); + +Deno.test('HttpKernel: 404 for unmatched route', async () => { + const kernel = new HttpKernel(); + const res = await kernel.handle(new Request('http://localhost/nothing')); + assertEquals(res.status, 404); +}); + +Deno.test('HttpKernel: skips route with wrong method', async () => { + const kernel = new HttpKernel(); + + kernel.route({ method: 'POST', path: '/only-post' }) + .handle((_ctx) => Promise.resolve(new Response('nope'))); + + const res = await kernel.handle( + new Request('http://localhost/only-post', { method: 'GET' }), + ); + assertEquals(res.status, 404); +}); + +Deno.test('HttpKernel: throws on next() called twice', async () => { + const kernel = new HttpKernel(); + + kernel.route({ method: 'GET', path: '/bad' }) + .middleware(async (_ctx, next) => { + await next(); + await next(); // ❌ + return new Response('should never reach'); + }) + .handle((_ctx) => Promise.resolve(new Response('OK'))); + + const res = await kernel.handle(new Request('http://localhost/bad')); + assertEquals(res.status, 500); + assertEquals(await res.text(), 'Internal Server Error'); +}); + +Deno.test('HttpKernel: handler throws β†’ error propagates', async () => { + const kernel = new HttpKernel(); + + kernel.route({ method: 'GET', path: '/throw' }) + .handle((_ctx) => { + throw new Error('fail!'); + }); + + const res = await kernel.handle(new Request('http://localhost/throw')); + assertEquals(res.status, 500); + assertEquals(await res.text(), 'Internal Server Error'); +}); + +Deno.test('HttpKernel: returns 500 if no handler or middleware defined', async () => { + const kernel = new HttpKernel(); + + // Force-manual Registrierung mit `handler: undefined` + // Umgehen des Builders zur Simulation dieses Edge-Cases + kernel['routes'].push({ + method: 'GET', + matcher: (url) => url.pathname === '/fail' ? { params: {} } : null, + middlewares: [], + // @ts-expect-error absichtlich ungΓΌltiger Handler + handler: undefined, + }); + + const res = await kernel.handle(new Request('http://localhost/fail')); + assertEquals(res.status, 500); + assertEquals(await res.text(), 'Internal Server Error'); +}); diff --git a/v0.2.1/src/__tests__/RouteBuilder.test.ts b/v0.2.1/src/__tests__/RouteBuilder.test.ts new file mode 100644 index 0000000..d54fdbe --- /dev/null +++ b/v0.2.1/src/__tests__/RouteBuilder.test.ts @@ -0,0 +1,140 @@ +import { + assert, + assertEquals, + assertNotEquals, + assertThrows, +} from 'https://deno.land/std@0.204.0/assert/mod.ts'; +import type { IInternalRoute, IRouteDefinition } from '../Interfaces/mod.ts'; +import { RouteBuilder } from '../mod.ts'; +import type { Handler, Middleware } from '../Types/mod.ts'; + +// Dummy objects +// deno-lint-ignore require-await +const dummyHandler: Handler = async (_) => new Response('ok'); +// deno-lint-ignore require-await +const wrongHandler: Handler = async () => new Response('ok'); // Wrong signature, no ctx +const dummyMiddleware: Middleware = async (_, next) => await next(); +// deno-lint-ignore require-await +const wrongMiddleware: Middleware = async () => new Response('ok'); // Wrong signature, no ctx, next +const dummyDef: IRouteDefinition = { method: 'GET', path: '/hello' }; +const dummyMatcher = () => ({ params: {} }); + +Deno.test('middleware: throws if middleware signature is wrong', () => { + const builder = new RouteBuilder(() => {}, dummyDef); + assertThrows( + () => builder.middleware(wrongMiddleware).handle(dummyHandler), + TypeError, + 'Middleware at index 0 is not a valid function.', + ); +}); + +Deno.test('middleware: single middleware is registered correctly', () => { + let registered: IInternalRoute | null = null as IInternalRoute | null; + + const builder = new RouteBuilder((r) => registered = r, dummyDef) + .middleware(dummyMiddleware); + + builder.handle(dummyHandler); + + assert(registered); + assertEquals(registered?.middlewares.length, 1); + assertEquals(registered?.middlewares[0], dummyMiddleware); +}); + +Deno.test('middleware: middleware is chained immutably', () => { + const builder1 = new RouteBuilder(() => {}, dummyDef); + const builder2 = builder1.middleware(dummyMiddleware); + + assertNotEquals(builder1, builder2); +}); + +Deno.test('middleware: preserves order of middleware', () => { + const mw1: Middleware = async (_, next) => await next(); + const mw2: Middleware = async (_, next) => await next(); + + let result: IInternalRoute | null = null as IInternalRoute | null; + + const builder = new RouteBuilder((r) => result = r, dummyDef) + .middleware(mw1) + .middleware(mw2); + + builder.handle(dummyHandler); + + assert(result); + assertEquals(result!.middlewares, [mw1, mw2]); +}); + +Deno.test('handle: throws if handler signature is wrong', () => { + const builder = new RouteBuilder(() => {}, dummyDef); + assertThrows( + () => builder.handle(wrongHandler), + TypeError, + 'Route handler must be a function returning a Promise.', + ); +}); + +Deno.test('handle: uppercases method', () => { + let result: IInternalRoute | null = null as IInternalRoute | null; + + new RouteBuilder((r) => result = r, { method: 'POST', path: '/x' }) + .handle(dummyHandler); + + assertEquals(result?.method, 'POST'); +}); + +Deno.test('handle: works with no middleware', async () => { + let route: IInternalRoute | null = null as IInternalRoute | null; + + const builder = new RouteBuilder((r) => route = r, dummyDef); + builder.handle(dummyHandler); + + assert(route); + assertEquals(route?.middlewares.length, 0); + + const request = new Request('http://localhost'); + + const res1 = await route?.handler({ + req: request, + params: {}, + state: {}, + query: {}, + }); + const res2 = await dummyHandler({ + req: request, + params: {}, + state: {}, + query: {}, + }); + + assertEquals(res1?.status, res2?.status); + assertEquals(await res1?.text(), await res2?.text()); +}); + +Deno.test('handle: uses custom matcher factory', () => { + let called = false; + + const factory = (_def: IRouteDefinition) => { + called = true; + return dummyMatcher; + }; + + let route: IInternalRoute | null = null as IInternalRoute | null; + + new RouteBuilder((r) => route = r, dummyDef, [], factory).handle( + dummyHandler, + ); + + assert(called); + assert(route); + assertEquals(route!.matcher, dummyMatcher); +}); + +Deno.test('handle: throws if matcher factory throws', () => { + const faultyFactory = () => { + throw new Error('matcher fail'); + }; + + const builder = new RouteBuilder(() => {}, dummyDef, [], faultyFactory); + + assertThrows(() => builder.handle(dummyHandler), Error, 'matcher fail'); +}); diff --git a/v0.2.1/src/mod.ts b/v0.2.1/src/mod.ts new file mode 100644 index 0000000..f93f7e5 --- /dev/null +++ b/v0.2.1/src/mod.ts @@ -0,0 +1,16 @@ +// deno-coverage-ignore-file +export { HttpKernel } from './HttpKernel.ts'; +export { RouteBuilder } from './RouteBuilder.ts'; +export { createRouteMatcher } from './Utils/createRouteMatcher.ts'; + +// Errors +export * from './Errors/mod.ts'; + +// Interfaces +export * from './Interfaces/mod.ts'; + +// Types +export * from './Types/mod.ts'; + +// Utils +export * from './Utils/mod.ts';