Skip to content

Commit

Permalink
model coding (#79)
Browse files Browse the repository at this point in the history
* add SQLRow decoding support

* change spelling to model:, add update builder helper

* row coder updates

* fix tests
  • Loading branch information
tanner0101 authored Dec 13, 2019
1 parent f0e0029 commit d09b552
Show file tree
Hide file tree
Showing 5 changed files with 231 additions and 3 deletions.
36 changes: 36 additions & 0 deletions Sources/SQLKit/Builders/SQLQueryFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ public protocol SQLQueryFetcher: SQLQueryBuilder { }

extension SQLQueryFetcher {
// MARK: First


public func first<D>(decoding: D.Type) -> EventLoopFuture<D?>
where D: Decodable
{
self.first().flatMapThrowing {
guard let row = $0 else {
return nil
}
return try row.decode(model: D.self)
}
}

/// Collects the first raw output and returns it.
///
Expand All @@ -18,6 +30,17 @@ extension SQLQueryFetcher {
}

// MARK: All


public func all<D>(decoding: D.Type) -> EventLoopFuture<[D]>
where D: Decodable
{
self.all().flatMapThrowing {
try $0.map {
try $0.decode(model: D.self)
}
}
}

/// Collects all raw output into an array and returns it.
///
Expand All @@ -31,6 +54,19 @@ extension SQLQueryFetcher {
}

// MARK: Run


public func run<D>(decoding: D.Type, _ handler: @escaping (Result<D, Error>) -> ()) -> EventLoopFuture<Void>
where D: Decodable
{
self.run {
do {
try handler(.success($0.decode(model: D.self)))
} catch {
handler(.failure(error))
}
}
}


/// Runs the query, passing output to the supplied closure as it is recieved.
Expand Down
12 changes: 9 additions & 3 deletions Sources/SQLKit/Builders/SQLUpdateBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,17 @@ public final class SQLUpdateBuilder: SQLQueryBuilder, SQLPredicateBuilder {
self.update = update
self.database = database
}

public func set<E>(model: E) throws -> Self where E: Encodable {
let row = try SQLQueryEncoder().encode(model)
row.forEach { column, value in
_ = self.set(SQLColumn(column), to: value)
}
return self
}

/// Sets a column (specified by an identifier) to an expression.
public func set<T>(_ column: String, to bind: T) -> Self
where T: Encodable
{
public func set(_ column: String, to bind: Encodable) -> Self {
return self.set(SQLIdentifier(column), to: SQLBind(bind))
}

Expand Down
11 changes: 11 additions & 0 deletions Sources/SQLKit/SQLRow.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
public protocol SQLRow {
var allColumns: [String] { get }
func contains(column: String) -> Bool
func decodeNil(column: String) throws -> Bool
func decode<D>(column: String, as type: D.Type) throws -> D
where D: Decodable
}

extension SQLRow {
public func decode<D>(model type: D.Type, prefix: String? = nil) throws -> D
where D: Decodable
{
try SQLRowDecoder().decode(D.self, from: self, prefix: prefix)
}
}
92 changes: 92 additions & 0 deletions Sources/SQLKit/SQLRowDecoder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
struct SQLRowDecoder {
func decode<T>(_ type: T.Type, from row: SQLRow, prefix: String? = nil) throws -> T
where T: Decodable
{
return try T.init(from: _Decoder(prefix: prefix, row: row))
}

enum _Error: Error {
case nesting
case unkeyedContainer
case singleValueContainer
}

struct _Decoder: Decoder {
let prefix: String?
let row: SQLRow
var codingPath: [CodingKey] = []
var userInfo: [CodingUserInfoKey : Any] {
[:]
}

func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key>
where Key: CodingKey
{
.init(_KeyedDecoder(prefix: self.prefix, row: self.row, codingPath: self.codingPath))
}

func unkeyedContainer() throws -> UnkeyedDecodingContainer {
throw _Error.unkeyedContainer
}

func singleValueContainer() throws -> SingleValueDecodingContainer {
throw _Error.singleValueContainer
}
}

struct _KeyedDecoder<Key>: KeyedDecodingContainerProtocol
where Key: CodingKey
{
let prefix: String?
let row: SQLRow
var codingPath: [CodingKey] = []
var allKeys: [Key] {
self.row.allColumns.compactMap {
Key.init(stringValue: $0)
}
}

func column(for key: Key) -> String {
if let prefix = self.prefix {
return prefix + key.stringValue
} else {
return key.stringValue
}
}

func contains(_ key: Key) -> Bool {
self.row.contains(column: self.column(for: key))
}

func decodeNil(forKey key: Key) throws -> Bool {
try self.row.decodeNil(column: self.column(for: key))
}

func decode<T>(_ type: T.Type, forKey key: Key) throws -> T
where T : Decodable
{
try self.row.decode(column: self.column(for: key), as: T.self)
}

func nestedContainer<NestedKey>(
keyedBy type: NestedKey.Type,
forKey key: Key
) throws -> KeyedDecodingContainer<NestedKey>
where NestedKey : CodingKey
{
throw _Error.nesting
}

func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
throw _Error.nesting
}

func superDecoder() throws -> Decoder {
_Decoder(prefix: self.prefix, row: self.row, codingPath: self.codingPath)
}

func superDecoder(forKey key: Key) throws -> Decoder {
throw _Error.nesting
}
}
}
83 changes: 83 additions & 0 deletions Tests/SQLKitTests/SQLKitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -247,4 +247,87 @@ CREATE TABLE `planets`(`id` BIGINT, `name` TEXT, `diameter` INTEGER, `galaxy_nam

XCTAssertEqual(db.results[2], "CREATE TABLE `planets3`(`galaxy_id` BIGINT, FOREIGN KEY (`galaxy_id`) REFERENCES `galaxies` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE)")
}

func testSQLRowDecoder() throws {
struct Foo: Codable {
let id: UUID
let foo: Int
let bar: Double?
let baz: String
}

do {
let row = TestRow(data: [
"id": UUID(),
"foo": 42,
"bar": Double?.none as Any,
"baz": "vapor"
])

let foo = try row.decode(model: Foo.self)
XCTAssertEqual(foo.foo, 42)
XCTAssertEqual(foo.bar, nil)
XCTAssertEqual(foo.baz, "vapor")
}
do {
let row = TestRow(data: [
"foos_id": UUID(),
"foos_foo": 42,
"foos_bar": Double?.none as Any,
"foos_baz": "vapor"
])

let foo = try row.decode(model: Foo.self, prefix: "foos_")
XCTAssertEqual(foo.foo, 42)
XCTAssertEqual(foo.bar, nil)
XCTAssertEqual(foo.baz, "vapor")
}
}
}

struct TestRow: SQLRow {
var data: [String: Any]

enum _Error: Error {
case missingColumn(String)
case typeMismatch(Any, Any.Type)
}

var allColumns: [String] {
.init(self.data.keys)
}

func contains(column: String) -> Bool {
self.data.keys.contains(column)
}

func decodeNil(column: String) throws -> Bool {
if let value = self.data[column], let optional = value as? OptionalType {
return optional.isNil
} else {
return false
}
}

func decode<D>(column: String, as type: D.Type) throws -> D
where D : Decodable
{
guard let value = self.data[column] else {
throw _Error.missingColumn(column)
}
guard let cast = value as? D else {
throw _Error.typeMismatch(value, D.self)
}
return cast
}
}

protocol OptionalType {
var isNil: Bool { get }
}

extension Optional: OptionalType {
var isNil: Bool {
self == nil
}
}

0 comments on commit d09b552

Please sign in to comment.