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