eudoxia0 / cl-yaml

YAML parser for Common Lisp
61 stars 7 forks source link

Add *yaml-tag-converter* special variable #18

Open christophejunke opened 9 months ago

christophejunke commented 9 months ago

Add a new package yaml.parser.extensions that exports a new special variable *yaml-tag-converter*, to use as a generic tag converter in case the tag is not already registered in conversion tables.

I need to handle tags but the existing mechanism is not very satisfactory to me: I'm writing a library on top of cl-yaml and would prefer not to make global changes to hashtables (for an application this would not be a problem but a library should try to avoid doing that). There is no unregistration or scoped registration mechanism. I could temporarily rebind the tables but the symbols are not exported. Less importantly I do not care in my case if the tag is applied to a scalar, a sequence or a mapping, so I find it easier to rely on a special variable bound to a function that works on all tags (possibly dispatching on the type of value).

I believe this change to be reasonably backward-compatible, given that I do not change the set of exported symbols of existing packages. There is a new package named yaml.parser.extensions which could break user code if some user also defines the same package, which is unlikely. Also the behavior is the same as before as long as the special variable is NIL. When it is non-NIL, the existing conversion tables are prioritary.

christophejunke commented 9 months ago

Usage example for completeness:

CL-USER> (use-package :yaml.parser.extensions)
T

CL-USER> (defstruct (tag (:constructor make-tag (name value))) name value)
TAG

CL-USER> (let ((*yaml-tag-converter* #'make-tag))
           (yaml:parse "!id 123456"))
#S(TAG :NAME "!id" :VALUE "123456")

More realistic example:

(defpackage :example.tags (:use) (:export #:uri #:date #:vec))

(defpackage :example (:use :cl :example.tags))
(in-package :example)

(defun intern-tag (name)
  (assert (char= #\! (char name 0)))
  (or (find-symbol (string-upcase (subseq name 1)) 
                   (load-time-value (find-package :example.tags)))
      (error "Unknown tag ~s" name)))

(defgeneric convert (tag value)
  (:method ((tag string) value)
    (restart-case (convert (intern-tag tag) value)
      (use-value (other) :report "Use alternative value" other)
      (ignore () :report "Use untagged value"  value))))

(defmethod convert ((_ (eql 'uri)) (s string))
  (quri:uri s))

(defmethod convert ((_ (eql 'date)) (s string))
  (local-time:parse-rfc3339-timestring s))

(defmethod convert ((_ (eql 'vec)) (v sequence))
  (coerce v 'vector))

(let ((yaml.parser.extensions:*yaml-tag-converter* #'convert))
  (yaml:parse "!vec [!uri https://example.com, !date 1937-01-01T12:00:27.87+00:20]"))

#(#<QURI.URI.HTTP:URI-HTTPS https://example.com> @1937-01-01T11:40:27.870000Z)
eudoxia0 commented 9 months ago

The code looks fine, but I feel like this project (which I have not maintained in years, and I have no interesting in maintaining CL projects) should be forked by someone who wants to fix the bugs/extend the features, and the Quicklisp distribution updated.

christophejunke commented 9 months ago

I can manage to find some time each month to maintain it if you want.

How does xach/quicklisp know which version to take? is it the master branch? Is there anything else I need to know?

Thanks

eudoxia0 commented 9 months ago

How does xach/quicklisp know which version to take? is it the master branch? Is there anything else I need to know?

I believe just raising an issue in the repo: https://github.com/quicklisp/quicklisp-projects