Skip to content

Commit 62eb16f

Browse files
committed
Add a caching bodyParser for XML-RPC calls
... avoid the repetitive parsing.
1 parent 324cde9 commit 62eb16f

File tree

1 file changed

+149
-0
lines changed

1 file changed

+149
-0
lines changed
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
//
2+
// BodyParser.swift
3+
// MacroXmlRpc
4+
//
5+
// Created by Helge Hess.
6+
// Copyright © 2020 ZeeZide GmbH. All rights reserved.
7+
//
8+
9+
import MacroExpress
10+
import enum XmlRpc.XmlRpc
11+
12+
public extension bodyParser {
13+
14+
enum XmlRpcBodyParserBody {
15+
case invalid
16+
case call (XmlRpc.Call)
17+
case response(XmlRpc.Response)
18+
}
19+
20+
/**
21+
* If available, parse an XML-RPC method call into the `request.xmlRpcBody`
22+
* extra slot.
23+
*
24+
* Usage:
25+
*
26+
* app.use(bodyParser.xmlRpcCall())
27+
*
28+
* app.post("/RPC2") { req, res, next in
29+
* guard let call = req.xmlRpcCall else { return next() }
30+
* console.log("received call:", call)
31+
* }
32+
*
33+
* This plays well w/ other body parsers. If no other parser was active,
34+
* it will fill `request.body` as `.text`.
35+
*/
36+
func xmlRpcCall() -> Middleware {
37+
return { req, res, next in
38+
if req.extra[xmlRpcRequestKey] != nil { return next() } // parsed already
39+
40+
// This deals w/ other bodyParsers being active. If we already have
41+
// content (e.g. from bodyParser.text or .raw) we reuse that.
42+
switch req.body {
43+
44+
case .notParsed:
45+
guard typeIs(req, [ "text/xml" ]) != nil else { return next() }
46+
47+
// Collect request content using the `concat` stream.
48+
return concatError(request: req, next: next) { bytes in
49+
do {
50+
let string = try bytes.toString()
51+
req.body = .text(string)
52+
req.xmlRpcBody = XmlRpc.parseCall(string).flatMap { .call($0) }
53+
?? .invalid
54+
return nil
55+
}
56+
catch {
57+
req.body = .error(BodyParserError.couldNotDecodeString(error))
58+
req.xmlRpcBody = .invalid
59+
return error
60+
}
61+
}
62+
63+
case .noBody, .error:
64+
req.xmlRpcBody = .invalid
65+
return next()
66+
67+
case .urlEncoded, .json: // TODO: we could try to map those!
68+
req.xmlRpcBody = .invalid
69+
return next()
70+
71+
case .raw(let bytes):
72+
// TBD: check for text/xml?
73+
do {
74+
let string = try bytes.toString()
75+
req.body = .text(string)
76+
req.xmlRpcBody = XmlRpc.parseCall(string).flatMap { .call($0) }
77+
?? .invalid
78+
}
79+
catch {
80+
// In this case, this doesn't have to be an error. Could be some
81+
// other raw data.
82+
req.xmlRpcBody = .invalid
83+
}
84+
return next()
85+
86+
case .text(let string):
87+
req.xmlRpcBody = XmlRpc.parseCall(string).flatMap { .call($0) }
88+
?? .invalid
89+
return next()
90+
}
91+
}
92+
}
93+
94+
}
95+
96+
@usableFromInline
97+
let xmlRpcRequestKey = "macro.xmlrpc.body-parser"
98+
99+
public extension IncomingMessage {
100+
101+
/**
102+
* Returns the XML-RPC body parsed by e.g. `bodyParser.xmlRpcCall`. It is
103+
* only filled when the middleware executed, otherwise it returns `.invalid`.
104+
*/
105+
@inlinable
106+
var xmlRpcBody: bodyParser.XmlRpcBodyParserBody {
107+
set { extra[xmlRpcRequestKey] = newValue }
108+
get {
109+
return (extra[xmlRpcRequestKey] as? bodyParser.XmlRpcBodyParserBody)
110+
?? .invalid
111+
}
112+
}
113+
114+
/**
115+
* Returns the XML-RPC body parsed by e.g. `bodyParser.xmlRpcCall`. It is
116+
* only filled when the middleware executed and the content was a proper body,
117+
* otherwise it returns `nil`.
118+
*/
119+
@inlinable
120+
var xmlRpcCall: XmlRpc.Call? {
121+
guard case .call(let call) = xmlRpcBody else { return nil }
122+
return call
123+
}
124+
}
125+
126+
127+
// MARK: - Helper
128+
129+
private func concatError(request : IncomingMessage,
130+
next : @escaping Next,
131+
handler : @escaping ( Buffer ) -> Swift.Error?)
132+
{
133+
var didCallNext = false
134+
135+
request | concat { bytes in
136+
guard !didCallNext else { return }
137+
if let error = handler(bytes) {
138+
next(error)
139+
}
140+
else {
141+
next()
142+
}
143+
}
144+
.onceError { error in
145+
guard !didCallNext else { return }
146+
didCallNext = true
147+
next(error)
148+
}
149+
}

0 commit comments

Comments
 (0)