Rope
Rope copied to clipboard
Abstracting away libpq
As discussed with @johanneserhardt I proposed that Rope have one more dependency: pq
This is how we do it:
C module map
we have Cpq
module providing module maps on Mac and Linux (or whatever they're called).
C bullshit-removal-layer (pq)
Then we have pq
module that provides a swift overlay to libpq – this is like how Apple built overlays to Foundation for Swift. Nothing extra, just removing the OpaquePointers and C strings, replacing them with actual types. The same libpq functions remain. Take a look at this: https://github.com/swizzlr/swift-redis/blob/master/Sources/hiredis/hiredis.swift
For instance:
var element: [redisReply]? {
guard cReply.memory.element != nil else { return nil }
return UnsafeBufferPointer(start: cReply.memory.element, count: Int(cReply.memory.elements)).map { creplyptr in
return redisReply(cReply: creplyptr)!
}
}
You can see that the API is identical, except it's hiding some of the "plumbing". This means that if I want to build an opinionated Redis wrapper (like we're doing with Rope) I can do it, but other people can write their own opinionated wrappers while not having to worry about low level C bullshit.
Opinionated layer (Rope)
This is up to us, the users. This depends on the bullshit removal layer.
The Trick
The bullshit removal layer is Protocol Oriented. All the types are erased by a protocol: PQconnection
conforms to PQconnectionType
which implements all the same methods and properties. In order to enable this, all top level functions will need to be abstracted inside a type. This shouldn't be hard, because a lot of them can be made more object oriented at the same time.
protocol ConnectionType {
func PQgetResult() -> PGresultType
}
public final class PGconn: ConnectionType {
private let cPGConn: Cpq.PGconn
public func PQgetResult() -> PGResultType {
return PGResult(Cpq.PQgetResult(self.cPGConn)) // the underlying C library call
}
}
Alternatively, and more simply (though I prefer the former):
protocol GlobalFunctionContainerType {
static func PQgetResult(conn: PGConnType) -> PGresultType
}
public final class GlobalFunctionContainer: GlobalFunctionContainerType {
static func PQgetResult(conn: PGConnType) -> PGresultType {
return PGresult(Cpq.PQgetResult(conn.toRealPGConn()))
}
}
I know this seems obtuse, but this means that we can very easily drop fake versions of all the types into a test suite. As a result, we could reimplement the above ConnectionType
with a version for the unit tests:
public final class FakePGconn: ConnectionType {
private var fakeQueuedResults = [String]()
public func PQgetResult() -> PGResult {
return FakePGResult(result: fakeQueuedResults.removeLast())
}
}
By doing this, we can run fast unit tests with fake "in memory" Postgres, and swap in real Postgres to perform integration tests.