Skip to content

Commit

Permalink
Update multipart-kit example (#71)
Browse files Browse the repository at this point in the history
  • Loading branch information
adam-fowler authored Apr 29, 2024
1 parent ea67722 commit 5bed557
Show file tree
Hide file tree
Showing 17 changed files with 169 additions and 157 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Examples converted to Hummingbird 2.0
- [html-form](https://github.com/hummingbird-project/hummingbird-examples/tree/main/html-form) - Link HTML form to Hummingbird application.
- [http2](https://github.com/hummingbird-project/hummingbird-examples/tree/main/http2) - Basic application with HTTP2 upgrade added.
- [jobs](https://github.com/hummingbird-project/hummingbird-examples/tree/main/jobs) - Demonstrating offloading of jobs to another server.
- [multipart-form](https://github.com/hummingbird-project/hummingbird-examples/tree/main/multipart-form) - HTML form using Multipart form data, using MultipartKit
- [proxy-server](https://github.com/hummingbird-project/hummingbird-examples/tree/main/proxy-server) - Using AsyncHTTPClient to build a proxy server
- [sessions](https://github.com/hummingbird-project/hummingbird-examples/tree/main/sessions) - Username/password and session authentication.
- [todos-dynamodb](https://github.com/hummingbird-project/hummingbird-examples/tree/main/todos-dynamodb) - Todos application, based off [TodoBackend](http://todobackend.com) spec, using DynamoDB.
Expand All @@ -26,10 +27,9 @@ And finally

Examples still working with Hummingbird 1.0

- [auth-srp](https://github.com/hummingbird-project/hummingbird-examples/tree/main/auth-srp) - Secure Remote Password authentication.
- [ios-image-server](https://github.com/hummingbird-project/hummingbird-examples/tree/main/ios-image-server) - iOS web server that provides access to iPhone photo library.
- [multipart-form](https://github.com/hummingbird-project/hummingbird-examples/tree/main/multipart-form) - HTML form using Multipart form data, using MultipartKit
- [todos-fluent](https://github.com/hummingbird-project/hummingbird-examples/tree/main/todos-fluent) - Todos application, based off [TodoBackend](http://todobackend.com) spec, using Fluent
- [upload-s3](https://github.com/hummingbird-project/hummingbird-examples/tree/main/upload-s3) - File uploading and downloading using AWS S3 as backing store.
- [auth-srp](https://github.com/hummingbird-project/hummingbird-examples/tree/1.x.x/auth-srp) - Secure Remote Password authentication.
- [ios-image-server](https://github.com/hummingbird-project/hummingbird-examples/tree/1.x.x/ios-image-server) - iOS web server that provides access to iPhone photo library.
- [todos-fluent](https://github.com/hummingbird-project/hummingbird-examples/tree/1.x.x/todos-fluent) - Todos application, based off [TodoBackend](http://todobackend.com) spec, using Fluent
- [upload-s3](https://github.com/hummingbird-project/hummingbird-examples/tree/1.x.x/upload-s3) - File uploading and downloading using AWS S3 as backing store.

The full set of Hummingbird 1.0 examples can be found at https://github.com/hummingbird-project/hummingbird-examples/tree/1.x.x
14 changes: 4 additions & 10 deletions multipart-form/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,22 @@ let package = Package(
name: "multipart-form",
platforms: [.macOS(.v14)],
products: [
.executable(name: "Server", targets: ["Server"]),
.executable(name: "App", targets: ["App"]),
],
dependencies: [
.package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0-beta"),
.package(url: "https://github.com/hummingbird-project/hummingbird-mustache.git", branch: "main"),
.package(url: "https://github.com/hummingbird-project/swift-mustache.git", from: "2.0.0-beta"),
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.0"),
.package(url: "https://github.com/swift-extras/swift-extras-base64.git", .upToNextMinor(from: "0.7.0")),
.package(url: "https://github.com/vapor/multipart-kit.git", from: "4.0.0"),
],
targets: [
.executableTarget(
name: "Server",
dependencies: [
.byName(name: "App"),
.product(name: "ArgumentParser", package: "swift-argument-parser"),
]
),
.target(
name: "App",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "Hummingbird", package: "hummingbird"),
.product(name: "HummingbirdMustache", package: "hummingbird-mustache"),
.product(name: "Mustache", package: "swift-mustache"),
.product(name: "ExtrasBase64", package: "swift-extras-base64"),
.product(name: "MultipartKit", package: "multipart-kit"),
],
Expand Down
4 changes: 2 additions & 2 deletions multipart-form/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

Application demonstrating working with HTML forms using multipart form data. Set the working directory to the local folder, run app and open up a web browser and go to localhost:8080.

This example uses the [MultipartKit](https://github.com/vapor/multipart-kit) package for decoding the multipart form data sent by the web browser. The HTML form is generated using the [HummingbirdMustache](https://github.com/hummingbird-project/hummingbird-mustache) library. A new type `HTML` is added that conforms to `HBResponseGenerator`. This generates a response with the `HTML` text contents and a `content-type` header set to `text/html`.
This example uses the [MultipartKit](https://github.com/vapor/multipart-kit) package for decoding the multipart form data sent by the web browser. The HTML form is generated using the [Mustache](https://github.com/hummingbird-project/swift-mustache) library. A new type `HTML` is added that conforms to `ResponseGenerator`. This generates a response with the `HTML` text contents and a `content-type` header set to `text/html`.

Added a new `HBRequestDecoder` that checks the header value `content-type` and if its media type is `.multipartForm` then decodes request using `FormDataDecoder` from the MultipartKit package. Otherwise returns unsupported media type.
Added a new `RequestDecoder` that checks the header value `content-type` and if its media type is `.multipartForm` then decodes request using `FormDataDecoder` from the MultipartKit package. Otherwise returns unsupported media type.
16 changes: 16 additions & 0 deletions multipart-form/Sources/App/App.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import ArgumentParser
import Hummingbird

@main
struct MultipartFormApp: AsyncParsableCommand, AppArguments {
@Option(name: .shortAndLong)
var hostname: String = "127.0.0.1"

@Option(name: .shortAndLong)
var port: Int = 8080

func run() async throws {
let app = try await buildApplication(self)
try await app.runService()
}
}
18 changes: 18 additions & 0 deletions multipart-form/Sources/App/Application+build.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Hummingbird
import MultipartKit
import Mustache

protocol AppArguments {
var hostname: String { get }
var port: Int { get }
}

func buildApplication(_ args: AppArguments) async throws -> some ApplicationProtocol {
let library = try await MustacheLibrary(directory: "templates")
assert(library.getTemplate(named: "page") != nil, "Set your working directory to the root folder of this example to get it to work")

let router = Router(context: MultipartRequestContext.self)
router.middlewares.add(FileMiddleware())
WebController(mustacheLibrary: library).addRoutes(to: router)
return Application(router: router, configuration: .init(address: .hostname(args.hostname, port: args.port)))
}
20 changes: 0 additions & 20 deletions multipart-form/Sources/App/Application+configure.swift

This file was deleted.

29 changes: 15 additions & 14 deletions multipart-form/Sources/App/Codable/FormDataCoding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,37 +17,38 @@ import Hummingbird
import MultipartKit
import NIOFoundationCompat

extension FormDataEncoder: HBResponseEncoder {
extension FormDataEncoder {
/// Extend JSONEncoder to support encoding `HBResponse`'s. Sets body and header values
/// - Parameters:
/// - value: Value to encode
/// - request: Request used to generate response
public func encode<T: Encodable>(_ value: T, from request: HBRequest) throws -> HBResponse {
var buffer = request.allocator.buffer(capacity: 0)
public func encode<T: Encodable>(_ value: T, from request: Request, context: some BaseRequestContext) throws -> Response {
var buffer = context.allocator.buffer(capacity: 0)

let boundary = "----HBFormBoundary" + String(base32Encoding: (0..<4).map { _ in UInt8.random(in: 0...255) })
try self.encode(value, boundary: boundary, into: &buffer)
return HBResponse(
return Response(
status: .ok,
headers: ["content-type": "multipart/form-data; boundary=\(boundary)"],
body: .byteBuffer(buffer)
headers: [.contentType: "multipart/form-data; boundary=\(boundary)"],
body: .init(byteBuffer: buffer)
)
}
}

extension FormDataDecoder: HBRequestDecoder {
extension FormDataDecoder {
/// Extend JSONDecoder to decode from `HBRequest`.
/// - Parameters:
/// - type: Type to decode
/// - request: Request to decode from
public func decode<T: Decodable>(_ type: T.Type, from request: HBRequest) throws -> T {
guard let buffer = request.body.buffer,
let contentType = request.headers["content-type"].first,
let mediaType = HBMediaType(from: contentType),
public func decode<T: Decodable>(_ type: T.Type, from request: Request, context: some BaseRequestContext) async throws -> T {
guard let contentType = request.headers[.contentType],
let mediaType = MediaType(from: contentType),
let parameter = mediaType.parameter,
parameter.name == "boundary" else {
throw HBHTTPError(.badRequest)
}
parameter.name == "boundary"
else {
throw HTTPError(.unsupportedMediaType)
}
let buffer = try await request.body.collect(upTo: 1_000_000)
return try self.decode(T.self, from: buffer, boundary: parameter.value)
}
}
13 changes: 4 additions & 9 deletions multipart-form/Sources/App/Codable/RequestDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,9 @@
import Hummingbird
import MultipartKit

struct RequestDecoder: HBRequestDecoder {
let decoder = FormDataDecoder()

func decode<T>(_ type: T.Type, from request: HBRequest) throws -> T where T: Decodable {
if let contentType = request.headers["content-type"].first,
HBMediaType(from: contentType)?.isType(.multipartForm) == true {
return try self.decoder.decode(type, from: request)
}
throw HBHTTPError(.unsupportedMediaType)
struct MultipartRequestDecoder: RequestDecoder {
func decode<T>(_ type: T.Type, from request: Request, context: some BaseRequestContext) async throws -> T where T: Decodable {
let decoder = FormDataDecoder()
return try await decoder.decode(type, from: request, context: context)
}
}
33 changes: 23 additions & 10 deletions multipart-form/Sources/App/Controllers/WebController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,41 @@
//===----------------------------------------------------------------------===//

import Hummingbird
import HummingbirdMustache
import Mustache

struct HTML: ResponseGenerator {
let html: String

public func response(from request: Request) throws -> Response {
let buffer = request.allocator.buffer(string: self.html)
return .init(status: .ok, headers: ["content-type": "text/html"], body: .byteBuffer(buffer))
public func response(from request: Request, context: some BaseRequestContext) throws -> Response {
let buffer = context.allocator.buffer(string: self.html)
return .init(status: .ok, headers: [.contentType: "text/html"], body: .init(byteBuffer: buffer))
}
}

struct WebController {
let mustacheLibrary: MustacheLibrary
let library: MustacheLibrary
let enterTemplate: MustacheTemplate
let enteredTemplate: MustacheTemplate

func input(request: Request) -> HTML {
let html = mustacheLibrary.render((), withTemplate: "enter-details")!
init(mustacheLibrary: MustacheLibrary) {
self.library = mustacheLibrary
self.enterTemplate = mustacheLibrary.getTemplate(named: "enter-details")!
self.enteredTemplate = mustacheLibrary.getTemplate(named: "details-entered")!
}

func addRoutes(to router: some RouterMethods<some BaseRequestContext>) {
router.get("/", use: self.input)
router.post("/", use: self.post)
}

@Sendable func input(request: Request, context: some BaseRequestContext) -> HTML {
let html = self.enterTemplate.render((), library: self.library)
return HTML(html: html)
}

func post(request: Request) throws -> HTML {
guard let user = try? request.decode(as: User.self) else { throw HTTPError(.badRequest) }
let html = mustacheLibrary.render(user, withTemplate: "details-entered")!
@Sendable func post(request: Request, context: some BaseRequestContext) async throws -> HTML {
let user = try await request.decode(as: User.self, context: context)
let html = self.enteredTemplate.render(user, library: self.library)
return HTML(html: html)
}
}
12 changes: 12 additions & 0 deletions multipart-form/Sources/App/RequestContext.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Hummingbird
import Logging
import NIOCore

struct MultipartRequestContext: RequestContext {
var requestDecoder: MultipartRequestDecoder { .init() }
var coreContext: CoreRequestContext

init(channel: Channel, logger: Logger) {
self.coreContext = .init(allocator: channel.allocator, logger: logger)
}
}
25 changes: 0 additions & 25 deletions multipart-form/Sources/Server/main.swift

This file was deleted.

56 changes: 29 additions & 27 deletions multipart-form/Tests/AppTests/AppTests.swift
Original file line number Diff line number Diff line change
@@ -1,35 +1,37 @@
import App
@testable import App
import Hummingbird
import HummingbirdXCT
import HummingbirdTesting
import XCTest

final class AppTests: XCTestCase {
func testApp() throws {
let app = HBApplication(testing: .live)
try app.configure()

try app.XCTStart()
defer { app.XCTStop() }
struct TestArguments: AppArguments {
let hostname = "127.0.0.1"
let port = 8080
}

let multipartForm = """
------HBTestFormBoundaryXD6BXJI\r
Content-Disposition: form-data; name="name"\r
\r
adam\r
------HBTestFormBoundaryXD6BXJI\r
Content-Disposition: form-data; name="age"\r
\r
50\r
------HBTestFormBoundaryXD6BXJI--\r
"""
let contentType = "multipart/form-data; boundary=----HBTestFormBoundaryXD6BXJI"
try app.XCTExecute(
uri: "/",
method: .POST,
headers: ["content-type": contentType],
body: ByteBufferAllocator().buffer(string: multipartForm)
) { response in
XCTAssertEqual(response.status, .ok)
func testApp() async throws {
let app = try await buildApplication(TestArguments())
try await app.test(.router) { client in
let multipartForm = """
------HBTestFormBoundaryXD6BXJI\r
Content-Disposition: form-data; name="name"\r
\r
adam\r
------HBTestFormBoundaryXD6BXJI\r
Content-Disposition: form-data; name="age"\r
\r
50\r
------HBTestFormBoundaryXD6BXJI--\r
"""
let contentType = "multipart/form-data; boundary=----HBTestFormBoundaryXD6BXJI"
try await client.execute(
uri: "/",
method: .post,
headers: [.contentType: contentType],
body: ByteBufferAllocator().buffer(string: multipartForm)
) { response in
XCTAssertEqual(response.status, .ok)
}
}
}
}
Binary file added multipart-form/public/images/hummingbird.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 9 additions & 13 deletions multipart-form/templates/details-entered.mustache
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
{{>head}}
<body>
<div>
<h1>
You entered
</h1>
<ul>
<li>Name: {{name}}</li>
<li>Age: {{age}}</li>
</ul>
</div>
</body>
</html>
{{<page}}
{{$body}}
<h1 class="text-xl">You entered</h1>
<ul>
<li>Name: {{name}}</li>
<li>Age: {{age}}</li>
</ul>
{{/body}}
{{/page}}
26 changes: 12 additions & 14 deletions multipart-form/templates/enter-details.mustache
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
{{>head}}
<body>
<div>
<h1>Please enter your details</h1>
<form action="/" method="post" enctype="multipart/form-data">
<label for="name">Name</label><br/>
<input type="text" id="name" name="name"/><br/>
<label for="age">Age</label><br/>
<input type="text" id="age" name="age"/><br/>
<input type="submit" value="Submit"/>
</form>
</div>
</body>
</html>
{{<page}}
{{$body}}
<h1 class="text-xl">Please enter your details</h1>
<form action="/" method="post" enctype="multipart/form-data">
<label for="name">Name</label><br/>
<input type="text" id="name" name="name" class="border"/><br/>
<label for="age">Age</label><br/>
<input type="text" id="age" name="age" class="border"/><br/>
<input type="submit" value="Submit" class="p-1 my-2 border shadow-lg" />
</form>
{{/body}}
{{/page}}
Loading

0 comments on commit 5bed557

Please sign in to comment.