Skip to content

Implement bool flag resolution #6

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 2 commits into from
Jan 27, 2025
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
18 changes: 18 additions & 0 deletions Sources/OFREP/OFREPClientTransport.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift OpenFeature open source project
//
// Copyright (c) 2025 the Swift OpenFeature project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import OpenAPIRuntime

public protocol OFREPClientTransport: ClientTransport {
func shutdownGracefully() async throws
}
60 changes: 59 additions & 1 deletion Sources/OFREP/OFREPProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,69 @@
//
// This source file is part of the Swift OpenFeature open source project
//
// Copyright (c) 2024 the Swift OpenFeature project authors
// Copyright (c) 2025 the Swift OpenFeature project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Foundation
import Logging
import OpenAPIRuntime
import OpenFeature
import ServiceLifecycle

public struct OFREPProvider<Transport: OFREPClientTransport>: OpenFeatureProvider, CustomStringConvertible {
public let metadata = OpenFeatureProviderMetadata(name: "OpenFeature Remote Evaluation Protocol Provider")
public let description = "OFREPProvider"
private let transport: Transport
private let client: Client
private let logger = Logger(label: "OFREPProvider")

package init(serverURL: URL, transport: Transport) {
self.transport = transport
self.client = Client(serverURL: serverURL, transport: transport)
}

public func resolution(
of flag: String,
defaultValue: Bool,
context: OpenFeatureEvaluationContext?
) async -> OpenFeatureResolution<Bool> {
let request: Components.Schemas.EvaluationRequest
do {
request = try Components.Schemas.EvaluationRequest(flag: flag, defaultValue: defaultValue, context: context)
} catch {
return error.resolution
}

do {
do {
let response = try await client.postOfrepV1EvaluateFlagsKey(
path: .init(key: flag),
headers: .init(accept: [.init(contentType: .json)]),
body: .json(request)
)
return OpenFeatureResolution(response, defaultValue: defaultValue)
} catch let error as ClientError {
throw error.underlyingError
}
} catch {
return OpenFeatureResolution(
value: defaultValue,
error: OpenFeatureResolutionError(code: .general, message: "\(error)"),
reason: .error
)
}
}

public func run() async throws {
try await gracefulShutdown()
logger.debug("Shutting down.")
try await transport.shutdownGracefully()
logger.debug("Shut down.")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,37 @@ import Foundation
import OpenAPIRuntime
import OpenFeature

extension Components.Schemas.EvaluationRequest {
package init<Value: OpenFeatureValue>(
flag: String,
defaultValue: Value,
context: OpenFeatureEvaluationContext?
) throws(EvaluationRequestSerializationError<Value>) {
let serializedContext: Components.Schemas.Context?
do {
serializedContext = try context.map(Components.Schemas.Context.init)
} catch {
throw EvaluationRequestSerializationError(
value: defaultValue,
error: OpenFeatureResolutionError(code: .invalidContext, message: "\(error)"),
reason: .error
)
}

self.init(context: serializedContext)
}
}

package struct EvaluationRequestSerializationError<Value: OpenFeatureValue>: Error {
let value: Value
let error: OpenFeatureResolutionError
let reason: OpenFeatureResolutionReason

var resolution: OpenFeatureResolution<Value> {
OpenFeatureResolution(value: value, error: error, reason: reason)
}
}

extension Components.Schemas.Context {
package init(_ context: OpenFeatureEvaluationContext) throws {
let additionalProperties = try OpenAPIObjectContainer(context.fields)
Expand Down
160 changes: 160 additions & 0 deletions Sources/OFREP/OpenFeatureResolution+OFREP.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift OpenFeature open source project
//
// Copyright (c) 2025 the Swift OpenFeature project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import OpenFeature

extension OpenFeatureResolution<Bool> {
package init(_ response: Operations.PostOfrepV1EvaluateFlagsKey.Output, defaultValue: Bool) {
switch response {
case .ok(let ok):
switch ok.body {
case .json(let responsePayload):
self = OpenFeatureResolution(responsePayload, defaultValue: defaultValue)
}
case .badRequest(let badRequest):
switch badRequest.body {
case .json(let responsePayload):
self = OpenFeatureResolution(
value: defaultValue,
error: .init(
code: .init(rawValue: responsePayload.errorCode.rawValue),
message: responsePayload.errorDetails
),
reason: .error
)
}
case .notFound(let notFound):
switch notFound.body {
case .json(let responsePayload):
self = OpenFeatureResolution(
value: defaultValue,
error: .init(
code: .init(rawValue: responsePayload.errorCode.rawValue),
message: responsePayload.errorDetails
),
reason: .error
)
}
case .unauthorized:
self = OpenFeatureResolution(
value: defaultValue,
error: OpenFeatureResolutionError(code: .general, message: "Unauthorized."),
reason: .error
)
case .forbidden:
self = OpenFeatureResolution(
value: defaultValue,
error: OpenFeatureResolutionError(code: .general, message: "Forbidden."),
reason: .error
)
case .tooManyRequests(let responsePayload):
let message: String
if let retryAfter = responsePayload.headers.retryAfter {
let dateString = retryAfter.ISO8601Format(.iso8601WithTimeZone())
message = #"Too many requests. Retry after "\#(dateString)"."#
} else {
message = "Too many requests."
}
self = OpenFeatureResolution(
value: defaultValue,
error: OpenFeatureResolutionError(code: .general, message: message),
reason: .error
)
case .internalServerError(let internalServerError):
switch internalServerError.body {
case .json(let responsePayload):
self = OpenFeatureResolution(
value: defaultValue,
error: OpenFeatureResolutionError(code: .general, message: responsePayload.errorDetails),
reason: .error
)
}
case .undocumented(let statusCode, _):
self = OpenFeatureResolution(
value: defaultValue,
error: OpenFeatureResolutionError(
code: .general,
message: #"Received unexpected response status code "\#(statusCode)"."#
),
reason: .error
)
}
}
}

extension OpenFeatureResolution<Bool> {
package init(
_ response: Components.Schemas.ServerEvaluationSuccess,
defaultValue: Bool
) {
let variant = response.value1.value1.variant
let flagMetadata = response.value1.value1.metadata.toFlagMetadata()

switch response.value1.value2 {
case .BooleanFlag(let boolContainer):
self.init(
value: boolContainer.value,
error: nil,
reason: response.value1.value1.reason.map(OpenFeatureResolutionReason.init),
variant: variant,
flagMetadata: flagMetadata
)
default:
self.init(
value: defaultValue,
error: OpenFeatureResolutionError(
code: .typeMismatch,
message: response.value1.value2.typeMismatchErrorMessage(expectedType: "\(Value.self)")
),
reason: .error,
variant: variant,
flagMetadata: flagMetadata
)
}
}
}

extension Components.Schemas.EvaluationSuccess.Value1Payload.MetadataPayload? {
package func toFlagMetadata() -> [String: OpenFeatureFlagMetadataValue] {
self?.additionalProperties.mapValues(OpenFeatureFlagMetadataValue.init) ?? [:]
}
}

extension OpenFeatureFlagMetadataValue {
package init(
_ payload: Components.Schemas.EvaluationSuccess.Value1Payload.MetadataPayload.AdditionalPropertiesPayload
) {
self =
switch payload {
case .case1(let value): .bool(value)
case .case2(let value): .string(value)
case .case3(let value): .double(value)
}
}
}

extension Components.Schemas.EvaluationSuccess.Value2Payload {
package var typeDescription: String {
switch self {
case .BooleanFlag: "Bool"
case .StringFlag: "String"
case .IntegerFlag: "Int"
case .FloatFlag: "Double"
case .ObjectFlag: "Object"
}
}

func typeMismatchErrorMessage(expectedType: String) -> String {
#"Expected flag value of type "\#(expectedType)" but received "\#(typeDescription)"."#
}
}
Loading