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

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
}

Related

Vapor pass data from postgres to a leaf template

I'm new to Vapor,
I try to pass data from postgres to leaf.
In the routes.swift I have the function to render the leaf template:
func routes(_ app: Application) throws {
app.get("all") { req -> EventloopFuture<View> in
let todos = Todo.query(on: req.db).all()
let context = TodoContext(todos: todos)
return req.view.render("index", context)
}
}
But I get an Error from the context line, it says Cannot convert value of type 'EventLoopFuture<[Todo]>' to expected argument type '[Todo]'.
How do I convert a EventLoopFuture<[Todo]> to '[Todo]' so I can use it in the context?
I try the map function after query .all(), but after this its still a EventLoopFuture<[Todo]>.
The TodoContext:
struct TodoContext: Content {
let todos: [Todos]
}
The Todo Model:
final class Todo: Model, Content {
static let schema = "todo"
#ID(key: .id)
var id: UUID?
#Field(key: "todo")
var todo: String
#Field(key: "complete")
var complete: Bool
init() { }
init(id: UUID? = nil, todo: string, complete: Bool) {
self.id = id
self.todo = todo
self.complete = complete
}
}
You're correct in that you need to handle the future but you should use flatMap since the render call returns a future. So your code should look like:
func routes(_ app: Application) throws {
app.get("all") { req -> EventloopFuture<View> in
return Todo.query(on: req.db).all().flatMap { todos in
let context = TodoContext(todos: todos)
return req.view.render("index", context)
}
}
}

Vapor: Date object is decoded in a string format, but the decoder expects a double

I have a data model that looks like this:
struct Post: Content, MySQLModel, Migration, Equatable {
var id: Int?
var userId: Int
var title: String
var body: String
var creationDate: Date?
var lastEditDate: Date?
static func prepare(on connection: MySQLConnection) -> Future<Void> {
return MySQLDatabase.create(self, on: connection) { builder in
builder.field(for: \.id, isIdentifier: true)
builder.field(for: \.userId)
builder.field(for: \.title)
builder.field(for: \.body, type: .text())
builder.field(for: \.creationDate)
builder.field(for: \.lastEditDate)
}
}
}
And if I have some instances in the database, I can safely make a query passing the id of the post in the path, and I would get an object with a creation/last edit date formatted in a string format:
func retrievePost(on req: Request) throws -> Future<Post> {
let id = try req.parameters.next(Int.self)
return Post.find(id, on: req).map(to: Post.self) { post in
guard let post = post else {
throw Abort(.notFound)
}
return post
}
}
If I make a GET query, this is what I get back in the response body:
{
"body": "value",
"id": 8723,
"title": "value",
"creationDate": "2020-05-27T15:24:41Z",
"userId": 0
}
And this is my PUT method implementation:
func updatePost(on req: Request) throws -> Future<Post> {
var updatedPost = try req.content.syncDecode(Post.self)
guard let id = updatedPost.id else {
throw Abort(.badRequest)
}
return Post.find(id, on: req).flatMap { post in
guard let _ = post else {
throw Abort(.notFound)
}
return updatedPost.save(on: req)
}
}
But if I send a PUT request passing the same exact fields that I got in the GET response body, with the creation date formatted as a string, I get this error:
{
"error": true,
"reason": "Could not convert to `Double`: str(\"2020-05-27T15:24:41Z\")"
}
It's expecting a double. If I try sending the number of seconds after 1970 it works, but I don't understand why the same date object is encoded using a string date and decoded using a double. How to go around this problem?
Using Vapor 4, I specified a custom JSONDecoder. This can be done globally, or as shown here for a single request.
In this scenario, I was decoding a POST'ed struct GetLogsRequest that included a date field.
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let request = try req.content.decode(GetLogsRequest.self, using: decoder)
See https://docs.vapor.codes/4.0/content/
Exactly like you, I use:
struct MyForm {
let myDate:Date
}
In the .leaf form, I use a different field name to allow the user to modify the date:
<input type="date" name="userDate" value="#date(myDate, "yyyy-MM-dd")">
Then, in the submit button's onclick event, I use javascript to calculate the timestamp value for the date and this gets returned in the 'original' field:
var d = new Date(f["userDate"].value);
f['myDate'].value = d.getTime()/1000;
return true;
The decode that is giving you trouble should now work okay.
I also use a customTag to represent the date in a shorter format:
struct DateTag:LeafTag
{
public func render(_ context:LeafContext) throws -> LeafData
{
try context.requireParameterCount(2)
guard let timestamp = context.parameters.first?.double else { return .string("") }
guard let format = context.parameters[1].string else { throw "DateTag needs a format to work on" }
let df = DateFormatter()
df.dateFormat = format
return .string(df.string(from:Date(timeIntervalSince1970:timestamp)))
}
}
The nested double-quotes are okay as Leaf only works on the inner set, leaving the outer set for the browser.

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

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

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
}
}

How to instantiate a mapped class? (swift - alamofireObjectMapper)

I have this mapped class caled Movie and I make an API request that returns me this type. How can I instantiate this class with the values of my API response?
Movie mapped class:
class Movie: Mappable {
var posterURL : String?
var title : String?
var runtime : String?
var director : String?
var actors : String?
var genre : String?
var plot : String?
var production : String?
var released : String?
var year : String?
var imdbID : String?
var imdbRating : String?
required init?(map: Map) {
}
func mapping(map: Map) {
posterURL <- map["Poster"]
title <- map["Title"]
runtime <- map["Runtime"]
director <- map["Director"]
actors <- map["Actors"]
genre <- map["Genre"]
plot <- map["Plot"]
production <- map["Production"]
released <- map["Released"]
year <- map["Year"]
imdbID <- map["imdbID"]
imdbRating <- map["imdbRating"]
}
}
And in my MovieViewController I'm making the API call and passing the values for my outlet label.
But I would like to instantiate this class by assigning the values ​​obtained in my API call.
func getMovieById() {
let requestURL = "https://www.omdbapi.com/?i=\(String(describing: imdbID!))"
print("URL: \(requestURL)")
Alamofire.request(requestURL).responseObject{ (response: DataResponse<Movie>) in
print("|MovieController| Response is: \(response)")
DispatchQueue.main.async {
let spinnerActivity = MBProgressHUD.showAdded(to: self.view, animated: true)
spinnerActivity.label.text = "Loading";
spinnerActivity.isUserInteractionEnabled = false;
}
let movie = response.result.value
if let posterURL = movie?.posterURL {
print("Poster URL: \(posterURL)")
let imgStg: String = posterURL
print("---> Image string: \(imgStg)")
let imgURL: URL? = URL(string: imgStg)
let imgSrc = ImageResource(downloadURL: imgURL!, cacheKey: imgStg)
self.movPosterImageView.layer.cornerRadius = self.movPosterImageView.frame.size.width/2
self.movPosterImageView.clipsToBounds = true
//image cache with KingFisher
self.movPosterImageView.kf.setImage(with: imgSrc)
}
if let title = movie?.title {
print("Title: \(title)")
self.movTitleLabel.text = title
}
if let runtime = movie?.runtime {
print("Runtime: \(runtime)")
self.movRuntimeLabel.text = runtime
}
if let genre = movie?.genre {
print("Genre: \(genre)")
self.movGenreLabel.text = genre
}
if let plot = movie?.plot {
print("Plot: \(plot)")
self.movPlotTextView.text = plot
}
if let rating = movie?.imdbRating {
print("Rating: \(rating)")
self.movRatingLabel.text = rating
}
if let director = movie?.director {
print("Director: \(director)")
self.movDirectorLabel.text = director
}
if let production = movie?.production {
print("Production: \(production)")
self.movProductionLabel.text = production
}
if let actors = movie?.actors {
print("Actors: \(actors)")
self.movActorsLabel.text = actors
}
if let released = movie?.released {
print("Released in: \(released)")
self.movReleasedLabel.text = released
}
DispatchQueue.main.async {
MBProgressHUD.hide(for: self.view, animated: true)
}
}//Alamofire.request
}//getMovieByID()
It would be something like
let movieDetails: Movie = Movie(plot = movie?.plot, title = movie?.title, ...)
How can I do this with a mappable class?
Update
I'm trying to organize this things and also I'll have to reuse code, so did this inside functions seems better for me. So, I started separating the API call putting like this:
file: OMDB.swift
import Foundation
import Alamofire
import AlamofireObjectMapper
func getMovieIdFromAPI(imdbID: String, completionHandler: #escaping (Movie) -> () ) {
let requestURL = "https://www.omdbapi.com/?i=\(imdbID)"
print("|getMovieIdFromAPI| URL: \(requestURL)")
Alamofire.request(requestURL).responseObject{ (response: DataResponse<Movie>) in
print("|Alamofire request| Response is: \(response)")
if let movieResult = response.result.value{
completionHandler(movieResult)
}
}
}
Next step, I'm trying to create a MovieDAO, and here I'll have to instantiate my object, right? So, in the same file as my Movie class is, I've created a MovieDAO class with this function:
class MovieDAO {
func getMovieDetailed<Movie: Mappable>(imdbID: String, completionHandler: #escaping (Movie) -> ()) {
getMovieIdFromAPI(imdbID: imdbID, completionHandler: {
(movieResult) in
let mapper = Mapper<Movie>()
let movieDetailed = mapper.map(movieResult)!
completionHandler(movieDetailed)
})
}
}
But I didn't understood very well the answer and the xcode gives me an error in
let movieDetailed = mapper.map(movieResult)!
^Error: Argument labels '(_:)' do not match any available overloads
Could you explain how can I use the answer given in this case?
ObjectMapper is what helps you get an instance of the model class, with the property values set as per your API response. You will need to do the last step where in you tell ObjectMapper to do the 'mapping' procedure with the json you provide it.You can use this generic method to parse response for any Mappable class
static func parseModel<Model: Mappable>(modelResponse modelResponse: AnyObject, modelClass: Model.Type) -> Model? {
let mapper = Mapper<Model>()
let modelObject = mapper.map(modelResponse)!
return modelObject
}