CI: Update Pages (2025-11-23 11:20:49)

This commit is contained in:
2025-11-23 11:20:49 +00:00
commit 24771db506
138 changed files with 7472 additions and 0 deletions

43
v0.1.0/CHANGELOG.md Normal file
View File

@@ -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))

1
v0.1.0/VERSION Normal file
View File

@@ -0,0 +1 @@
0.1.0

104
v0.1.0/cliff.toml Normal file
View File

@@ -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 = '<GITEA_URL>', 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() -%}
<GITEA_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}](<REPO>/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 = "<!-- 0 -->🚀 Features" },
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
{ message = "^doc", group = "<!-- 3 -->📚 Documentation" },
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
{ message = "^test", group = "<!-- 6 -->🧪 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 = "<!-- 7 -->⚙️ Miscellaneous Tasks" },
{ body = ".*security", group = "<!-- 8 -->🛡️ Security" },
{ message = "^revert", group = "<!-- 9 -->◀️ Revert" },
{ message = ".*", group = "<!-- 10 -->💼 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"

32
v0.1.0/deno.jsonc Normal file
View File

@@ -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"
}

155
v0.1.0/deno.lock generated Normal file
View File

@@ -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"
}
}

View File

@@ -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;
}
}

3
v0.1.0/src/Errors/mod.ts Normal file
View File

@@ -0,0 +1,3 @@
// deno-coverage-ignore-file
export { InvalidHttpMethodError } from './InvalidHttpMethodError.ts';

199
v0.1.0/src/HttpKernel.ts Normal file
View File

@@ -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<TContext extends IContext = IContext>
implements IHttpKernel<TContext> {
private cfg: IHttpKernelConfig<TContext>;
/**
* The list of registered route definitions, including method, matcher,
* middleware pipeline, and final handler.
*/
private routes: IInternalRoute<TContext>[] = [];
/**
* 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<IHttpKernelConfig<TContext>>,
) {
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<TContext>;
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<Response> {
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<TContext>(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<TContext>);
}
/**
* 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<TContext>[],
handler: Handler<TContext>,
): Promise<Response> {
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<Response> => {
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);
}
}
}

View File

@@ -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;
}

View File

@@ -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<TContext extends IContext = IContext>
extends
Partial<
Record<
Exclude<typeof validHttpErrorCodes[number], 404 | 500>,
HttpErrorHandler<TContext>
>
> {
/** Required error handler for HTTP 404 (Not Found). */
404: HttpErrorHandler<TContext>;
/** Required error handler for HTTP 500 (Internal Server Error). */
500: HttpErrorHandler<TContext>;
}

View File

@@ -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<TContext extends IContext = IContext> {
/**
* 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<Response>;
}

View File

@@ -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<TContext extends IContext = IContext> {
decorateResponse: ResponseDecorator<TContext>;
routeBuilderFactory: IRouteBuilderFactory;
httpErrorHandlers: IHttpErrorHandlers<TContext>;
}

View File

@@ -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<TContext extends IContext = IContext> {
/**
* 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<TContext>[];
/**
* The final handler that generates the HTTP response after all middleware has run.
*/
handler: Handler<TContext>;
}

View File

@@ -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<TContext extends IContext = IContext> {
new (
registerRoute: (route: IInternalRoute<TContext>) => void,
def: IRouteDefinition,
mws?: Middleware<TContext>[],
): IRouteBuilder<TContext>;
}
/**
* Provides a fluent API to build a single route configuration by chaining
* middleware and setting the final request handler.
*/
export interface IRouteBuilder<TContext extends IContext = IContext> {
/**
* 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<TContext>,
): IRouteBuilder<TContext>;
/**
* 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<TContext>,
): void;
}

View File

@@ -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)
);
}

View File

@@ -0,0 +1,6 @@
import { Params, Query } from '../Types/mod.ts';
export interface IRouteMatch {
params?: Params;
query?: Query;
}

View File

@@ -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;
}

View File

@@ -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);
});

View File

@@ -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';

View File

@@ -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<TContext extends IContext = IContext>
implements IRouteBuilder<TContext> {
/**
* 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<TContext>,
private readonly def: IRouteDefinition,
private readonly mws: Middleware<TContext>[] = [],
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<TContext>,
): IRouteBuilder<TContext> {
return new RouteBuilder<TContext>(
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<TContext>,
): void {
const matcher = this.matcherFactory(this.def);
this.registerRoute({
method: this.def.method,
matcher,
middlewares: this.mws,
handler: handler,
});
}
}

View File

@@ -0,0 +1,4 @@
export type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]>
: T[P];
};

View File

@@ -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<TContext extends IContext = IContext> = (
ctx: TContext,
) => Promise<Response>;
/**
* 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<TContext extends IContext = IContext> =
& Handler<TContext>
& { 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<Response>` 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<IContext>
* }
* ```
*/
export function isHandler<TContext extends IContext = IContext>(
value: unknown,
): value is Handler<TContext> {
return (
typeof value === 'function' &&
value.length === 1 // ctx
);
}

View File

@@ -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<TContext extends IContext = IContext> = (
context?: Partial<TContext>,
error?: Error,
) => Promise<Response> | Response;

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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<TContext extends IContext = IContext> = (
ctx: TContext,
next: () => Promise<Response>,
) => Promise<Response>;
/**
* 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<TContext extends IContext = IContext> =
& Middleware<TContext>
& { 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<TContext extends IContext = IContext>(
value: unknown,
): value is Middleware<TContext> {
return (
typeof value === 'function' &&
value.length === 2 // ctx, next
);
}

View File

@@ -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<string, string>;

12
v0.1.0/src/Types/Query.ts Normal file
View File

@@ -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<string, string | string[]>;

View File

@@ -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<TContext extends IContext = IContext> = (
route: IInternalRoute<TContext>,
) => void;

View File

@@ -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<TContext extends IContext = IContext> = (
res: Response,
ctx: TContext,
) => Response;

View File

@@ -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<string, unknown>;

View File

@@ -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)}`,
);
}
});

View File

@@ -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`,
);
}
});

45
v0.1.0/src/Types/mod.ts Normal file
View File

@@ -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';

View File

@@ -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<MyContext>(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);
});

View File

@@ -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']);
});

View File

@@ -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));
});

View File

@@ -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<MyContext>(request);
* return httpErrorHandlers[404](ctx);
* ```
*/
export function createEmptyContext<TContext extends IContext = IContext>(
req: Request,
): TContext {
return {
req,
params: {} as Params,
query: {} as Query,
state: {} as State,
} as TContext;
}

View File

@@ -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 };
};
}

5
v0.1.0/src/Utils/mod.ts Normal file
View File

@@ -0,0 +1,5 @@
// deno-coverage-ignore-file
export { createEmptyContext } from './createEmptyContext.ts';
export { createRouteMatcher } from './createRouteMatcher.ts';
export { normalizeError } from './normalizeError.ts';

View File

@@ -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),
);
}

View File

@@ -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');
});

View File

@@ -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');
});

4
v0.1.0/src/mod.ts Normal file
View File

@@ -0,0 +1,4 @@
// deno-coverage-ignore-file
export { HttpKernel } from './HttpKernel.ts';
export { RouteBuilder } from './RouteBuilder.ts';
export { createRouteMatcher } from './Utils/createRouteMatcher.ts';