gRPC and Server Side Swift: Getting Started

Learn how to define an API with gRPC and how to integrate it in a Vapor application. By Walter Tyree.

5 (2) · 2 Reviews

Download materials
Save for later
Share
You are currently viewing page 3 of 5 of this article. Click here to view the first page.

Messages Become Structs

Open the todo.pb.swift file. In addition to generating Swift versions of properties of a Todo, the new code also has a convenience method to work with the optional todoID property, as displayed in the excerpt below.

struct Todos_Todo {
  // SwiftProtobuf.Message conformance is added in an extension below. See the
  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
  // methods supported on all messages.

  var todoID: String {
    get {return _todoID ?? String()}
    set {_todoID = newValue}
  }
  /// Returns true if `todoID` has been explicitly set.
  var hasTodoID: Bool {return self._todoID != nil}
  /// Clears the value of `todoID`. Subsequent reads from it will 
  /// return its default value.
  mutating func clearTodoID() {self._todoID = nil}

  var title: String = String()
 
  var unknownFields = SwiftProtobuf.UnknownStorage()
  
  init() {}
  fileprivate var _todoID: String? = nil
}
Note: It’s important to remember that this code is all generated, so never edit the .pb.swift or .grpc.swift files as they’ll eventually be overwritten.

In addition to the clearTodoID() convenience function, the comment indicates that there are many more convenience methods for Message structs. The best way to explore them is to open the swift-protobuf package and read the comments in the Messages.swift and the Messages+*.swift files. There you’ll find a number of convenience methods for working with JSON, text and data objects.

Notice how gRPC combines the package name with the message name when creating the Swift structure. This naming convention occurs throughout all of your gRPC code. Though it looks a little out of place in Swift code, it does help you remember when you’re working with the gRPC objects versus any other objects.

Working with Generated Code

If you’ve worked with other code generators before, you’ll be familiar with the pattern of using extensions and type aliases to separate your code from the generated, and often overwritten, code. For the sake of code organization, open Sources/App/Models/Todo.swift and replace //TODO: Create initializer methods with the following:

extension Todos_Todo {
  init (_ todo: Todo) {
    if let todoid = todo.id {
      self.todoID = todoid.uuidString
    }
    self.title = todo.title
  }
}

This will help you to work directly with the Fluent model. Since the id property is optional, you don’t want to set it unless your object already has one.

Now, add an extension to initialize a Todo with a Todos_Todo. At the end of the Todo.swift file, add the following code:

extension Todo {
  convenience init (_ todo: Todos_Todo) {
    self.init(id: UUID(uuidString: todo.todoID), title: todo.title)
  }
}

Having these two initializers allows you to convert your Todo models between the form that the PostgreSQL server wants and the form that gRPC wants.

Build the app again to make sure everything is still correct. The command is the same you have already used:

swift build

With the data model in place, now it’s time to check out the services.

Turning Services Into Functions

Now, open Sources/App/Controllers/todo.grpc.swift and find the section that defines the protocol Todos_TodoServiceProvider. The spec in the proto file has been turned into Swift code, and the comments have also carried over!

By default, protoc generated client and server code in this file. There are options to generate only client or server code.

To make your server functional, you need to create a class that implements this Todos_TodoServiceProvider protocol.

In the Controllers folder, add a new file called TodoProvider.swift. Calling something a ‘Provider’ isn’t really common in Swift, but it’s the convention when working with gRPC.

In your new file, add the following imports:

import Foundation
import GRPC
import Vapor
import Fluent

Next, add the following class:

class TodoProvider: Todos_TodoServiceProvider {
  var interceptors: Todos_TodoServiceServerInterceptorFactoryProtocol?
 
  //TODO: Add reference to Vapor

  func fetchTodos(request: Todos_Empty, context: StatusOnlyCallContext) 
    -> EventLoopFuture<Todos_TodoList> {
   //TODO: Add fetchTodos 
  }

  func createTodo(request: Todos_Todo, context: StatusOnlyCallContext) 
    -> EventLoopFuture<Todos_Todo> {
   //TODO: Add createTodo 
  }

  func deleteTodo(request: Todos_TodoID, context: StatusOnlyCallContext) 
    -> EventLoopFuture<Todos_Empty> {
   //TODO: Add deleteTodo 
  }
}

The name, request type and return type are all recognizable from the .proto file. In addition, there’s a context parameter, which provides you with a logger, the event loop for the call as well as the ability to set a response status and some other properties.

Next, you’ll write functions for the service provider.

Implementing Service Functions

Before you start adding code to the functions, add a reference to the Vapor app itself so you can work with the database. Replace //TODO: Add reference to Vapor with:

var app: Application
  
init(_ app: Application) {
  self.app = app
}

This code adds a variable to hold a reference to the Vapor app and adds an init method so you can pass in the app when everything starts up.

The next step is to add the code for each of the functions. You’ll query the database for each function and return data or an error.

Replace //TODO: Add fetchTodos with:

let todos = Todo.query(on: app.db(.psql)).all()
              .map { todos -> Todos_TodoList in //1
  var listToReturn = Todos_TodoList()
  for td in todos {
    listToReturn.todos.append(Todos_Todo(td)) //2
  }
  return listToReturn
  }

return todos //3

Here’s what this code is doing:

  1. Queries for all the TODO records and passes the results into a .map.
  2. Loops through the TODO records and converts each one into a gRPC Todos_Todo object using the initializer you created earlier.
  3. Returns the array of gRPC TODOs.

Now replace //TODO: Add createTodo with:

let todo = Todo(request) //1
return todo.save(on: app.db(.psql)).map { //2
  Todos_Todo(todo) //3
}

Here’s what this code is doing:

  1. Converts the gRPC Todos_Todo in the request to a Todo to use with PostreSQL.
  2. Saves the new todo to the database.
  3. Converts the todo into the gRPC format and sends it back to the client.

The function to delete is a little longer because it validates the request and needs to find the corresponding entry in the database before it can delete it. Replace //TODO: Add deleteTodo with the following:

guard let uuid = UUID(uuidString: request.todoID) else { //1
  return context.eventLoop.makeFailedFuture(
    GRPCStatus(code: .invalidArgument, message: "Invalid TodoID")) //2
}
return Todo.find(uuid, on: app.db(.psql)).unwrap(or: Abort(.notFound))
  .flatMap { [self] todo in //3
    todo.delete(on: app.db(.psql))
      .transform(to: context.eventLoop.makeSucceededFuture(Todos_Empty())) //4
}

Here’s what this code is doing:

  1. Attempts to make a UUID out of the .todoID property of the request.
  2. If a UUID can’t be made, creates a failed future to pass back with a gRPC error object.
  3. Finds the Todo record in the database using the uuid.
  4. Deletes the record and returns a succeeded future.

Whew, that’s a lot of code. Run the build command again to make sure everything still builds with no issues:

swift build

The expected output is something like this:

starter> swift build
[5/5] Build complete!
starter> 

Now it’s time to integrate gRPC in the existing Vapor app.