Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make Services in Vapor Usable #2901

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 9 additions & 0 deletions Sources/Vapor/Services/App+Service.swift
@@ -0,0 +1,9 @@
extension Application {
public struct Services {
public let application: Application
}

public var services: Services {
.init(application: self)
}
}
12 changes: 12 additions & 0 deletions Sources/Vapor/Services/Req+Service.swift
@@ -0,0 +1,12 @@
extension Request {
public struct Services {
public let request: Request
init(request: Request) {
self.request = request
}
}

public var services: Services {
Services(request: self)
}
}
53 changes: 53 additions & 0 deletions Sources/Vapor/Services/Service.swift
@@ -0,0 +1,53 @@
public extension Application {
struct Service<ServiceType> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public struct instead of public extension? I tend to be explicit in marking things public in libraries/frameworks, so future changes don't accidentally expose internal API.


let application: Application

public init(application: Application) {
self.application = application
}

public struct Provider {
let run: (Application) -> ()

public init(_ run: @escaping (Application) -> ()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we mark these @Sendable from now on?

self.run = run
}
}

final class Storage {
var makeService: ((Application) -> ServiceType)?
init() { }
}

struct Key: StorageKey {
typealias Value = Storage
}

public var service: ServiceType {
guard let makeService = self.storage.makeService else {
fatalError("No service configured for \(ServiceType.self)")
}
return makeService(self.application)
}

public func use(_ provider: Provider) {
provider.run(self.application)
}

public func use(_ makeService: @escaping (Application) -> ServiceType) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Sendable?

self.storage.makeService = makeService
}

func initialize() {
self.application.storage[Key.self] = .init()
}

private var storage: Storage {
if self.application.storage[Key.self] == nil {
self.initialize()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this works fine, but can we make initialize() return the newly created entity? Then return that here.

}
return self.application.storage[Key.self]!
}
}
}
75 changes: 64 additions & 11 deletions Tests/VaporTests/ServiceTests.swift
Expand Up @@ -4,55 +4,108 @@ final class ServiceTests: XCTestCase {
func testReadOnly() throws {
let app = Application(.testing)
defer { app.shutdown() }

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit, but there's a lot of whitespace changes.

app.get("test") { req in
req.readOnly.foos()
}

try app.test(.GET, "test") { res in
XCTAssertEqual(res.status, .ok)
try XCTAssertEqual(res.content.decode([String].self), ["foo"])
}
}

func testWritable() throws {
let app = Application(.testing)
defer { app.shutdown() }

app.writable = .init(apiKey: "foo")
XCTAssertEqual(app.writable?.apiKey, "foo")
}

func testLifecycle() throws {
let app = Application(.testing)
defer { app.shutdown() }

app.lifecycle.use(Hello())
app.environment.arguments = ["serve"]
try app.start()
app.running?.stop()
}

func testLocks() throws {
let app = Application(.testing)
defer { app.shutdown() }

app.sync.withLock {
// Do something.
}

struct TestKey: LockKey { }

let test = app.locks.lock(for: TestKey.self)
test.withLock {
// Do something.
}
}

func testServiceHelpers() throws {
let app = Application(.testing)
defer { app.shutdown() }

let testString = "This is a test - \(Int.random())"
let myFakeServicce = MyTestService(cannedResponse: testString, eventLoop: app.eventLoopGroup.next(), logger: app.logger)

app.services.myService.use { _ in
myFakeServicce
}

app.get("myService") { req -> String in
let thing = req.services.myService.doSomething()
return thing
}

try app.test(.GET, "myService", afterResponse: { res in
XCTAssertEqual(res.status, .ok)
XCTAssertEqual(res.body.string, testString)
})
}
}

protocol MyService {
func `for`(_ request: Request) -> MyService
func doSomething() -> String
}

extension Application.Services {
var myService: Application.Service<MyService> {
.init(application: self.application)
}
}

extension Request.Services {
var myService: MyService {
self.request.application.services.myService.service.for(request)
}
}

struct MyTestService: MyService {
let cannedResponse: String
let eventLoop: EventLoop
let logger: Logger

func `for`(_ request: Vapor.Request) -> MyService {
return MyTestService(cannedResponse: self.cannedResponse, eventLoop: request.eventLoop, logger: request.logger)
}

func doSomething() -> String {
return cannedResponse
}
}

private struct ReadOnly {
let client: Client

func foos() -> EventLoopFuture<[String]> {
self.client.eventLoop.makeSucceededFuture(["foo"])
}
Expand Down