Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Add/Retrieve Codable objects in a session #54

Merged
merged 10 commits into from
Dec 13, 2018
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,40 @@ router.all(middleware: session)
```
First an instance of `RedisStore` is created (see [`KituraSessionRedis`](https://github.com/IBM-Swift/Kitura-Session-Redis) for more information), then an instance of `Session` with the store as parameter is created, and finally it is connected to the desired path.

## Codable Session Example

The example below defines a `User` struct and a `Router` with the sessions middleware.
The router has a POST route that decodes a `User` instance from the request body
and stores it in the request session using the user's id as the key.
The router has a GET route that reads a user id from the query parameters
and decodes the instance of `User` that is in the session for that id.

```
public struct User: Codable {
let id: String
let name: String
}
let router = Router()
router.all(middleware: Session(secret: "secret"))
router.post("/user") { request, response, next in
let user = try request.read(as: User.self)
request.session?[user.id] = user
response.status(.created)
response.send(user)
next()
}
router.get("/user") { request, response, next in
guard let userID = request.queryParameters["userid"] else {
return try response.status(.notFound).end()
}
guard let user: User = request.session?[userID] else {
return try response.status(.internalServerError).end()
}
response.status(.OK)
response.send(user)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does response.send with an optional Codable do what you expect?

next()
}
```
## Plugins

* [Redis store](https://github.com/IBM-Swift/Kitura-Session-Redis)
Expand Down
53 changes: 52 additions & 1 deletion Sources/KituraSession/SessionState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,12 @@ public class SessionState {
isDirty = true
}

/// Retrieve an entry from the session data.
/// Retrieve or store an entry from the session data.
///
/// - Parameter key: The key of the entry to retrieve.
public subscript(key: String) -> Any? {
// This function allows you to store values which will fail when you try to serialize them to JSON.
// This should be removed in the next major release of Kitura-Session in favour of Codable subscript.
get {
return state[key]
}
Expand All @@ -110,4 +112,53 @@ public class SessionState {
isDirty = true
}
}

/// Retrieve or store a Codable entry from the session data.
///
/// - Parameter key: The Codable key of the entry to retrieve/save.
public subscript<T: Codable>(key: String) -> T? {
get {
guard let value = state[key] else {
return nil
}
if let primitive = value as? T {
return primitive
} else {
guard let data = try? JSONSerialization.data(withJSONObject: value) else {
return nil
}
return try? JSONDecoder().decode(T.self, from: data)
}
}
set {
let json: Any
guard let value = newValue else {
state[key] = nil
isDirty = true
return
}
if let data = try? JSONEncoder().encode(value) {
let mirror = Mirror(reflecting: value)
if mirror.displayStyle == .collection {
guard let array = try? JSONSerialization.jsonObject(with: data) as? [Any] else {
return
}
json = array as Any
} else {
guard let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
return
}
json = dict as Any
}
} else {
json = value
}
state[key] = json
isDirty = true
}
}
}




252 changes: 252 additions & 0 deletions Tests/KituraSessionTests/TestCodableSession.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
/**
* Copyright IBM Corporation 2018
*
* 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 Kitura
import KituraNet

import Foundation
import XCTest

@testable import KituraSession

struct CodableSessionTest: Codable, Equatable {
let sessionKey: String
public static func == (lhs: CodableSessionTest, rhs: CodableSessionTest) -> Bool {
return lhs.sessionKey == rhs.sessionKey
}
}
enum SessionEnum: String, Codable {
case one, two
}
let CodableSessionTestArray = ["sessionValue1", "sessionValue2", "sessionValue3"]
let CodableSessionTestDict = ["sessionKey1": "sessionValue1", "sessionKey2": "sessionValue2", "sessionKey3": "sessionValue3"]
let CodableSessionTestCodableArray = [CodableSessionTest(sessionKey: "sessionValue1"), CodableSessionTest(sessionKey: "sessionValue2"), CodableSessionTest(sessionKey: "sessionValue3")]
let CodableSessionTestCodableDict = ["sessionKey1": CodableSessionTest(sessionKey: "sessionValue1"), "sessionKey2": CodableSessionTest(sessionKey: "sessionValue2"), "sessionKey3": CodableSessionTest(sessionKey: "sessionValue3")]

class TestCodableSession: XCTestCase, KituraTest {

static var allTests: [(String, (TestCodableSession) -> () throws -> Void)] {
return [
("testCodableSessionAddReadArray", testCodableSessionAddReadArray),
("testCodableSessionAddReadCodable", testCodableSessionAddReadCodable),
("testCodableSessionAddReadDict", testCodableSessionAddReadDict),
("testCodableSessionAddReadCodableArray", testCodableSessionAddReadCodableArray),
]
}

func testCodableSessionAddReadArray() {
let router = Router()
router.all(middleware: Session(secret: "secret"))

router.post("/codable") { request, response, next in
request.session?[sessionTestKey] = CodableSessionTestArray
response.status(.created)
next()
}

router.get("/codable") { request, response, next in
guard let codable: [String] = request.session?[sessionTestKey] else {
return try response.send(status: .notFound).end()
}
response.status(.OK)
response.send(codable)
next()
}
performServerTest(router: router, asyncTasks: {
self.performRequest(method: "post", path: "/codable", callback: { response in
XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil")
guard let response = response else {
return XCTFail()
}
let (cookie, _) = CookieUtils.cookieFrom(response: response, named: cookieDefaultName)
XCTAssertNotNil(cookie, "Cookie \(cookieDefaultName) wasn't found in the response.")
guard let cookieValue = cookie?.value else {
return XCTFail()
}
self.performRequest(method: "get", path: "/codable", callback: { response in
XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil")
guard let response = response else {
return XCTFail()
}
XCTAssertEqual(response.statusCode, HTTPStatusCode.OK, "HTTP Status code was \(response.statusCode)")
do {
guard let body = try response.readString(), let sessionData = body.data(using: .utf8) else {
XCTFail("No response body")
return
}
let decoder = JSONDecoder()
let returnedSession = try decoder.decode([String].self, from: sessionData)
XCTAssertEqual(returnedSession, CodableSessionTestArray)
} catch {
XCTFail("No response body")
}
}, headers: ["Cookie": "\(cookieDefaultName)=\(cookieValue)"])
})
})
}

func testCodableSessionAddReadCodable() {
let router = Router()
router.all(middleware: Session(secret: "secret"))

router.post("/codable") { request, response, next in
let codableSession = CodableSessionTest(sessionKey: sessionTestValue)
request.session?[sessionTestKey] = codableSession
response.status(.created)
next()
}

router.get("/codable") { request, response, next in
let codable: CodableSessionTest? = request.session?[sessionTestKey]
response.status(.OK)
response.send(codable)
next()
}
performServerTest(router: router, asyncTasks: {
self.performRequest(method: "post", path: "/codable", callback: { response in
XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil")
guard let response = response else {
return XCTFail()
}
let (cookie, _) = CookieUtils.cookieFrom(response: response, named: cookieDefaultName)
XCTAssertNotNil(cookie, "Cookie \(cookieDefaultName) wasn't found in the response.")
guard let cookieValue = cookie?.value else {
return XCTFail()
}
self.performRequest(method: "get", path: "/codable", callback: { response in
XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil")
guard let response = response else {
return XCTFail()
}
XCTAssertEqual(response.statusCode, HTTPStatusCode.OK, "HTTP Status code was \(response.statusCode)")
do {
guard let body = try response.readString(), let sessionData = body.data(using: .utf8) else {
XCTFail("No response body")
return
}
let decoder = JSONDecoder()
let returnedSession = try decoder.decode(CodableSessionTest.self, from: sessionData)
XCTAssertEqual(returnedSession.sessionKey, sessionTestValue)
} catch {
XCTFail("No response body")
}
}, headers: ["Cookie": "\(cookieDefaultName)=\(cookieValue)"])
})
})
}

func testCodableSessionAddReadDict() {
let router = Router()
router.all(middleware: Session(secret: "secret"))

router.post("/codable") { request, response, next in
request.session?[sessionTestKey] = CodableSessionTestDict
response.status(.created)
next()
}

router.get("/codable") { request, response, next in
guard let codable: [String: String] = request.session?[sessionTestKey] else {
return try response.send(status: .notFound).end()
}
response.status(.OK)
response.send(codable)
next()
}
performServerTest(router: router, asyncTasks: {
self.performRequest(method: "post", path: "/codable", callback: { response in
XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil")
guard let response = response else {
return XCTFail()
}
let (cookie, _) = CookieUtils.cookieFrom(response: response, named: cookieDefaultName)
XCTAssertNotNil(cookie, "Cookie \(cookieDefaultName) wasn't found in the response.")
guard let cookieValue = cookie?.value else {
return XCTFail()
}
self.performRequest(method: "get", path: "/codable", callback: { response in
XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil")
guard let response = response else {
return XCTFail()
}
XCTAssertEqual(response.statusCode, HTTPStatusCode.OK, "HTTP Status code was \(response.statusCode)")
do {
guard let body = try response.readString(), let sessionData = body.data(using: .utf8) else {
XCTFail("No response body")
return
}
let decoder = JSONDecoder()
let returnedSession = try decoder.decode([String: String].self, from: sessionData)
XCTAssertEqual(returnedSession, CodableSessionTestDict)
} catch {
XCTFail("No response body")
}
}, headers: ["Cookie": "\(cookieDefaultName)=\(cookieValue)"])
})
})
}

func testCodableSessionAddReadCodableArray() {
let router = Router()
router.all(middleware: Session(secret: "secret"))

router.post("/codable") { request, response, next in
request.session?[sessionTestKey] = CodableSessionTestCodableArray
response.status(.created)
next()
}

router.get("/codable") { request, response, next in
guard let codable: [CodableSessionTest] = request.session?[sessionTestKey] else {
return try response.send(status: .notFound).end()
}
response.status(.OK)
response.send(codable)
next()
}
performServerTest(router: router, asyncTasks: {
self.performRequest(method: "post", path: "/codable", callback: { response in
XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil")
guard let response = response else {
return XCTFail()
}
let (cookie, _) = CookieUtils.cookieFrom(response: response, named: cookieDefaultName)
XCTAssertNotNil(cookie, "Cookie \(cookieDefaultName) wasn't found in the response.")
guard let cookieValue = cookie?.value else {
return XCTFail()
}
self.performRequest(method: "get", path: "/codable", callback: { response in
XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil")
guard let response = response else {
return XCTFail()
}
XCTAssertEqual(response.statusCode, HTTPStatusCode.OK, "HTTP Status code was \(response.statusCode)")
do {
guard let body = try response.readString(), let sessionData = body.data(using: .utf8) else {
XCTFail("No response body")
return
}
let decoder = JSONDecoder()
let returnedSession = try decoder.decode([CodableSessionTest].self, from: sessionData)
XCTAssertEqual(returnedSession.map({$0.sessionKey}), CodableSessionTestCodableArray.map({$0.sessionKey}))
} catch {
XCTFail("No response body")
}
}, headers: ["Cookie": "\(cookieDefaultName)=\(cookieValue)"])
})
})
}
}
Loading