git.fiddlerwoaroof.com
Browse code

Merge pull request #11 from jasonmelbye/master

Additional parser functionality, new emitter interface

Fernando Borretti authored on 24/12/2016 23:59:34
Showing 8 changed files
... ...
@@ -16,6 +16,8 @@
16 16
                  (:file "scalar")
17 17
                  (:file "parser")
18 18
                  (:file "emitter")
19
+                 (:file "emitter2")
20
+                 (:file "round-trip")
19 21
                  (:file "spec")
20 22
                  (:file "bench")
21 23
                  (:file "cl-yaml")))))
... ...
@@ -1,9 +1,52 @@
1 1
 (in-package :cl-user)
2 2
 (defpackage yaml.emitter
3 3
   (:use :cl)
4
-  (:export :encode
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
+                :set-output
12
+                :stream-start-event-initialize
13
+                :stream-end-event-initialize
14
+                :document-start-event-initialize
15
+                :document-end-event-initialize
16
+                :scalar-event-initialize
17
+                :sequence-start-event-initialize
18
+                :sequence-end-event-initialize
19
+                :mapping-start-event-initialize
20
+                :mapping-end-event-initialize)
21
+  (:import-from :libyaml.event
22
+                :allocate-event
23
+                :event-delete)
24
+  (:import-from :libyaml.write-handler
25
+                :*write-handler-callback*
26
+                :*write-handler-stream*)
27
+  (:export ;; Original interface
5 28
            :emit
6
-           :emit-to-string)
29
+           :emit-to-string
30
+           :encode
31
+           ;; libyaml based interface
32
+           :stream-start-event
33
+           :stream-end-event
34
+           :document-start-event
35
+           :document-end-event
36
+           :scalar-event
37
+           :sequence-start-event
38
+           :sequence-end-event
39
+           :mapping-start-event
40
+           :mapping-end-event
41
+           :emit-stream
42
+           :emit-document
43
+           :emit-sequence
44
+           :emit-mapping
45
+           :emit-scalar
46
+           :emit-object
47
+           :print-scalar
48
+           :with-emitter-to-stream
49
+           :with-emitter-to-string)
7 50
   (:documentation "The YAML emitter."))
8 51
 (in-package :yaml.emitter)
9 52
 
... ...
@@ -67,4 +110,196 @@
67 110
 (defun emit-to-string (value)
68 111
   "Emit a value to string."
69 112
   (with-output-to-string (stream)
70
-    (emit value stream)))
113
+(emit value stream)))
114
+
115
+;;; Wrappers around cl-libyaml event interface with defaults and keyword args
116
+
117
+(defun stream-start-event (event &key (encoding :utf8-encoding))
118
+  (stream-start-event-initialize event encoding))
119
+
120
+(defun stream-end-event (event)
121
+  (stream-end-event-initialize event))
122
+
123
+(defun document-start-event (event &key (version-directive (null-pointer))
124
+                                     (tag-directive-start (null-pointer))
125
+                                     (tag-directive-end (null-pointer))
126
+                                     (implicit nil))
127
+  (document-start-event-initialize event version-directive
128
+                                   tag-directive-start
129
+                                   tag-directive-end
130
+                                   implicit))
131
+
132
+(defun document-end-event (event &key (implicit nil))
133
+  (document-end-event-initialize event implicit))
134
+
135
+(defun sequence-start-event (event &key (anchor (null-pointer))
136
+                                     (tag (null-pointer))
137
+                                     (implicit nil)
138
+                                     (style :any-sequence-style))
139
+  (sequence-start-event-initialize event anchor tag implicit style))
140
+
141
+(defun sequence-end-event (event)
142
+  (sequence-end-event-initialize event))
143
+
144
+(defun mapping-start-event (event &key (anchor (null-pointer))
145
+                                    (tag (null-pointer))
146
+                                    (implicit nil)
147
+                                    (style :any-mapping-style))
148
+  (mapping-start-event-initialize event anchor tag implicit style))
149
+
150
+(defun mapping-end-event (event)
151
+  (mapping-end-event-initialize event))
152
+
153
+(defun scalar-event (event value length &key (anchor (null-pointer))
154
+                                          (tag (null-pointer))
155
+                                          (plain-implicit t)
156
+                                          (quoted-implicit t)
157
+                                          (style :plain-scalar-style))
158
+  (scalar-event-initialize event anchor tag value length
159
+                           plain-implicit quoted-implicit style))
160
+
161
+;;; Emitter macros and output functions
162
+
163
+;;; When passing a foreign emitter object, it is also paired with a
164
+;;; foreign event object.
165
+
166
+(defun foreign-emitter (emitter) (car emitter))
167
+
168
+(defun foreign-event (emitter) (cdr emitter))
169
+
170
+(defmacro with-emitter-to-stream ((emitter-var output-stream) &rest body)
171
+  (let ((foreign-emitter (gensym "EMITTER"))
172
+        (foreign-event (gensym "EVENT")))
173
+    `(let* ((,foreign-emitter (allocate-emitter))
174
+            (,foreign-event (allocate-event))
175
+            (,emitter-var (cons ,foreign-emitter ,foreign-event))
176
+            (*write-handler-stream* ,output-stream))
177
+      (unwind-protect
178
+           (progn
179
+             (emitter-initialize ,foreign-emitter)
180
+             (set-output ,foreign-emitter *write-handler-callback* (null-pointer))
181
+             ,@body)
182
+        (libyaml.event:event-delete ,foreign-event)
183
+        (libyaml.emitter:emitter-delete ,foreign-emitter)
184
+        (foreign-free ,foreign-event)
185
+        (foreign-free ,foreign-emitter)))))
186
+
187
+(defmacro with-emitter-to-string ((emitter-var) &rest body)
188
+  (let ((str (gensym "STR")))
189
+    `(with-output-to-string (,str)
190
+       (with-emitter-to-stream (,emitter-var ,str)
191
+         ,@body))))
192
+
193
+(defmacro emit-stream ((emitter &key (encoding :utf8-encoding)) &body body)
194
+  (let ((emitter-value (gensym "EMITTER"))
195
+        (foreign-emitter (gensym "FOREIGN-EMITTER"))
196
+        (foreign-event (gensym "FOREIGN-EVENT")))
197
+    `(let* ((,emitter-value ,emitter)
198
+            (,foreign-emitter (foreign-emitter ,emitter-value))
199
+            (,foreign-event (foreign-event ,emitter-value)))
200
+       (stream-start-event ,foreign-event :encoding ,encoding)
201
+       (libyaml.emitter:emit ,foreign-emitter ,foreign-event)
202
+       ,@body
203
+       (stream-end-event ,foreign-event)
204
+       (libyaml.emitter:emit ,foreign-emitter ,foreign-event))))
205
+
206
+(defmacro emit-document ((emitter &rest rest
207
+                                  &key version-directive
208
+                                  tag-directive-start 
209
+                                  tag-directive-end
210
+                                  (implicit nil)) &body body)
211
+  (declare (ignorable version-directive tag-directive-start
212
+                      tag-directive-end implicit))
213
+  (let ((emitter-value (gensym "EMITTER"))
214
+        (foreign-emitter (gensym "FOREIGN-EMITTER"))
215
+        (foreign-event (gensym "FOREIGN-EVENT")))
216
+    `(let* ((,emitter-value ,emitter)
217
+            (,foreign-emitter (foreign-emitter ,emitter-value))
218
+            (,foreign-event (foreign-event ,emitter-value)))
219
+       (apply #'document-start-event ,foreign-event (list ,@rest))
220
+       (libyaml.emitter:emit ,foreign-emitter ,foreign-event)
221
+       ,@body
222
+       (document-end-event ,foreign-event :implicit ,implicit)
223
+       (libyaml.emitter:emit ,foreign-emitter ,foreign-event))))
224
+
225
+(defmacro emit-mapping ((emitter &rest rest &key anchor tag implicit style)
226
+                        &body body)
227
+  (declare (ignorable anchor tag implicit style))
228
+  (let ((emitter-value (gensym "EMITTER"))
229
+        (foreign-emitter (gensym "FOREIGN-EMITTER"))
230
+        (foreign-event (gensym "FOREIGN-EVENT")))
231
+    `(let* ((,emitter-value ,emitter)
232
+            (,foreign-emitter (foreign-emitter ,emitter-value))
233
+            (,foreign-event (foreign-event ,emitter-value)))
234
+       (apply #'mapping-start-event ,foreign-event (list ,@rest))
235
+       (libyaml.emitter:emit ,foreign-emitter ,foreign-event)
236
+       ,@body
237
+       (mapping-end-event ,foreign-event)
238
+       (libyaml.emitter:emit ,foreign-emitter ,foreign-event))))
239
+
240
+(defmacro emit-sequence ((emitter &rest rest &key anchor tag implicit style)
241
+                         &body body)
242
+  (declare (ignorable anchor tag implicit style))
243
+  (let ((emitter-value (gensym "EMITTER"))
244
+        (foreign-emitter (gensym "FOREIGN-EMITTER"))
245
+        (foreign-event (gensym "FOREIGN-EVENT")))
246
+    `(let* ((,emitter-value ,emitter)
247
+            (,foreign-emitter (foreign-emitter ,emitter-value))
248
+            (,foreign-event (foreign-event ,emitter-value)))
249
+       (apply #'sequence-start-event ,foreign-event (list ,@rest))
250
+       (libyaml.emitter:emit ,foreign-emitter ,foreign-event)
251
+       ,@body
252
+       (sequence-end-event ,foreign-event)
253
+       (libyaml.emitter:emit ,foreign-emitter ,foreign-event))))
254
+
255
+(defun emit-scalar (emitter value &rest rest &key anchor tag
256
+                                               plain-implicit
257
+                                               quoted-implicit
258
+                                               style)
259
+  (declare (ignorable anchor tag plain-implicit quoted-implicit style))
260
+  (let ((printed-value (print-scalar value)))
261
+    (apply #'scalar-event (foreign-event emitter)
262
+           printed-value (length printed-value) rest)
263
+    (libyaml.emitter:emit (foreign-emitter emitter) (foreign-event emitter))))
264
+
265
+(defgeneric print-scalar (scalar)
266
+  (:documentation "Convert a scalar object into its printed representation"))
267
+
268
+(defmethod print-scalar ((scalar (eql 't)))
269
+  "true")
270
+
271
+(defmethod print-scalar ((scalar (eql 'nil)))
272
+  "false")
273
+
274
+(defmethod print-scalar ((scalar symbol))
275
+  (symbol-name scalar))
276
+
277
+(defmethod print-scalar ((scalar string))
278
+  scalar)
279
+
280
+(defmethod print-scalar ((scalar integer))
281
+  (princ-to-string scalar))
282
+
283
+(defmethod print-scalar ((scalar single-float))
284
+  (let ((*read-default-float-format* 'single-float))
285
+    (princ-to-string scalar)))
286
+
287
+(defmethod print-scalar ((scalar double-float))
288
+  (let ((*read-default-float-format* 'double-float))
289
+    (princ-to-string scalar)))
290
+
291
+(defgeneric emit-object (emitter obj)
292
+  (:documentation "Emit YAML representation of obj"))
293
+
294
+(defmethod emit-object (emitter (obj symbol))
295
+  (emit-scalar emitter obj))
296
+
297
+(defmethod emit-object (emitter (obj string))
298
+  (emit-scalar emitter obj))
299
+
300
+(defmethod emit-object (emitter (obj integer))
301
+  (emit-scalar emitter obj))
302
+
303
+(defmethod emit-object (emitter (obj float))
304
+  (emit-scalar emitter obj))
305
+
... ...
@@ -6,10 +6,44 @@
6 6
   (:import-from :libyaml.macros
7 7
                 :with-parser
8 8
                 :with-event)
9
-  (:export :parse-string)
9
+  (:export :parse-string
10
+	   :register-scalar-converter
11
+	   :register-sequence-converter
12
+	   :register-mapping-converter)
10 13
   (:documentation "The YAML parser."))
11 14
 (in-package :yaml.parser)
12 15
 
16
+(defvar +scalar-converters+ (make-hash-table :test #'equalp))
17
+(defvar +sequence-converters+ (make-hash-table :test #'equalp))
18
+(defvar +mapping-converters+ (make-hash-table :test #'equalp))
19
+
20
+(defun scalar-converter (tag)
21
+  (gethash tag +scalar-converters+))
22
+
23
+(defun convert-scalar (string tag &optional (style :plain-scalar-stype))
24
+  (let ((converter (scalar-converter tag)))
25
+    (if converter
26
+	(funcall converter string)
27
+	(yaml.scalar:parse-scalar string style))))
28
+
29
+(defun sequence-converter (tag)
30
+  (gethash tag +sequence-converters+))
31
+
32
+(defun convert-sequence (list tag)
33
+  (let ((converter (sequence-converter tag)))
34
+    (if converter
35
+	(funcall converter list)
36
+	list)))
37
+
38
+(defun mapping-converter (tag)
39
+  (gethash tag +mapping-converters+))
40
+
41
+(defun convert-mapping (hashtable tag)
42
+  (let ((converter (mapping-converter tag)))
43
+    (if converter
44
+	(funcall converter hashtable)
45
+	hashtable)))
46
+
13 47
 ;;; The parser
14 48
 
15 49
 (defun signal-reader-error (parser)
... ...
@@ -94,31 +128,32 @@
94 128
          t)
95 129
         |#
96 130
         ;; Scalar
97
-        ((:scalar-event &key anchor tag value)
98
-         (declare (ignore anchor tag))
131
+        ((:scalar-event &key anchor tag value length plain-implicit quoted-implicit style)
132
+         (declare (ignore anchor length plain-implicit quoted-implicit))
99 133
          (setf (first contexts)
100 134
                (append (first contexts)
101
-                       (list (yaml.scalar:parse-scalar value)))))
135
+                       (list (convert-scalar value tag style)))))
102 136
         ;; Sequence start event
103
-        ((:sequence-start-event &key anchor tag)
104
-         (declare (ignore anchor tag))
105
-         (push (list) contexts))
137
+        ((:sequence-start-event &key anchor tag implicit style)
138
+         (declare (ignore anchor implicit style))
139
+         (push (list tag) contexts))
106 140
         ;; Mapping start event
107
-        ((:mapping-start-event &key anchor tag)
108
-         (declare (ignore anchor tag))
109
-         (push (list) contexts))
141
+        ((:mapping-start-event &key anchor tag implicit style)
142
+         (declare (ignore anchor implicit style))
143
+         (push (list tag) contexts))
110 144
         ;; End events
111 145
         ((:sequence-end-event)
112
-         (let ((con (pop contexts)))
146
+         (destructuring-bind (tag &rest seq) (pop contexts)
113 147
            (setf (first contexts)
114 148
                  (append (first contexts)
115
-                         (list con)))))
149
+                         (list (convert-sequence seq tag))))))
116 150
         ((:mapping-end-event)
117
-         (let ((con (pop contexts)))
151
+         (destructuring-bind (tag &rest plist) (pop contexts)
118 152
            (setf (first contexts)
119 153
                  (append (first contexts)
120
-                         (list
121
-                          (alexandria:plist-hash-table con :test #'equal))))))
154
+                         (list (convert-mapping
155
+				(alexandria:plist-hash-table plist :test #'equalp)
156
+				tag))))))
122 157
         ;; Do nothing
123 158
         ((t &rest rest)
124 159
          (declare (ignore rest)))))
... ...
@@ -126,5 +161,14 @@
126 161
 
127 162
 ;;; The public interface
128 163
 
164
+(defun register-scalar-converter (tag converter)
165
+  (setf (gethash tag +scalar-converters+) converter))
166
+
167
+(defun register-sequence-converter (tag converter)
168
+  (setf (gethash tag +sequence-converters+) converter))
169
+
170
+(defun register-mapping-converter (tag converter)
171
+  (setf (gethash tag +mapping-converters+) converter))
172
+
129 173
 (defun parse-string (yaml-string)
130 174
   (parse-tokens (parse-yaml yaml-string)))
... ...
@@ -15,6 +15,9 @@
15 15
 
16 16
 ;;; Regular expressions or lists of names
17 17
 
18
+(defparameter +quoted-scalar-styles+
19
+  (list :single-quoted-scalar-style :double-quoted-scalar-style))
20
+
18 21
 (defparameter +null-names+
19 22
   (list "null" "Null" "NULL" "~"))
20 23
 
... ...
@@ -48,9 +51,12 @@
48 51
 
49 52
 ;;; The actual parser
50 53
 
51
-(defun parse-scalar (string)
54
+(defun parse-scalar (string &optional (style :plain-scalar-style))
52 55
   "Parse a YAML scalar string into a Lisp scalar value."
53 56
   (cond
57
+    ;; Quoted string
58
+    ((member style +quoted-scalar-styles+)
59
+     string)
54 60
     ;; Null
55 61
     ((member string +null-names+ :test #'equal)
56 62
      +null+)
... ...
@@ -2,9 +2,21 @@
2 2
 (defpackage cl-yaml
3 3
   (:use :cl)
4 4
   (:nicknames :yaml)
5
+  (:import-from :yaml.parser
6
+                :register-scalar-converter
7
+                :register-sequence-converter
8
+                :register-mapping-converter)
9
+  (:import-from :yaml.emitter
10
+                :emit-object
11
+                :print-scalar)
5 12
   (:export :parse
6 13
            :emit
7
-           :emit-to-string)
14
+           :emit-to-string
15
+           :register-scalar-converter
16
+           :register-sequence-converter
17
+           :register-mapping-converter
18
+           :emit-object
19
+           :print-scalar)
8 20
   (:documentation "The main YAML interface."))
9 21
 (in-package :yaml)
10 22
 
... ...
@@ -10,5 +10,7 @@
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.emitter2:emitter2)
14
+  (run! 'cl-yaml-test.round-trip:round-trip)
13 15
   (run! 'cl-yaml-test.spec:spec)
14 16
   (run! 'cl-yaml-test.bench:bench))
15 17
new file mode 100644
... ...
@@ -0,0 +1,79 @@
1
+(in-package :cl-user)
2
+(defpackage cl-yaml-test.emitter2
3
+  (:use :cl :fiveam)
4
+  (:export :emitter2)
5
+  (:documentation "Emitter tests - libyaml-based emitter."))
6
+(in-package :cl-yaml-test.emitter2)
7
+
8
+(defmacro define-test-cases ((name) &rest pairs)
9
+  `(test ,name
10
+     ,@(loop for (form string) in pairs collecting
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))))))
25
+
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))))))
35
+
36
+(def-suite emitter2
37
+    :description "YAML libyaml-based emitter tests.")
38
+(in-suite emitter2)
39
+
40
+(define-test-cases (boolean)
41
+  (t "true")
42
+  (nil "false"))
43
+
44
+(define-test-cases (integers)
45
+  (1 "1")
46
+  (123 "123")
47
+  (+123 "123")
48
+  (-123 "-123"))
49
+
50
+(define-test-cases (floats)
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
+")))
66
+
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
+")))
72
+
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
+")))
0 80
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
+