Hot questions for Using Vapor in generics

Question:

I have an API, written in Vapor. I would like to follow the JSON API Spec.

I am struggling with understanding how I can create my response object in the correct format.

For example, I would like my responses to be structured as so...

{
  "links": {
    "self": "http://example.com/dish",
    "next": "http://example.com/dish?page=2",
    "last": "http://example.com/dish?page=10"
  },
  "data": [{
    "title": "Spag Bol",
    "course": "main",
    "description": "BasGetti",
    "price": 3.9900000000000002
  },
  {
    "title": "Ice Cream",
    "course": "desert",
    "description": "Vanilla",
    "price": 0.98999999999999999
  }]
}

I can return the contents of data quite simply if POST to this endpoint (pseudo code)

router.post(Dish.self, at: "api/dish") { req, data -> Future<Dish> in
    return Future.map(on: req, { () -> Dish in
        data.id = 001
        return data
    })
}

I tried creating an ApiResponse class and passing in the data so I could structure the response but this did not work with the error Cannot convert return expression of type 'ApiResonse' to return type 'Dish'

   router.post(Dish.self, at: "api/dish") { req, data -> Future<Dish> in
        return Future.map(on: req, { () -> Dish in
            data.id = 001
            return ApiResonse(links: Links(self: "http://google.com", next: "http://google.com", last: "http://google.com"), data: data)
        })
    }

I am not sure how I can do this. These are the attempted classes

final class Dish: Content {
    var id: Int?
    var title: String
    var description: String
    var course: String
    var price: Double

    init(title: String, description: String, course: String, price: Double) {
        self.title = title
        self.description = description
        self.course = course
        self.price = price
    }
}

struct Links {
    var `self`: String?
    var next: String?
    var last: String?
}

class ApiResonse {
    var links: Links?
    var data: Any

    init(links: Links, data: Any) {
        self.links = links
        self.data = data
    }
}

Do I need to use Generics to set up the response class? Is anyone able to provide an example?


Answer:

  1. Each class or struct in the compound object ApiResponse needs to comply with the Content protocol. The Content protocol includes the Codable protocol for JSON decoding and encoding.

  2. Note that Any does not comply with the Codable protocol, and therefore Any can not be used as any component part of a Response. See Vapor Docs: "Using Content" for more detailed information.

    In Vapor 3, all content types (JSON, protobuf, URLEncodedForm, Multipart, etc) are treated the same. All you need to parse and serialize content is a Codable class or struct.

  3. An object or compound object which fully complies with Content can be used as a ResponseEncodable response.

  4. The ApiResponse model can be generic when each route endpoint resolves to a specific Content protocol compliant type.

An example project with the code below is on GitHub: VaporExamplesLab/Example-SO-VaporJsonResponse.

Example Models

struct Dish: Content {
    var id: Int?
    var title: String
    var description: String
    var course: String
    var price: Double

    init(id: Int? = nil, title: String, description: String, course: String, price: Double) {
        self.id = id
        self.title = title
        self.description = description
        self.course = course
        self.price = price
    }
}

struct Links: Content {
    var current: String?
    var next: String?
    var last: String?
}

struct ApiResponse: Content {
    var links: Links?
    var dishes: [Dish]

    init(links: Links, dishes: [Dish]) {
        self.links = links
        self.dishes = dishes
    }
}

Example POST: Returns ApiResponse

router.post(Dish.self, at: "api/dish") { 
    (request: Request, dish: Dish) -> ApiResponse in
    var dishMutable = dish
    dishMutable.id = 001

    var links = Links()
    links.current = "http://example.com"
    links.next = "http://example.com"
    links.last = "http://example.com"

    return ApiResponse(links: links, dishes: [dishMutable])
}

Example POST: Returns Future<ApiResponse>

router.post(Dish.self, at: "api/dish-future") { 
    (request: Request, dish: Dish) -> Future<ApiResponse> in
    var dishMutable = dish
    dishMutable.id = 002

    var links = Links()
    links.current = "http://example.com"
    links.next = "http://example.com"
    links.last = "http://example.com"

    return Future.map(on: request, { 
        () -> ApiResponse in
        return ApiResponse(links: links, dishes: [dishMutable])
    }) 
}

JSON Response Received

The Response body for the above code produces the following:

{
  "links": {
    "current": "http://example.com",
    "next": "http://example.com",
    "last": "http://example.com"
  },
  "dishes": [
    {
      "id": 1,
      "title": "Aztec Salad",
      "description": "Flavorful Southwestern ethos with sweet potatos and black beans.",
      "course": "salad",
      "price": 1.82
    }
  ]
}

Generic Model

struct ApiResponseGeneric<T> : Content where T: Content { 
    var links: Links?
    var data: T

    init(links: Links, data: T) {
        self.links = links
        self.data = data
    }
}

Concrete Route Endpoint

router.post(Dish.self, at: "api/dish-generic-future") { 
    (request: Request, dish: Dish) -> Future<ApiResponseGeneric<[Dish]>> in
    var dishMutable = dish
    dishMutable.id = 004

    var links = Links()
    links.current = "http://example.com"
    links.next = "http://example.com"
    links.last = "http://example.com"

    return Future.map(on: request, { 
        () -> ApiResponseGeneric<[Dish]> in
        return ApiResponseGeneric<[Dish]>(links: links, data: [dishMutable])
    }) 
}

Question:

I am making a server with Swift 5 and Vapor 3. When setting up a route I want to call a function from my controller that returns an optional like so:

//Person.swift

struct Person: Content {
    ...
}
//PersonController.swift

func update(_ request: Request) throws -> Future<Person> {
    let uuid = try request.parameters.next(UUID.self)

    return try request.content.decode(Person.self).flatMap { content in
        request.withPooledConnection(to: DatabaseIdentifier<PostgreSQLDatabase>.psql) { connection in
            /*
            *  No code completion beyond this point,
            *  even connection appears as type '_' instead of
            *  PostgreSQLConnection (not relevant to the question tho,
            *  just worth noting)
            */
            if content.lastName != nil {
                return connection.raw("Very long SQL query...")
                .binds([...])
                .first(decoding: Person.self)
            }

            return connection.raw("Other long SQL query")
            .binds([...])
            .first(decoding: Person.self)
        }
    }

}
router.put("people", UUID.parameter, use: personController.update)

But then I get this error

Cannot convert value of type '(Request) throws -> EventLoopFuture<Person?>' to expected argument type '(Request) throws -> _'

I see many instances when working with Vapor in which Xcode just gives up on autocompleting, and everything is just typed as _. Mostly inside closures that are used as callbacks. It is VERY annoying, and quite frankly I am unsure if it is caused because of Vapor, Swift, or Xcode. It is a huge PITA but it all gets resolved once I compile, the types just get sorted out. However in this case it is just not working.

So the question is: Why is does Xcode say the expected type is (Request) throws -> _ when the actual definition for Request.put(_:use:) requires a (Request) throws -> T and how does that make a difference between T being Future<Person> and Future<Person?>?


Answer:

The .first method you are calling here:

return connection.raw("Other long SQL query")
.binds([...])
.first(decoding: Person.self)

Returns an Future<Optional<Person>>, or Future<Person?>. You route handler's return type is Future<Person>, so your return type is incorrect. But even if you did change the handler's return type, that wouldn't fix it.

Your main problem is that you can't return an optional from a route handler, because in no way is Optional ever conformed to ResponseEncodable. You could add the conformance yourself if you wanted to.

If you don't want to add the conformance, you can just use the .unwrap(or:) method after you decode the query result:

return connection.raw("Other long SQL query")
.binds([...])
.first(decoding: Person.self)
.unwrap(or: Abort(.notFound))

This will check to see if the value in the future exists. If it does, then the value is passed on. Otherwise, the future chain receives the error you pass in and that will be returned instead.

Question:

I have already gone through the proper way of the fluent provider to build a relation for my model (Swift) using vapor (server side) and PostgreSQL provider (database), I follow the general method of fluent, but I don't know where I am doing mistake, in extension for modelName preparation, below the code of my modelName.swift and main.swift.

 import Foundation
 import Vapor
import FluentProvider
import PostgreSQLProvider


final class modelName: Model {


    let storage = Storage()
    var id: Node?
    var name:String
    var displayName:String
       public var content: String

    init(content: String, displayName:String, name:String) {

        self.content = content
        self.displayName = displayName
        self.name = name
    }

    func forDataBase() {


        let array:[berichtDataPoint] = [intDataPoint(), boolDataPoint(), doubleDataPoint()]

        let _ = array[0] as! intDataPoint
        let _ = array[1] as! doubleDataPoint


        for point in array {

            switch point {
            case is  intDataPoint:
                print("int")
            case is doubleDataPoint:
                print("double")
            case is boolDataPoint:
                print("bool")
            default:
                print("error")
            }
        }

    }

  func makeRow() throws -> Row {
        var row = Row()
        try row.set("id", idKey)
        try row.set("displayName", displayName)
        try row.set("name", name)
        return row
    }

    init(row: Row) throws {
        content = try row.get("content")
        displayName = try row.get("displayName")
        name = try row.get("name")
    }

    func makeNode(context: Context) throws -> Node {
        return try Node(node: [
            "id": id,
            "content": content,
            "displayName": displayName,
            "name": name
            ])
    }
}


extension modelName: Preparation {
    static func prepare(_ database: Database) throws {
        try database.create(self) { modelName in
            modelName.id()
            modelName.string("displayName")
            modelName.string("name")

        }
    }

    static func revert(_ database: Database) throws {
        try database.delete(self)
    }
}

main.swift

import App
import Vapor
import FluentProvider
import PostgreSQLProvider

/// We have isolated all of our App's logic into
/// the App module because it makes our app
/// more testable.
///
/// In general, the executable portion of our App
/// shouldn't include much more code than is presented
/// here.
///
/// We simply initialize our Droplet, optionally
/// passing in values if necessary
/// Then, we pass it to our App's setup function
/// this should setup all the routes and special
/// features of our app
///
/// .run() runs the Droplet's commands, 
/// if no command is given, it will default to "serve"
let config = try Config()
config.preparations.append(modelName.self) \\error is here '(Use of 
unresolved identifier 'modelName')

let drop = try Droplet(config)
try drop.setup()

try drop.run()

Answer:

I think the root cause is that modules are separated. If you created vapor project as vapor new, main.swift is in Run module, modelName.swift is in App module.

// Package.swift

let package = Package(
    name: "hello",
    targets: [
        Target(name: "App"),
        Target(name: "Run", dependencies: ["App"]),
    ],

When access to other module class, target class's access level is must use open or public.

// modelName.swift

public class moduleName: Model {
...

Please note that you must also modify other method declarations according to this change.

Thanks.