Skip to content

Commit

Permalink
Kitura Evolution Kitura#2 & Kitura#3
Browse files Browse the repository at this point in the history
  • Loading branch information
Aaron Liberatore authored and Aaron Liberatore committed Dec 8, 2017
1 parent 4d36213 commit bd35fb5
Show file tree
Hide file tree
Showing 82 changed files with 6,569 additions and 300 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/IBM-Swift/Kitura-net.git", .upToNextMinor(from: "2.0.0")),
.package(url: "https://github.com/IBM-Swift/Kitura-TemplateEngine.git", .upToNextMinor(from: "1.7.0")),
.package(url: "https://github.com/IBM-Swift/KituraContracts.git", .upToNextMinor(from: "0.0.15"))
.package(url: "https://github.com/IBM-Swift/KituraContracts.git", .branch("query"))
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
Expand Down
101 changes: 100 additions & 1 deletion Sources/Kitura/CodableRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,27 @@ extension Router {
getSafely(route, handler: handler)
}

/**
Setup a (QueryParameter, CodableArrayResultClosure) -> Void on the provided route which will be invoked when a request comes to the server.
### Usage Example: ###
````
// MyQuery is a codable struct defining the supported query parameters
// User is a struct object that conforms to Codable
router.get("/query") { (query: MyQuery, respondWith: ([User]?, RequestError?) -> Void) in
...
respondWith(users, nil)
}
````
- Parameter route: A String specifying the pattern that needs to be matched, in order for the handler to be invoked.
- Parameter handler: A (Query, CodableArrayResultClosure) -> Void that gets invoked when a request comes to the server.
*/
public func get<Q: Query, O: Codable>(_ route: String, handler: @escaping (Q, @escaping CodableArrayResultClosure<O>) -> Void) {
getSafely(route, handler: handler)
}

/**
Setup a NonCodableClosure on the provided route which will be invoked when a request comes to the server.
Expand Down Expand Up @@ -123,6 +144,25 @@ extension Router {
deleteSafely(route, handler: handler)
}

/**
Setup a (Query, ResultClosure) -> Void on the provided route which will be invoked when a request comes to the server.
### Usage Example: ###
````
// MyQuery is a codable struct defining the supported query parameters
router.delete("/query") { (query: MyQuery, respondWith: (RequestError?) -> Void) in
...
respondWith(nil)
}
````
- Parameter route: A String specifying the pattern that needs to be matched, in order for the handler to be invoked.
- Parameter handler: A (QueryParameter, ResultClosure) -> Void that gets invoked when a request comes to the server.
*/
public func delete<Q: Query>(_ route: String, handler: @escaping (Q, @escaping ResultClosure) -> Void) {
deleteSafely(route, handler: handler)
}
/**
Setup a CodableClosure on the provided route which will be invoked when a POST request comes to the server.
In this scenario, the ID (i.e. unique identifier) is a field in the Codable instance.
Expand Down Expand Up @@ -454,6 +494,39 @@ extension Router {
}
}

// Get w/Query Parameters
fileprivate func getSafely<Q: Query, O: Codable>(_ route: String, handler: @escaping (Q, @escaping CodableArrayResultClosure<O>) -> Void) {
get(route) { request, response, next in
Log.verbose("Received GET (plural) type-safe request with Query Parameters")
// Define result handler
let resultHandler: CodableArrayResultClosure<O> = { result, error in
do {
if let err = error {
let status = self.httpStatusCode(from: err)
response.status(status)
} else {
let encoded = try JSONEncoder().encode(result)
response.status(.OK)
response.send(data: encoded)
}
} catch {
// Http 500 error
response.status(.internalServerError)
}
next()
}
Log.verbose("Query Parameters: \(request.queryParameters)")
do {
let query: Q = try QueryDecoder(dictionary: request.queryParameters).decode(Q.self)
handler(query, resultHandler)
} catch {
// Http 422 error
response.status(.unprocessableEntity)
next()
}
}
}

// GET single identified element
fileprivate func getSafely<Id: Identifier, O: Codable>(_ route: String, handler: @escaping IdentifierSimpleCodableClosure<Id, O>) {
if parameterIsPresent(in: route) {
Expand Down Expand Up @@ -531,7 +604,33 @@ extension Router {
let identifier = try Id(value: id)
handler(identifier, resultHandler)
} catch {
// Http 422 error
// Http 422 error
response.status(.unprocessableEntity)
next()
}
}
}

// DELETE w/Query Parameters
fileprivate func deleteSafely<Q: Codable>(_ route: String, handler: @escaping (Q, @escaping ResultClosure) -> Void) {
delete(route) { request, response, next in
Log.verbose("Received DELETE type-safe request with Query Parameters")
// Define result handler
let resultHandler: ResultClosure = { error in
if let err = error {
let status = self.httpStatusCode(from: err)
response.status(status)
} else {
response.status(.OK)
}
next()
}
Log.verbose("Query Parameters: \(request.queryParameters)")
do {
let query: Q = try QueryDecoder(dictionary: request.queryParameters).decode(Q.self)
handler(query, resultHandler)
} catch {
// Http 422 error
response.status(.unprocessableEntity)
next()
}
Expand Down
128 changes: 128 additions & 0 deletions Sources/Kitura/String+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
import Foundation
import LoggerAPI

/// String Utils
extension String {

/// Parses percent encoded string into query parameters
var urlDecodedFieldValuePairs: [String : String] {
var result: [String:String] = [:]
Expand Down Expand Up @@ -47,3 +49,129 @@ extension String {
return result
}
}

/// Codable String Conversion Extension
extension String {

/// Converts the given String to an Int?
public var int: Int? {
return Int(self)
}

/// Converts the given String to a UInt?
public var uInt: UInt? {
return UInt(self)
}

/// Converts the given String to a Float?
public var float: Float? {
return Float(self)
}

/// Converts the given String to a Double?
public var double: Double? {
return Double(self)
}

/// Converts the given String to a Bool?
public var boolean: Bool? {
return Bool(self)
}

/// Converts the given String to a String
public var string: String {
return self
}

/// Converts the given String to an [Int]?
public var intArray: [Int]? {
let strs: [String] = self.components(separatedBy: ",")
let ints: [Int] = strs.map { Int($0) }.filter { $0 != nil }.map { $0! }
if ints.count == strs.count {
return ints
}
return nil
}

/// Converts the given String to an [UInt]?
public var uIntArray: [UInt]? {
let strs: [String] = self.components(separatedBy: ",")
let uInts: [UInt] = strs.map { UInt($0) }.filter { $0 != nil }.map { $0! }
if uInts.count == strs.count {
return uInts
}
return nil
}

/// Converts the given String to a [Float]?
public var floatArray: [Float]? {
let strs: [String] = self.components(separatedBy: ",")
let floats: [Float] = strs.map { Float($0) }.filter { $0 != nil }.map { $0! }
if floats.count == strs.count {
return floats
}
return nil
}

/// Converts the given String to a [Double]?
public var doubleArray: [Double]? {
let strs: [String] = self.components(separatedBy: ",")
let doubles: [Double] = strs.map { Double($0) }.filter { $0 != nil }.map { $0! }
if doubles.count == strs.count {
return doubles
}
return nil
}

/// Converts the given String to a [Bool]?
public var booleanArray: [Bool]? {
let strs: [String] = self.components(separatedBy: ",")
let bools: [Bool] = strs.map { Bool($0) }.filter { $0 != nil }.map { $0! }
if bools.count == strs.count {
return bools
}
return nil
}

/// Converts the given String to a [String]
public var stringArray: [String] {
let strs: [String] = self.components(separatedBy: ",")
return strs
}

/// Method used to decode a string into the given type T
///
/// - Parameters:
/// - _ type: The Decodable type to convert the string into.
/// - Returns: The Date? object. Some on success / nil on failure
public func decodable<T: Decodable>(_ type: T.Type) -> T? {
guard let data = self.data(using: .utf8) else {
return nil
}
let obj: T? = try? JSONDecoder().decode(type, from: data)
return obj
}

/// Converts the given String to a Date?
///
/// - Parameters:
/// - _ formatter: The designated DateFormatter to convert the string with.
/// - Returns: The Date? object. Some on success / nil on failure
public func date(_ formatter: DateFormatter) -> Date? {
return formatter.date(from: self)
}

/// Converts the given String to a [Date]?
///
/// - Parameters:
/// - _ formatter: The designated DateFormatter to convert the string with.
/// - Returns: The [Date]? object. Some on success / nil on failure
public func dateArray(_ formatter: DateFormatter) -> [Date]? {
let strs: [String] = self.components(separatedBy: ",")
let dates = strs.map { formatter.date(from: $0) }.filter { $0 != nil }.map { $0! }
if dates.count == strs.count {
return dates
}
return nil
}
}
37 changes: 37 additions & 0 deletions Sources/Kitura/queryCoding/Coder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright IBM Corporation 2017
*
* 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 Foundation

/// Class defining shared resources for the QueryDecoder and QueryEncoder
public class Coder {

/// The designated DateFormatter used for encoding and decoding query parameters
public let dateFormatter: DateFormatter

/// Initializes a Coder instance with a Date Formatter
/// using the "UTC" timezone and "yyyy-MM-dd'T'HH:mm:ssZ" date format
public init() {
self.dateFormatter = DateFormatter()
self.dateFormatter.timeZone = TimeZone(identifier: "UTC")
self.dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
}

/// Helper method to extract the field name from a CodingKey array
public static func getFieldName(from codingPath: [CodingKey]) -> String {
return codingPath.flatMap({"\($0)"}).joined(separator: ".")
}
}

0 comments on commit bd35fb5

Please sign in to comment.