Closed bensyverson closed 6 years ago
Okay, a bit more detail. With the following extension, I can more easily conform Int
enums to PostgreSQLDataCustomConvertible
:
extension RawRepresentable where RawValue == Int {
static var postgreSQLDataType: PostgreSQLDataType {
return Self.postgreSQLDataType
}
static var postgreSQLDataArrayType: PostgreSQLDataType {
return Self.postgreSQLDataArrayType
}
static func convertFromPostgreSQLData(_ data: PostgreSQLData) throws -> Self {
let intData = try RawValue.convertFromPostgreSQLData(data)
guard let auth = Self(rawValue: intData) else {
throw Abort(.badRequest, reason: "Invalid enum value")
}
return auth
}
func convertToPostgreSQLData() throws -> PostgreSQLData {
return try self.rawValue.convertToPostgreSQLData()
}
}
Now I can do the following:
enum Status : Int, Codable {
case uninitialized, created, claimed, expired
}
extension Status : PostgreSQLDataCustomConvertible { }
…and the enum can be used within a Model
as expected. Personally, I think it's acceptable for the user to manually extend the enum to PostgreSQLDataCustomConvertible
by using the line:
extension Status : PostgreSQLDataCustomConvertible { }
This follows the pattern set by other extensions such as:
extension User: PostgreSQLModel
etc.
It turns out String enums are a bit more gnarly. I start with a global RawRepresentable
extension which is identical to the Int version except for the where
clause:
extension RawRepresentable where RawValue == String {
static var postgreSQLDataType: PostgreSQLDataType {
return Self.postgreSQLDataType
}
static var postgreSQLDataArrayType: PostgreSQLDataType {
return Self.postgreSQLDataArrayType
}
static func convertFromPostgreSQLData(_ data: PostgreSQLData) throws -> Self {
let aRawValue = try RawValue.convertFromPostgreSQLData(data)
guard let enumValue = Self(rawValue: aRawValue) else {
throw Abort(.badRequest, reason: "Invalid enum value")
}
return enumValue
}
func convertToPostgreSQLData() throws -> PostgreSQLData {
return try self.rawValue.convertToPostgreSQLData()
}
}
Now I can define String enums like this:
enum Status : String, Codable {
case uninitialized, created, claimed, expired
}
extension Status : KeyStringDecodable, PostgreSQLDataCustomConvertible, PostgreSQLColumnStaticRepresentable {
static var postgreSQLColumn: PostgreSQLColumn = RawValue.postgreSQLColumn
static var keyStringTrue: Status = .created
static var keyStringFalse: Status = .uninitialized
}
Interestingly, I have to define postgreSQLColumn
in the enum extension. If I try to conform to it in a protocol extension, the compiler can't figure out which method to use, because some other protocol (maybe PostgreSQLDataCustomConvertible
?) already defines postgreSQLColumn
.
In any case, it feels pretty clunky. Especially the KeyStringDecodable
conformance, which forces me to specify a "truthiness" for the enum…
I would try:
extension RawRepresentable where RawValue: PostgreSQLDataCustomConvertible {
...
}
Then at least it would be a simple matter of just adding:
extension MyEnum: PostgreSQLDataCustomConvertible { }
If that works, we can definitely add the generic RawRepresentable
extension to the vapor/postgresql package.
extension RawRepresentable where RawValue: PostgreSQLColumnStaticRepresentable {
...
}
This ^ as well, which we could add to the vapor/fluent-postgresql package to make column representation just another simple conformance.
Regarding the KeyStringDecodable
, unfortunately there's no way around that as Swift doesn't offer us any methods for discovering the available cases on an enum at runtime (cc @gwynne). There are certainly ways to hack this using knowledge of the ABI, but we'd really like to avoid that.
IIRC, there are some swift evolution proposals up for solutions to this enum case discovery problem.
With the above extensions added by vapor/fluent-postgres and vapor/postgresql, we could at least simplify things to look like:
enum Status : String, Codable {
case uninitialized, created, claimed, expired
}
extension Status: KeyStringDecodable, PostgreSQLType {
static var keyStringTrue: Status = .created
static var keyStringFalse: Status = .uninitialized
}
@tanner0101 Oh, cool, that worked! Using the following extension:
extension RawRepresentable where RawValue: PostgreSQLDataCustomConvertible {
static var postgreSQLDataType: PostgreSQLDataType {
return Self.postgreSQLDataType
}
static var postgreSQLDataArrayType: PostgreSQLDataType {
return Self.postgreSQLDataArrayType
}
static func convertFromPostgreSQLData(_ data: PostgreSQLData) throws -> Self {
let aRawValue = try RawValue.convertFromPostgreSQLData(data)
guard let enumValue = Self(rawValue: aRawValue) else {
throw Abort(.badRequest, reason: "Invalid enum value")
}
return enumValue
}
func convertToPostgreSQLData() throws -> PostgreSQLData {
return try self.rawValue.convertToPostgreSQLData()
}
}
I can now do this:
enum Status : String, Codable {
case uninitialized, created, claimed, expired
}
extension Status : KeyStringDecodable, PostgreSQLDataCustomConvertible, PostgreSQLColumnStaticRepresentable {
static var postgreSQLColumn: PostgreSQLColumn = RawValue.postgreSQLColumn
static var keyStringTrue: Status = .created
static var keyStringFalse: Status = .uninitialized
}
Thanks! Should I wrap the extension up in a PR?
@bensyverson, yeah that would be great! The PostgreSQLDataCustomConvertible
extension should go in vapor/postgresql
.
Merged in vapor/postgresql, thanks!
@tanner0101 What needs to happen in vapor/fluent-postgresql
inside the closure you mentioned? Is it just the following?
extension RawRepresentable where RawValue: PostgreSQLColumnStaticRepresentable {
static var postgreSQLColumn: PostgreSQLColumn = RawValue.postgreSQLColumn
}
If so, I can send a PR through on this repo so we can get that tighter syntax you outlined!
Summary
It might just be temporarily broken, but I haven't been able to get Int or String enums to encode & decode to PostgreSQL.
Details
It should be possible to do the following:
However, this results in the following error:
Despite the error, the table is being created properly:
Workarounds
I can manually serialize the value, but it's gross:
Similarly, I can manually conform to
PostgreSQLDataCustomConvertible
, but this is unwieldy and fragile: