diff --git a/.swift-version b/.swift-version index f2c6cb6..6b244dc 100644 --- a/.swift-version +++ b/.swift-version @@ -1 +1 @@ -4.2.3 +5.0.1 diff --git a/.travis.yml b/.travis.yml index 13d41a7..2ce5c3f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,7 @@ branches: matrix: include: - os: linux + # Trusty build retained to provide 14.04 coverage (as paid support is still available) dist: trusty sudo: required env: SWIFT_SNAPSHOT=4.0.3 @@ -24,24 +25,30 @@ matrix: dist: xenial sudo: required services: docker - env: DOCKER_IMAGE=swift:4.2.3 -# Temporarily disabled: appid-serversdk-swift does not yet compile on Swift 5 -# https://github.com/ibm-cloud-security/appid-serversdk-swift/issues/83 -# - os: linux -# dist: xenial -# sudo: required -# services: docker -# env: DOCKER_IMAGE=swift:4.2.3 SWIFT_SNAPSHOT=$SWIFT_DEVELOPMENT_SNAPSHOT + env: DOCKER_IMAGE=swift:4.2.4 SWIFT_SNAPSHOT=4.2.4 - os: linux dist: xenial sudo: required services: docker - env: DOCKER_IMAGE=swift:4.2.3 KITURA_NIO=1 + # Note: Ubuntu 16.04 + env: DOCKER_IMAGE=swift:5.0.1-xenial - os: linux dist: xenial sudo: required services: docker - env: DOCKER_IMAGE=ubuntu:18.04 KITURA_NIO=1 + # Note: Kitura-WebSocket-NIO requires zlib + env: DOCKER_IMAGE=swift:5.0.1-xenial KITURA_NIO=1 DOCKER_PACKAGES="libz-dev" + - os: linux + dist: xenial + sudo: required + services: docker + # Note: Ubuntu 18.04 + env: DOCKER_IMAGE=swift:5.0.1 SWIFT_SNAPSHOT=$SWIFT_DEVELOPMENT_SNAPSHOT + - os: linux + dist: xenial + sudo: required + services: docker + env: DOCKER_IMAGE=swift:5.0.1 SWIFT_SNAPSHOT=$SWIFT_DEVELOPMENT_SNAPSHOT KITURA_NIO=1 DOCKER_PACKAGES="libz-dev" - os: osx osx_image: xcode9.2 sudo: required @@ -53,10 +60,22 @@ matrix: - os: osx osx_image: xcode10.1 sudo: required + env: SWIFT_SNAPSHOT=4.2.1 - os: osx - osx_image: xcode10.1 + osx_image: xcode10.2 + sudo: required + - os: osx + osx_image: xcode10.2 + sudo: required + env: KITURA_NIO=1 + - os: osx + osx_image: xcode10.2 sudo: required env: SWIFT_SNAPSHOT=$SWIFT_DEVELOPMENT_SNAPSHOT + - os: osx + osx_image: xcode10.2 + sudo: required + env: SWIFT_SNAPSHOT=$SWIFT_DEVELOPMENT_SNAPSHOT KITURA_NIO=1 before_install: - git clone https://github.com/IBM-Swift/Package-Builder.git diff --git a/Package.swift b/Package.swift index 31332c9..846ac8f 100644 --- a/Package.swift +++ b/Package.swift @@ -1,8 +1,8 @@ -// swift-tools-version:4.0 +// swift-tools-version:5.0 // The swift-tools-version declares the minimum version of Swift required to build this package. /** - * Copyright IBM Corporation 2016, 2017 + * Copyright IBM Corporation 2016, 2017, 2019 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,9 +21,9 @@ import PackageDescription import Foundation var dependencies: [Package.Dependency] = [ - .package(url: "https://github.com/IBM-Swift/Kitura.git", from: "2.5.0"), + .package(url: "https://github.com/IBM-Swift/Kitura.git", from: "2.6.0"), .package(url: "https://github.com/IBM-Swift/HeliumLogger.git", from: "1.7.1"), - .package(url: "https://github.com/IBM-Swift/CloudEnvironment.git", from: "8.0.0"), + .package(url: "https://github.com/IBM-Swift/CloudEnvironment.git", from: "9.0.0"), .package(url: "https://github.com/RuntimeTools/SwiftMetrics.git", from: "2.5.0"), .package(url: "https://github.com/IBM-Swift/Health.git", from: "1.0.0"), .package(url: "https://github.com/IBM-Swift/Kitura-OpenAPI.git", from: "1.1.0"), @@ -34,7 +34,7 @@ var dependencies: [Package.Dependency] = [ .package(url: "https://github.com/IBM-Swift/Kitura-CredentialsGoogle.git", from: "2.2.0"), .package(url: "https://github.com/IBM-Swift/Kitura-CredentialsFacebook.git", from: "2.2.0"), .package(url: "https://github.com/IBM-Swift/Swift-JWT", from: "3.0.0"), - .package(url: "https://github.com/IBM-Swift/Swift-Kuery-ORM.git", .upToNextMinor(from: "0.3.1")), + .package(url: "https://github.com/IBM-Swift/Swift-Kuery-ORM.git", .upToNextMinor(from: "0.5.0")), ] var targetDependencies: [Target.Dependency] = [ "Kitura", "CloudEnvironment","SwiftMetrics","Health", "KituraOpenAPI", "KituraMarkdown", "KituraStencil", "CredentialsHTTP", "KituraSession", "CredentialsGoogle", "CredentialsFacebook", "SwiftJWT", "SwiftKueryORM", ] @@ -49,9 +49,9 @@ dependencies.append(.package(url: "https://github.com/ibm-cloud-security/appid-s targetDependencies.append("IBMCloudAppID") #endif -// Temporarily use alternate branch of Kitura-WebSocket while building in NIO mode +// Use alternate implementation of Kitura-WebSocket while building in NIO mode if ProcessInfo.processInfo.environment["KITURA_NIO"] != nil { - dependencies.append(.package(url: "https://github.com/IBM-Swift/Kitura-WebSocket.git", .exact("0.1.0-nio"))) + dependencies.append(.package(url: "https://github.com/IBM-Swift/Kitura-WebSocket-NIO.git", from: "2.0.0")) } else { dependencies.append(.package(url: "https://github.com/IBM-Swift/Kitura-WebSocket.git", from: "2.0.0")) } diff --git a/Package@swift-4.swift b/Package@swift-4.swift new file mode 100644 index 0000000..0aa976e --- /dev/null +++ b/Package@swift-4.swift @@ -0,0 +1,68 @@ +// swift-tools-version:4.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +/** + * Copyright IBM Corporation 2016, 2017, 2019 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +import PackageDescription +import Foundation + +var dependencies: [Package.Dependency] = [ + .package(url: "https://github.com/IBM-Swift/Kitura.git", .upToNextMinor(from: "2.6.0")), + .package(url: "https://github.com/IBM-Swift/HeliumLogger.git", from: "1.7.1"), + .package(url: "https://github.com/IBM-Swift/CloudEnvironment.git", from: "9.0.0"), + .package(url: "https://github.com/RuntimeTools/SwiftMetrics.git", from: "2.5.0"), + .package(url: "https://github.com/IBM-Swift/Health.git", from: "1.0.0"), + .package(url: "https://github.com/IBM-Swift/Kitura-OpenAPI.git", from: "1.1.0"), + .package(url: "https://github.com/IBM-Swift/Kitura-StencilTemplateEngine.git", from: "1.9.0"), + .package(url: "https://github.com/IBM-Swift/Kitura-Markdown.git", from: "1.0.0"), + .package(url: "https://github.com/IBM-Swift/Kitura-CredentialsHTTP.git", from: "2.1.0"), + .package(url: "https://github.com/IBM-Swift/Kitura-Session.git", from: "3.3.0"), + .package(url: "https://github.com/IBM-Swift/Kitura-CredentialsGoogle.git", from: "2.2.0"), + .package(url: "https://github.com/IBM-Swift/Kitura-CredentialsFacebook.git", from: "2.2.0"), + .package(url: "https://github.com/IBM-Swift/Swift-JWT", from: "3.0.0"), + .package(url: "https://github.com/IBM-Swift/Swift-Kuery-ORM.git", .upToNextMinor(from: "0.5.0")), +] +var targetDependencies: [Target.Dependency] = [ "Kitura", "CloudEnvironment","SwiftMetrics","Health", "KituraOpenAPI", "KituraMarkdown", "KituraStencil", "CredentialsHTTP", "KituraSession", "CredentialsGoogle", "CredentialsFacebook", "SwiftJWT", "SwiftKueryORM", +] + +// Uncomment to use PostgreSQL +// dependencies.append(.package(url: "https://github.com/IBM-Swift/Swift-Kuery-PostgreSQL.git", from: "1.2.0")) +// targetDependencies.append("SwiftKueryPostgreSQL") + +// IBMCloudAppID requires OpenSSL that is not included on Mac by default. +#if os(Linux) +dependencies.append(.package(url: "https://github.com/ibm-cloud-security/appid-serversdk-swift", .branch("development"))) +targetDependencies.append("IBMCloudAppID") +#endif + +// Use alternate implementation of Kitura-WebSocket while building in NIO mode +if ProcessInfo.processInfo.environment["KITURA_NIO"] != nil { + dependencies.append(.package(url: "https://github.com/IBM-Swift/Kitura-WebSocket-NIO.git", from: "1.0.0")) +} else { + dependencies.append(.package(url: "https://github.com/IBM-Swift/Kitura-WebSocket.git", from: "2.0.0")) +} + +let package = Package( + name: "Kitura-Sample", + dependencies: dependencies, + targets: [ + .target(name: "Kitura-Sample", dependencies: [ .target(name: "Application"), .target(name: "ChatService"), "Kitura" , "HeliumLogger"]), + .target(name: "Application", dependencies: targetDependencies), + .target(name: "ChatService", dependencies: ["Kitura-WebSocket"]), + .testTarget(name: "KituraSampleRouterTests" , dependencies: [.target(name: "Application"), "Kitura","HeliumLogger" ]) + ] +) diff --git a/Sources/Application/DummyDatabase.swift b/Sources/Application/DummyDatabase.swift index 91c4116..593603f 100644 --- a/Sources/Application/DummyDatabase.swift +++ b/Sources/Application/DummyDatabase.swift @@ -20,18 +20,11 @@ import SwiftKuery // This class is a dummy SwiftKuery plugin that allows the saving and retrieval of "Grade" objects in local storage. // This allows the Database example to run prior to connecting to a real database. class DummyConnection: Connection { - var grades = [Grade]() - let queryBuilder: QueryBuilder - - init(withDeleteRequiresUsing: Bool = false, withUpdateRequiresFrom: Bool = false, createAutoIncrement: ((String, Bool) -> String)? = nil) { - self.queryBuilder = QueryBuilder(withDeleteRequiresUsing: withDeleteRequiresUsing, withUpdateRequiresFrom: withUpdateRequiresFrom, createAutoIncrement: createAutoIncrement) + func execute(preparedStatement: PreparedStatement, parameters: [String : Any?], onCompletion: @escaping ((QueryResult) -> ())) { + onCompletion(.successNoData) } - func connect(onCompletion: (QueryError?) -> ()) {} - - public var isConnected: Bool { return true } - - func closeConnection() {} + var grades = [Grade]() func execute(query: Query, onCompletion: @escaping ((QueryResult) -> ())) { do { @@ -39,7 +32,11 @@ class DummyConnection: Connection { switch queryComponents[0] { case "SELECT": if queryComponents[1] == "*" { - return onCompletion(.resultSet(ResultSet(DummyResultFetcher(grades: grades)))) + if grades.isEmpty { + return onCompletion(.successNoData) + } else { + return onCompletion(QueryResult.resultSet(ResultSet(DummyResultFetcher(grades: grades), connection: self))) + } } case "DELETE": grades = [] @@ -64,7 +61,7 @@ class DummyConnection: Connection { case "SELECT": if let course = parameters[0] as? String { let matchingGrades = grades.filter { $0.course == course } - return onCompletion(.resultSet(ResultSet(DummyResultFetcher(grades: matchingGrades)))) + return onCompletion(.resultSet(ResultSet(DummyResultFetcher(grades: matchingGrades), connection: self))) } default: return onCompletion(QueryResult.successNoData) @@ -94,35 +91,45 @@ class DummyConnection: Connection { return (try? query.build(queryBuilder: queryBuilder)) ?? "" } - private func returnResult(_ onCompletion: @escaping ((QueryResult) -> ())) { - return onCompletion(.successNoData) + func execute(preparedStatement: PreparedStatement, parameters: [Any?], onCompletion: @escaping ((QueryResult) -> ())) { + onCompletion(.successNoData) } - func startTransaction(onCompletion: @escaping ((QueryResult) -> ())) {} + func execute(preparedStatement: PreparedStatement, onCompletion: @escaping ((QueryResult) -> ())) { + onCompletion(.successNoData) + } - func commit(onCompletion: @escaping ((QueryResult) -> ())) {} + init(withDeleteRequiresUsing: Bool = false, withUpdateRequiresFrom: Bool = false) { + self.queryBuilder = QueryBuilder(columnBuilder: DummySQLColumnBuilder()) + } - func rollback(onCompletion: @escaping ((QueryResult) -> ())) {} + var queryBuilder: QueryBuilder - func create(savepoint: String, onCompletion: @escaping ((QueryResult) -> ())) {} + func connect(onCompletion: @escaping (QueryResult) -> ()) {} - func rollback(to savepoint: String, onCompletion: @escaping ((QueryResult) -> ())) {} + func connectSync() -> QueryResult { return QueryResult.successNoData } + func closeConnection() {} - func release(savepoint: String, onCompletion: @escaping ((QueryResult) -> ())) {} + var isConnected: Bool = true + + func prepareStatement(_ query: Query, onCompletion: @escaping ((QueryResult) -> ())) {} + + func prepareStatement(_ raw: String, onCompletion: @escaping ((QueryResult) -> ())) {} - struct TestPreparedStatement: PreparedStatement {} + func release(preparedStatement: PreparedStatement, onCompletion: @escaping ((QueryResult) -> ())) {} + + func startTransaction(onCompletion: @escaping ((QueryResult) -> ())) { } - func prepareStatement(_ query: Query) throws -> PreparedStatement { return TestPreparedStatement() } + func commit(onCompletion: @escaping ((QueryResult) -> ())) {} - func prepareStatement(_ raw: String) throws -> PreparedStatement { return TestPreparedStatement() } + func rollback(onCompletion: @escaping ((QueryResult) -> ())) {} - func execute(preparedStatement: PreparedStatement, onCompletion: @escaping ((QueryResult) -> ())) {} + func create(savepoint: String, onCompletion: @escaping ((QueryResult) -> ())) {} - func execute(preparedStatement: PreparedStatement, parameters: [Any?], onCompletion: @escaping ((QueryResult) -> ())) {} + func rollback(to savepoint: String, onCompletion: @escaping ((QueryResult) -> ())) {} - func execute(preparedStatement: PreparedStatement, parameters: [String:Any?], onCompletion: @escaping ((QueryResult) -> ())) {} + func release(savepoint: String, onCompletion: @escaping ((QueryResult) -> ())) {} - func release(preparedStatement: PreparedStatement, onCompletion: @escaping ((QueryResult) -> ())) {} } class DummyResultFetcher: ResultFetcher { @@ -138,19 +145,55 @@ class DummyResultFetcher: ResultFetcher { } } - func fetchNext() -> [Any?]? { + func fetchNext(callback: @escaping (([Any?]?, Error?)) -> ()) { if fetched < numberOfRows { fetched += 1 - return rows[fetched - 1] + callback((rows[fetched - 1], nil)) } - return nil + callback((nil, nil)) } - func fetchNext(callback: ([Any?]?) ->()) { - callback(fetchNext()) + func fetchTitles(callback: @escaping (([String]?, Error?)) -> ()) { + callback((titles, nil)) } - func fetchTitles() -> [String] { - return titles + func done() {} +} + +class DummySQLColumnBuilder: ColumnCreator { + func buildColumn(for column: Column, using queryBuilder: QueryBuilder) -> String? { + guard let type = column.type else { + return nil + } + + var result = column.name + let identifierQuoteCharacter = queryBuilder.substitutions[QueryBuilder.QuerySubstitutionNames.identifierQuoteCharacter.rawValue] + if !result.hasPrefix(identifierQuoteCharacter) { + result = identifierQuoteCharacter + result + identifierQuoteCharacter + " " + } + + var typeString = type.create(queryBuilder: queryBuilder) + if let length = column.length { + typeString += "(\(length))" + } + + if column.isPrimaryKey { + result += " PRIMARY KEY" + } + if column.isNotNullable { + result += " NOT NULL" + } + if column.isUnique { + result += " UNIQUE" + } + if let checkExpression = column.checkExpression { + result += checkExpression.contains(column.name) ? " CHECK (" + checkExpression.replacingOccurrences(of: column.name, with: "\"\(column.name)\"") + ")" : " CHECK (" + checkExpression + ")" + } + if let collate = column.collate { + result += " COLLATE \"" + collate + "\"" + } + return result } + + } diff --git a/Tests/KituraSampleRouterTests/KituraTest.swift b/Tests/KituraSampleRouterTests/KituraTest.swift index 3616444..89c9c06 100644 --- a/Tests/KituraSampleRouterTests/KituraTest.swift +++ b/Tests/KituraSampleRouterTests/KituraTest.swift @@ -169,13 +169,17 @@ class KituraTest: XCTestCase { expectedStatusCode: HTTPStatusCode = HTTPStatusCode.OK, bodyChecker: BodyChecker? = nil) { XCTAssertEqual(response.statusCode, expectedStatusCode, "No success status code returned") - if let optionalBody = try? response.readString(), let body = optionalBody { - if let expectedResponseText = expectedResponseText { - XCTAssertEqual(body, expectedResponseText, "mismatch in body") + do { + if let body = try response.readString() { + if let expectedResponseText = expectedResponseText { + XCTAssertEqual(body, expectedResponseText, "mismatch in body") + } + bodyChecker?(body) + } else { + XCTFail("No response body") } - bodyChecker?(body) - } else { - XCTFail("No response body") + } catch { + XCTFail("Error thrown getting response body: \(error)") } } @@ -229,22 +233,26 @@ class KituraTest: XCTestCase { expectedStatusCode: HTTPStatusCode = HTTPStatusCode.OK) { XCTAssertEqual(response.statusCode, expectedStatusCode, "No success status code returned") - if let optionalBody = try? response.readString(), let body = optionalBody { - if body.isEmpty { - XCTAssertNil(expectedResponseArray) - } else { - let json = body.data(using: .utf8)! - do { - let myStruct = try JSONDecoder().decode([T].self, from: json) - if let expectedResponseArray = expectedResponseArray { - XCTAssertTrue(myStruct.elementsEqual(expectedResponseArray)) + do { + if let body = try response.readString() { + if body.isEmpty { + XCTAssertNil(expectedResponseArray) + } else { + let json = body.data(using: .utf8)! + do { + let myStruct = try JSONDecoder().decode([T].self, from: json) + if let expectedResponseArray = expectedResponseArray { + XCTAssertTrue(myStruct.elementsEqual(expectedResponseArray)) + } + } catch { + print("Error") } - } catch { - print("Error") } + } else { + XCTFail("No response body") } - } else { - XCTFail("No response body") + } catch { + XCTFail("Error thrown getting response body: \(error)") } } @@ -252,16 +260,20 @@ class KituraTest: XCTestCase { expectedStatusCode: HTTPStatusCode = HTTPStatusCode.OK) { XCTAssertEqual(response.statusCode, expectedStatusCode, "No success status code returned") - if let optionalBody = try? response.readString(), let body = optionalBody { - let json = body.data(using: .utf8)! - do { - let myStruct = try JSONDecoder().decode(T.self, from: json) - XCTAssertTrue(myStruct == expectedResponse) - } catch { - print("Error") + do { + if let body = try response.readString() { + let json = body.data(using: .utf8)! + do { + let myStruct = try JSONDecoder().decode(T.self, from: json) + XCTAssertTrue(myStruct == expectedResponse) + } catch { + print("Error") + } + } else { + XCTFail("No response body") } - } else { - XCTFail("No response body") + } catch { + XCTFail("Error thrown getting response body: \(error)") } }