Skip to content

fix: Update async functionality of server #70

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

Merged
merged 4 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 28 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,25 +99,40 @@ The `webhookKey` should match the [webhookKey on the Parse Server](https://githu
The aforementioned environment variables automatically configure [Parse-Swift<sup>OG</sup> SDK](https://github.com/netreconlab/Parse-Swift). If you need a more custom configuration, see the [documentation](https://netreconlab.github.io/Parse-Swift/release/documentation/parseswift/).

### Initializing ParseSwiftServer
To levergage the aforementioned environment variables, you should modify `configure.swift` in your project to look similar to below:
To levergage the aforementioned environment variables, you should modify `entrypoint.swift` in your project to look similar to below:

```swift
public func configure(_ app: Application) throws {
// Initialize ParseServerSwift
let configuration = try ParseServerConfiguration(app: app)
try ParseServerSwift.initialize(configuration, app: app)

// Add any additional code to configure your server here...

// register routes
try routes(app)
import Vapor
import Dispatch
import Logging
import NIOCore
import NIOPosix
import ParseServerSwift

@main
enum Entrypoint {
static func main() async throws {
var env = try Environment.detect()
try LoggingSystem.bootstrap(from: &env)

let app = try await Application.make(env)

// This attempts to install NIO as the Swift Concurrency global executor.
// You should not call any async functions before this point.
let executorTakeoverSuccess = NIOSingletons.unsafeTryInstallSingletonPosixEventLoopGroupAsConcurrencyGlobalExecutor()
app.logger.debug("Running with \(executorTakeoverSuccess ? "SwiftNIO" : "standard") Swift Concurrency default executor")

try await parseServerSwiftConfigure(app)
try await app.execute()
try await app.asyncShutdown()
}
}
```

If you want to pass the configuration parameters programitically, your `configure` method should look similar to below:
If you want to pass the configuration parameters programitically, you can add a `configure` method to `configure.swift` should look similar to below:

```swift
public func configure(_ app: Application) throws {
public func configure(_ app: Application) async throws {
// Initialize ParseServerSwift
let configuration = try ParseServerConfiguration(app: app,
hostName: "hostName",
Expand All @@ -126,7 +141,7 @@ public func configure(_ app: Application) throws {
primaryKey: "primaryKey",
webhookKey: hookKey,
parseServerURLString: "primaryKey")
try ParseServerSwift.initialize(configuration, app: app)
try await ParseServerSwift.initialize(configuration, app: app)

// Add any additional code to configure your server here...

Expand Down
31 changes: 11 additions & 20 deletions Sources/App/entrypoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,10 @@
import Vapor
import Dispatch
import Logging
import NIOCore
import NIOPosix
import ParseServerSwift

/// This extension is temporary and can be removed once Vapor gets this support.
private extension Vapor.Application {
static let baseExecutionQueue = DispatchQueue(label: "vapor.codes.entrypoint")

func runFromAsyncMainEntrypoint() async throws {
try await withCheckedThrowingContinuation { continuation in
Vapor.Application.baseExecutionQueue.async { [self] in
do {
try self.run()
continuation.resume()
} catch {
continuation.resume(throwing: error)
}
}
}
}
}

@main
enum Entrypoint {
static func main() async throws {
Expand All @@ -36,9 +20,16 @@ enum Entrypoint {

let app = try await Application.make(env)

defer { app.shutdown() }
// This attempts to install NIO as the Swift Concurrency global executor.
// You should not call any async functions before this point.
// swiftlint:disable:next line_length
let executorTakeoverSuccess = NIOSingletons.unsafeTryInstallSingletonPosixEventLoopGroupAsConcurrencyGlobalExecutor()
app.logger.debug(
"Running with \(executorTakeoverSuccess ? "SwiftNIO" : "standard") Swift Concurrency default executor"
)

try await parseServerSwiftConfigure(app)
try await app.runFromAsyncMainEntrypoint()
try await app.execute()
try await app.asyncShutdown()
}
}
21 changes: 10 additions & 11 deletions Sources/ParseServerSwift/Extensions/Parse+Vapor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@
- note: This options method should be used in a multi Parse Server environment.
In a single Parse Server environment, use options().
*/
func options(_ request: Request,
// swiftlint:disable:next line_length
parseServerURLStrings: [String] = ParseServerSwift.configuration.parseServerURLStrings) throws -> API.Options {
func options(
_ request: Request,
parseServerURLStrings: [String] = ParseServerSwift.configuration.parseServerURLStrings
) throws -> API.Options {
var options = self.options()
options.insert(.serverURL(try serverURLString(request.url,
parseServerURLStrings: parseServerURLStrings)))
Expand All @@ -42,15 +43,13 @@
- note: The default cache policy for this method is `.reloadIgnoringLocalCacheData`. If a developer
desires a different policy, it should be inserted in `options`.
*/
func hydrateUser(options: API.Options = [],
request: Request,
// swiftlint:disable:next line_length
parseServerURLStrings: [String] = ParseServerSwift.configuration.parseServerURLStrings) async throws -> Self {
func hydrateUser(
options: API.Options = [],
request: Request,
parseServerURLStrings: [String] = ParseServerSwift.configuration.parseServerURLStrings
) async throws -> Self {

Check warning on line 50 in Sources/ParseServerSwift/Extensions/Parse+Vapor.swift

View check run for this annotation

Codecov / codecov/patch

Sources/ParseServerSwift/Extensions/Parse+Vapor.swift#L50

Added line #L50 was not covered by tests
var updatedOptions = try self.options(request, parseServerURLStrings: parseServerURLStrings)
updatedOptions = options.union(updatedOptions)
return try await withCheckedThrowingContinuation { continuation in
self.hydrateUser(options: updatedOptions,
completion: continuation.resume)
}
return try await self.hydrateUser(options: updatedOptions)

Check warning on line 53 in Sources/ParseServerSwift/Extensions/Parse+Vapor.swift

View check run for this annotation

Codecov / codecov/patch

Sources/ParseServerSwift/Extensions/Parse+Vapor.swift#L53

Added line #L53 was not covered by tests
}
}
2 changes: 1 addition & 1 deletion Sources/ParseServerSwift/Parse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public var configuration: ParseServerConfiguration {

/**
Configure `ParseServerSwift`. This should only be called once when starting your
Vapor app. Typically in the `configure(_ app: Application)`.
Vapor app. Typically in the `parseServerSwiftConfigure(_ app: Application)`.
- parameter configuration: The ParseServer configuration.
- parameter app: Core type representing a Vapor application.
- throws: An error of `ParseError` type.
Expand Down
14 changes: 12 additions & 2 deletions Sources/ParseServerSwift/configure.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import Vapor

public func parseServerSwiftConfigure(_ app: Application) async throws {
/**
A helper method for configuring your `ParseServerSwift`. This should only be called once when starting your
Vapor app.
- parameter app: Core type representing a Vapor application.
- parameter configuration: A `ParseServerConfiguration`. If `nil`, the vapor
environment variables will be used for configuration.
*/
public func parseServerSwiftConfigure(
_ app: Application,
with configuration: ParseServerConfiguration? = nil
) async throws {

Check warning on line 13 in Sources/ParseServerSwift/configure.swift

View check run for this annotation

Codecov / codecov/patch

Sources/ParseServerSwift/configure.swift#L13

Added line #L13 was not covered by tests
// Initialize ParseServerSwift
let configuration = try ParseServerConfiguration(app: app)
let configuration = try configuration ?? ParseServerConfiguration(app: app)

Check warning on line 15 in Sources/ParseServerSwift/configure.swift

View check run for this annotation

Codecov / codecov/patch

Sources/ParseServerSwift/configure.swift#L15

Added line #L15 was not covered by tests
try await ParseServerSwift.initialize(configuration, app: app)

// register routes
Expand Down
57 changes: 35 additions & 22 deletions Tests/ParseServerSwiftTests/AppTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ final class AppTests: XCTestCase {
}

func setupAppForTesting(hookKey: String? = nil) async throws -> Application {
let app = Application(.testing)
let app = try await Application.make(.testing)
let configuration = try ParseServerConfiguration(app: app,
hostName: "hostName",
port: 8080,
Expand All @@ -35,49 +35,52 @@ final class AppTests: XCTestCase {
return app
}

func testConfigRequiresKeys() throws {
let app = Application(.testing)
defer { app.shutdown() }
func testConfigRequiresKeys() async throws {
let app = try await Application.make(.testing)
XCTAssertThrowsError(try ParseServerConfiguration(app: app))
try await app.asyncShutdown()
}

func testAllowInitConfigOnce() throws {
let app = Application(.testing)
defer { app.shutdown() }
func testAllowInitConfigOnce() async throws {
let app = try await Application.make(.testing)
let configuration = try ParseServerConfiguration(app: app,
hostName: "hostName",
port: 8080,
applicationId: "applicationId",
primaryKey: "primaryKey",
parseServerURLString: "primaryKey")
XCTAssertNoThrow(try setConfiguration(configuration))
try await app.asyncShutdown()
}

func testDoNotInitConfigTwice() async throws {
let app = try await setupAppForTesting()
defer { app.shutdown() }
let configuration = try ParseServerConfiguration(app: app,
hostName: "hostName",
port: 8080,
applicationId: "applicationId",
primaryKey: "primaryKey",
parseServerURLString: "primaryKey")
XCTAssertThrowsError(try setConfiguration(configuration))
try await app.asyncShutdown()
}

func testFooBar() async throws {
let app = try await setupAppForTesting()
defer { app.shutdown() }

try app.test(.GET, "foo", afterResponse: { res in
try await app.test(
.GET,
"foo"
) { res async throws in
XCTAssertEqual(res.status, .ok)
XCTAssertEqual(res.body.string, "foo bar")
})
}

try await app.asyncShutdown()
}

func testCheckServerHealth() async throws {
let app = try await setupAppForTesting()
defer { app.shutdown() }

XCTAssertGreaterThan(configuration.parseServerURLStrings.count, 0)
do {
Expand All @@ -86,6 +89,7 @@ final class AppTests: XCTestCase {
} catch {
XCTAssertTrue(error.localizedDescription.contains("Unable to connect"))
}
try await app.asyncShutdown()
}

func testGetParseServerURLs() async throws {
Expand All @@ -106,7 +110,6 @@ final class AppTests: XCTestCase {

func testDeleteHooks() async throws {
let app = try await setupAppForTesting()
defer { app.shutdown() }

let urlString = "https://parse.com/parse"
guard let url = URL(string: urlString) else {
Expand All @@ -132,31 +135,39 @@ final class AppTests: XCTestCase {
let currentTriggers2 = await configuration.hooks.getTriggers()
XCTAssertEqual(currentFunctions2.count, 0)
XCTAssertEqual(currentTriggers2.count, 0)
try await app.asyncShutdown()
}

func testFunctionWebhookKeyNotEqual() async throws {
let app = try await setupAppForTesting(hookKey: "wow")
defer { app.shutdown() }

try app.test(.POST, "hello", afterResponse: { res in
try await app.test(
.POST,
"hello"
) { res async throws in
XCTAssertEqual(res.status, .ok)
XCTAssertTrue(res.body.string.contains("Webhook keys"))
})
}

try await app.asyncShutdown()
}

func testTriggerWebhookKeyNotEqual() async throws {
let app = try await setupAppForTesting(hookKey: "wow")
defer { app.shutdown() }

try app.test(.POST, "score/save/before", afterResponse: { res in
try await app.test(
.POST,
"score/save/before"
) { res async throws in
XCTAssertEqual(res.status, .ok)
XCTAssertTrue(res.body.string.contains("Webhook keys"))
})
}

try await app.asyncShutdown()
}

func testMatchServerURLString() async throws {
let app = try await setupAppForTesting()
defer { app.shutdown() }
let urlString = "https://parse.com/parse"
let uri = URI(stringLiteral: urlString)
let serverString = try serverURLString(uri, parseServerURLStrings: [urlString])
Expand All @@ -171,21 +182,22 @@ final class AppTests: XCTestCase {
let serverString3 = try serverURLString(uri,
parseServerURLStrings: configuration.parseServerURLStrings)
XCTAssertEqual(serverString3, configuration.parseServerURLStrings.first)

try await app.asyncShutdown()
}

func testMatchServerURLStringThrowsError() async throws {
let app = try await setupAppForTesting()
Parse.configuration.parseServerURLStrings.removeAll()
defer { app.shutdown() }
let urlString = "https://parse.com/parse"
let uri = URI(stringLiteral: urlString)
XCTAssertThrowsError(try serverURLString(uri,
parseServerURLStrings: configuration.parseServerURLStrings))
try await app.asyncShutdown()
}

func testParseHookOptions() async throws {
let app = try await setupAppForTesting()
defer { app.shutdown() }
let installationId = "naw"
let urlString = "https://parse.com/parse"
Parse.configuration.parseServerURLStrings.append(urlString)
Expand All @@ -208,6 +220,7 @@ final class AppTests: XCTestCase {
XCTAssertEqual(options2.count, 2)
XCTAssertTrue(installationOption2.debugDescription.contains(installationId))
XCTAssertTrue(serverURLOption.debugDescription.contains("\"\(urlString)\""))
try await app.asyncShutdown()
}

func testHooksFunctions() async throws {
Expand Down