swiftvaporleaf

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.


Solution

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