Expressions rendered as both XML and JSON
(require flexpr) | package: flexpr |
1 Rationale
A flexpr? is like a xexpr? or jsexpr?, but more flexible. A flexpr? can be sensibly converted to either XML or JSON.
Example use case: Your web service wants to offer the same response data in the client’s choice of XML or JSON (as expressed by the client in a URI query parameter or Accept header). Early in your response pipeline, define your response as a flexpr?. Convert to the desired format as/when needed.
A small benefit: A flexpr? doesn’t make you prematurely represent numbers as strings. They can remain numbers for JSON. They’re converted to strings only for XML. This makes it a bit more convenient to write flexpr?s by hand. Unlike xexpr?s you don’t need to use ~a on non-string values.
So what should a flexpr? be?
It’s tempting to start with an xexpr?. However the general form of XML is awkward to convert to JSON satisfactorily. An XML element essentially has two, parallel key/value dictionaries: the attributes and the child elements. A JSON dict would need to store the children in some sub-dict with a magic key name like say "body" (or equally weirdly, store the attributes in a sub-dict).
Another reason to prefer child elements is that attribute values are limited to primitives like strings, numbers, and booleans; there’s no arbitrary nesting.
Let’s back up. Let’s stipulate that having a "fork" of two dicts at the same level is weird. It’s weird even for XML. In fact most web service response XML I’ve seen doesn’t use attributes, just child elements.
Instead let’s start with a jsexpr?. A flexpr? is a slightly restricted form of jsexpr?:
flexpr | = | boolean? | ||
| | string? | |||
| | exact-integer? | |||
| | inexact-real? | |||
| | (hasheq key-val ...) | |||
key-val | = | [symbol? flexpr] | ||
| | [plural-symbol? (list flexpr ...)] |
Given this definition:
The conversion to jsexpr? is trivial. A flexpr? already is a jsexpr?.
The conversion to xexpr? is simple, with one bit of implicit "magic":
Attribute lists are always empty.
A hasheq becomes a list of xexpr?s – the key becomes the element tag and the value is the element body. The list is spliced into a parent element:
> (flexpr->xexpr (hasheq 'a "a" 'x 0) #:root 'Response) '(Response () (a () "a") (x () "0"))
Although I generally dislike implicit "magic", this seems like the least-worst way to specify data that can be rendered to both reasonably idiomatic XML and JSON.
A list? is allowed only as the value in a hasheq – and provided its key ends with #\s (for example 'items but not 'item). The plural is the parent element tag, and the singular is used for each child tag.
> (flexpr->xexpr (hasheq 'Items (list 1 2)) #:root 'Response) '(Response () (Items () (Item () "1") (Item () "2")))
2 A more realistic example
> (define v (hasheq 'ResponseId 123123 'Students (list (hasheq 'FirstName "John" 'LastName "Doe" 'Age 12 'Active #f 'GPA 3.4) (hasheq 'FirstName "Alyssa" 'LastName "Hacker" 'Age 14 'Active #t 'GPA 4.0)))) > (flexpr? v) #t
> (pretty-print (flexpr->xexpr v))
'(Response
()
(ResponseId () "123123")
(Students
()
(Student
()
(Active () "false")
(Age () "12")
(FirstName () "John")
(GPA () "3.4")
(LastName () "Doe"))
(Student
()
(Active () "true")
(Age () "14")
(FirstName () "Alyssa")
(GPA () "4.0")
(LastName () "Hacker"))))
> (display-xml/content (xexpr->xml (flexpr->xexpr v)))
<Response>
<ResponseId>
123123
</ResponseId>
<Students>
<Student>
<Active>
false
</Active>
<Age>
12
</Age>
<FirstName>
John
</FirstName>
<GPA>
3.4
</GPA>
<LastName>
Doe
</LastName>
</Student>
<Student>
<Active>
true
</Active>
<Age>
14
</Age>
<FirstName>
Alyssa
</FirstName>
<GPA>
4.0
</GPA>
<LastName>
Hacker
</LastName>
</Student>
</Students>
</Response>
> (pretty-print (flexpr->jsexpr v))
'#hasheq((ResponseId . 123123)
(Students
.
(#hasheq((FirstName . "John")
(LastName . "Doe")
(Age . 12)
(Active . #f)
(GPA . 3.4))
#hasheq((FirstName . "Alyssa")
(LastName . "Hacker")
(Age . 14)
(Active . #t)
(GPA . 4.0)))))
> (write-json (flexpr->jsexpr v)) {"ResponseId":123123,"Students":[{"FirstName":"John","LastName":"Doe","Age":12,"Active":false,"GPA":3.4},{"FirstName":"Alyssa","LastName":"Hacker","Age":14,"Active":true,"GPA":4.0}]}
3 Basics
3.1 XML
procedure
(flexpr->xexpr v [#:root root]) → xexpr?
v : flexpr? root : symbol? = 'Response
While traversing v, flexpr->xexpr performs equivalent tests as flexpr?, but will provide an error message specific to the item that failed. For example when a plural key is required for a (listof flexpr?) value, the error message is:
> (flexpr->xexpr (hasheq 'Item (list 0 1))) flexpr->xexpr: hash table key must be plural-symbol?
expected: 'Items
given: 'Item
in: '#hasheq((Item . (0 1)))
providing a hint that instead you should do:
> (flexpr->xexpr (hasheq 'Items (list 0 1))) '(Response () (Items () (Item () "0") (Item () "1")))
procedure
(write-flexpr-xml/content v [out #:root root]) → void?
v : flexpr? out : output-port? = (current-output-port) root : symbol? = 'Response
Effectively the composition of write-xml/content and flexpr->xexpr.
procedure
(display-flexpr-xml/content v [ out #:root root]) → void? v : flexpr? out : output-port? = (current-output-port) root : symbol? = 'Response
Effectively the composition of display-xml/content and flexpr->xexpr.
3.2 JSON
procedure
(flexpr->jsexpr v) → jsexpr?
v : flexpr?
Because a flexpr? is a subset of a jsexpr?, this is approximately the composition of values with flexpr?.
procedure
(write-flexpr-json v [ out #:null jsnull #:encode encode]) → void? v : flexpr? out : output-port? = (current-output-port) jsnull : any/c = (json-null) encode : (or/c 'control 'all) = 'control
Effectively the composition of write-json and flexpr->jsexpr.
4 Customizing "plural" symbols
Although the default idea of a "plural symbol" is simply default-singular-symbol, you may enhance this by supplying a different function as the value of the current-singular-symbol parameter.
procedure
(default-singular-symbol v) → (or/c symbol? #f)
v : symbol?
> (default-singular-symbol 'vampires) 'vampire
> (default-singular-symbol 'vampire) #f
parameter
(current-singular-symbol proc) → void? proc : singular-symbol/c
= default-singular-symbol
value
singular-symbol/c : (symbol? -> (or/c symbol? #f))
You may customize this to extend or replace default-singular-symbol.
> (parameterize ([current-singular-symbol (λ (s) (or (and (eq? s 'Werewolves) 'Werewolf) (default-singular-symbol s)))]) (pretty-print (flexpr->xexpr (hasheq 'Werewolves (list (hasheq 'FirstName "John" 'FangLength 6.4) (hasheq 'FirstName "Alyssa" 'FangLength 5.0)) 'Vampires (list (hasheq 'FirstName "John" 'FangLength 3.4) (hasheq 'FirstName "Alyssa" 'FangLength 4.0))))))
'(Response
()
(Vampires
()
(Vampire () (FangLength () "3.4") (FirstName () "John"))
(Vampire () (FangLength () "4.0") (FirstName () "Alyssa")))
(Werewolves
()
(Werewolf () (FangLength () "6.4") (FirstName () "John"))
(Werewolf () (FangLength () "5.0") (FirstName () "Alyssa"))))
procedure
(plural-symbol? v) → boolean?
v : any/c
(and ((current-singular-symbol) v) #t)