CI: Update Pages (2025-11-23 11:20:49)
This commit is contained in:
43
v0.1.0/CHANGELOG.md
Normal file
43
v0.1.0/CHANGELOG.md
Normal 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
1
v0.1.0/VERSION
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0.1.0
|
||||||
104
v0.1.0/cliff.toml
Normal file
104
v0.1.0/cliff.toml
Normal 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
32
v0.1.0/deno.jsonc
Normal 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
155
v0.1.0/deno.lock
generated
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
25
v0.1.0/src/Errors/InvalidHttpMethodError.ts
Normal file
25
v0.1.0/src/Errors/InvalidHttpMethodError.ts
Normal 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
3
v0.1.0/src/Errors/mod.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// deno-coverage-ignore-file
|
||||||
|
|
||||||
|
export { InvalidHttpMethodError } from './InvalidHttpMethodError.ts';
|
||||||
199
v0.1.0/src/HttpKernel.ts
Normal file
199
v0.1.0/src/HttpKernel.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
53
v0.1.0/src/Interfaces/IContext.ts
Normal file
53
v0.1.0/src/Interfaces/IContext.ts
Normal 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;
|
||||||
|
}
|
||||||
40
v0.1.0/src/Interfaces/IHttpErrorHandlers.ts
Normal file
40
v0.1.0/src/Interfaces/IHttpErrorHandlers.ts
Normal 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>;
|
||||||
|
}
|
||||||
49
v0.1.0/src/Interfaces/IHttpKernel.ts
Normal file
49
v0.1.0/src/Interfaces/IHttpKernel.ts
Normal 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>;
|
||||||
|
}
|
||||||
10
v0.1.0/src/Interfaces/IHttpKernelConfig.ts
Normal file
10
v0.1.0/src/Interfaces/IHttpKernelConfig.ts
Normal 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>;
|
||||||
|
}
|
||||||
39
v0.1.0/src/Interfaces/IInternalRoute.ts
Normal file
39
v0.1.0/src/Interfaces/IInternalRoute.ts
Normal 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>;
|
||||||
|
}
|
||||||
39
v0.1.0/src/Interfaces/IRouteBuilder.ts
Normal file
39
v0.1.0/src/Interfaces/IRouteBuilder.ts
Normal 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;
|
||||||
|
}
|
||||||
91
v0.1.0/src/Interfaces/IRouteDefinition.ts
Normal file
91
v0.1.0/src/Interfaces/IRouteDefinition.ts
Normal 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)
|
||||||
|
);
|
||||||
|
}
|
||||||
6
v0.1.0/src/Interfaces/IRouteMatch.ts
Normal file
6
v0.1.0/src/Interfaces/IRouteMatch.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { Params, Query } from '../Types/mod.ts';
|
||||||
|
|
||||||
|
export interface IRouteMatch {
|
||||||
|
params?: Params;
|
||||||
|
query?: Query;
|
||||||
|
}
|
||||||
36
v0.1.0/src/Interfaces/IRouteMatcher.ts
Normal file
36
v0.1.0/src/Interfaces/IRouteMatcher.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
19
v0.1.0/src/Interfaces/mod.ts
Normal file
19
v0.1.0/src/Interfaces/mod.ts
Normal 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';
|
||||||
67
v0.1.0/src/RouteBuilder.ts
Normal file
67
v0.1.0/src/RouteBuilder.ts
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
4
v0.1.0/src/Types/DeepPartial.ts
Normal file
4
v0.1.0/src/Types/DeepPartial.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export type DeepPartial<T> = {
|
||||||
|
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]>
|
||||||
|
: T[P];
|
||||||
|
};
|
||||||
57
v0.1.0/src/Types/Handler.ts
Normal file
57
v0.1.0/src/Types/Handler.ts
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
28
v0.1.0/src/Types/HttpErrorHandler.ts
Normal file
28
v0.1.0/src/Types/HttpErrorHandler.ts
Normal 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;
|
||||||
52
v0.1.0/src/Types/HttpMethod.ts
Normal file
52
v0.1.0/src/Types/HttpMethod.ts
Normal 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);
|
||||||
|
}
|
||||||
189
v0.1.0/src/Types/HttpStatusCode.ts
Normal file
189
v0.1.0/src/Types/HttpStatusCode.ts
Normal 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);
|
||||||
|
}
|
||||||
51
v0.1.0/src/Types/Middleware.ts
Normal file
51
v0.1.0/src/Types/Middleware.ts
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
10
v0.1.0/src/Types/Params.ts
Normal file
10
v0.1.0/src/Types/Params.ts
Normal 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
12
v0.1.0/src/Types/Query.ts
Normal 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[]>;
|
||||||
16
v0.1.0/src/Types/RegisterRoute.ts
Normal file
16
v0.1.0/src/Types/RegisterRoute.ts
Normal 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;
|
||||||
30
v0.1.0/src/Types/ResponseDecorator.ts
Normal file
30
v0.1.0/src/Types/ResponseDecorator.ts
Normal 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;
|
||||||
9
v0.1.0/src/Types/State.ts
Normal file
9
v0.1.0/src/Types/State.ts
Normal 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>;
|
||||||
40
v0.1.0/src/Types/__tests__/HttpMethod.test.ts
Normal file
40
v0.1.0/src/Types/__tests__/HttpMethod.test.ts
Normal 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)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
35
v0.1.0/src/Types/__tests__/HttpStatusCode.test.ts
Normal file
35
v0.1.0/src/Types/__tests__/HttpStatusCode.test.ts
Normal 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
45
v0.1.0/src/Types/mod.ts
Normal 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';
|
||||||
28
v0.1.0/src/Utils/__tests__/createEmptyContext.test.ts
Normal file
28
v0.1.0/src/Utils/__tests__/createEmptyContext.test.ts
Normal 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);
|
||||||
|
});
|
||||||
118
v0.1.0/src/Utils/__tests__/createRouteMatcher.test.ts
Normal file
118
v0.1.0/src/Utils/__tests__/createRouteMatcher.test.ts
Normal 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']);
|
||||||
|
});
|
||||||
35
v0.1.0/src/Utils/__tests__/normalizeError.test.ts
Normal file
35
v0.1.0/src/Utils/__tests__/normalizeError.test.ts
Normal 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));
|
||||||
|
});
|
||||||
30
v0.1.0/src/Utils/createEmptyContext.ts
Normal file
30
v0.1.0/src/Utils/createEmptyContext.ts
Normal 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;
|
||||||
|
}
|
||||||
54
v0.1.0/src/Utils/createRouteMatcher.ts
Normal file
54
v0.1.0/src/Utils/createRouteMatcher.ts
Normal 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
5
v0.1.0/src/Utils/mod.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// deno-coverage-ignore-file
|
||||||
|
|
||||||
|
export { createEmptyContext } from './createEmptyContext.ts';
|
||||||
|
export { createRouteMatcher } from './createRouteMatcher.ts';
|
||||||
|
export { normalizeError } from './normalizeError.ts';
|
||||||
32
v0.1.0/src/Utils/normalizeError.ts
Normal file
32
v0.1.0/src/Utils/normalizeError.ts
Normal 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),
|
||||||
|
);
|
||||||
|
}
|
||||||
180
v0.1.0/src/__tests__/HttpKernel.test.ts
Normal file
180
v0.1.0/src/__tests__/HttpKernel.test.ts
Normal 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');
|
||||||
|
});
|
||||||
118
v0.1.0/src/__tests__/RouteBuilder.test.ts
Normal file
118
v0.1.0/src/__tests__/RouteBuilder.test.ts
Normal 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
4
v0.1.0/src/mod.ts
Normal 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';
|
||||||
93
v0.2.0/CHANGELOG.md
Normal file
93
v0.2.0/CHANGELOG.md
Normal file
@@ -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))
|
||||||
|
|
||||||
|
|
||||||
18
v0.2.0/LICENSE
Normal file
18
v0.2.0/LICENSE
Normal file
@@ -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.
|
||||||
135
v0.2.0/README.md
Normal file
135
v0.2.0/README.md
Normal file
@@ -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<S = Record<string, unknown>> {
|
||||||
|
req: Request; // original request
|
||||||
|
params: Record<string>; // route params e.g. { id: "42" }
|
||||||
|
query: Record<string | string[]>; // 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)
|
||||||
1
v0.2.0/VERSION
Normal file
1
v0.2.0/VERSION
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0.2.0
|
||||||
104
v0.2.0/cliff.toml
Normal file
104
v0.2.0/cliff.toml
Normal 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"
|
||||||
36
v0.2.0/deno.jsonc
Normal file
36
v0.2.0/deno.jsonc
Normal file
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
155
v0.2.0/deno.lock
generated
Normal file
155
v0.2.0/deno.lock
generated
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
25
v0.2.0/src/Errors/InvalidHttpMethodError.ts
Normal file
25
v0.2.0/src/Errors/InvalidHttpMethodError.ts
Normal 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.2.0/src/Errors/mod.ts
Normal file
3
v0.2.0/src/Errors/mod.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// deno-coverage-ignore-file
|
||||||
|
|
||||||
|
export { InvalidHttpMethodError } from './InvalidHttpMethodError.ts';
|
||||||
144
v0.2.0/src/HttpKernel.ts
Normal file
144
v0.2.0/src/HttpKernel.ts
Normal file
@@ -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<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;
|
||||||
|
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<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>);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleInternalError = (
|
||||||
|
ctx: TContext,
|
||||||
|
err?: unknown,
|
||||||
|
): Response | Promise<Response> => {
|
||||||
|
return this.cfg.httpErrorHandlers[HTTP_500_INTERNAL_SERVER_ERROR](
|
||||||
|
ctx,
|
||||||
|
normalizeError(err),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
53
v0.2.0/src/Interfaces/IContext.ts
Normal file
53
v0.2.0/src/Interfaces/IContext.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
40
v0.2.0/src/Interfaces/IHttpErrorHandlers.ts
Normal file
40
v0.2.0/src/Interfaces/IHttpErrorHandlers.ts
Normal file
@@ -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<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>;
|
||||||
|
}
|
||||||
49
v0.2.0/src/Interfaces/IHttpKernel.ts
Normal file
49
v0.2.0/src/Interfaces/IHttpKernel.ts
Normal file
@@ -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<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>;
|
||||||
|
}
|
||||||
10
v0.2.0/src/Interfaces/IHttpKernelConfig.ts
Normal file
10
v0.2.0/src/Interfaces/IHttpKernelConfig.ts
Normal file
@@ -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<TContext extends IContext = IContext> {
|
||||||
|
decorateResponse: ResponseDecorator<TContext>;
|
||||||
|
routeBuilderFactory: IRouteBuilderFactory;
|
||||||
|
httpErrorHandlers: IHttpErrorHandlers<TContext>;
|
||||||
|
}
|
||||||
64
v0.2.0/src/Interfaces/IInternalRoute.ts
Normal file
64
v0.2.0/src/Interfaces/IInternalRoute.ts
Normal file
@@ -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<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>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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> | Response;
|
||||||
|
}
|
||||||
39
v0.2.0/src/Interfaces/IRouteBuilder.ts
Normal file
39
v0.2.0/src/Interfaces/IRouteBuilder.ts
Normal file
@@ -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<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;
|
||||||
|
}
|
||||||
91
v0.2.0/src/Interfaces/IRouteDefinition.ts
Normal file
91
v0.2.0/src/Interfaces/IRouteDefinition.ts
Normal file
@@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
6
v0.2.0/src/Interfaces/IRouteMatch.ts
Normal file
6
v0.2.0/src/Interfaces/IRouteMatch.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import type { Params, Query } from '../Types/mod.ts';
|
||||||
|
|
||||||
|
export interface IRouteMatch {
|
||||||
|
params?: Params;
|
||||||
|
query?: Query;
|
||||||
|
}
|
||||||
35
v0.2.0/src/Interfaces/IRouteMatcher.ts
Normal file
35
v0.2.0/src/Interfaces/IRouteMatcher.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
19
v0.2.0/src/Interfaces/mod.ts
Normal file
19
v0.2.0/src/Interfaces/mod.ts
Normal 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';
|
||||||
148
v0.2.0/src/RouteBuilder.ts
Normal file
148
v0.2.0/src/RouteBuilder.ts
Normal file
@@ -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<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,
|
||||||
|
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<Response>`.
|
||||||
|
*
|
||||||
|
* @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<TContext>,
|
||||||
|
'runRoute' | 'matcher' | 'method'
|
||||||
|
>,
|
||||||
|
): (
|
||||||
|
ctx: TContext,
|
||||||
|
) => Promise<Response> {
|
||||||
|
if (!isHandler<TContext>(route.handler)) {
|
||||||
|
throw new TypeError(
|
||||||
|
'Route handler must be a function returning a Promise<Response>.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let composed = route.handler;
|
||||||
|
|
||||||
|
for (let i = route.middlewares.length - 1; i >= 0; i--) {
|
||||||
|
if (!isMiddleware<TContext>(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<Response> => {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
4
v0.2.0/src/Types/DeepPartial.ts
Normal file
4
v0.2.0/src/Types/DeepPartial.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export type DeepPartial<T> = {
|
||||||
|
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]>
|
||||||
|
: T[P];
|
||||||
|
};
|
||||||
57
v0.2.0/src/Types/Handler.ts
Normal file
57
v0.2.0/src/Types/Handler.ts
Normal file
@@ -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<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
|
||||||
|
);
|
||||||
|
}
|
||||||
28
v0.2.0/src/Types/HttpErrorHandler.ts
Normal file
28
v0.2.0/src/Types/HttpErrorHandler.ts
Normal file
@@ -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<TContext extends IContext = IContext> = (
|
||||||
|
context?: Partial<TContext>,
|
||||||
|
error?: Error,
|
||||||
|
) => Promise<Response> | Response;
|
||||||
52
v0.2.0/src/Types/HttpMethod.ts
Normal file
52
v0.2.0/src/Types/HttpMethod.ts
Normal 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);
|
||||||
|
}
|
||||||
189
v0.2.0/src/Types/HttpStatusCode.ts
Normal file
189
v0.2.0/src/Types/HttpStatusCode.ts
Normal 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);
|
||||||
|
}
|
||||||
51
v0.2.0/src/Types/Middleware.ts
Normal file
51
v0.2.0/src/Types/Middleware.ts
Normal file
@@ -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<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
|
||||||
|
);
|
||||||
|
}
|
||||||
10
v0.2.0/src/Types/Params.ts
Normal file
10
v0.2.0/src/Types/Params.ts
Normal 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.2.0/src/Types/Query.ts
Normal file
12
v0.2.0/src/Types/Query.ts
Normal 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[]>;
|
||||||
16
v0.2.0/src/Types/RegisterRoute.ts
Normal file
16
v0.2.0/src/Types/RegisterRoute.ts
Normal file
@@ -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<TContext extends IContext = IContext> = (
|
||||||
|
route: IInternalRoute<TContext>,
|
||||||
|
) => void;
|
||||||
30
v0.2.0/src/Types/ResponseDecorator.ts
Normal file
30
v0.2.0/src/Types/ResponseDecorator.ts
Normal file
@@ -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<TContext extends IContext = IContext> = (
|
||||||
|
res: Response,
|
||||||
|
ctx: TContext,
|
||||||
|
) => Response;
|
||||||
9
v0.2.0/src/Types/State.ts
Normal file
9
v0.2.0/src/Types/State.ts
Normal 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>;
|
||||||
40
v0.2.0/src/Types/__tests__/HttpMethod.test.ts
Normal file
40
v0.2.0/src/Types/__tests__/HttpMethod.test.ts
Normal 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)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
35
v0.2.0/src/Types/__tests__/HttpStatusCode.test.ts
Normal file
35
v0.2.0/src/Types/__tests__/HttpStatusCode.test.ts
Normal 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.2.0/src/Types/mod.ts
Normal file
45
v0.2.0/src/Types/mod.ts
Normal 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';
|
||||||
28
v0.2.0/src/Utils/__tests__/createEmptyContext.test.ts
Normal file
28
v0.2.0/src/Utils/__tests__/createEmptyContext.test.ts
Normal file
@@ -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<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);
|
||||||
|
});
|
||||||
118
v0.2.0/src/Utils/__tests__/createRouteMatcher.test.ts
Normal file
118
v0.2.0/src/Utils/__tests__/createRouteMatcher.test.ts
Normal file
@@ -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']);
|
||||||
|
});
|
||||||
35
v0.2.0/src/Utils/__tests__/normalizeError.test.ts
Normal file
35
v0.2.0/src/Utils/__tests__/normalizeError.test.ts
Normal 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));
|
||||||
|
});
|
||||||
30
v0.2.0/src/Utils/createEmptyContext.ts
Normal file
30
v0.2.0/src/Utils/createEmptyContext.ts
Normal file
@@ -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<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;
|
||||||
|
}
|
||||||
54
v0.2.0/src/Utils/createRouteMatcher.ts
Normal file
54
v0.2.0/src/Utils/createRouteMatcher.ts
Normal file
@@ -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 };
|
||||||
|
};
|
||||||
|
}
|
||||||
5
v0.2.0/src/Utils/mod.ts
Normal file
5
v0.2.0/src/Utils/mod.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// deno-coverage-ignore-file
|
||||||
|
|
||||||
|
export { createEmptyContext } from './createEmptyContext.ts';
|
||||||
|
export { createRouteMatcher } from './createRouteMatcher.ts';
|
||||||
|
export { normalizeError } from './normalizeError.ts';
|
||||||
30
v0.2.0/src/Utils/normalizeError.ts
Normal file
30
v0.2.0/src/Utils/normalizeError.ts
Normal file
@@ -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),
|
||||||
|
);
|
||||||
|
}
|
||||||
87
v0.2.0/src/__bench__/HttpKernel.bench.ts
Normal file
87
v0.2.0/src/__bench__/HttpKernel.bench.ts
Normal file
@@ -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();
|
||||||
|
});
|
||||||
185
v0.2.0/src/__tests__/HttpKernel.test.ts
Normal file
185
v0.2.0/src/__tests__/HttpKernel.test.ts
Normal file
@@ -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<Response>.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
140
v0.2.0/src/__tests__/RouteBuilder.test.ts
Normal file
140
v0.2.0/src/__tests__/RouteBuilder.test.ts
Normal file
@@ -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<Response>.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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.2.0/src/mod.ts
Normal file
4
v0.2.0/src/mod.ts
Normal 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';
|
||||||
107
v0.2.1/CHANGELOG.md
Normal file
107
v0.2.1/CHANGELOG.md
Normal file
@@ -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))
|
||||||
|
|
||||||
|
|
||||||
18
v0.2.1/LICENSE
Normal file
18
v0.2.1/LICENSE
Normal file
@@ -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.
|
||||||
135
v0.2.1/README.md
Normal file
135
v0.2.1/README.md
Normal file
@@ -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<S = Record<string, unknown>> {
|
||||||
|
req: Request; // original request
|
||||||
|
params: Record<string>; // route params e.g. { id: "42" }
|
||||||
|
query: Record<string | string[]>; // 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)
|
||||||
1
v0.2.1/VERSION
Normal file
1
v0.2.1/VERSION
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0.2.1
|
||||||
104
v0.2.1/cliff.toml
Normal file
104
v0.2.1/cliff.toml
Normal 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"
|
||||||
36
v0.2.1/deno.jsonc
Normal file
36
v0.2.1/deno.jsonc
Normal file
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
155
v0.2.1/deno.lock
generated
Normal file
155
v0.2.1/deno.lock
generated
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
25
v0.2.1/src/Errors/InvalidHttpMethodError.ts
Normal file
25
v0.2.1/src/Errors/InvalidHttpMethodError.ts
Normal 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.2.1/src/Errors/mod.ts
Normal file
3
v0.2.1/src/Errors/mod.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// deno-coverage-ignore-file
|
||||||
|
|
||||||
|
export { InvalidHttpMethodError } from './InvalidHttpMethodError.ts';
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user