Skip to content

Commit

Permalink
fix: Handle Empty values from HTML Forms (#26)
Browse files Browse the repository at this point in the history
Changed decoding of Empty values in Queries to be as follows:
   - Any Optional type (including String?) defaults to nil
   - Non-optional String successfully decodes to ""
   - Non-optional Bool decodes to false
   - All other non-optional types throw a decoding error
  • Loading branch information
EnriqueL8 authored and Andrew-Lees11 committed Jun 25, 2018
1 parent 7176189 commit fca1d07
Show file tree
Hide file tree
Showing 4 changed files with 44 additions and 8 deletions.
2 changes: 1 addition & 1 deletion Sources/KituraContracts/CodableQuery/Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ extension String {

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

/// Converts the given String to a String.
Expand Down
12 changes: 10 additions & 2 deletions Sources/KituraContracts/CodableQuery/QueryDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ import LoggerAPI
return
}
````
### Decoding Empty Values:
When an HTML form is sent with an empty or unchecked field, the corresponding key/value pair is sent with an empty value (i.e. `&key1=&key2=`).
The corresponding mapping to Swift types performed by `QueryDecoder` is as follows:
- Any Optional type (including `String?`) defaults to `nil`
- Non-optional `String` successfully decodes to `""`
- Non-optional `Bool` decodes to `false`
- All other non-optional types throw a decoding error
*/
public class QueryDecoder: Coder, Decoder {

Expand Down Expand Up @@ -253,9 +261,9 @@ public class QueryDecoder: Coder, Decoder {
return try decoder.decode(T.self)
}

// If it is not in the dictionary it should be nil
// If it is not in the dictionary or it is a empty string it should be nil
func decodeNil(forKey key: Key) throws -> Bool {
return !contains(key)
return decoder.dictionary[key.stringValue]?.isEmpty ?? true
}

func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
Expand Down
22 changes: 21 additions & 1 deletion Sources/KituraContracts/Contracts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,27 @@ public extension RequestError {
}

/**
An identifier for a query parameter object.
An object that conforms to QueryParams is identified as being decodable from URLEncoded data.
This can be applied to a Codable route to define the names and types of the expected query parameters, and provide type-safe access to their values. The `QueryDecoder` is used to decode the URL encoded parameters into an instance of the conforming type.
### Usage Example: ###
```swift
struct Query: QueryParams {
let id: Int
}
router.get("/user") { (query: Query, respondWith: (User?, RequestError?) -> Void) in
guard let user: User = userArray[query.id] else {
return respondWith(nil, .notFound)
}
respondWith(user, nil)
}
```
### Decoding Empty Values:
When an HTML form is sent with an empty or unchecked field, the corresponding key/value pair is sent with an empty value (i.e. `&key1=&key2=`).
The corresponding mapping to Swift types performed by `QueryDecoder` is as follows:
- Any Optional type (including `String?`) defaults to `nil`
- Non-optional `String` successfully decodes to `""`
- Non-optional `Bool` decodes to `false`
- All other non-optional types throw a decoding error
*/
public protocol QueryParams: Codable {
}
Expand Down
16 changes: 12 additions & 4 deletions Tests/KituraContractsTests/QueryCoderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ class QueryCoderTests: XCTestCase {
public let intField: Int
public let optionalIntField: Int?
public let stringField: String
public let emptyStringField: String
public let optionalStringField: String?
public let intArray: [Int]
public let dateField: Date
public let optionalDateField: Date?
Expand All @@ -103,6 +105,8 @@ class QueryCoderTests: XCTestCase {
lhs.intField == rhs.intField &&
lhs.optionalIntField == rhs.optionalIntField &&
lhs.stringField == rhs.stringField &&
lhs.emptyStringField == rhs.emptyStringField &&
lhs.optionalStringField == rhs.optionalStringField &&
lhs.intArray == rhs.intArray &&
lhs.dateField == rhs.dateField &&
lhs.optionalDateField == rhs.optionalDateField &&
Expand Down Expand Up @@ -142,9 +146,9 @@ class QueryCoderTests: XCTestCase {
}


let expectedDict = ["boolField": "true", "intField": "23", "stringField": "a string", "intArray": "1,2,3", "dateField": "2017-10-31T16:15:56+0000", "optionalDateField": "2017-10-31T16:15:56+0000", "nested": "{\"nestedIntField\":333,\"nestedStringField\":\"nested string\"}" ]
let expectedDict = ["boolField": "true", "intField": "23", "stringField": "a string", "emptyStringField": "", "optionalStringField": "", "intArray": "1,2,3", "dateField": "2017-10-31T16:15:56+0000", "optionalDateField": "", "nested": "{\"nestedIntField\":333,\"nestedStringField\":\"nested string\"}" ]

let expectedQueryString = "?boolField=true&intArray=1%2C2%2C3&stringField=a%20string&intField=23&dateField=2017-12-07T21:42:06%2B0000&nested=%7B\"nestedStringField\":\"nested%20string\"%2C\"nestedIntField\":333%7D"
let expectedQueryString = "?boolField=true&intArray=1%2C2%2C3&stringField=a%20string&emptyStringField=&optionalStringField=&intField=23&dateField=2017-12-07T21:42:06%2B0000&nested=%7B\"nestedStringField\":\"nested%20string\"%2C\"nestedIntField\":333%7D"

let expectedDateStr = "2017-10-31T16:15:56+0000"
let expectedDate = Coder().dateFormatter.date(from: "2017-10-31T16:15:56+0000")!
Expand All @@ -153,9 +157,11 @@ class QueryCoderTests: XCTestCase {
intField: 23,
optionalIntField: nil,
stringField: "a string",
emptyStringField: "",
optionalStringField: nil,
intArray: [1, 2, 3],
dateField: Coder().dateFormatter.date(from: "2017-10-31T16:15:56+0000")!,
optionalDateField: Coder().dateFormatter.date(from: "2017-10-31T16:15:56+0000")!,
optionalDateField: nil,
nested: Nested(nestedIntField: 333, nestedStringField: "nested string"))

let expectedFiltersDict = ["greaterThan": "8", "greaterThanOrEqual": "10", "lowerThan": "7.0", "lowerThanOrEqual": "12.0", "inclusiveRange": "0,5", "exclusiveRange": "4,15", "ordering": "asc(name),desc(age)", "pagination": "8,14"]
Expand Down Expand Up @@ -190,7 +196,7 @@ class QueryCoderTests: XCTestCase {

func testQueryEncoder() {

let query = MyQuery(boolField: true, intField: -1, optionalIntField: 282, stringField: "a string", intArray: [1, -1, 3], dateField: expectedDate, optionalDateField: expectedDate, nested: Nested(nestedIntField: 333, nestedStringField: "nested string"))
let query = MyQuery(boolField: true, intField: -1, optionalIntField: 282, stringField: "a string", emptyStringField: "", optionalStringField: "", intArray: [1, -1, 3], dateField: expectedDate, optionalDateField: expectedDate, nested: Nested(nestedIntField: 333, nestedStringField: "nested string"))

let myInts = SimpleStruct(intField: 1)

Expand All @@ -216,6 +222,8 @@ class QueryCoderTests: XCTestCase {
XCTAssertEqual(myQueryDict["intField"], "-1")
XCTAssertEqual(myQueryDict["optionalIntField"], "282")
XCTAssertEqual(myQueryDict["stringField"], "a string")
XCTAssertEqual(myQueryDict["emptyStringField"], "")
XCTAssertEqual(myQueryDict["optionalStringField"], "")
XCTAssertEqual(myQueryDict["intArray"], "1,-1,3")
XCTAssertEqual(myQueryDict["dateField"], expectedDateStr)
XCTAssertEqual(myQueryDict["optionalDateField"], expectedDateStr)
Expand Down

0 comments on commit fca1d07

Please sign in to comment.