Skip to content

Commit

Permalink
Add migration guide
Browse files Browse the repository at this point in the history
  • Loading branch information
adam-fowler committed Jan 12, 2024
1 parent fbc2efe commit c54304f
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 7 deletions.
154 changes: 154 additions & 0 deletions Hummingbird.docc/Articles/MigratingToV2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# Migrating to Hummingbird v2

Migration guide for converting Hummingbird v1 applications to Hummingbird v2

## Overview

In the short lifetime of the Hummingbird server framework there have been many major changes to the Swift language. Hummingbird v2 has been designed to take advantage of all the latest changes to Swift. In addition Hummingbird v1 was our first attempt at writing a server framework and we didn't necessarily get everything right, so v2 includes some changes where we feel we made the wrong design first time around. Below we cover most of the major changes in the library and how you should deal with them.

## SwiftNIO and Swift Concurrency

In the time that the Hummingbird server framework has been around there has been a seismic shift in the Swift language. When it was first in development the initial pitches for Swift Concurrency were only just being posted. It wasn't for another 9 months before we actually saw a release of Swift with any concurrency features. As features have become available we have tried to support them but the internals of Hummingbird were still SwiftNIO EventLoop based and held us back from providing full support for Concurrency.

Hummingbird v2 is now exclusively Swift concurrency based. All EventLoop based APIs have been removed.

If you have libraries you are calling into that still only provide EventLoop based APIs you can convert them to Swift concurrency using the `get` method from `EventLoopFuture`.

```swift
let value = try await eventLoopBasedFunction().get()
```

If you need to provide an `EventLoopGroup`, use either the one you provided to `HBApplication.init` or `MultiThreadedEventLoopGroup.singleton`. And when you need an `EventLoop` use `EventLoopGroup.any`.

```swift
let service = MyService(eventLoopGroup: MultiThreadedEventLoopGroup.singleton)
let result = try await service.doStuff(eventLoop: MultiThreadedEventLoopGroup.singleton.any()).get()
```

Otherwise any `EventLoopFuture` based logic you had will have to be converted to Swift concurrency. The advantage of this is, it should be a lot easier to read after.

## Extending HBApplication and HBRequest

In Hummingbird v1 you could extend the `HBApplication` and `HBRequest` types to include your own custom data. This is no longer possible in version 2.

### HBApplication

In the case of the application we decided we didn't want to make `HBApplication` this huge mega global that held everything. We have moved to a model of explicit dependency injection. For each route controller you supply the dependencies you need at initialization, instead of extracting them from the application when you use them. This makes it clearer what dependencies you are using in each controller. eg

```swift
struct UserController {
// The user authentication routes use fluent and session storage
init(fluent: HBFluent, sessions: HBSessionStorage) {
...
}
}
```

### HBRequest and HBRequestContext

We have replaced extending of `HBRequest` with a custom request context type that is passed along with the request. This means `HBRequest` is just the HTTP request data (as it should be). The additional request context parameter will hold any custom data required. In situations in the past where you would use data attached to `HBRequest` or `HBApplication` you should now use the context.

```swift
router.get { request, context in
// logger is attached to the context
context.logger.info("The logger attached to the context includes an id.")
// request decoder is attached to the context instead of the application
let myObject = try await request.decode(as: MyObject.self, context: context)
}
```

The request context is a generic value. As long as it conforms to ``HBRequestContext`` it can hold anything you like.

```swift
/// Example request context with an additional data attached
struct MyRequestContext: HBRequestContext {
// required by HBRequestContext
var coreContext: HBCoreRequestContext
var additionalData: String?

// required by HBRequestContext
init(allocator: ByteBufferAllocator, logger: Logger) {
self.coreContext = .init(allocator: allocator, logger: logger)
self.additionalData = nil
}
}
```
When you create your router you pass in the request context type you'd like to use. If you don't pass one in it will default to using ``HBBasicRequestContext`` which provides enough data for the router to run but not much else.

```swift
let router = HBRouter(context: MyRequestContext.self)
```

## Router

Instead of creating an application and adding routes to it, in v2 you create a router and add routes to it and then create an application using that router.

```swift
let app = HBApplication()
app.router.get { request in
"hello"
}
```

is now implemented as

```swift
let router = HBRouter()
router.get { request, context in
"hello"
}
let app = HBApplication(router: router)
```
When we are passing in the router we are actually passing in a type that can build a ``HBResponder`` a protocol for a type with one function that takes a request and context and returns a response.

### Router Builder

An alternative router is also provided in the ``HummingbirdRouter`` module. It uses a result builder to generate the router.

```swift
let router = HBRouterBuilder(context: MyContext.self) {
// add logging middleware
HBLogRequestsMiddleware(.info)
// add route to return ok
Get("health") { _,_ -> HTTPResponse.Status in
.ok
}
// for all routes starting with '/user'
RouteGroup("user") {
// add router supplied by UserController
UserController(fluent: fluent).routes()
}
}
let app = HBApplication(router: router)
```

## Miscellaneous

Below is a list of other smaller changes that might catch you out

### Request body streaming

In Hummingbird v1 it was assumed request bodies would be collated into one ByteBuffer and if you didn't want that to happen you had to flag the route to not collate your request body. In v2 this assumption has been reversed. It is assumed that request bodies are a stream of buffers and if you want to collate them into one buffer you need to call a method to do that.

To treat the request body as a stream of buffers
```swift
router.put { request, context in
for try await buffer in request.body {
process(buffer)
}
}
```

To treat the request body as a single buffer.
```swift
router.put { request, context in
let body = try await request.body.collate(maxSize: 1_000_000)
process(body)
}
```

### OpenAPI style URI capture parameters

In Hummingbird v1.3.0 partial path component matching and capture was introduced. For this a new syntax was introduced for parameter capture: `${parameter}` alongside the standard `:parameter` syntax. It has been decided to change the new form of the syntax to `{parameter}` to coincide with the syntax used by OpenAPI.


Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Request Context
# Request Contexts

Controlling contextual data provided to middleware and route handlers

Expand Down
7 changes: 6 additions & 1 deletion Hummingbird.docc/Hummingbird/Hummingbird.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,19 @@ try await app.runService()

### Guides

- <doc:MigratingToV2>
- <doc:Router>
- <doc:RequestContext>
- <doc:RequestContexts>
- <doc:EncodingAndDecoding>
- <doc:ErrorHandling>
- <doc:LoggingMetricsAndTracing>
- <doc:PersistentData>
- <doc:Testing>

### Tutorials

- <doc:Todos>

### Application

- ``HBApplication``
Expand Down
4 changes: 3 additions & 1 deletion Hummingbird.docc/HummingbirdAuth/Authenticators.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ router.middlewares.add(IsAuthenticatedMiddleware<User>())

Or you can use ``HummingbirdAuth/HBLoginCache/require(_:)`` to access the authentication data. In both of these cases if data is not available a unauthorised error is thrown and a 404 response is returned by the server.

## See Also
## Topics

### Reference

- ``HummingbirdAuth/HBAuthenticator``
- ``HummingbirdAuth/HBAuthenticatable``
Expand Down
7 changes: 7 additions & 0 deletions Hummingbird.docc/HummingbirdAuth/OneTimePasswords.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,10 @@ let password = TOTP(secret: sharedSecret).compute()
```

Compare it with the password provided by the user to verify the user credentials.

## Topics

### Reference

- ``HummingbirdAuth/HOTP``
- ``HummingbirdAuth/TOTP``
4 changes: 3 additions & 1 deletion Hummingbird.docc/HummingbirdAuth/Sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ router.group()

Your route will be able to access the authenticated user via `context.auth.require` or `context.auth.get`.

## See Also
## Topics

### Reference

- ``HummingbirdAuth/HBSessionStorage``
- ``HummingbirdAuth/HBSessionAuthenticator``
2 changes: 1 addition & 1 deletion Hummingbird.docc/Tutorials/Todos/Todos.tutorial
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@Tutorials(name: "Todo backend") {
@Intro(title: "Build a Todos application.") {
This is a tutorial showing you how to build a simple Todos application that allows you to store, access, edit and delete Todos in a database, using Hummingbird and PostgresNIO.
A tutorial showing you how to build a simple Todos application that allows you to store, access, edit and delete Todos in a database, using Hummingbird and PostgresNIO.

@Image(source: "hummingbird.png", alt: "Hummingbird logo")
}
Expand Down
15 changes: 13 additions & 2 deletions Hummingbird.docc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,31 @@ Hummingbird is designed to require the least number of dependencies possible, bu

### Guides

- <doc:MigratingToV2>
- <doc:Router>
- <doc:RequestContext>
- <doc:RequestContexts>
- <doc:EncodingAndDecoding>
- <doc:ErrorHandling>
- <doc:LoggingMetricsAndTracing>
- <doc:PersistentData>
- <doc:Testing>
- <doc:Authenticators>
- <doc:Sessions>
- <doc:OneTimePasswords>

### Tutorials

- <doc:Todos>

## See Also

- ``/Hummingbird``
- ``/HummingbirdCore``
- ``/HummingbirdAuth``
- ``/HummingbirdCompression``
- ``/HummingbirdFluent``
- ``/HummingbirdFoundation``
- ``/HummingbirdJobs``
- ``/HummingbirdRedis``
- ``/HummingbirdLambda``
- ``/HummingbirdWebSocket``
- ``/HummingbirdXCT``
2 changes: 2 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,15 @@ let package = Package(
.product(name: "HummingbirdHTTP2", package: "hummingbird"),
.product(name: "HummingbirdFoundation", package: "hummingbird"),
.product(name: "HummingbirdJobs", package: "hummingbird"),
.product(name: "HummingbirdRouter", package: "hummingbird"),
.product(name: "HummingbirdXCT", package: "hummingbird"),
.product(name: "HummingbirdAuth", package: "hummingbird-auth"),
// .product(name: "HummingbirdCompression", package: "hummingbird-compression"),
.product(name: "HummingbirdFluent", package: "hummingbird-fluent"),
// .product(name: "HummingbirdLambda", package: "hummingbird-lambda"),
.product(name: "HummingbirdMustache", package: "hummingbird-mustache"),
.product(name: "HummingbirdRedis", package: "hummingbird-redis"),
.product(name: "HummingbirdJobsRedis", package: "hummingbird-redis"),
// .product(name: "HummingbirdWebSocket", package: "hummingbird-websocket"),
]),
]
Expand Down

0 comments on commit c54304f

Please sign in to comment.