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
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,38 @@ 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)
try request.session?.add(user, forKey: user.id)
response.status(.created)
response.send(user)
next()
}
router.get("/user") { request, response, next in
guard let userID = request.queryParameters["userid"] else {
return response.status(.notFound).end()
}
let user = try request.session?.read(as: User.self, forKey: userID)
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
30 changes: 30 additions & 0 deletions Sources/KituraSession/SessionCodingError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* 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.
**/

// MARK StoreError

/// An error indicating the failure of an operation to encode/decode into/from the session `Store`.
public enum SessionCodingError: Swift.Error {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please make this a struct instead of an enum so we can add to it without doing a major release.


//Thrown when the provided Key is not found in the session.
case keyNotFound(key: String)

//Thrown when a primative Decodable or array of primative Decodables fails to be cast to the provided type.
case failedPrimativeCast()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Primative -> Primitive (throughout this PR)

Copy link
Collaborator

Choose a reason for hiding this comment

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

() is unnecessary.


//Throw when the provided Encodable fails to be serialized to JSON.
case failedToSerializeJSON()
}
114 changes: 114 additions & 0 deletions Sources/KituraSession/SessionState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,118 @@ public class SessionState {
isDirty = true
}
}

// Check if the provided value is a primative JSON type.
private func isPrimative(value: Any) -> Bool {
if value is [Any] {
return value is [String] ||
value is [Int] ||
value is [Double] ||
value is [Bool]
} else {
return value is String ||
value is Int ||
value is Double ||
value is Bool
}
}
// MARK: Codable session

/**
Encode an encodable value as JSON and store it in the session for the provided key.
### Usage Example: ###
The example below defines a `User` struct.
It decodes a `User` instance from the request body
and stores it in the request session using the user's id as the key.
```swift
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)
try request.session?.add(user, forKey: user.id)
response.status(.created)
response.send(user)
next()
}
```
- Parameter value: The Encodable object which will be added to the session.
- Parameter forKey: The key that the Encodable object will be stored under.
- Throws: `EncodingError` if value to be stored fails to be encoded as JSON.
- Throws: `SessionCodingError.failedToSerializeJSON` if value to be stored fails to be serialized as JSON.
*/
public func add<T: Encodable>(_ value: T, forKey key: String) throws {
let json: Any
if isPrimative(value: value) {
json = value
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why do we need this special casing for primitives?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

JSONEncoder and JSONDecoder do not have an .allowFragments option and will throw an error if you try and encode/decode a primitive type using them. This has been raised as a bug in SR-6163.
Until that is implemented, isPrimitive handles the case where someone wants to store/retrieve a primitive type or array of primitives type

Copy link
Collaborator

Choose a reason for hiding this comment

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

OK - I think we should consider not allowing primitives and throwing if the serialization fails. What do you think?

} else {
let data = try JSONEncoder().encode(value)
let mirror = Mirror(reflecting: value)
if mirror.displayStyle == .collection {
guard let array = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [Any] else {
throw SessionCodingError.failedToSerializeJSON()
}
json = array
} else {
guard let dict = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else {
throw SessionCodingError.failedToSerializeJSON()
}
json = dict
}

}
state[key] = json
isDirty = true
}

/**
Decode the JSON value that is stored in the session for the provided key as a Decodable object.
### Usage Example: ###
The example below defines a `User` struct.
It then reads a user id from the query parameters
and decodes the instance of `User` that is stored in the session for that id.
```swift
public struct User: Codable {
let id: String
let name: String
}
let router = Router()
router.all(middleware: Session(secret: "secret"))
router.get("/user") { request, response, next in
guard let userID = request.queryParameters["userid"] else {
return response.status(.notFound).end()
}
let user = try request.session?.read(as: User.self, forKey: userID)
response.status(.OK)
response.send(user)
next()
}
```
- Parameter as: The Decodable object type which the session will be decoded as.
- Parameter forKey: The key that the Decodable object was stored under.
- Throws: `SessionCodingError.keyNotFound` if a value is not found for the provided key.
- Throws: `SessionCodingError.failedPrimativeCast` if value stored for the key fails to be decoded as a primative JSON type.
- Throws: `DecodingError` if value stored for the key fails to be decoded as the provided type.
- Returns: The instantiated Decodable object
*/
public func read<T: Decodable>(as type: T.Type, forKey key: String) throws -> T {
guard let dict = state[key] else {
throw SessionCodingError.keyNotFound(key: key)
}
if isPrimative(value: dict) {
guard let primative = dict as? T else {
throw SessionCodingError.failedPrimativeCast()
}
return primative
}
let data = try JSONSerialization.data(withJSONObject: dict)
return try JSONDecoder().decode(type, from: data)
}
}




Loading