summaryrefslogtreecommitdiff
path: root/src/tg/type-macros.lisp
blob: 5f99cba1ed50ea8bb75cc8d144f10eba4b629354 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
;; SPDX-License-Identifier: EUPL-1.2
;; SPDX-FileCopyrightText: 2025 Uko Kokņevičs <perkontevs@gmail.com>
(defpackage :ukkoclot/tg/type-macros
  (:documentation "Macros for easy defining TG types.")
  (:use :c2cl :iterate)
  (:import-from :alexandria :with-gensyms)
  (:import-from :com.dieggsy.f-string :enable-f-strings)
  (:import-from :ukkoclot/serializing :parse-value)
  (:import-from :ukkoclot/hash-tables :gethash-lazy)
  (:import-from :ukkoclot/strings :lisp->snake-case)
  (:local-nicknames
   (:jzon :com.inuoe.jzon))
  (:export :define-tg-type))
(in-package :ukkoclot/tg/type-macros)

(enable-f-strings)

(eval-when (:compile-toplevel :load-toplevel :execute)
  (defstruct (field (:constructor make-field%)) name type default skip-if-default)

  (defparameter +unique+ (gensym))

  ;; TODO: Fix optional-and-key !
  (defun make-field (name type          ; lint:suppress avoid-optional-and-key
                     &optional (default +unique+)
                     &key (skip-if-default (not (eq default +unique+))))
    (let ((default (if (eq default +unique+)
                       `(error ,#f"No value given for {name}")
                       default)))
      (make-field% :name name
                   :type type
                   :default default
                   :skip-if-default skip-if-default)))

  (defun type-constructor (name)
    (intern (concatenate 'string "MAKE-" (symbol-name name))))

  (defun field-accessor (name field)
    (intern (concatenate 'string (symbol-name name) "-" (symbol-name (field-name field)))))

  (defun field-hash-key (field)
    (string-downcase (lisp->snake-case (symbol-name (field-name field)))))

  (defun field-keyword (field)
    (intern (symbol-name (field-name field)) :keyword))

  (defun parse-field-specs (field-specs)
    (iter (for field-spec in field-specs)
          (collect (apply #'make-field field-spec))))

  (defun emit-append-to-pprint-args (field value pprint-args)
    `(setf ,pprint-args (list* ',(field-name field) ,value ,pprint-args)))

  (defun emit-coerced-field (field value)
    `(list ,(field-hash-key field) ,value ',(field-type field)))

  (defun emit-collect-nondefault-fields (name fields obj collector)
    (with-gensyms (value)
      (iter (for field in (reverse fields))
            (collect
                (if (field-skip-if-default field)
                    `(let ((,value (,(field-accessor name field) ,obj)))
                       (unless (equal ,value ,(field-default field))
                         ,(funcall collector field value)))
                    (funcall collector field (list (field-accessor name field) obj)))))))

  (defun emit-constructor-args (field)
    `(,(field-keyword field) ,(field-name field)))

  (defun emit-gethash (field source)
    `(gethash-lazy ,(field-hash-key field) ,source ,(field-default field)))

  (defun emit-jzon-coerced-fields (name fields)
    (with-gensyms (obj result)
      `(defmethod jzon:coerced-fields ((,obj ,name))
         (let (,result)
           ,@(emit-collect-nondefault-fields
              name fields obj
              (lambda (field value)
                `(push ,(emit-coerced-field field value) ,result)))
           ,result))))

  (defun emit-let-gethash (field source)
    `(,(field-name field)
      (parse-value ',(field-type field) ,(emit-gethash field source))))

  (defun emit-parse-value (name fields)
    (with-gensyms (source type)
      `(defmethod parse-value ((,type (eql ',name)) ,source)
         (let (,@(iter (for field in fields)
                       (collect (emit-let-gethash field source))))
           (,(type-constructor name)
            ,@(iter (for field in fields)
                    (appending (emit-constructor-args field))))))))

  (defun emit-printer (name printer-name fields)
    (with-gensyms (depth obj pprint-args stream)
      `(defun ,printer-name (,obj ,stream ,depth) ; lint:suppress lambda-list-invalid
         (declare (ignore ,depth))
         (let (,pprint-args)
           ,@(emit-collect-nondefault-fields
              name fields obj
              (lambda (field value)
                `(setf ,pprint-args (list* ',(field-name field) ,value ,pprint-args))))
           (format ,stream "~A~<[~;~@{~_~1I~W = ~W~^, ~}~;]~:>" ',name ,pprint-args)))))

  (defun emit-struct (name printer-name fields)
    `(defstruct (,name (:print-function ,printer-name))
       ,@(iter (for field in fields)
               (collect (emit-struct-field field)))))

  (defun emit-struct-field (field)
    `(,(field-name field) ,(field-default field) :type ,(field-type field))))

(defmacro define-tg-type (name &body field-specs)
  (let ((fields (parse-field-specs field-specs)))
    (with-gensyms (printer)
      `(progn
         ,(emit-struct name printer fields)
         ,(emit-printer name printer fields)
         ,(emit-parse-value name fields)
         ,(emit-jzon-coerced-fields name fields)))))