;;;; lazybones.lisp (in-package #:lazybones) ;;; DYNAMIC VARIABLES (defvar *request* nil "Dynamic Variable holding the current request object. Dynamically bound and available to each handler. The exact object bound to *request* varies according to the current backend.") (defvar *response* nil "Dynamic variable holding the current response object. Dynamically bound and available to each handler. The exact object bound *response* varies according to the current backend. ") (defvar *app* nil "Dynamic variable holding the an APP instance. Dynamically bound by RUN-ENDPOINT so that it is available if needed in request handlers.") ;;; LAZYBONES CLASSES (defclass app () ((name :reader app-name :initarg :name :initform (error "Appname is required") :type symbol) (version :reader app-version :initarg :vsn :initarg :version :initform "0.0.1" :type string) (endpoints :accessor app-endpoints :initform nil))) (defclass endpoint () ((method :reader endpoint-method :initarg :method :initform :get) (template :reader endpoint-template :initarg :template :initform (error "endpoint template required")) (dispatch-pattern :reader endpoint-dispatch-pattern) (handler-function :reader endpoint-request-handler) (documentation :reader endpoint-documentation :initarg :doc :initform ""))) (defun routekey-term-match-p (pattern-term routekey-term) "Internal helper function. Returns T if both arguments are strings and they compare with STRING-EQUAL. Otherwise, PATTERN-TERM is assumed to be a route variable representation, in which case T is returned, indicating that the variable should bind to anything." (if (stringp pattern-term) (string-equal pattern-term routekey-term) t)) (defun matches-routekey-p (pattern key) "PATTERN is a list, each member of which is a string or a variable representation. PATTERN will have been generated by PARSE-ROUTE-STRING-TEMPLATE. If there are no variables in PATTERN, MATCHES-ROUTEKEY-P returns T or NIL. If there are variables in the pattern, MATCHES-ROUTEKEY-P returns a list of values, in the case of success, or NIL in the case of failure." (when (= (length pattern) (length key)) (loop for pterm in pattern for rterm in key for matchp = (routekey-term-match-p pterm rterm) unless matchp return nil when (listp pterm) ; looks like (var) or (var value-parser) collect (if (second pterm) (funcall (second pterm) rterm) ; parse value from rterm if we can rterm) ; otherwise use rterm string into arguments finally (return (or arguments t))))) (defun find-endpoint (app &optional (request *request*)) (find-endpoint-matching-key app (request-method request) (request-routekey request))) (defun find-endpoint-matching-key (app method key) "Returns a list. NIL represents failure to find match. Otherwise the result is (ENDPOINT . ARGS) where ENDPOINT is an endpoint instanceq and ARGS is a list of arguments to pass to ENDPOINT's handler function." (loop for endpoint in (app-endpoints app) for match = (and (eql method (endpoint-method endpoint)) (matches-routekey-p endpoint key)) when match return (cons endpoint (when (listp match) match)))) (defparameter +http-methods+ (list :get :head :put :post :delete :patch)) (defun url-path->request-routekey (path) "A routekey is used to match urls to endpoints that handle them." (str:split #\/ path)) (defun request-routekey (request) (url-path->request-routekey (request-path request))) (defun parse-route-string-template (template) "Routes are of the form /foo/bar/<>/blah /foo/bar/<>/blah On success returns things like: (\"foo\" \"bar\" (VARIABLE) \"blah\") (\"foo\" \"bar\" (VAR PARSE-INTEGER) \"blah\") Returns NIL on failure" (cond ((equal "" template) nil) (t (when (search "//" template) (warn "The proposed route ~s contains a double forward-slash (//), is this intended?" template)) (when (a:ends-with #\/ template) (warn "The proposed route ~s ends with a forward-slash (/), is this intended?" template)) (unless (eql #\/ (elt template 0)) (warn "The proposed route ~s does not begin with a forward-slash, is this intended?" template)) (loop for field in (str:split #\/ template) for var? = (parse-route-variable-string field) when var? collect var? else collect (string-downcase field))))) (defun parse-route-variable-string (string) "A route variable string looks like <> or <> In the case of a successful parse, a list of one or two symbols is returned. These symbosl are created using read-from-string, which allows for these symbols' packages to be specified if desired. Returns NIL on failure." (when (and (a:starts-with-subseq "<<" string) (a:ends-with-subseq ">>" string)) (destructuring-bind (var-name . decoder?) (re:split " +" (string-trim " " (subseq string 2 (- (length string) 2)))) (if decoder? (list (gensym var-name) (read-from-string (first decoder?))) (list (gensym var-name)))))) (defun run-endpoint (endpoint args request response app) (let ((*request* request) (*response* response) (*app* app)) (apply (endpoint-request-handler endpoint) args)))