Sources/Confidence/RemoteResolveConfidenceClient.swift (134 lines of code) (raw):
import Foundation
class RemoteConfidenceResolveClient: ConfidenceResolveClient {
private let targetingKey = "targeting_key"
private var options: ConfidenceClientOptions
private let metadata: ConfidenceMetadata
private var httpClient: HttpClient
private var applyOnResolve: Bool
init(
options: ConfidenceClientOptions,
session: URLSession? = nil,
applyOnResolve: Bool = false,
metadata: ConfidenceMetadata
) {
self.options = options
self.applyOnResolve = applyOnResolve
self.metadata = metadata
self.httpClient = NetworkClient(
session: session,
baseUrl: BaseUrlMapper.from(region: options.region),
timeoutIntervalForRequests: options.timeoutIntervalForRequest)
}
// MARK: Resolver
public func resolve(flags: [String], ctx: ConfidenceStruct) async throws -> ResolvesResult {
let request = ResolveFlagsRequest(
flags: flags.map { "flags/\($0)" },
evaluationContext: TypeMapper.convert(structure: ctx),
clientSecret: options.credentials.getSecret(),
apply: applyOnResolve,
sdk: Sdk(id: metadata.name, version: metadata.version)
)
do {
let result: HttpClientResult<ResolveFlagsResponse> =
try await self.httpClient.post(path: ":resolve", data: request)
switch result {
case .success(let successData):
guard successData.response.status == .ok else {
throw successData.response.mapStatusToError(error: successData.decodedError)
}
guard let response = successData.decodedData else {
throw ConfidenceError.parseError(message: "Unable to parse request response")
}
let resolvedValues = try response.resolvedFlags.map { resolvedFlag in
try convert(resolvedFlag: resolvedFlag)
}
return ResolvesResult(resolvedValues: resolvedValues, resolveToken: response.resolveToken)
case .failure(let errorData):
throw handleError(error: errorData)
}
}
}
public func resolve(ctx: ConfidenceStruct) async throws -> ResolvesResult {
return try await resolve(flags: [], ctx: ctx)
}
// MARK: Private
private func convert(resolvedFlag: ResolvedFlag) throws -> ResolvedValue {
guard let responseFlagSchema = resolvedFlag.flagSchema,
let responseValue = resolvedFlag.value,
!responseValue.fields.isEmpty
else {
return ResolvedValue(
value: nil,
flag: try displayName(resolvedFlag: resolvedFlag),
resolveReason: resolvedFlag.reason)
}
let value = ConfidenceValue(
structure: try TypeMapper.convert(structure: responseValue, schema: responseFlagSchema)
)
let variant = resolvedFlag.variant.isEmpty ? nil : resolvedFlag.variant
return ResolvedValue(
variant: variant,
value: value,
flag: try displayName(resolvedFlag: resolvedFlag),
resolveReason: resolvedFlag.reason
)
}
private func handleError(error: Error) -> Error {
if error is ConfidenceError {
return error
} else {
return ConfidenceError.grpcError(message: "\(error)")
}
}
}
struct ResolveFlagsRequest: Codable {
var flags: [String]
var evaluationContext: NetworkStruct
var clientSecret: String
var apply: Bool
var sdk: Sdk
}
struct ResolveFlagsResponse: Codable {
var resolvedFlags: [ResolvedFlag]
var resolveToken: String?
}
struct ResolvedFlag: Codable {
var flag: String
var value: NetworkStruct? = NetworkStruct(fields: [:])
var variant: String = ""
var flagSchema: StructFlagSchema? = StructFlagSchema(schema: [:])
var reason: ResolveReason
}
public enum ResolveReason: String, Codable, CaseIterableDefaultsLast {
case unspecified = "RESOLVE_REASON_UNSPECIFIED"
case match = "RESOLVE_REASON_MATCH"
case stale = "RESOLVE_REASON_STALE"
case noSegmentMatch = "RESOLVE_REASON_NO_SEGMENT_MATCH"
case noTreatmentMatch = "RESOLVE_REASON_NO_TREATMENT_MATCH"
case archived = "RESOLVE_REASON_FLAG_ARCHIVED"
case targetingKeyError = "RESOLVE_REASON_TARGETING_KEY_ERROR"
case error = "RESOLVE_REASON_ERROR"
case unknown
}
struct AppliedFlagRequestItem: Codable {
let flag: String
let applyTime: String
init(flag: String, applyTime: Date) {
self.flag = "flags/\(flag)"
self.applyTime = Date.backport.toISOString(date: applyTime)
}
}
struct ApplyFlagsRequest: Codable {
var flags: [AppliedFlagRequestItem]
var sendTime: String
var clientSecret: String
var resolveToken: String
var sdk: Sdk
}
struct ApplyFlagsResponse: Codable {
}
private func displayName(resolvedFlag: ResolvedFlag) throws -> String {
let flagNameComponents = resolvedFlag.flag.components(separatedBy: "/")
if flagNameComponents.count <= 1 || flagNameComponents[0] != "flags" {
throw ConfidenceError.internalError(message: "Unxpected flag name: \(resolvedFlag.flag)")
}
return resolvedFlag.flag.components(separatedBy: "/")[1]
}