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