Browse Source

Added route wildcards, error handlers, header management and test suite

master
Ben Ashton 2 years ago
parent
commit
abf3fed939
  1. 1
      .gitignore
  2. 33
      README.md
  3. 33
      examples/cats.sh
  4. 10
      run_tests.sh
  5. 184
      src/exprash.sh
  6. 55
      tests/routes.sh
  7. 23
      tests/utils.sh

1
.gitignore vendored

@ -0,0 +1 @@
watch.sh

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

33
examples/cats.sh

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

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

@ -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;
}
# =====================
# 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; 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[@]};
# Not a match if pathArr and routeArr have different length local maxLen=$(( routeLen >= pathLen ? routeLen : pathLen ));
[ "${#pathArr[@]}" -ne "${#routeArr[@]}" ] && return 1;
for ((i=0; i<${#routeArr[@]}; i++)); do for ((i=0; i<maxLen; 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

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

@ -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…
Cancel
Save