rpc.lisp (5482B)
1 (in-package :cohost.rpc) 2 3 (defparameter +user-agent+ (concatenate 'string "CL-Cohost/0.1 " (drakma::user-agent-string :drakma))) 4 5 (defun %cohost-rpc (client rpc-fn method &key request-content params content-type) 6 "Low-level implementation for RPC methods. Usage identical to COHOST-RPC but can take either PARAMS (prestructured alist) or REQUEST-CONTENT and CONTENT-TYPE (raw POST data) as request body." 7 (let* ((drakma:*text-content-types* (acons :application :json drakma:*text-content-types*)) 8 (rpc-uri (concatenate 'string (base-uri client) (api-path client) rpc-fn))) 9 (multiple-value-bind (content http-code headers uri) 10 (funcall (http-request client) rpc-uri :method method 11 :content request-content 12 :content-type content-type 13 :parameters params 14 :user-agent +user-agent+ 15 :cookie-jar (cookie-jar client) 16 :decode-content t) 17 (values (decode-json-from-string content) http-code headers uri)))) 18 19 (defun cohost-rpc (client rpc-fn method &rest params) 20 "JSON RPC primitive. 21 22 Usage: (COHOST-RPC [initialized client object] [API function to call, eg \"login\"] [HTTP method (:GET or :POST)] (zero or more KEYWORD value pairs expressing parameters, eg :EMAIL \"foo@bar.baz\"))" 23 (labels ((param-pairs (params) 24 (loop for (key val) on params by #'cddr 25 while val 26 collect (cons (sym->str key) (if (numberp val) 27 (write-to-string val) 28 val))))) 29 (let* ((param-alist (param-pairs params))) 30 (%cohost-rpc client rpc-fn method :params param-alist)))) 31 32 (defun cohost-json-rpc (client rpc-fn json &key (method :post)) 33 "JSON RPC primitive for RPC calls that consume a JSON blob directly. 34 35 Usage: (COHOST-JSON-RPC [initialized client object] [API function to call, eg \"post\"] [JSON POST data] (:METHOD :POST or :PUT (default is :POST))" 36 (%cohost-rpc client rpc-fn method :request-content json :content-type "application/json")) 37 38 (defun cohost-trpc-batch (client requests) 39 "Consumes a set of tRPC request objects to construct a batch call. 40 41 Usage: (COHOST-TRPC-BATCH [initialized client object] [alist or batch items]) 42 43 Each batch item is a CONS structured as ([request] . [input data]). For requests with no data, eg login.loggedIn, the CDR is left NIL." 44 (multiple-value-bind (batch-items input-items) 45 (loop for pair in requests 46 for idx = 0 then (1+ idx) 47 if (> idx 0) 48 collect "," into batch-items 49 collect (car pair) into batch-items 50 if (cdr pair) 51 collect (cons idx (cdr pair)) into input-items 52 finally (return (values batch-items input-items))) 53 (let* ((batch (apply #'concatenate (append (list 'string "trpc/") batch-items))) 54 (json::+json-lisp-symbol-tokens+ '(("true" . T) 55 ("null" . nil) 56 ("false" . nil) 57 ("false" . :false))) 58 (batch-input (encode-json-alist-to-string input-items))) 59 (cohost-rpc client batch :get :batch 1 :input batch-input)))) 60 61 (defmacro define-trpc-request (name (&rest lambda-list)) 62 "Macro for defining tRPC request objects. 63 64 Usage: (DEFINE-TRPC-REQUEST [String name of request, eg \"login.loggedIn\"] ((NIL for requests with no parameters, :atom for requests that take a single atomic value, eg a post ID, or a list of string names of parameters for requests that take JSON input)) 65 66 As an example (DEFINE-TRPC-REQUEST \"foo.wibbleFrotz\" ()) would create a function named FOO.WIBBLE-FROTZ that takes no parameters and returns a CONS (\"foo.wibbleFrotz\" . NIL). 67 68 Functions created this way that construct a JSON input can take keyword :FALSE to express JSON false unambiguously. For instance: 69 (DEFINE-TRPC-REQUEST \"test.bool\" (\"booleanParam\")) 70 would create a function TEST.BOOL that takes a single parameter. (TEST.BOOL NIL) will result in a null value, {\"booleanParam\":null}, while (TEST.BOOL :FALSE) will emit a Javascript false, {\"booleanParam\":false}. This is clunky but unfortunately necessary as Javascript has different ideas of falsity and nullity than CL." 71 (labels ((bomb () 72 (error "Invalid lambda list for tRPC request (~A)" lambda-list)) 73 (create-ll-and-datum (raw-ll) 74 (loop for param in raw-ll 75 for sym = (gensym) 76 collect sym into lambda-list 77 collect `(cons ,param ,sym) into a-list 78 if (not (stringp param)) 79 do (bomb) 80 finally (return (cons lambda-list `(list ,@a-list)))))) 81 (let* ((lisp-name (intern (json:camel-case-to-lisp name))) 82 (data-sym (gensym)) 83 (ll-and-datum (cond 84 ((eq (car lambda-list) :atom) (cons `(,data-sym) data-sym)) 85 ((null lambda-list) '()) 86 ((listp lambda-list) (create-ll-and-datum lambda-list)) 87 (t (bomb))))) 88 `(defun ,lisp-name ,(car ll-and-datum) 89 (cons ,name ,(cdr ll-and-datum))))))