#lang racket
(require (planet jim/webit:1:4/xml)
(only-in net/base64 base64-decode)
(only-in "tests/util.rkt" base64-encode)
racket/date
"base.rkt")
(provide serialise
deserialise
encode-string
decode-string)
(define replace-&-and-<
(let ((amp-re (regexp (regexp-quote "&")))
(lt-re (regexp (regexp-quote "<"))))
(lambda (str)
(regexp-replace* lt-re
(regexp-replace* amp-re str "\\&")
"\\<"))))
(define replace-entities
(let ((amp-re (regexp (regexp-quote "&")))
(lt-re (regexp (regexp-quote "<"))))
(lambda (str)
(regexp-replace* amp-re
(regexp-replace* lt-re str "<")
"\\&"))))
(define identity
(lambda (x) x))
(define/contract encode-string-guard
(-> boolean? any)
(lambda (replace?)
(if replace?
replace-&-and-<
identity)))
(define/contract decode-string-guard
(-> boolean? any)
(lambda (replace?)
(if replace?
replace-entities
identity)))
(define encode-string
(make-parameter replace-&-and-< encode-string-guard))
(define decode-string
(make-parameter replace-entities decode-string-guard))
(define (date->iso8601-string date)
(define (pad number)
(let ((str (number->string number)))
(if (< (string-length str) 2)
(string-append "0" str)
str)))
(string-append
(number->string (date-year date))
(pad (date-month date))
(pad (date-day date))
"T"
(pad (date-hour date))
":"
(pad (date-minute date))
":"
(pad (date-second date))))
(define (serialise val)
(cond
[(or (eq? +nan.0 val) (eq? +inf.0 val) (eq? -inf.0 val))
(raise-exn:xml-rpc
(format "Given ~s to serialise to XML-RPC. XML-RPC does not allow NaN or infinities; and so this value cannot be serialised" val))]
[(and (number? val) (inexact? val))
`(value (double ,(number->string val)))]
[(integer? val)
(if (and (<= val (expt 2 31))
(>= val (- (expt 2 31))))
`(value (int ,(number->string val)))
(raise-exn:xml-rpc
(format "The Racket number ~s is out of range for an XML-RPC integer" val)))]
[(string? val) `(value (string ,val))]
[(symbol? val) `(value (string ,((encode-string) (symbol->string val))))]
[(boolean? val) `(value (boolean ,(if val "1" "0")))]
[(date? val) `(value (dateTime.iso8601
,(date->iso8601-string val)))]
[(hash? val)
`(value (struct ,@(hash-map
val
(lambda (k v)
`(member (name ,(symbol->string k))
,(serialise v))))))]
[(list? val)
`(value (array (data ,@(map serialise val))))]
[(vector? val)
`(value (array (data ,@(map serialise (vector->list val)))))]
[(bytes? val)
`(value (base64 ,(base64-encode val)))]
[else
(raise-exn:xml-rpc
(format "Cannot convert Racket value ~s to XML-RPC" val))]))
(define (deserialise-struct member*)
(let ([h (make-hash)])
(for-each
(lambda (member)
(xml-match member
[(member (name ,name) (value))
(hash-set! h (string->symbol name) "")]
[(member (name ,name) (value ,[deserialise -> v]))
(hash-set! h (string->symbol name) v)]
[,else
(raise-exn:xml-rpc
(format "The XML-RPC struct data ~s is badly formed and cannot be converted to Racket" else))]))
member*)
h))
(define (deserialize-iso8601 v)
(let ([pieces (regexp-match
#px"(\\d\\d\\d\\d)(\\d\\d)(\\d\\d)T(\\d\\d):(\\d\\d):(\\d\\d)" v)])
(if pieces
(let-values ([(all year month day h m s)
(apply values (map string->number pieces))])
(let* ([given-date (seconds->date (find-seconds s m h day month year))]
[tzo
(date-time-zone-offset (seconds->date (current-seconds)))])
(struct-copy date given-date (time-zone-offset tzo))
))
(raise-exn:xml-rpc
(format
"The XML-RPC date ~s badly formatted; cannot be converted to Racket" v)))))
(define (deserialise val)
(xml-match val
[,bare-string (guard (string? bare-string)) bare-string]
[(value ,type)
(cond
[(list? type)
(deserialise type)]
[(string? type)
type])]
[(value) ""]
[(int ,v) (string->number v)]
[(i4 ,v) (string->number v)]
[(double ,v) (string->number v)]
[(string) ""]
[(string ,v) v]
[(boolean ,v) (string=? v "1")]
[(dateTime.iso8601 ,v)
(deserialize-iso8601 v)]
[(base64) #""]
[(base64 ,v)
(base64-decode (string->bytes/utf-8 v))]
[(struct ,member* ...)
(deserialise-struct member*)]
[(array (data ,[value*] ...))
value*]
[,else
(raise-exn:xml-rpc
(format "Cannot convert the XML-RPC type ~s to Racket" else))]))