Added route wildcards, error handlers, header management and test suite
This commit is contained in:
parent
928e909f2f
commit
abf3fed939
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
watch.sh
|
33
README.md
33
README.md
@ -10,37 +10,52 @@ Here is some leaked source code for my brand new Cat website. Do not steal!
|
|||||||
|
|
||||||
```shell
|
```shell
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
printf '%s\n\n' 'Content-Type: text/html';
|
|
||||||
|
|
||||||
source exprash.sh;
|
source exprash.sh;
|
||||||
|
|
||||||
|
redirectStdout 'log';
|
||||||
|
|
||||||
declare -A cats;
|
declare -A cats;
|
||||||
cats[calico]="Calico";
|
cats[calico]="Calico";
|
||||||
cats[sphynx]="Sphynx";
|
cats[sphynx]="Sphynx";
|
||||||
cats[ragdoll]="Ragdoll";
|
cats[ragdoll]="Ragdoll";
|
||||||
|
cats[holland_lop]="Holland Lop";
|
||||||
cats[scottish_fold]="Scottish Fold";
|
cats[scottish_fold]="Scottish Fold";
|
||||||
|
cats[orange]="Orange";
|
||||||
|
|
||||||
get '/' && {
|
get '/' && {
|
||||||
printf '<h1>Cats</h1>\n';
|
html='<h1>Cats</h1><ul>';
|
||||||
printf '<ul>\n';
|
|
||||||
for key in "${!cats[@]}"; do
|
for key in "${!cats[@]}"; do
|
||||||
printf '<li><a href="cats/%s">%s</a></li>\n' "$key" "${cats[$key]}";
|
html+="<li><a href=\"cats/${key}\">${cats[$key]}</a></li>";
|
||||||
done
|
done
|
||||||
printf '</ul>\n';
|
html+='</ul>';
|
||||||
|
|
||||||
|
printf '%s' "$html" | send;
|
||||||
}
|
}
|
||||||
|
|
||||||
get '/cats/:cat' && {
|
get '/cats/:cat' && {
|
||||||
key=$(param 'cat');
|
key=$(param 'cat');
|
||||||
|
|
||||||
if [[ -v "cats[$key]" ]]; then
|
if [[ -v "cats[$key]" ]]; then
|
||||||
printf '<h1>Your Cat: %s</h1>\n' "${cats[$key]}";
|
if [ "$key" == 'holland_lop' ]; then
|
||||||
|
next "That's not a cat!";
|
||||||
|
else
|
||||||
|
printf '<h1>Your Cat: %s</h1>\n' "${cats[$key]}" | send;
|
||||||
|
fi;
|
||||||
else
|
else
|
||||||
next;
|
next;
|
||||||
fi;
|
fi;
|
||||||
}
|
}
|
||||||
|
|
||||||
use && {
|
get '/cats/*' && {
|
||||||
printf '<h1>Error: Cannot find that cat</h1>\n';
|
printf '<h1>Error: Cannot find that cat</h1>\n' | send;
|
||||||
|
}
|
||||||
|
|
||||||
|
getError '/cats/*' && {
|
||||||
|
printf '<h1>%s</h1>' "$(errorMessage)" | send;
|
||||||
|
}
|
||||||
|
|
||||||
|
(use || useError) && {
|
||||||
|
printf '<h1>404</h1>' | send;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -1,33 +1,48 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
printf '%s\n\n' 'Content-Type: text/html';
|
|
||||||
|
|
||||||
source exprash.sh;
|
source exprash.sh;
|
||||||
|
|
||||||
|
redirectStdout 'log';
|
||||||
|
|
||||||
declare -A cats;
|
declare -A cats;
|
||||||
cats[calico]="Calico";
|
cats[calico]="Calico";
|
||||||
cats[sphynx]="Sphynx";
|
cats[sphynx]="Sphynx";
|
||||||
cats[ragdoll]="Ragdoll";
|
cats[ragdoll]="Ragdoll";
|
||||||
|
cats[holland_lop]="Holland Lop";
|
||||||
cats[scottish_fold]="Scottish Fold";
|
cats[scottish_fold]="Scottish Fold";
|
||||||
|
cats[orange]="Orange";
|
||||||
|
|
||||||
get '/' && {
|
get '/' && {
|
||||||
printf '<h1>Cats</h1>\n';
|
html='<h1>Cats</h1><ul>';
|
||||||
printf '<ul>\n';
|
|
||||||
for key in "${!cats[@]}"; do
|
for key in "${!cats[@]}"; do
|
||||||
printf '<li><a href="cats/%s">%s</a></li>\n' "$key" "${cats[$key]}";
|
html+="<li><a href=\"cats/${key}\">${cats[$key]}</a></li>";
|
||||||
done
|
done
|
||||||
printf '</ul>\n';
|
html+='</ul>';
|
||||||
|
|
||||||
|
printf '%s' "$html" | send;
|
||||||
}
|
}
|
||||||
|
|
||||||
get '/cats/:cat' && {
|
get '/cats/:cat' && {
|
||||||
key=$(param 'cat');
|
key=$(param 'cat');
|
||||||
|
|
||||||
if [[ -v "cats[$key]" ]]; then
|
if [[ -v "cats[$key]" ]]; then
|
||||||
printf '<h1>Your Cat: %s</h1>\n' "${cats[$key]}";
|
if [ "$key" == 'holland_lop' ]; then
|
||||||
|
next "That's not a cat!";
|
||||||
|
else
|
||||||
|
printf '<h1>Your Cat: %s</h1>\n' "${cats[$key]}" | send;
|
||||||
|
fi;
|
||||||
else
|
else
|
||||||
next;
|
next;
|
||||||
fi;
|
fi;
|
||||||
}
|
}
|
||||||
|
|
||||||
use && {
|
get '/cats/*' && {
|
||||||
printf '<h1>Error: Cannot find that cat</h1>\n';
|
printf '<h1>Error: Cannot find that cat</h1>\n' | send;
|
||||||
|
}
|
||||||
|
|
||||||
|
getError '/cats/*' && {
|
||||||
|
printf '<h1>%s</h1>' "$(errorMessage)" | send;
|
||||||
|
}
|
||||||
|
|
||||||
|
(use || useError) && {
|
||||||
|
printf '<h1>404</h1>' | send;
|
||||||
}
|
}
|
||||||
|
10
run_tests.sh
Executable file
10
run_tests.sh
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
source "$(dirname "$0")/src/exprash.sh";
|
||||||
|
|
||||||
|
source "$(dirname "$0")/tests/utils.sh";
|
||||||
|
|
||||||
|
printf '%s\n' "Routes:";
|
||||||
|
source "$(dirname "$0")/tests/routes.sh";
|
||||||
|
|
||||||
|
printf '\n';
|
||||||
|
testSummary;
|
184
src/exprash.sh
184
src/exprash.sh
@ -1,8 +1,30 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# shellcheck disable=SC2034 # false flags nameref params
|
# shellcheck disable=SC2034 # false flags nameref params
|
||||||
|
|
||||||
|
# =====================
|
||||||
|
# Require shell options
|
||||||
|
# =====================
|
||||||
|
|
||||||
|
# Necessary for piping to "send" funciton
|
||||||
|
shopt -s lastpipe
|
||||||
|
|
||||||
|
# ===============
|
||||||
|
# Setup Functions
|
||||||
|
# ===============
|
||||||
|
|
||||||
|
# $1: Log file to redirect output
|
||||||
|
function redirectStdout() {
|
||||||
|
local output="$1";
|
||||||
|
[ -z "$output" ] && output="/dev/null";
|
||||||
|
|
||||||
|
exec 5<&1;
|
||||||
|
_exprashRedirectStdout="$1";
|
||||||
|
exec 1>>"$_exprashRedirectStdout";
|
||||||
|
printf '%s: %s\n' "${REQUEST_METHOD:-UNKNOWN}" "${PATH_INFO:-/}"
|
||||||
|
}
|
||||||
|
|
||||||
# =============
|
# =============
|
||||||
# Route Methods
|
# Route Function
|
||||||
# =============
|
# =============
|
||||||
|
|
||||||
# $1: path
|
# $1: path
|
||||||
@ -31,24 +53,96 @@ function delete() {
|
|||||||
|
|
||||||
# $1: path
|
# $1: path
|
||||||
function all() {
|
function all() {
|
||||||
[ "$routeHandled" -eq 1 ] && return 1;
|
[ "$_exprashRouteHandled" -eq 1 ] && return 1;
|
||||||
|
[ -n "$_exprashErrorMessage" ] && return 1;
|
||||||
|
|
||||||
# Reset params
|
# Reset params
|
||||||
params=();
|
_exprashParams=();
|
||||||
pathMatch "$1" params || return 1;
|
|
||||||
|
pathMatch "$1" _exprashParams || return 1;
|
||||||
|
_exprashRouteHandled=1;
|
||||||
|
|
||||||
routeHandled=1;
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function use() {
|
function use() {
|
||||||
[ "$routeHandled" -eq 1 ] && return 1;
|
[ "$_exprashRouteHandled" -eq 1 ] && return 1;
|
||||||
routeHandled=1;
|
[ -n "$_exprashErrorMessage" ] && return 1;
|
||||||
|
|
||||||
|
_exprashRouteHandled=1;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# =====================
|
||||||
|
# Error Route Functions
|
||||||
|
# =====================
|
||||||
|
|
||||||
|
# $1: path
|
||||||
|
function getError() {
|
||||||
|
[ "$REQUEST_METHOD" != "GET" ] && return 1;
|
||||||
|
allError "$1";
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1: path
|
||||||
|
function postError() {
|
||||||
|
[ "$REQUEST_METHOD" != "POST" ] && return 1;
|
||||||
|
allError "$1";
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1: path
|
||||||
|
function putError() {
|
||||||
|
[ "$REQUEST_METHOD" != "PUT" ] && return 1;
|
||||||
|
allError "$1";
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1: path
|
||||||
|
function deleteError() {
|
||||||
|
[ "$REQUEST_METHOD" != "DELETE" ] && return 1;
|
||||||
|
allError "$1";
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1: path
|
||||||
|
function allError() {
|
||||||
|
[ "$_exprashRouteHandled" -eq 1 ] && return 1;
|
||||||
|
[ -z "$_exprashErrorMessage" ] && return 1;
|
||||||
|
|
||||||
|
# Reset params
|
||||||
|
_exprashParams=();
|
||||||
|
|
||||||
|
pathMatch "$1" _exprashParams || return 1;
|
||||||
|
_exprashRouteHandled=1;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function useError() {
|
||||||
|
[ "$_exprashRouteHandled" -eq 1 ] && return 1;
|
||||||
|
[ -z "$_exprashErrorMessage" ] && return 1;
|
||||||
|
|
||||||
|
_exprashRouteHandled=1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ====
|
||||||
|
# Next
|
||||||
|
# ====
|
||||||
|
|
||||||
|
# $1 (optional): error message
|
||||||
function next() {
|
function next() {
|
||||||
routeHandled=0;
|
_exprashRouteHandled=0;
|
||||||
|
[ -n "$1" ] && _exprashErrorMessage="$1";
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============
|
||||||
|
# App Functions
|
||||||
|
# =============
|
||||||
|
|
||||||
|
function errorMessage() {
|
||||||
|
printf '%s' "$_exprashErrorMessage";
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasErrorMessage() {
|
||||||
|
[ -n "$_exprashErrorMessage" ];
|
||||||
}
|
}
|
||||||
|
|
||||||
# =================
|
# =================
|
||||||
@ -57,18 +151,51 @@ function next() {
|
|||||||
|
|
||||||
# $1: param name
|
# $1: param name
|
||||||
function param() {
|
function param() {
|
||||||
printf '%s\n' "${params[$1]}";
|
printf '%s\n' "${_exprashParams[$1]}";
|
||||||
}
|
}
|
||||||
|
|
||||||
# $1: param name
|
|
||||||
function hasParam() {
|
function hasParam() {
|
||||||
[[ -v "params[$1]" ]];
|
[[ -v "_exprashParams[$1]" ]];
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==================
|
||||||
|
# Response Functions
|
||||||
|
# ==================
|
||||||
|
|
||||||
|
function send() {
|
||||||
|
if [ "$_exprashHeadersSent" -eq 0 ]; then
|
||||||
|
sendHeaders
|
||||||
|
_exprashHeadersSent=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sendRaw
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1: Header Name
|
||||||
|
# $2: Header Value
|
||||||
|
function setHeader() {
|
||||||
|
_exprashHeaders["$1"]="$2";
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendHeaders() {
|
||||||
|
printf '%s\n' "Content-Type: ${_exprashHeaders['Content-Type']}" | sendRaw
|
||||||
|
for key in "${!_exprashHeaders[@]}"; do
|
||||||
|
[ "$key" == 'Content-Type' ] && continue
|
||||||
|
printf '%s\n' "${key}: ${_exprashHeaders[$key]}" | sendRaw
|
||||||
|
done;
|
||||||
|
printf '\n' | sendRaw
|
||||||
}
|
}
|
||||||
|
|
||||||
# =========
|
# =========
|
||||||
# Internals
|
# Internals
|
||||||
# =========
|
# =========
|
||||||
|
|
||||||
|
function sendRaw() {
|
||||||
|
[ -n "$_exprashRedirectStdout" ] && exec >&5;
|
||||||
|
cat;
|
||||||
|
[ -n "$_exprashRedirectStdout" ] && exec 1>>"$_exprashRedirectStdout";
|
||||||
|
}
|
||||||
|
|
||||||
# $1: path
|
# $1: path
|
||||||
# $2: (nameref) array
|
# $2: (nameref) array
|
||||||
function pathToArray() {
|
function pathToArray() {
|
||||||
@ -84,25 +211,31 @@ function pathMatch() {
|
|||||||
local path="$PATH_INFO";
|
local path="$PATH_INFO";
|
||||||
local route="$1";
|
local route="$1";
|
||||||
|
|
||||||
|
# Params associative array
|
||||||
|
local -n routeParams="$2";
|
||||||
|
|
||||||
local pathArr;
|
local pathArr;
|
||||||
pathToArray "$path" pathArr;
|
pathToArray "$path" pathArr;
|
||||||
|
|
||||||
local routeArr;
|
local routeArr;
|
||||||
pathToArray "$route" routeArr;
|
pathToArray "$route" routeArr;
|
||||||
|
|
||||||
# Params associative array
|
# Get max path length
|
||||||
local -n routeParams="$2";
|
local routeLen=${#routeArr[@]};
|
||||||
|
local pathLen=${#pathArr[@]};
|
||||||
|
local maxLen=$(( routeLen >= pathLen ? routeLen : pathLen ));
|
||||||
|
|
||||||
# Not a match if pathArr and routeArr have different length
|
for ((i=0; i<maxLen; i++)); do
|
||||||
[ "${#pathArr[@]}" -ne "${#routeArr[@]}" ] && return 1;
|
|
||||||
|
|
||||||
for ((i=0; i<${#routeArr[@]}; i++)); do
|
|
||||||
local routeComponent="${routeArr[$i]}";
|
local routeComponent="${routeArr[$i]}";
|
||||||
local pathComponent="${pathArr[$i]}";
|
local pathComponent="${pathArr[$i]}";
|
||||||
|
|
||||||
# If route component starts with ":"
|
# If route component starts with ":"
|
||||||
if [[ "$routeComponent" == :* ]]; then
|
if [[ "$routeComponent" == :* ]]; then
|
||||||
routeParams["${routeComponent:1}"]="$pathComponent";
|
routeParams["${routeComponent:1}"]="$pathComponent";
|
||||||
|
elif [[ "$routeComponent" == '*' ]] && [ -n "$pathComponent" ]; then
|
||||||
|
continue;
|
||||||
|
elif [[ "$routeComponent" == '**' ]] && [ -n "$pathComponent" ]; then
|
||||||
|
break;
|
||||||
else
|
else
|
||||||
# Confirm paths match
|
# Confirm paths match
|
||||||
[ "$routeComponent" != "$pathComponent" ] && return 1;
|
[ "$routeComponent" != "$pathComponent" ] && return 1;
|
||||||
@ -115,5 +248,18 @@ function pathMatch() {
|
|||||||
# =======
|
# =======
|
||||||
# Globals
|
# Globals
|
||||||
# =======
|
# =======
|
||||||
routeHandled=0;
|
|
||||||
declare -A params;
|
# Route globals
|
||||||
|
function _exprashResetRouteGlobals() {
|
||||||
|
_exprashRouteHandled=0
|
||||||
|
_exprashErrorMessage=''
|
||||||
|
declare -gA _exprashParams
|
||||||
|
|
||||||
|
_exprashHeadersSent=0
|
||||||
|
declare -gA _exprashHeaders
|
||||||
|
_exprashHeaders['Content-Type']='text/html'
|
||||||
|
}
|
||||||
|
_exprashResetRouteGlobals
|
||||||
|
|
||||||
|
# Setup globals
|
||||||
|
_exprashRedirectStdout=''
|
||||||
|
55
tests/routes.sh
Executable file
55
tests/routes.sh
Executable file
@ -0,0 +1,55 @@
|
|||||||
|
it "Should match plain route" $({
|
||||||
|
_exprashResetRouteGlobals
|
||||||
|
PATH_INFO='/plain/route'
|
||||||
|
all '/plain/route'
|
||||||
|
})
|
||||||
|
|
||||||
|
it "Should not match incorrect plain route" $({
|
||||||
|
_exprashResetRouteGlobals
|
||||||
|
PATH_INFO='/plain/WRONG'
|
||||||
|
! all '/plain/route'
|
||||||
|
})
|
||||||
|
|
||||||
|
it "Should extract parameter" $({
|
||||||
|
_exprashResetRouteGlobals
|
||||||
|
PATH_INFO='/cats/calico/pet'
|
||||||
|
all '/cats/:cat/pet' && hasParam 'cat' && [ "$(param 'cat')" == 'calico' ]
|
||||||
|
})
|
||||||
|
|
||||||
|
it "Should match wildcard route" $({
|
||||||
|
_exprashResetRouteGlobals
|
||||||
|
PATH_INFO='/cats/calico/pet'
|
||||||
|
all '/cats/*/pet'
|
||||||
|
})
|
||||||
|
|
||||||
|
it "Should not match incorrect wildcard route" $({
|
||||||
|
_exprashResetRouteGlobals
|
||||||
|
PATH_INFO='/cats/calico/'
|
||||||
|
! all '/cats/*/pet'
|
||||||
|
})
|
||||||
|
|
||||||
|
it "Should match multi-wildcard route" $({
|
||||||
|
_exprashResetRouteGlobals
|
||||||
|
PATH_INFO='/cats/calico/pet/donkey'
|
||||||
|
all '/cats/**'
|
||||||
|
})
|
||||||
|
|
||||||
|
it "Should not match incorrect multi-wildcard route" $({
|
||||||
|
_exprashResetRouteGlobals
|
||||||
|
PATH_INFO='/INCORRECT/calico/pet/donkey'
|
||||||
|
! all '/cats/**'
|
||||||
|
})
|
||||||
|
|
||||||
|
it "Should match get route" $({
|
||||||
|
_exprashResetRouteGlobals
|
||||||
|
REQUEST_METHOD='GET'
|
||||||
|
PATH_INFO='/simple/route'
|
||||||
|
get '/simple/route'
|
||||||
|
})
|
||||||
|
|
||||||
|
it "Should not match get route with incorrect method" $({
|
||||||
|
_exprashResetRouteGlobals
|
||||||
|
REQUEST_METHOD='POST'
|
||||||
|
PATH_INFO='/simple/route'
|
||||||
|
! get '/simple/route'
|
||||||
|
})
|
23
tests/utils.sh
Executable file
23
tests/utils.sh
Executable file
@ -0,0 +1,23 @@
|
|||||||
|
_testPassCount=0;
|
||||||
|
_testTotalCount=0;
|
||||||
|
|
||||||
|
function it() {
|
||||||
|
local exitCode=$?;
|
||||||
|
local message=$1;
|
||||||
|
local status;
|
||||||
|
|
||||||
|
_testTotalCount=$((_testTotalCount+1));
|
||||||
|
|
||||||
|
if [ "$exitCode" -eq 0 ]; then
|
||||||
|
status='✓';
|
||||||
|
_testPassCount=$((_testPassCount+1));
|
||||||
|
else
|
||||||
|
status='✗';
|
||||||
|
fi;
|
||||||
|
|
||||||
|
printf '%s %s\n' "$status" "$message";
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSummary() {
|
||||||
|
printf 'Passed: %s of %s tests\n' "$_testPassCount" "$_testTotalCount";
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user