Skip to content

Commit a8c60fd

Browse files
committed
Implement bool flag resolution
1 parent 30d301b commit a8c60fd

5 files changed

+698
-4
lines changed

Sources/OFREP/OFREPProvider.swift

+30-2
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,54 @@
1111
//
1212
//===----------------------------------------------------------------------===//
1313

14+
import Foundation
1415
import Logging
16+
import OpenAPIRuntime
1517
import OpenFeature
1618
import ServiceLifecycle
1719

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

24-
package init(transport: Transport) {
27+
package init(serverURL: URL, transport: Transport) {
2528
self.transport = transport
29+
self.client = Client(serverURL: serverURL, transport: transport)
2630
}
2731

2832
public func resolution(
2933
of flag: String,
3034
defaultValue: Bool,
3135
context: OpenFeatureEvaluationContext?
3236
) async -> OpenFeatureResolution<Bool> {
33-
OpenFeatureResolution(value: defaultValue)
37+
let request: Components.Schemas.EvaluationRequest
38+
do {
39+
request = try Components.Schemas.EvaluationRequest(flag: flag, defaultValue: defaultValue, context: context)
40+
} catch {
41+
return error.resolution
42+
}
43+
44+
do {
45+
do {
46+
let response = try await client.postOfrepV1EvaluateFlagsKey(
47+
path: .init(key: flag),
48+
headers: .init(accept: [.init(contentType: .json)]),
49+
body: .json(request)
50+
)
51+
return OpenFeatureResolution(response, defaultValue: defaultValue)
52+
} catch let error as ClientError {
53+
throw error.underlyingError
54+
}
55+
} catch {
56+
return OpenFeatureResolution(
57+
value: defaultValue,
58+
error: OpenFeatureResolutionError(code: .general, message: "\(error)"),
59+
reason: .error
60+
)
61+
}
3462
}
3563

3664
public func run() async throws {

Sources/OFREP/OpenFeatureEvaluationContext+OFREP.swift renamed to Sources/OFREP/OpenFeatureEvaluation+OFREP.swift

+31
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,37 @@ import Foundation
1515
import OpenAPIRuntime
1616
import OpenFeature
1717

18+
extension Components.Schemas.EvaluationRequest {
19+
package init<Value: OpenFeatureValue>(
20+
flag: String,
21+
defaultValue: Value,
22+
context: OpenFeatureEvaluationContext?
23+
) throws(EvaluationRequestSerializationError<Value>) {
24+
let serializedContext: Components.Schemas.Context?
25+
do {
26+
serializedContext = try context.map(Components.Schemas.Context.init)
27+
} catch {
28+
throw EvaluationRequestSerializationError(
29+
value: defaultValue,
30+
error: OpenFeatureResolutionError(code: .invalidContext, message: "\(error)"),
31+
reason: .error
32+
)
33+
}
34+
35+
self.init(context: serializedContext)
36+
}
37+
}
38+
39+
package struct EvaluationRequestSerializationError<Value: OpenFeatureValue>: Error {
40+
let value: Value
41+
let error: OpenFeatureResolutionError
42+
let reason: OpenFeatureResolutionReason
43+
44+
var resolution: OpenFeatureResolution<Value> {
45+
OpenFeatureResolution(value: value, error: error, reason: reason)
46+
}
47+
}
48+
1849
extension Components.Schemas.Context {
1950
package init(_ context: OpenFeatureEvaluationContext) throws {
2051
let additionalProperties = try OpenAPIObjectContainer(context.fields)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift OpenFeature open source project
4+
//
5+
// Copyright (c) 2025 the Swift OpenFeature project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
//
10+
// SPDX-License-Identifier: Apache-2.0
11+
//
12+
//===----------------------------------------------------------------------===//
13+
14+
import OpenFeature
15+
16+
extension OpenFeatureResolution<Bool> {
17+
package init(_ response: Operations.PostOfrepV1EvaluateFlagsKey.Output, defaultValue: Bool) {
18+
switch response {
19+
case .ok(let ok):
20+
switch ok.body {
21+
case .json(let responsePayload):
22+
self = OpenFeatureResolution(responsePayload, defaultValue: defaultValue)
23+
}
24+
case .badRequest(let badRequest):
25+
switch badRequest.body {
26+
case .json(let responsePayload):
27+
self = OpenFeatureResolution(
28+
value: defaultValue,
29+
error: .init(
30+
code: .init(rawValue: responsePayload.errorCode.rawValue),
31+
message: responsePayload.errorDetails
32+
),
33+
reason: .error
34+
)
35+
}
36+
case .notFound(let notFound):
37+
switch notFound.body {
38+
case .json(let responsePayload):
39+
self = OpenFeatureResolution(
40+
value: defaultValue,
41+
error: .init(
42+
code: .init(rawValue: responsePayload.errorCode.rawValue),
43+
message: responsePayload.errorDetails
44+
),
45+
reason: .error
46+
)
47+
}
48+
case .unauthorized:
49+
self = OpenFeatureResolution(
50+
value: defaultValue,
51+
error: OpenFeatureResolutionError(code: .general, message: "Unauthorized."),
52+
reason: .error
53+
)
54+
case .forbidden:
55+
self = OpenFeatureResolution(
56+
value: defaultValue,
57+
error: OpenFeatureResolutionError(code: .general, message: "Forbidden."),
58+
reason: .error
59+
)
60+
case .tooManyRequests(let responsePayload):
61+
let message: String
62+
if let retryAfter = responsePayload.headers.retryAfter {
63+
let dateString = retryAfter.ISO8601Format(.iso8601WithTimeZone())
64+
message = #"Too many requests. Retry after "\#(dateString)"."#
65+
} else {
66+
message = "Too many requests."
67+
}
68+
self = OpenFeatureResolution(
69+
value: defaultValue,
70+
error: OpenFeatureResolutionError(code: .general, message: message),
71+
reason: .error
72+
)
73+
case .internalServerError(let internalServerError):
74+
switch internalServerError.body {
75+
case .json(let responsePayload):
76+
self = OpenFeatureResolution(
77+
value: defaultValue,
78+
error: OpenFeatureResolutionError(code: .general, message: responsePayload.errorDetails),
79+
reason: .error
80+
)
81+
}
82+
case .undocumented(let statusCode, _):
83+
self = OpenFeatureResolution(
84+
value: defaultValue,
85+
error: OpenFeatureResolutionError(
86+
code: .general,
87+
message: #"Received unexpected response status code "\#(statusCode)"."#
88+
),
89+
reason: .error
90+
)
91+
}
92+
}
93+
}
94+
95+
extension OpenFeatureResolution<Bool> {
96+
package init(
97+
_ response: Components.Schemas.ServerEvaluationSuccess,
98+
defaultValue: Bool
99+
) {
100+
let variant = response.value1.value1.variant
101+
let flagMetadata = response.value1.value1.metadata.toFlagMetadata()
102+
103+
switch response.value1.value2 {
104+
case .BooleanFlag(let boolContainer):
105+
self.init(
106+
value: boolContainer.value,
107+
error: nil,
108+
reason: response.value1.value1.reason.map(OpenFeatureResolutionReason.init),
109+
variant: variant,
110+
flagMetadata: flagMetadata
111+
)
112+
default:
113+
self.init(
114+
value: defaultValue,
115+
error: OpenFeatureResolutionError(
116+
code: .typeMismatch,
117+
message: response.value1.value2.typeMismatchErrorMessage(expectedType: "\(Value.self)")
118+
),
119+
reason: .error,
120+
variant: variant,
121+
flagMetadata: flagMetadata
122+
)
123+
}
124+
}
125+
}
126+
127+
extension Components.Schemas.EvaluationSuccess.Value1Payload.MetadataPayload? {
128+
package func toFlagMetadata() -> [String: OpenFeatureFlagMetadataValue] {
129+
self?.additionalProperties.mapValues(OpenFeatureFlagMetadataValue.init) ?? [:]
130+
}
131+
}
132+
133+
extension OpenFeatureFlagMetadataValue {
134+
package init(
135+
_ payload: Components.Schemas.EvaluationSuccess.Value1Payload.MetadataPayload.AdditionalPropertiesPayload
136+
) {
137+
self =
138+
switch payload {
139+
case .case1(let value): .bool(value)
140+
case .case2(let value): .string(value)
141+
case .case3(let value): .double(value)
142+
}
143+
}
144+
}
145+
146+
extension Components.Schemas.EvaluationSuccess.Value2Payload {
147+
package var typeDescription: String {
148+
switch self {
149+
case .BooleanFlag: "Bool"
150+
case .StringFlag: "String"
151+
case .IntegerFlag: "Int"
152+
case .FloatFlag: "Double"
153+
case .ObjectFlag: "Object"
154+
}
155+
}
156+
157+
func typeMismatchErrorMessage(expectedType: String) -> String {
158+
#"Expected flag value of type "\#(expectedType)" but received "\#(typeDescription)"."#
159+
}
160+
}

0 commit comments

Comments
 (0)