;;;; lazybones.lisp (in-package #:lazybones) ;;; Generic Functions (defgeneric handle-request (what request) (:documentation "Implemented for APP and ENDPOINT instances.")) (defgeneric dispatch-handler-p (endpoint request) (:documentation "T if ENDPOINT should handle REQUEST, NIL otherwise")) (defgeneric uri-path (request) (:documentation "Returns the path associated with the request")) (defgeneric uri-query (request &key rawp) (:documentation "Returns the whole query associated with a request. If RAWP is truthy, should return the raw query string. Otherwise should parse it somehow.")) (defgeneric request-body (request) (:documentation "Returns the body of a request that has one, or NIL if not.")) (defgeneric request-authorized-p (endpoint request) (:documentation "Returns T if the REQUEST has authorization to dispatch the handler for ENDPOINT")) ;;; 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) (root :reader app-root :initarg :root :initform "/" :type string) (default-request-authorizer :initarg :default-authorizier :initarg :auth-with :initform nil) (default-http-responders :initarg :default-responders :initform nil :documentation "A PLIST with keys being integers that represent HTTP response codes and with values that are symbols naming responder functions.") (endpoints :accessor app-endpoints :initform nil))) (defmethod handle-request ((app app) request) (a:if-let (endpoint (lookup-endpoint-for app request)) (handle-request endpoint request) (error-response )) ) (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) (app :reader endpoint-app :initarg :app :initform (error "every endpoint must have backlink to an app") :documentation "backlink to the app that this endpoint is a part of.") (documentation :reader endpoint-documentation :initarg :doc :initform ""))) (defparameter +http-methods+ (list :get :head :put :post :delete :patch)) (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" (when (stringp template) (cond ((equal "" template) nil) (t (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 (read-from-string var-name) (read-from-string (first decoder?))) (list (read-from-string var-name)))))) ;; (defun add-route (method routestring handler-function) ;; (assert (member method +http-methods+) nil ;; "~a is not a valid HTTP method indicator." ;; method) ;; )