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
|
||||
#!/bin/bash
|
||||
printf '%s\n\n' 'Content-Type: text/html';
|
||||
|
||||
source exprash.sh;
|
||||
|
||||
redirectStdout 'log';
|
||||
|
||||
declare -A cats;
|
||||
cats[calico]="Calico";
|
||||
cats[sphynx]="Sphynx";
|
||||
cats[ragdoll]="Ragdoll";
|
||||
cats[holland_lop]="Holland Lop";
|
||||
cats[scottish_fold]="Scottish Fold";
|
||||
cats[orange]="Orange";
|
||||
|
||||
get '/' && {
|
||||
printf '<h1>Cats</h1>\n';
|
||||
printf '<ul>\n';
|
||||
html='<h1>Cats</h1><ul>';
|
||||
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
|
||||
printf '</ul>\n';
|
||||
html+='</ul>';
|
||||
|
||||
printf '%s' "$html" | send;
|
||||
}
|
||||
|
||||
get '/cats/:cat' && {
|
||||
key=$(param 'cat');
|
||||
|
||||
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
|
||||
next;
|
||||
fi;
|
||||
}
|
||||
|
||||
use && {
|
||||
printf '<h1>Error: Cannot find that cat</h1>\n';
|
||||
get '/cats/*' && {
|
||||
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
|
||||
printf '%s\n\n' 'Content-Type: text/html';
|
||||
|
||||
source exprash.sh;
|
||||
|
||||
redirectStdout 'log';
|
||||
|
||||
declare -A cats;
|
||||
cats[calico]="Calico";
|
||||
cats[sphynx]="Sphynx";
|
||||
cats[ragdoll]="Ragdoll";
|
||||
cats[holland_lop]="Holland Lop";
|
||||
cats[scottish_fold]="Scottish Fold";
|
||||
cats[orange]="Orange";
|
||||
|
||||
get '/' && {
|
||||
printf '<h1>Cats</h1>\n';
|
||||
printf '<ul>\n';
|
||||
html='<h1>Cats</h1><ul>';
|
||||
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
|
||||
printf '</ul>\n';
|
||||
html+='</ul>';
|
||||
|
||||
printf '%s' "$html" | send;
|
||||
}
|
||||
|
||||
get '/cats/:cat' && {
|
||||
key=$(param 'cat');
|
||||
|
||||
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
|
||||
next;
|
||||
fi;
|
||||
}
|
||||
|
||||
use && {
|
||||
printf '<h1>Error: Cannot find that cat</h1>\n';
|
||||
get '/cats/*' && {
|
||||
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
|
||||
# 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
|
||||
@ -31,24 +53,96 @@ function delete() {
|
||||
|
||||
# $1: path
|
||||
function all() {
|
||||
[ "$routeHandled" -eq 1 ] && return 1;
|
||||
[ "$_exprashRouteHandled" -eq 1 ] && return 1;
|
||||
[ -n "$_exprashErrorMessage" ] && return 1;
|
||||
|
||||
# Reset params
|
||||
params=();
|
||||
pathMatch "$1" params || return 1;
|
||||
_exprashParams=();
|
||||
|
||||
pathMatch "$1" _exprashParams || return 1;
|
||||
_exprashRouteHandled=1;
|
||||
|
||||
routeHandled=1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function use() {
|
||||
[ "$routeHandled" -eq 1 ] && return 1;
|
||||
routeHandled=1;
|
||||
[ "$_exprashRouteHandled" -eq 1 ] && return 1;
|
||||
[ -n "$_exprashErrorMessage" ] && return 1;
|
||||
|
||||
_exprashRouteHandled=1;
|
||||
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() {
|
||||
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
|
||||
function param() {
|
||||
printf '%s\n' "${params[$1]}";
|
||||
printf '%s\n' "${_exprashParams[$1]}";
|
||||
}
|
||||
|
||||
# $1: param name
|
||||
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
|
||||
# =========
|
||||
|
||||
function sendRaw() {
|
||||
[ -n "$_exprashRedirectStdout" ] && exec >&5;
|
||||
cat;
|
||||
[ -n "$_exprashRedirectStdout" ] && exec 1>>"$_exprashRedirectStdout";
|
||||
}
|
||||
|
||||
# $1: path
|
||||
# $2: (nameref) array
|
||||
function pathToArray() {
|
||||
@ -84,25 +211,31 @@ function pathMatch() {
|
||||
local path="$PATH_INFO";
|
||||
local route="$1";
|
||||
|
||||
# Params associative array
|
||||
local -n routeParams="$2";
|
||||
|
||||
local pathArr;
|
||||
pathToArray "$path" pathArr;
|
||||
|
||||
local routeArr;
|
||||
pathToArray "$route" routeArr;
|
||||
|
||||
# Params associative array
|
||||
local -n routeParams="$2";
|
||||
# Get max path length
|
||||
local routeLen=${#routeArr[@]};
|
||||
local pathLen=${#pathArr[@]};
|
||||
local maxLen=$(( routeLen >= pathLen ? routeLen : pathLen ));
|
||||
|
||||
# Not a match if pathArr and routeArr have different length
|
||||
[ "${#pathArr[@]}" -ne "${#routeArr[@]}" ] && return 1;
|
||||
|
||||
for ((i=0; i<${#routeArr[@]}; i++)); do
|
||||
for ((i=0; i<maxLen; i++)); do
|
||||
local routeComponent="${routeArr[$i]}";
|
||||
local pathComponent="${pathArr[$i]}";
|
||||
|
||||
# If route component starts with ":"
|
||||
if [[ "$routeComponent" == :* ]]; then
|
||||
routeParams["${routeComponent:1}"]="$pathComponent";
|
||||
elif [[ "$routeComponent" == '*' ]] && [ -n "$pathComponent" ]; then
|
||||
continue;
|
||||
elif [[ "$routeComponent" == '**' ]] && [ -n "$pathComponent" ]; then
|
||||
break;
|
||||
else
|
||||
# Confirm paths match
|
||||
[ "$routeComponent" != "$pathComponent" ] && return 1;
|
||||
@ -115,5 +248,18 @@ function pathMatch() {
|
||||
# =======
|
||||
# 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