Handle different kinds of json on the same route with vapor 3 - swift

I'm trying to add a POST endpoint on my Vapor 3 server to create a new resource, but I want to have 3 different JSON formats that can create that resource. So I have 3 different Content structs that are each fairly similar.
struct IDJson: Content, DeckConvertible {
var name: String?
var format: Int
var hero: Int
var cards: [Int]
}
struct NameJson: Content, DeckConvertible {
var name: String?
var format: Int
var hero: String
var cards: [String]
}
struct DeckstringJson: Content, DeckConvertible {
var name: String?
var deckstring: String
}
And I would like to add them all to my router like this
class DeckRouteController: RouteCollection {
func boot(router: Router) throws {
router.post(IDJson.self, at: "user", "collection", use: createDeckHandler)
router.post(NameJson.self, at: "user", "collection", use: createDeckHandler)
router.post(DeckstringJson.self, at: "user", "collection", use: createDeckHandler)
}
}
...
private extension DeckRouteController {
func createDeckHandler(_ request: Request, container: DeckConvertible) throws -> Future<Response> {
// Create deck
}
}
Is this possible? at the moment only the last one gets used but is there a way I can do this with some middleware handler or something?

You can handle the decoding in the function itself, ie:
router.post("user", "collection", use: createDeckHandler)
func createDeckHandler(_ request: Request) throws -> String {
let asId = try? request.content.syncDecode(IDJson.self)
let asName = try? request.content.syncDecode(NameJson.self)
if let asId = asId {
// do stuff
} else if let asName = asName {
// do something else
}
}

Related

Manually modifying model property values in vapor 4 response

I have a vapor 4 application. I do a query from database for getting some items and I want to perform some manual calculation based on the returned values before finishing the request. here a sample code of what I am trying to achieve.
final class Todo: Model, Content {
static var schema: String = "todos"
#ID(custom: .id)
var id: Int?
#Field(key: "title")
var title: String
var someValue: Int?
}
/// Allows `Todo` to be used as a dynamic migration.
struct CreateTodo: Migration {
func prepare(on database: Database) -> EventLoopFuture<Void> {
database.schema(Todo.schema)
.field("id", .int, .identifier(auto: true))
.field("title", .string, .required)
.create()
}
func revert(on database: Database) -> EventLoopFuture<Void> {
database.schema(Todo.schema).delete()
}
}
final class TodoController:RouteCollection{
func boot(routes: RoutesBuilder) throws {
routes.get("tmp", use: temp)
}
func temp(_ req:Request) throws -> EventLoopFuture<[Todo]> {
Todo.query(on: req.db).all().map { todos in
todos.map {
$0.someValue = (0...10).randomElement()!
return $0
}
}
}
}
The problem is that those manual changes, aren't available in response. In this case someValue property.
Thanks.
[
{
"title": "item 1",
"id": 1
},
{
"title": "item 2",
"id": 2
}
]
The problem you're hitting is that Models override the Codable implementations. This allows you to do things like passing around parents and not adding children etc.
However, this breaks your case. What you should do, is create a new type if you want to return a Todo with another field that isn't stored in the database, something like:
struct TodoResponse: Content {
let id: Int
let title: String
let someValue: Int
}
And then convert from your database type to your response type in your route handler (this is a pretty common pattern and the recommended way to do it in Vapor)

How to save Parent-Child relation from a JSON response in Vapor 3

I am developing a REST API using Vapor 3. This API uses another API to create content that will later be consumed by an app.
So I created a function that fetches content from this API (Leagues and Seasons) and store them on my MySQL DB. The response from the API also contains nested objects that I would want to store too, if it possible all in the same request. Here is the API response:
{
"data": [
{
"id": 271,
"name": "Superliga",
"current_season_id": 16020,
"season": {
"data": {
"id": 16020,
"name": "2019/2020",
"league_id": 271,
}
}
}
]
}
Here are the model:
final class League: MySQLModel {
var id: League.ID?
var name: String
var current_season_id: Season.ID
var currentSeason: Parent<League, Season> {
return parent(\League.current_season_id)
}
}
final class Season: MySQLModel {
var id: Season.ID?
var name: String
var league_id: League.ID
var league: Parent<Season, League> {
return parent(\.league_id)
}
}
Here is the function performing the request and storing on the DB.
func getLeagues(using context: CommandContext) throws -> EventLoopFuture<Void> {
guard let url = URL(string: "SOME_API_URL") else { return .done(on: context.container) }
let client = try context.container.client()
return client.get(url).flatMap({ (response) -> EventLoopFuture<Void> in // do the request
let leagues = response.content.get([League].self, at: "data") // get the array of EventLoopFuture<[League]>
return context.container.requestPooledConnection(to: .mysql).flatMap({ (connection) -> EventLoopFuture<Void> in // connecto to DB
let savedLeagues = leagues.flatMap(to: [League].self, { (flattenLeagues) -> EventLoopFuture<[League]> in
return flattenLeagues.map { (league) -> EventLoopFuture<League> in
return league.create(orUpdate: true, on: connection) // save on the DB
}.flatten(on: context.container)
})
return savedLeagues.flatMap { (_) -> EventLoopFuture<Void> in
return .done(on: context.container)
}
})
})
}
The question would be: it is possible to save a Parent-Child relation? Do I have to do it manually using decode/encode functions?
I did implemented encode/decode and created the League, but don't know how to create te Season and how all can be saved when doing league.create(orUpdate: true, on: connection)
Any help would be appriciated.
As I see it you could decode an APIModel first, then save two objects inside flatten loop like this
struct APILeague: Content {
let id: League.ID
let name: String
let current_season_id: Season.ID
struct _Season: Codable {
let data: Season
}
let season: _Season
}
final class League: MySQLModel {
var id: League.ID?
var name: String
var current_season_id: Season.ID
var currentSeason: Parent<League, Season> {
return parent(\League.current_season_id)
}
init (_ data: APILeague) {
self.id = data.id
self.name = data.name
self.current_season_id = data.current_season_id
}
}
func getLeagues(using context: CommandContext) throws -> Future<Void> {
guard let url = URL(string: "SOME_API_URL") else { return .done(on: context.container) }
let client = try context.container.client()
return client.get(url).flatMap { response in // do the request
return response.content.get([APILeague].self, at: "data").flatMap { leagues in
return context.container.requestPooledConnection(to: .mysql).flatMap { connection in // connecto to DB
let operations = leagues.map { league in
return League(league).create(orUpdate: true, on: connection).flatMap { _ in
return league.season.data.create(orUpdate: true, on: connection).transform(to: ()) // transforming to Void
}
}
return operations.flatten(on: context.container)flatMap {
return .done(on: context.container)
}
}
}
}
}

How can I save an object of a custom class in Userdefaults in swift 5/ Xcode 10.2

I want to save the array patientList in UserDefaults. Patient is an custom class so I need to transfer it into Data object, but this doesn't work on Swift 5 like it did before.
func addFirstPatient(){
let newPatient = Patient(name: nameField.text!, number: numberField.text!, resultArray: resultArray, diagnoseArray: diagnoseArray)
let patientList: [Patient] = [newPatient]
let encodeData: Data = NSKeyedArchiver.archivedData(withRootObject: patientList)
UserDefaults.standard.set(encodeData, forKey: "patientList")
UserDefaults.standard.synchronize()
}
struct Patient {
var diagnoseArray: [Diagnose]
var resultArray: [Diagnose]
var name: String
var number: String
init(name: String, number: String, resultArray: [Diagnose], diagnoseArray: [Diagnose]) {
self.diagnoseArray = diagnoseArray
self.name = name
self.number = number
self.resultArray = resultArray
}
}
struct Diagnose{
var name: String
var treatments: [Treatment]
var isPositiv = false
var isExtended = false
init(name: String, treatments: [Treatment]) {
self.name = name
self.treatments = treatments
}
}
struct Treatment {
var name: String
var wasMade = false
init(name: String) {
self.name = name
}
}
This is what the function looks like.
The problem is in the line where I initialize encodeData.
let encodeData: Data = try! NSKeyedArchiver.archivedData(withRootObject: patientList, requiringSecureCoding: false)
This is what Swift suggests but when I try it like this it always crashes and I don't get the error
You cannot use NSKeyedArchiver with structs at all. The objects must be subclasses of NSObject which adopt NSCoding and implement the required methods.
As suggested in the comments Codable is the better choice for example
struct Patient : Codable {
var name: String
var number: String
var resultArray: [Diagnose]
var diagnoseArray: [Diagnose]
}
struct Diagnose : Codable {
var name: String
var treatments: [Treatment]
var isPositiv : Bool
var isExtended : Bool
}
struct Treatment : Codable {
var name: String
var wasMade : Bool
}
let newPatient = Patient(name: "John Doe",
number: "123",
resultArray: [Diagnose(name: "Result", treatments: [Treatment(name: "Treat1", wasMade: false)], isPositiv: false, isExtended: false)],
diagnoseArray: [Diagnose(name: "Diagnose", treatments: [Treatment(name: "Treat2", wasMade: false)], isPositiv: false, isExtended: false)])
let patientList: [Patient] = [newPatient]
do {
let encodeData = try JSONEncoder().encode(patientList)
UserDefaults.standard.set(encodeData, forKey: "patientList")
// synchronize is not needed
} catch { print(error) }
If you want to provide default values for the Bool values you have to write an initializer.
Vadian's answer is correct, you cannot use NSKeyedArchiver with structs. Having all your objects conform to Codable is the best way to reproduce the behavior you are looking for. I do what Vadian does, but I you can also use protocol extensions to make this safer.
import UIKit
struct Patient: Codable {
var name: String
var number: String
var resultArray: [Diagnose]
var diagnoseArray: [Diagnose]
}
struct Diagnose: Codable {
var name: String
var treatments: [Treatment]
var isPositiv : Bool
var isExtended : Bool
}
struct Treatment: Codable {
var name: String
var wasMade : Bool
}
let newPatient = Patient(name: "John Doe",
number: "123",
resultArray: [Diagnose(name: "Result", treatments: [Treatment(name: "Treat1", wasMade: false)], isPositiv: false, isExtended: false)],
diagnoseArray: [Diagnose(name: "Diagnose", treatments: [Treatment(name: "Treat2", wasMade: false)], isPositiv: false, isExtended: false)])
let patientList: [Patient] = [newPatient]
Introduce a protocol to manage the encoding and saving of objects.
This does not have to inherit from Codable but it does for this example for simplicity.
/// Objects conforming to `CanSaveToDisk` have a save method and provide keys for saving individual objects or a list of objects.
protocol CanSaveToDisk: Codable {
/// Provide default logic for encoding this value.
static var defaultEncoder: JSONEncoder { get }
/// This key is used to save the individual object to disk. This works best by using a unique identifier.
var storageKeyForObject: String { get }
/// This key is used to save a list of these objects to disk. Any array of items conforming to `CanSaveToDisk` has the option to save as well.
static var storageKeyForListofObjects: String { get }
/// Persists the object to disk.
///
/// - Throws: useful to throw an error from an encoder or a custom error if you use stage different from user defaults like the keychain
func save() throws
}
Using protocol extensions we add an option to save an array of these objects.
extension Array where Element: CanSaveToDisk {
func dataValue() throws -> Data {
return try Element.defaultEncoder.encode(self)
}
func save() throws {
let storage = UserDefaults.standard
storage.set(try dataValue(), forKey: Element.storageKeyForListofObjects)
}
}
We extend our patient object so it can know what to do when saving.
I use "storage" so that this could be swapped with NSKeychain. If you are saving sensitive data (like patient information) you should be using the keychain instead of UserDefaults. Also, make sure you comply with security and privacy best practices for health data in whatever market you're offering your app. Laws can be a very different experience between countries. UserDefaults might not be safe enough storage.
There are lots of great keychain wrappers to make things easier. UserDefaults simply sets data using a key. The Keychain does the same. A wrapper like https://github.com/evgenyneu/keychain-swift will behave similar to how I use UserDefaults below. I have commented out what the equivalent use would look like for completeness.
extension Patient: CanSaveToDisk {
static var defaultEncoder: JSONEncoder {
let encoder = JSONEncoder()
// add additional customization here
// like dates or data handling
return encoder
}
var storageKeyForObject: String {
// "com.myapp.patient.123"
return "com.myapp.patient.\(number)"
}
static var storageKeyForListofObjects: String {
return "com.myapp.patientList"
}
func save() throws {
// you could also save to the keychain easily
//let keychain = KeychainSwift()
//keychain.set(dataObject, forKey: storageKeyForObject)
let data = try Patient.defaultEncoder.encode(self)
let storage = UserDefaults.standard
storage.setValue(data, forKey: storageKeyForObject)
}
}
Saving is simplified, check out the 2 examples below!
do {
// saving just one patient record
// this saves this patient to the storageKeyForObject
try patientList.first?.save()
// saving the entire list
try patientList.save()
} catch { print(error) }
struct Employee: Codable{
var name: String
}
var emp1 = Employee(name: "John")
let encoder = JSONEncoder()
do {
let data = try encoder.encode(emp1)
UserDefaults.standard.set(data, forKey: "employee")
UserDefaults.standard.synchronize()
} catch {
print("error")
}

creating a parent child relationship in vapor 3

I am using Vapor 3 to try and create just a sample project where I have a dish, the parent, and the reviews for the dish, the child. All the tutorials that I have been seeing haven't been very clear on how to create the relationship or they are using it in conjecture with leaf. I do not want to use leaf for this, I just want to be able to show all the reviews when for the dish when I give it's id, and it seems that it is different than it was for vapor 2.
My 2 models are Dish and Review
Dish.swift: The parent,
import Foundation
import Vapor
import FluentSQLite
final class Dish: Content {
var id: Int?
var name: String
var course: String
var price: Double
var imageURL: String
var description: String
init(name: String, course: String, price: Double, imageURL: String, description: String) {
self.name = name
self.course = course
self.price = price
self.imageURL = imageURL
self.description = description
}
}
extension Dish {
var reviews: Children<Dish, Review> {
return children(\.dishId)
}
}
extension Dish: Parameter { }
extension Dish: SQLiteModel {
static let entity: String = "Dishes"
}
extension Dish: Migration { }
Review.swift, the child,
import Foundation
import Vapor
import FluentSQLite
final class Review: Content {
var id: Int?
var title: String
var body: String
var dishId: Dish.ID
init(title: String, body: String, dishId: Dish.ID) {
self.title = title
self.body = body
self.dishId = dishId
}
}
extension Review {
var dish: Parent<Review, Dish> {
return parent(\.dishId)
}
}
extension Review: Migration { }
extension Review: SQLiteModel {
static let entity: String = "Reviews"
}
extension Review: Parameter { }
the controller for Dish, DishController,
import Foundation
import Vapor
import FluentSQLite
class DishesController: RouteCollection {
func boot(router: Router) throws {
let dishesRoutes = router.grouped("api/dishes")
dishesRoutes.get("/", use: getAll)
dishesRoutes.get(Dish.parameter, use: getById)
dishesRoutes.post(Dish.self, at: "/", use: createDish)
dishesRoutes.delete(Dish.parameter, use: deleteDish)
}
func deleteDish(req: Request) throws -> Future<Dish> {
return try req.parameters.next(Dish.self).delete(on: req)
}
func createDish(req: Request, dish: Dish) -> Future<Dish> {
return dish.save(on: req)
}
func getAll(req: Request) -> Future<[Dish]> {
return Dish.query(on: req).all()
}
func getById(req: Request) throws -> Future<Dish> {
return try req.parameters.next(Dish.self)
}
}
and the controller for reviews. ReviewController,
import Foundation
import Vapor
import FluentSQLite
class ReviewController: RouteCollection {
func boot(router: Router) throws {
let reviewRoutes = router.grouped("api/reviews")
reviewRoutes.get("/", use: getAll)
reviewRoutes.get(Review.parameter, use: getById)
reviewRoutes.post(Review.self, at: "/", use: createReview)
reviewRoutes.delete(Review.parameter, use: deleteReview)
}
func deleteReview(req: Request) throws -> Future<Review> {
return try req.parameters.next(Review.self).delete(on: req)
}
func createReview(req: Request, review: Review) -> Future<Review> {
return review.save(on: req)
}
func getAll(req: Request) -> Future<[Review]> {
return Review.query(on: req).all()
}
func getById(req: Request) throws -> Future<Review> {
return try req.parameters.next(Review.self)
}
}
this is the routes.swift,
import Vapor
/// Register your application's routes here.
public func routes(_ router: Router) throws {
router.get("/reviews", Dish.parameter,"dish") { request -> Future<Dish> in
return try request.parameters.next(Review.self).flatMap(to: Dish.self) { review in
return review.dish.get(on: request)
}
}
let dishesController = DishesController()
try router.register(collection: dishesController)
let reviewController = ReviewController()
try router.register(collection: reviewController)
}
I just want a simple one to many relationship where one dish can have many reviews, but when I use postman to try and access the reviews for the particular dish, all I get is an error. I know that I used the correct syntax in postman because I can use all the other requests from the controllers just fine, just not any for the relationships. Please tell me what i am missing, because I am getting confused as to what I am doing wrong. If there is anything else I can add please ask.
Thank you very much
If you want to access all reviews for the particular dish, try the following code.
router.get("/dish", Dish.parameter,"reviews") { request -> Future<[Review]> in
return try request.parameters.next(Dish.self).flatMap(to: [Review].self) { (dish) in
return try dish.reviews.query(on: request).all()
}
}
Now In postman, pass a dish id as below:
GET: http://localhost:8080/dish/1/reviews

What's the best way to return a collection of response representable objects in Swift Vapor?

Context:
Recently, I've decided to take up Swift server side development because I think the Vapor framework is extremely cool. I've gotten a bit stuck while experimenting and would like some advice on templating with leaf and vapor.
I've reviewed the documentation several times when it comes to rendering views. Rendering a templated view with variables requires the name of the leaf template and a Response Representable node object containing the variables.
Trying to work out a scenario with templating and the framework itself (because that's how I learn best), I tried to mock a blog format. This is my class/get request:
// MARK: Blog Post Object
final class BlogPost: NodeRepresentable {
var postId: Int
var postTitle: String
var postContent: String
var postPreview: String
func makeNode(context: Context) throws -> Node {
return try Node(node: [
"postId":self.postId,
"postTitle":self.postTitle,
"postContent":self.postContent,
"postPreview":self.postPreview
])
}
init(_ postId: Int, _ postTitle: String, _ postContent: String) {
self.postId = postId
self.postTitle = postTitle
self.postContent = postContent
self.postPreview = postContent.trunc(100)
}
}
// MARK: Blog view request; iterate over blog objects
drop.get("blog") { request in
let result = try drop.database?.driver.raw("SELECT * FROM Posts;")
guard let posts = result?.nodeArray else {
throw Abort.serverError
}
var postCollection = [BlogPost]()
for post in posts {
guard let postId = post["postId"]?.int,
let postTitle = post["postTitle"]?.string,
let postContent = post["postPreview"]?.string else {
throw Abort.serverError
}
let post = BlogPost(postId, postTitle, postContent)
postCollection.append(post)
}
// Pass posts to be tokenized
/* THIS CODE DOESN'T WORK BECAUSE "CANNOT CONVERT VALUE OF TYPE
* '[BLOGPOST]' TO EXPECTED DICTIONARY VALUE OF TYPE "NODE"
* LOOKING FOR THE BEST METHOD TO PASS THIS LIST OF OBJECTS
*/
drop.view.make("blog", [
"posts":postCollection
])
}
and this is my blog.leaf file:
#extend("base")
#export("head") {
<title>Blog</title>
}
#export("body") {
<h1 class="page-header">Blog Posts</h1>
<div class="page-content-container">
#loop(posts, "posts") {
<div class="post-container">
<h3 style="post-title">#(posts["postTitle"])</h3>
<p style="post-preview">#(posts["postPreview"])</h3>
</div>
}
</div>
}
Problem:
As you can see, I'm a bit stuck on finding the best method for iterating over objects and templating their properties into the leaf file. Anyone have any suggestions? Sorry for the bad programming conventions, by the way. I'm fairly new in Object/Protocol Oriented Programming.
What I ended up doing is, making the Post model conform to the Model protocol.
import Foundation
import HTTP
import Vapor
// MARK: Post Class
final class Post: Model {
var id: Node?
var title: String
var content: String
var date: Date
var isVisible: Bool
// TODO: Implement truncate extension for String and set preview
// to content truncated to 100 characters
var preview = "placeholder"
var exists: Bool = false
init(title: String, content: String, isVisible: Bool = true) {
self.title = title
self.content = content
self.date = Date()
self.isVisible = isVisible
}
init(node: Node, in context: Context) throws {
let dateInt: Int = try node.extract("date")
let isVisibleInt: Int = try node.extract("isVisible")
id = try node.extract("id")
title = try node.extract("title")
content = try node.extract("content")
date = Date(timeIntervalSinceNow: TimeInterval(dateInt))
isVisible = Bool(isVisibleInt as NSNumber)
exists = false
}
func makeNode(context: Context) throws -> Node {
return try Node(node: [
"id": id,
"title": title,
"content": content,
"date": Int(date.timeIntervalSince1970),
"isVisible": Int(isVisible as NSNumber)
])
}
static func prepare(_ database: Database) throws {
try database.create("Posts") { posts in
posts.id()
posts.string("title", optional: false)
posts.string("content", optional: false)
posts.int("date", optional: false)
posts.int("isVisible", optional: false)
}
}
static func revert(_ database: Database) throws {
try database.delete("posts")
}
}
Then to return/create instances of the Post object:
import Vapor
import Foundation
import HTTP
final class BlogController {
func addRoutes(_ drop: Droplet) {
let blogRouter = drop.grouped("blog")
let blogAPIRouter = drop.grouped("api","blog")
blogRouter.get("posts", handler: getPostsView)
blogAPIRouter.get("posts", handler: getPosts)
blogAPIRouter.post("newPost", handler: newPost)
}
// MARK: Get Posts
func getPosts(_ request: Request) throws -> ResponseRepresentable {
let posts = try Post.all().makeNode()
return try JSON(node: [
"Posts":posts
])
}
// Mark: New Post
func newPost(_ request: Request) throws -> ResponseRepresentable {
guard let title = request.data["title"]?.string,
let content = request.data["content"]?.string else {
throw Abort.badRequest
}
var post = Post(title: title, content: content)
try post.save()
return "success"
}
// Mark: Get Posts Rendered
func getPostsView(_ request: Request) throws -> ResponseRepresentable {
return try getPosts(request)
}
}
I'm not an expert on Vapor yet, but I think you need to use .makeNode() so your postCollection object get converted to something you can later use on the template.
Something like this:
drop.view.make("blog", ["posts":postCollection.makeNode()])
func list(_ req: Request) throws -> ResponseRepresentable {
let list = try User.all()
let node = try list.makeNode(in: nil)
let json = try JSON(node: [ "list":node ])
return json
}