Skip to content

Commit

Permalink
Implement bool flag resolution
Browse files Browse the repository at this point in the history
  • Loading branch information
slashmo committed Jan 27, 2025
1 parent 30d301b commit a8c60fd
Show file tree
Hide file tree
Showing 5 changed files with 698 additions and 4 deletions.
32 changes: 30 additions & 2 deletions Sources/OFREP/OFREPProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,54 @@
//
//===----------------------------------------------------------------------===//

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(transport: Transport) {
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> {
OpenFeatureResolution(value: defaultValue)
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 {
Expand 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

0 comments on commit a8c60fd

Please sign in to comment.