diff options
Diffstat (limited to 'src/tg-types/macros.lisp')
| -rw-r--r-- | src/tg-types/macros.lisp | 134 |
1 files changed, 134 insertions, 0 deletions
diff --git a/src/tg-types/macros.lisp b/src/tg-types/macros.lisp new file mode 100644 index 0000000..668df17 --- /dev/null +++ b/src/tg-types/macros.lisp | |||
| @@ -0,0 +1,134 @@ | |||
| 1 | ;; SPDX-License-Identifier: EUPL-1.2 | ||
| 2 | ;; SPDX-FileCopyrightText: 2025 Uko Kokņevičs <perkontevs@gmail.com> | ||
| 3 | (defpackage :ukkoclot/tg-types/macros | ||
| 4 | (:use :c2cl) | ||
| 5 | (:import-from :ukkoclot/bot/impl :arg-encode :bot :do-call :will-arg-encode) | ||
| 6 | (:import-from :ukkoclot/hash-tables :gethash-lazy) | ||
| 7 | (:import-from :ukkoclot/strings :lisp->snake-case) | ||
| 8 | (:export :define-tg-method :define-tg-type)) | ||
| 9 | (in-package :ukkoclot/tg-types/macros) | ||
| 10 | |||
| 11 | (eval-when (:compile-toplevel :load-toplevel :execute) | ||
| 12 | (defstruct (field (:constructor make-field%)) name type default skip-if-default (parser 'identity)) | ||
| 13 | |||
| 14 | (defparameter +unique+ (gensym)) | ||
| 15 | |||
| 16 | (defun make-field (name type &optional (default +unique+) &key (parser 'identity) (skip-if-default (not (eq default +unique+)))) | ||
| 17 | (let ((default (if (eq default +unique+) | ||
| 18 | (list 'error (format nil "No value given for ~A" name)) | ||
| 19 | default))) | ||
| 20 | (make-field% :name name | ||
| 21 | :type type | ||
| 22 | :default default | ||
| 23 | :skip-if-default skip-if-default | ||
| 24 | :parser parser))) | ||
| 25 | |||
| 26 | (defun parse-field-specs (field-specs) | ||
| 27 | (loop for field-spec in field-specs | ||
| 28 | collect (apply #'make-field field-spec))) | ||
| 29 | |||
| 30 | (defun field-hash-key (field) | ||
| 31 | (string-downcase (lisp->snake-case (symbol-name (field-name field))))) | ||
| 32 | |||
| 33 | (defun field-accessor (struc-name field) | ||
| 34 | (intern (concatenate 'string (symbol-name struc-name) "-" (symbol-name (field-name field))))) | ||
| 35 | |||
| 36 | (defun field->defun-spec (field) | ||
| 37 | (list (field-name field) (field-default field))) | ||
| 38 | |||
| 39 | (defun field->format-arg (field name struc) | ||
| 40 | `(',(field-name field) (,(field-accessor name field) ,struc))) | ||
| 41 | |||
| 42 | (defun field->ftype-spec (field) | ||
| 43 | (list (intern (symbol-name (field-name field)) :keyword) (field-type field))) | ||
| 44 | |||
| 45 | (defun field->gethash-spec (field hash-table-sym) | ||
| 46 | (let ((hash-key (field-hash-key field))) | ||
| 47 | (list 'gethash-lazy hash-key hash-table-sym (field-default field)))) | ||
| 48 | |||
| 49 | (defun field->sethash-spec (field name struc hash-table-sym) | ||
| 50 | (let ((hash-key (field-hash-key field)) | ||
| 51 | (skip-if-default (field-skip-if-default field)) | ||
| 52 | (default (field-default field))) | ||
| 53 | (if skip-if-default | ||
| 54 | (let ((tmpsym (gensym "TMP"))) | ||
| 55 | `(let ((,tmpsym (,(field-accessor name field) ,struc))) | ||
| 56 | (unless (equal ,tmpsym ,default) | ||
| 57 | (setf (gethash ,hash-key ,hash-table-sym) ,tmpsym)))) | ||
| 58 | `(setf (gethash ,hash-key ,hash-table-sym) (,(field-accessor name field) ,struc))))) | ||
| 59 | |||
| 60 | (defun field->let-gethash-spec (field hash-table-sym) | ||
| 61 | (list (field-name field) | ||
| 62 | (list 'funcall | ||
| 63 | (list 'function (field-parser field)) | ||
| 64 | (field->gethash-spec field hash-table-sym)))) | ||
| 65 | |||
| 66 | (defun field->make-spec (field) | ||
| 67 | (list (intern (symbol-name (field-name field)) :keyword) | ||
| 68 | (field-name field))) | ||
| 69 | |||
| 70 | (defun field->struct-spec (field) | ||
| 71 | (list (field-name field) (field-default field) :type (field-type field)))) | ||
| 72 | |||
| 73 | ;; TODO: Automatically derive path from name | ||
| 74 | ;; TODO: Automatically derive mapfn from type | ||
| 75 | ;; TODO: Skip values that are already their defaults | ||
| 76 | (defmacro define-tg-method ( | ||
| 77 | (name type path mapfn &optional (method :POST)) | ||
| 78 | &body field-specs) | ||
| 79 | (let ((fields (parse-field-specs field-specs)) | ||
| 80 | (args-plist (gensym "ARGS-PLIST-")) | ||
| 81 | (bot (gensym "BOT-"))) | ||
| 82 | `(progn | ||
| 83 | (declaim (ftype (function (bot &key ,@(loop for field in fields | ||
| 84 | collect (field->ftype-spec field))) | ||
| 85 | ,type) | ||
| 86 | ,name)) | ||
| 87 | (defun ,name (,bot &rest ,args-plist &key ,@(loop for field in fields collect (field->defun-spec field))) | ||
| 88 | (declare ,@(loop for field in fields collect (list 'ignore (field-name field)))) | ||
| 89 | (do-call ,bot ,method ,path ,mapfn ,args-plist))))) | ||
| 90 | |||
| 91 | (defmacro define-tg-type (name &body field-specs) | ||
| 92 | (let* ((fields (parse-field-specs field-specs)) | ||
| 93 | (revfields (reverse fields)) | ||
| 94 | (make-name (intern (concatenate 'string "MAKE-" (symbol-name name)))) | ||
| 95 | (hash->name (intern (concatenate 'string "HASH->" (symbol-name name)))) | ||
| 96 | (parse-name-array (intern (concatenate 'string "PARSE-" (symbol-name name) "-ARRAY"))) | ||
| 97 | (printer (gensym (concatenate 'string "PRINT-" (symbol-name name)))) | ||
| 98 | (hash (gensym "HASH-")) | ||
| 99 | (array (gensym "ARRAY-")) | ||
| 100 | (struc (gensym (symbol-name name))) | ||
| 101 | (stream (gensym "STREAM")) | ||
| 102 | (depth (gensym "DEPTH")) | ||
| 103 | (pprint-args (gensym "PPRINT-ARGS"))) | ||
| 104 | `(progn | ||
| 105 | (defstruct (,name (:print-function ,printer)) | ||
| 106 | ,@(loop for field in fields | ||
| 107 | collect (field->struct-spec field))) | ||
| 108 | (defun ,printer (,struc ,stream ,depth) | ||
| 109 | (declare (ignore ,depth)) | ||
| 110 | (let (,pprint-args) | ||
| 111 | ,@(loop for field in revfields | ||
| 112 | collecting | ||
| 113 | (if (field-skip-if-default field) | ||
| 114 | `(let ((value (,(field-accessor name field) ,struc))) | ||
| 115 | (unless (equal value ,(field-default field)) | ||
| 116 | (setf ,pprint-args (list* ',(field-name field) value ,pprint-args)))) | ||
| 117 | `(setf ,pprint-args (list* ',(field-name field) (,(field-accessor name field) ,struc) ,pprint-args)))) | ||
| 118 | (format ,stream "~A~<[~;~@{~_~1I~W = ~W~^, ~}~;]~:>" ',name ,pprint-args))) | ||
| 119 | (defun ,hash->name (,hash) | ||
| 120 | (when ,hash | ||
| 121 | (let ,(loop for field in fields | ||
| 122 | collect (field->let-gethash-spec field hash)) | ||
| 123 | (,make-name ,@(loop for field in fields | ||
| 124 | append (field->make-spec field)))))) | ||
| 125 | (defmethod arg-encode ((,struc ,name)) | ||
| 126 | (let ((,hash (make-hash-table))) | ||
| 127 | ,@(loop for field in fields | ||
| 128 | collect (field->sethash-spec field name struc hash)) | ||
| 129 | ,hash)) | ||
| 130 | (defmethod will-arg-encode ((,struc ,name)) | ||
| 131 | t) | ||
| 132 | (defun ,parse-name-array (,array) | ||
| 133 | (when ,array | ||
| 134 | (map 'vector #',hash->name ,array)))))) | ||