novi / mysql-swift

A type safe MySQL client for Swift
MIT License
163 stars 40 forks source link

[Feature request] Swift 4 Codable Encoder Decoder #58

Closed patrick-zippenfenig closed 6 years ago

patrick-zippenfenig commented 7 years ago

Hi, I'm currently evaluating Swift Mysql integration and was wondering if Swift 4 Codable protocol would be possible to integrate?

struct User: Codable {
  let id: Int
  let userName: String
  let age: Int?

  // Generated automatically by the compiler if not specified
  private enum CodingKeys: String, CodingKey {
    case id
    case userName = "user_name"
    case age
  }

  // Generated automatically by the compiler if not specified
  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(id, forKey: . id)
    try container.encode(userName, forKey: . userName)
    try container.encode(age, forKey: . age)
  }

 // Generated automatically by the compiler if not specified
  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    id = try container.decode(Int.self, forKey: .id)
    userName = try container.decode(String.self, forKey: .userName)
    age = try container.decode(Int.self, forKey: .age)
  }
}

CodingKeys encode and decode are generated automatically. Reference https://www.mikeash.com/pyblog/friday-qa-2017-07-14-swiftcodable.html

An mysql ORM could now implement the necessary encode and decode container for arrays of objects. It looks similar to your current implementation. Would such an integration be possible?

Regards, Patrick

novi commented 7 years ago

Hello. We could update the library to treat current QueryRowResultType and Swift.Codable as query results and parameters, and could make more use cases and API design for the integration.

patrick-zippenfenig commented 7 years ago

Decode functionality would be worth a first try and simplifies library usage:

struct User: Codable {
  let id: Int
  let user_name: String
  let age: Int
}
let rows: [User] = try conn.query("SELECT id, user_name, age FROM users")

Encoding could be used for INSERT/UPDATE statements. I assume you already use reflection to serialise structs to SQL statements. Some ORM patterns to represent a table like User.findById(id: int), user.save(), user.delete() would also be nice.

I will try to make some example code for decode functionality as soon as i figure out how to use KeyedDecodingContainer.

patrick-zippenfenig commented 7 years ago

I prepared a simplified decoder to decode one object. Decoding an array of objects adds a lot of boiler plate code and complicates stuff a lot.

struct User: Codable {
    let id: Int
    let username: String
    let age: Int? // only set if RowKeyedDecodingContainer.contains() returns true
}
let decoder = RowDecoder()
let user = try User(from: decoder)

The decoder could represent a single row and hold colmnNames and data. Close to QueryRowResult. User(from: decoder) calls decoder.container().

struct RowDecoder : Decoder {
    var codingPath = [CodingKey]()
    var userInfo = [CodingUserInfoKey : Any]()

    var columnNames = ["id", "username", "age"]

    public func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> {
        return KeyedDecodingContainer(RowKeyedDecodingContainer<Key>(decoder: self))
    }

    public func unkeyedContainer() throws -> UnkeyedDecodingContainer {
        throw "UnkeyedDecodingContainer not implemented"
    }

    public func singleValueContainer() throws -> SingleValueDecodingContainer {
        throw "SingleValueDecodingContainer not implemented"
    }
}

RowKeyedDecodingContainer decodes keys to values. User(from: decoder) calls decode for each attribute. Optional attributes call contains() before calling decode(). Functions for nesting other container are not required just throw an error.

struct RowKeyedDecodingContainer<K : CodingKey> : KeyedDecodingContainerProtocol {
    typealias Key = K

    let decoder : RowDecoder

    let allKeys = [Key]()

    let codingPath = [CodingKey]()

    func decodeNil(forKey key: K) throws -> Bool {
        return false
    }

    func contains(_ key: K) -> Bool {
        return decoder.columnNames.contains(key.stringValue)
    }

    func decode(_ type: Bool.Type, forKey key: K) throws -> Bool {
        return false
    }

    func decode(_ type: Int.Type, forKey key: K) throws -> Int {
        return 123
    }

    func decode(_ type: Int8.Type, forKey key: K) throws -> Int8 {
        return 123
    }

    func decode(_ type: Int16.Type, forKey key: K) throws -> Int16 {
        return 123
    }

    func decode(_ type: Int32.Type, forKey key: K) throws -> Int32 {
        return 123
    }

    func decode(_ type: Int64.Type, forKey key: K) throws -> Int64 {
        return 123
    }

    func decode(_ type: UInt.Type, forKey key: K) throws -> UInt {
        return 123
    }

    func decode(_ type: UInt8.Type, forKey key: K) throws -> UInt8 {
        return 123
    }

    func decode(_ type: UInt16.Type, forKey key: K) throws -> UInt16 {
        return 123
    }

    func decode(_ type: UInt32.Type, forKey key: K) throws -> UInt32 {
        return 123
    }

    func decode(_ type: UInt64.Type, forKey key: K) throws -> UInt64 {
        return 123
    }

    func decode(_ type: Float.Type, forKey key: K) throws -> Float {
        return 123
    }

    func decode(_ type: Double.Type, forKey key: K) throws -> Double {
        return 123
    }

    func decode(_ type: String.Type, forKey key: K) throws -> String {
        if !self.contains(key) {
            throw "Key '\(key.stringValue)' not in columnNames"
        }
        return "some string"
    }

    func decode<T>(_ type: T.Type, forKey key: K) throws -> T where T : Decodable {
        throw "decode<T> not implemented"
    }

    func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: K) throws -> KeyedDecodingContainer<NestedKey> {
        throw "nestedContainer not implemented"
    }

    func nestedUnkeyedContainer(forKey key: K) throws -> UnkeyedDecodingContainer {
        throw "nestedUnkeyedContainer not implemented"
    }

    func superDecoder() throws -> Decoder {
        throw "superDecoder not implemented"
    }

    func superDecoder(forKey key: K) throws -> Decoder {
        throw "superDecoder forKey not implemented"
    }
}

I am not familiar with this library and it probably would take a few days work to integrate. Could try to implement it or hint me in the right direction?

Thanks!

novi commented 7 years ago

Thanks. I will check it soon.

patrick-zippenfenig commented 6 years ago

@novi Any news? Edit: I have implemented a first version. PR will follow tomorrow

novi commented 6 years ago

@patrick-zippenfenig Sorry for late. I'm still trying to design for the Codable API support. Thanks for PR! I will check that.

florianreinhart commented 6 years ago

The implementation in #64 causes an issue in my code. My model types conform to both QueryRowResultType and Decodable. The compiler does not know which query method to use: Ambiguous use of 'query'.