git.fiddlerwoaroof.com
Browse code

Parser interface for constructing object. New emitter interface

Jason Melbye authored on 18/11/2016 04:37:17
Showing 6 changed files
... ...
@@ -16,6 +16,7 @@
16 16
                  (:file "scalar")
17 17
                  (:file "parser")
18 18
                  (:file "emitter")
19
+		 (:file "round-trip")
19 20
                  (:file "spec")
20 21
                  (:file "bench")
21 22
                  (:file "cl-yaml")))))
... ...
@@ -1,70 +1,228 @@
1 1
 (in-package :cl-user)
2 2
 (defpackage yaml.emitter
3 3
   (:use :cl)
4
-  (:export :encode
5
-           :emit
6
-           :emit-to-string)
4
+  (:import-from :cffi
5
+		:foreign-free
6
+		:null-pointer)
7
+  (:import-from :libyaml.emitter
8
+		:allocate-emitter
9
+		:emitter-initialize
10
+		:emitter-delete
11
+		:emit
12
+		:set-output
13
+		:stream-start-event-initialize
14
+		:stream-end-event-initialize
15
+		:document-start-event-initialize
16
+		:document-end-event-initialize
17
+		:scalar-event-initialize
18
+		:sequence-start-event-initialize
19
+		:sequence-end-event-initialize
20
+		:mapping-start-event-initialize
21
+		:mapping-end-event-initialize)
22
+  (:import-from :libyaml.event
23
+		:allocate-event
24
+		:event-delete)
25
+  (:import-from :libyaml.write-handler
26
+		:*write-handler-callback*
27
+		:*write-handler-stream*)
28
+  (:export :stream-start-event
29
+	   :stream-end-event
30
+	   :document-start-event
31
+	   :document-end-event
32
+	   :scalar-event
33
+	   :sequence-start-event
34
+	   :sequence-end-event
35
+	   :mapping-start-event
36
+	   :mapping-end-event
37
+	   :emit-stream
38
+	   :emit-document
39
+	   :emit-sequence
40
+	   :emit-mapping
41
+	   :emit-scalar
42
+           :emit-object
43
+           :print-scalar
44
+	   :with-emitter-to-stream
45
+	   :with-emitter-to-string)
7 46
   (:documentation "The YAML emitter."))
8 47
 (in-package :yaml.emitter)
9 48
 
10
-;;; Encoder functions
11
-
12
-(defgeneric encode (value stream)
13
-  (:documentation "Write the YAML corresponding to value to a stream."))
14
-
15
-(defmethod encode ((true (eql 't)) stream)
16
-  "Encode true."
17
-  (write-string "true" stream))
18
-
19
-(defmethod encode ((true (eql 'nil)) stream)
20
-  "Encode false."
21
-  (write-string "false" stream))
22
-
23
-(defmethod encode ((integer integer) stream)
24
-  "Encode an integer."
25
-  (princ integer stream))
26
-
27
-(defmethod encode ((float float) stream)
28
-  "Encode a float."
29
-  (princ float stream))
30
-
31
-(defmethod encode ((string string) stream)
32
-  "Encode a string."
33
-  ;; (write-string string stream)
34
-  (format stream "~s" string))
35
-
36
-(defmethod encode ((list list) stream)
37
-  "Encode a list."
38
-  (write-string "[" stream)
39
-  (loop for sublist on list do
40
-    (encode (first sublist) stream)
41
-    (when (rest sublist)
42
-      (write-string ", " stream)))
43
-  (write-string "]" stream))
44
-
45
-(defmethod encode ((vector vector) stream)
46
-  "Encode a vector."
47
-  (encode (loop for elem across vector collecting elem) stream))
48
-
49
-(defmethod encode ((table hash-table) stream)
50
-  "Encode a hash table."
51
-  (write-string "{ " stream)
52
-  (loop for sublist on (alexandria:hash-table-keys table) do
53
-    (let ((key (first sublist)))
54
-      (encode key stream)
55
-      (write-string ": " stream)
56
-      (encode (gethash key table) stream)
57
-      (when (rest sublist)
58
-        (write-string ", " stream))))
59
-  (write-string " }" stream))
49
+;;; Wrappers around cl-libyaml event interface with defaults and keyword args
60 50
 
61
-;;; Interface
51
+(defun stream-start-event (event &key (encoding :utf8-encoding))
52
+  (stream-start-event-initialize event encoding))
53
+
54
+(defun stream-end-event (event)
55
+  (stream-end-event-initialize event))
56
+
57
+(defun document-start-event (event &key (version-directive (null-pointer))
58
+				     (tag-directive-start (null-pointer))
59
+				     (tag-directive-end (null-pointer))
60
+				     (implicit nil))
61
+  (document-start-event-initialize event version-directive
62
+				   tag-directive-start
63
+				   tag-directive-end
64
+				   implicit))
65
+
66
+(defun document-end-event (event &key (implicit nil))
67
+  (document-end-event-initialize event implicit))
68
+
69
+(defun sequence-start-event (event &key (anchor (null-pointer))
70
+				     (tag (null-pointer))
71
+				     (implicit nil)
72
+				     (style :any-sequence-style))
73
+  (sequence-start-event-initialize event anchor tag implicit style))
74
+
75
+(defun sequence-end-event (event)
76
+  (sequence-end-event-initialize event))
77
+
78
+(defun mapping-start-event (event &key (anchor (null-pointer))
79
+				    (tag (null-pointer))
80
+				    (implicit nil)
81
+				    (style :any-mapping-style))
82
+  (mapping-start-event-initialize event anchor tag implicit style))
83
+
84
+(defun mapping-end-event (event)
85
+  (mapping-end-event-initialize event))
86
+
87
+(defun scalar-event (event value length &key (anchor (null-pointer))
88
+					  (tag (null-pointer))
89
+					  (plain-implicit t)
90
+					  (quoted-implicit t)
91
+					  (style :plain-scalar-style))
92
+  (scalar-event-initialize event anchor tag value length
93
+			   plain-implicit quoted-implicit style))
94
+
95
+;;; Emitter macros and output functions
96
+
97
+;;; When passing a foreign emitter object, it is also paired with a
98
+;;; foreign event object.
99
+
100
+(defun foreign-emitter (emitter) (car emitter))
101
+
102
+(defun foreign-event (emitter) (cdr emitter))
103
+
104
+(defmacro with-emitter-to-stream ((emitter-var output-stream) &rest body)
105
+  (let ((foreign-emitter (gensym "EMITTER"))
106
+	(foreign-event (gensym "EVENT")))
107
+    `(let* ((,foreign-emitter (allocate-emitter))
108
+	    (,foreign-event (allocate-event))
109
+	    (,emitter-var (cons ,foreign-emitter ,foreign-event))
110
+	    (*write-handler-stream* ,output-stream))
111
+      (unwind-protect
112
+	   (progn
113
+	     (emitter-initialize ,foreign-emitter)
114
+	     (set-output ,foreign-emitter *write-handler-callback* (null-pointer))
115
+	     ,@body)
116
+	(libyaml.event:event-delete ,foreign-event)
117
+	(libyaml.emitter:emitter-delete ,foreign-emitter)
118
+	(foreign-free ,foreign-event)
119
+	(foreign-free ,foreign-emitter)))))
120
+
121
+(defmacro with-emitter-to-string ((emitter-var) &rest body)
122
+  (let ((str (gensym "STR")))
123
+    `(with-output-to-string (,str)
124
+       (with-emitter-to-stream (,emitter-var ,str)
125
+	 ,@body))))
62 126
 
63
-(defun emit (value stream)
64
-  "Emit a value to a stream."
65
-  (encode value stream))
127
+(defmacro emit-stream ((emitter &key (encoding :utf8-encoding)) &body body)
128
+  (let ((emitter-value (gensym "EMITTER"))
129
+	(foreign-emitter (gensym "FOREIGN-EMITTER"))
130
+	(foreign-event (gensym "FOREIGN-EVENT")))
131
+    `(let* ((,emitter-value ,emitter)
132
+	    (,foreign-emitter (foreign-emitter ,emitter-value))
133
+	    (,foreign-event (foreign-event ,emitter-value)))
134
+       (stream-start-event ,foreign-event :encoding ,encoding)
135
+       (libyaml.emitter:emit ,foreign-emitter ,foreign-event)
136
+       ,@body
137
+       (stream-end-event ,foreign-event)
138
+       (libyaml.emitter:emit ,foreign-emitter ,foreign-event))))
139
+
140
+(defmacro emit-document ((emitter &rest rest
141
+				  &key version-directive
142
+				  tag-directive-start 
143
+				  tag-directive-end
144
+				  (implicit 0)) &body body)
145
+  (declare (ignorable version-directive tag-directive-start
146
+		      tag-directive-end implicit))
147
+  (let ((emitter-value (gensym "EMITTER"))
148
+	(foreign-emitter (gensym "FOREIGN-EMITTER"))
149
+	(foreign-event (gensym "FOREIGN-EVENT")))
150
+    `(let* ((,emitter-value ,emitter)
151
+	    (,foreign-emitter (foreign-emitter ,emitter-value))
152
+	    (,foreign-event (foreign-event ,emitter-value)))
153
+       (apply #'document-start-event ,foreign-event (list ,@rest))
154
+       (libyaml.emitter:emit ,foreign-emitter ,foreign-event)
155
+       ,@body
156
+       (document-end-event ,foreign-event :implicit ,implicit)
157
+       (libyaml.emitter:emit ,foreign-emitter ,foreign-event))))
158
+
159
+(defmacro emit-mapping ((emitter &rest rest &key anchor tag implicit style)
160
+			&body body)
161
+  (declare (ignorable anchor tag implicit style))
162
+  (let ((emitter-value (gensym "EMITTER"))
163
+	(foreign-emitter (gensym "FOREIGN-EMITTER"))
164
+	(foreign-event (gensym "FOREIGN-EVENT")))
165
+    `(let* ((,emitter-value ,emitter)
166
+	    (,foreign-emitter (foreign-emitter ,emitter-value))
167
+	    (,foreign-event (foreign-event ,emitter-value)))
168
+       (apply #'mapping-start-event ,foreign-event (list ,@rest))
169
+       (libyaml.emitter:emit ,foreign-emitter ,foreign-event)
170
+       ,@body
171
+       (mapping-end-event ,foreign-event)
172
+       (libyaml.emitter:emit ,foreign-emitter ,foreign-event))))
173
+
174
+(defmacro emit-sequence ((emitter &rest rest &key anchor tag implicit style)
175
+			 &body body)
176
+  (declare (ignorable anchor tag implicit style))
177
+  (let ((emitter-value (gensym "EMITTER"))
178
+	(foreign-emitter (gensym "FOREIGN-EMITTER"))
179
+	(foreign-event (gensym "FOREIGN-EVENT")))
180
+    `(let* ((,emitter-value ,emitter)
181
+	    (,foreign-emitter (foreign-emitter ,emitter-value))
182
+	    (,foreign-event (foreign-event ,emitter-value)))
183
+       (apply #'sequence-start-event ,foreign-event (list ,@rest))
184
+       (libyaml.emitter:emit ,foreign-emitter ,foreign-event)
185
+       ,@body
186
+       (sequence-end-event ,foreign-event)
187
+       (libyaml.emitter:emit ,foreign-emitter ,foreign-event))))
188
+
189
+(defun emit-scalar (emitter value &rest rest &key anchor tag
190
+					       plain-implicit
191
+					       quoted-implicit
192
+					       style)
193
+  (declare (ignorable anchor tag plain-implicit quoted-implicit style))
194
+  (let ((printed-value (print-scalar value)))
195
+    (apply #'scalar-event (foreign-event emitter)
196
+	   printed-value (length printed-value) rest)
197
+    (emit (foreign-emitter emitter) (foreign-event emitter))))
198
+
199
+(defgeneric print-scalar (scalar)
200
+  (:documentation "Convert a scalar object into its printed representation"))
201
+
202
+(defmethod print-scalar ((scalar (eql 't)))
203
+  "true")
204
+
205
+(defmethod print-scalar ((scalar (eql 'nil)))
206
+  "false")
207
+
208
+(defmethod print-scalar ((scalar symbol))
209
+  (symbol-name scalar))
210
+
211
+(defmethod print-scalar ((scalar string))
212
+  scalar)
213
+
214
+(defmethod print-scalar ((scalar integer))
215
+  (princ-to-string scalar))
216
+
217
+(defmethod print-scalar ((scalar single-float))
218
+  (let ((*read-default-float-format* 'single-float))
219
+    (princ-to-string scalar)))
220
+
221
+(defmethod print-scalar ((scalar double-float))
222
+  (let ((*read-default-float-format* 'double-float))
223
+    (princ-to-string scalar)))
224
+
225
+;;; Interface
66 226
 
67
-(defun emit-to-string (value)
68
-  "Emit a value to string."
69
-  (with-output-to-string (stream)
70
-    (emit value stream)))
227
+(defgeneric emit-object (emitter obj)
228
+  (:documentation "Emit YAML representation of obj"))
... ...
@@ -3,8 +3,9 @@
3 3
   (:use :cl)
4 4
   (:nicknames :yaml)
5 5
   (:export :parse
6
-           :emit
7
-           :emit-to-string)
6
+           ;:emit
7
+           ;:emit-to-string
8
+	   )
8 9
   (:documentation "The main YAML interface."))
9 10
 (in-package :yaml)
10 11
 
... ...
@@ -22,8 +23,8 @@
22 23
   (parse (uiop:read-file-string input)
23 24
          :multi-document-p multi-document-p))
24 25
 
25
-(defun emit (value stream)
26
-  (yaml.emitter:emit value stream))
26
+;; (defun emit (value stream)
27
+;;   (yaml.emitter:emit value stream))
27 28
 
28
-(defun emit-to-string (value)
29
-  (yaml.emitter:emit-to-string value))
29
+;; (defun emit-to-string (value)
30
+;;   (yaml.emitter:emit-to-string value))
... ...
@@ -10,5 +10,6 @@
10 10
   (run! 'cl-yaml-test.scalar:scalar)
11 11
   (run! 'cl-yaml-test.parser:parser)
12 12
   (run! 'cl-yaml-test.emitter:emitter)
13
+  (run! 'cl-yaml-test.round-trip:round-trip)
13 14
   (run! 'cl-yaml-test.spec:spec)
14 15
   (run! 'cl-yaml-test.bench:bench))
... ...
@@ -1,67 +1,80 @@
1 1
 (in-package :cl-user)
2 2
 (defpackage cl-yaml-test.emitter
3 3
   (:use :cl :fiveam)
4
-  (:import-from :alexandria
5
-                :alist-hash-table)
6 4
   (:export :emitter)
7 5
   (:documentation "Emitter tests."))
8 6
 (in-package :cl-yaml-test.emitter)
9 7
 
10
-;;; Macros
11
-
12 8
 (defmacro define-test-cases ((name) &rest pairs)
13 9
   `(test ,name
14 10
      ,@(loop for (form string) in pairs collecting
15
-         `(is (equal (yaml.emitter:emit-to-string ,form)
16
-                     ,string)))))
11
+	    `(is (equal (yaml.emitter:with-emitter-to-string (emitter)
12
+			  (yaml.emitter:emit-stream (emitter)
13
+			    (yaml.emitter:emit-document (emitter :implicit t)
14
+			      (yaml.emitter:emit-scalar emitter ,form))))
15
+			,(format nil "~a~%...~%" string)))))) ;; Document end marker
16
+                                                              ;; is libyaml behavior
17
+
18
+(defun test-emit-sequence (sequence style)
19
+  (yaml.emitter:with-emitter-to-string (emitter)
20
+    (yaml.emitter:emit-stream (emitter)
21
+      (yaml.emitter:emit-document (emitter :implicit t)
22
+	(yaml.emitter:emit-sequence (emitter :style style)
23
+	  (mapcar (lambda (element) (yaml.emitter:emit-scalar emitter element))
24
+		  sequence))))))
17 25
 
18
-;;; Tests
26
+(defun test-emit-mapping (mapping style)
27
+  (yaml.emitter:with-emitter-to-string (emitter)
28
+    (yaml.emitter:emit-stream (emitter)
29
+      (yaml.emitter:emit-document (emitter :implicit t)
30
+	(yaml.emitter:emit-mapping (emitter :style style)
31
+	  (mapcar (lambda (pair)
32
+		    (yaml.emitter:emit-scalar emitter (car pair))
33
+		    (yaml.emitter:emit-scalar emitter (cdr pair)))
34
+		  mapping))))))
19 35
 
20 36
 (def-suite emitter
21
-  :description "YAML emitter tests.")
37
+    :description "YAML emitter tests.")
22 38
 (in-suite emitter)
23 39
 
24 40
 (define-test-cases (boolean)
25
-  (t
26
-   "true")
27
-  (nil
28
-   "false"))
41
+  (t "true")
42
+  (nil "false"))
29 43
 
30 44
 (define-test-cases (integers)
31
-  (1
32
-   "1")
33
-  (123
34
-   "123")
35
-  (+123
36
-   "123")
37
-  (-123
38
-   "-123"))
45
+  (1 "1")
46
+  (123 "123")
47
+  (+123 "123")
48
+  (-123 "-123"))
39 49
 
40 50
 (define-test-cases (floats)
41
-  (1.23
42
-   "1.23")
43
-  (6.62607e-34
44
-   "6.62607e-34"))
51
+  (1.23 "1.23")
52
+  (6.62607e-34 "6.62607e-34"))
53
+
54
+(test flow-sequence
55
+  (is (equal (test-emit-sequence '(1 "a" 3.14f0 3.14d0) :flow-sequence-style)
56
+	     "[1, a, 3.14, 3.14]
57
+")))
58
+
59
+(test block-sequence
60
+  (is (equal (test-emit-sequence '(1 "a" 3.14f0 3.14d0) :block-sequence-style)
61
+	     "- 1
62
+- a
63
+- 3.14
64
+- 3.14
65
+")))
45 66
 
46
-(define-test-cases (lists)
47
-  ((list 1 2 3)
48
-   "[1, 2, 3]")
49
-  ((vector 1 2 3)
50
-   "[1, 2, 3]"))
67
+(test flow-mapping
68
+  (is (equal (test-emit-mapping '(("integer" . 1) ("string" . "test") ("bool" . nil))
69
+				:flow-mapping-style)
70
+	     "{integer: 1, string: test, bool: false}
71
+")))
51 72
 
52
-(test hash-tables
53
-  (let ((table (alexandria:alist-hash-table
54
-                (list (cons "a" 1)
55
-                      (cons "b" 2)))))
56
-    (is
57
-     (equal (yaml:emit-to-string table)
58
-            "{ \"b\": 2, \"a\": 1 }"))))
73
+(test block-mapping
74
+  (is (equal (test-emit-mapping '(("integer" . 1) ("string" . "test") ("bool" . nil))
75
+				:block-mapping-style)
76
+	     "integer: 1
77
+string: test
78
+bool: false
79
+")))
59 80
 
60
-(test toplevel-function
61
-  (is
62
-    (equal (yaml:emit-to-string 1)
63
-           "1"))
64
-  (is
65
-    (equal (with-output-to-string (stream)
66
-             (yaml:emit 1 stream))
67
-           "1")))
68 81
new file mode 100644
... ...
@@ -0,0 +1,165 @@
1
+(in-package :cl-user)
2
+(defpackage cl-yaml-test.round-trip
3
+  (:use :cl :fiveam)
4
+  (:export :round-trip)
5
+  (:documentation "Round-trip Emitter/Parser tests."))
6
+(in-package :cl-yaml-test.round-trip)
7
+
8
+(def-suite round-trip
9
+    :description "YAML emitter/parser tests.")
10
+(in-suite round-trip)
11
+
12
+(defclass invoice ()
13
+  ((number :initarg :number :accessor invoice-number)
14
+   (date :initarg :date :accessor invoice-date)
15
+   (bill-to :initarg :bill-to :accessor invoice-bill-to)
16
+   ;; skip ship-to until aliases and anchors are more developed)
17
+   (product :initarg :product :accessor invoice-product)
18
+   (tax :initarg :tax :accessor invoice-tax)
19
+   (total :initarg :total :accessor invoice-total)
20
+   (comments :initarg :comments :accessor invoice-comments)))
21
+
22
+(defclass address ()
23
+  ((lines :initarg :lines :accessor address-lines)
24
+   (city :initarg :city :accessor address-city)
25
+   (state :initarg :state :accessor address-state)
26
+   (postal :initarg :postal :accessor address-postal)))
27
+
28
+(defclass product-order ()
29
+  ((sku :initarg :sku :accessor product-order-sku)
30
+   (quantity :initarg :quantity :accessor product-order-quantity)
31
+   (description :initarg :description :accessor product-order-description)
32
+   (price :initarg :price :accessor product-order-price)))
33
+
34
+(defun make-test-invoice ()
35
+  (make-instance 'invoice
36
+		 :number 34843
37
+		 :date "2001-01-23"
38
+		 :bill-to (list "Chris" "Dumars"
39
+				(make-instance 'address
40
+					       :lines "458 Walkman Dr.
41
+Suite #292"
42
+					       :city "Royal Oak"
43
+					       :state "MI"
44
+					       :postal "48046"))
45
+		 :product (list (make-instance 'product-order
46
+					       :sku "BL394D"
47
+					       :quantity 4
48
+					       :description "Basketball"
49
+					       :price 450.00)
50
+				(make-instance 'product-order
51
+					       :sku "BL4438H"
52
+					       :quantity 1
53
+					       :description "Super Hoop"
54
+					       :price 2392.00))
55
+		 :tax 251.42
56
+		 :total 4443.52
57
+		 :comments "Late afternoon is best. Backup contact is Nancy Billsmer @ 338-4338."))
58
+
59
+;;; Methods to emit CLOS objects
60
+
61
+(defmethod yaml.emitter:emit-object (emitter (obj address))
62
+  (yaml.emitter:emit-mapping (emitter :style :block-mapping-style)
63
+    (yaml.emitter:emit-scalar emitter "lines")
64
+    (yaml.emitter:emit-scalar emitter (address-lines obj)
65
+			      :style :literal-scalar-style)
66
+    (yaml.emitter:emit-scalar emitter "city")
67
+    (yaml.emitter:emit-scalar emitter (address-city obj))
68
+    (yaml.emitter:emit-scalar emitter "state")
69
+    (yaml.emitter:emit-scalar emitter (address-state obj))
70
+    (yaml.emitter:emit-scalar emitter "postal")
71
+    (yaml.emitter:emit-scalar emitter (address-postal obj))))
72
+
73
+(defmethod yaml.emitter:emit-object (emitter (obj product-order))
74
+  (yaml.emitter:emit-mapping (emitter :style :block-mapping-style)
75
+    (yaml.emitter:emit-scalar emitter "sku")
76
+    (yaml.emitter:emit-scalar emitter (product-order-sku obj))
77
+    (yaml.emitter:emit-scalar emitter "quantity")
78
+    (yaml.emitter:emit-scalar emitter (product-order-quantity obj))
79
+    (yaml.emitter:emit-scalar emitter "description")
80
+    (yaml.emitter:emit-scalar emitter (product-order-description obj))
81
+    (yaml.emitter:emit-scalar emitter "price")
82
+    (yaml.emitter:emit-scalar emitter (product-order-price obj))))
83
+
84
+(defmethod yaml.emitter:emit-object (emitter (obj invoice))
85
+  (yaml.emitter:emit-mapping (emitter :style :block-mapping-style
86
+				      :tag "clarkevans.com,2002:invoice")
87
+    (yaml.emitter:emit-scalar emitter "invoice")
88
+    (yaml.emitter:emit-scalar emitter (invoice-number obj))
89
+    (yaml.emitter:emit-scalar emitter "date")
90
+    (yaml.emitter:emit-scalar emitter (invoice-date obj))
91
+    (yaml.emitter:emit-scalar emitter "bill-to")
92
+    (yaml.emitter:emit-mapping (emitter :style :block-mapping-style)
93
+      (yaml.emitter:emit-scalar emitter "given")
94
+      (yaml.emitter:emit-scalar emitter (first (invoice-bill-to obj)))
95
+      (yaml.emitter:emit-scalar emitter "family")
96
+      (yaml.emitter:emit-scalar emitter (second (invoice-bill-to obj)))
97
+      (yaml.emitter:emit-scalar emitter "address")
98
+      (yaml.emitter:emit-object emitter (third (invoice-bill-to obj))))
99
+    (yaml.emitter:emit-scalar emitter "product")
100
+    (yaml.emitter:emit-sequence (emitter :style :block-sequence-style)
101
+      (dolist (product (invoice-product obj))
102
+	(yaml.emitter:emit-object emitter product)))
103
+    (yaml.emitter:emit-scalar emitter "tax")
104
+    (yaml.emitter:emit-scalar emitter (invoice-tax obj))
105
+    (yaml.emitter:emit-scalar emitter "total")
106
+    (yaml.emitter:emit-scalar emitter (invoice-total obj))
107
+    (yaml.emitter:emit-scalar emitter "comments")
108
+    (yaml.emitter:emit-scalar emitter (invoice-comments obj)
109
+			      :plain-implicit t
110
+			      :quoted-implicit t
111
+			      :style :plain-scalar-style)))
112
+
113
+;;; Functions to construct objects from YAML
114
+
115
+(defun construct-address (mapping)
116
+  (make-instance 'address
117
+		 :lines (gethash "lines" mapping)
118
+		 :city (gethash "city" mapping)
119
+		 :state (gethash "state" mapping)
120
+		 :postal (gethash "postal" mapping)))
121
+
122
+(defun construct-product-order (mapping)
123
+  (make-instance 'product-order
124
+		 :sku (gethash "sku" mapping)
125
+		 :quantity (gethash "quantity" mapping)
126
+		 :description (gethash "description" mapping)
127
+		 :price (gethash "price" mapping)))
128
+
129
+(defun construct-invoice (mapping)
130
+  (let ((bill-to-mapping (gethash "bill-to" mapping)))
131
+    (make-instance 'invoice
132
+		   :number (gethash "invoice" mapping)
133
+		   :date (gethash "date" mapping)
134
+		   :bill-to (list (gethash "given" bill-to-mapping)
135
+				  (gethash "family" bill-to-mapping)
136
+				  (construct-address (gethash "address" bill-to-mapping)))
137
+		   :product (mapcar #'construct-product-order
138
+				    (gethash "product" mapping))
139
+		   :tax (gethash "tax" mapping)
140
+		   :total (gethash "total" mapping)
141
+		   :comments (gethash "comments" mapping))))
142
+
143
+;;; Register a construction function with a YAML tag
144
+
145
+(eval-when (:load-toplevel)
146
+  (yaml.parser:register-mapping-converter "clarkevans.com,2002:invoice"
147
+					  #'construct-invoice))
148
+
149
+;;; Helper
150
+
151
+(defun test-emit (obj)
152
+  (yaml.emitter:with-emitter-to-string (emitter)
153
+    (yaml.emitter:emit-stream (emitter)
154
+      (yaml.emitter:emit-document (emitter :implicit t)
155
+	(yaml.emitter:emit-object emitter obj)))))
156
+
157
+;;; Round-trip test
158
+
159
+(test invoice
160
+  (let* ((invoice-a (make-test-invoice))
161
+	 (yaml-a (test-emit invoice-a))
162
+	 (invoice-b (yaml:parse yaml-a))
163
+	 (yaml-b (test-emit invoice-b)))
164
+    (is (string= yaml-a yaml-b))))
165
+