Skip to content

Commit

Permalink
Feat: Add/Retrieve Codable objects in a session (#54)
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrew-Lees11 authored and djones6 committed Dec 13, 2018
1 parent 0c37164 commit 7d2d50e
Show file tree
Hide file tree
Showing 5 changed files with 372 additions and 41 deletions.
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)
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

0 comments on commit 7d2d50e

Please sign in to comment.