diff --git a/IntegrationTests/Tests/Integration/OFREPIntegrationTests.swift b/IntegrationTests/Tests/Integration/OFREPIntegrationTests.swift index b8e173b..3f2952d 100644 --- a/IntegrationTests/Tests/Integration/OFREPIntegrationTests.swift +++ b/IntegrationTests/Tests/Integration/OFREPIntegrationTests.swift @@ -88,12 +88,231 @@ struct OFREPIntegrationTests { reason: .error, variant: "a" ) - let flag = "static-a-b" + let flag = "static-a" await #expect(provider.resolution(of: flag, defaultValue: defaultValue, context: nil) == resolution) } } } + @Suite("String Flag Resolution") + struct StringResolutionTests { + @Test("Static", arguments: [("static-a", "a", "value-a"), ("static-b", "b", "value-b")]) + func staticBool(flag: String, variant: String, expectedValue: String) async throws { + let provider = OFREPProvider(serverURL: URL(string: "http://localhost:8016")!) + + try await withOFREPProvider(provider) { + let resolution = OpenFeatureResolution( + value: expectedValue, + error: nil, + reason: .static, + variant: variant, + flagMetadata: [:] + ) + await #expect(provider.resolution(of: flag, defaultValue: "default", context: nil) == resolution) + } + } + + @Test("Targeting Match") + func targetingMatch() async throws { + let provider = OFREPProvider(serverURL: URL(string: "http://localhost:8016")!) + + try await withOFREPProvider(provider) { + let resolution = OpenFeatureResolution( + value: "value-b", + error: nil, + reason: .targetingMatch, + variant: "b", + flagMetadata: [:] + ) + let flag = "targeting-b" + let context = OpenFeatureEvaluationContext(targetingKey: "swift") + await #expect(provider.resolution(of: flag, defaultValue: "default", context: context) == resolution) + } + } + + @Test("No Targeting Match") + func noTargetingMatch() async throws { + let provider = OFREPProvider(serverURL: URL(string: "http://localhost:8016")!) + + try await withOFREPProvider(provider) { + let resolution = OpenFeatureResolution( + value: "value-a", + error: nil, + reason: .default, + variant: "a", + flagMetadata: [:] + ) + let flag = "targeting-b" + await #expect(provider.resolution(of: flag, defaultValue: "default", context: nil) == resolution) + } + } + + @Test("Type mismatch") + func typeMismatch() async throws { + let provider = OFREPProvider(serverURL: URL(string: "http://localhost:8016")!) + + try await withOFREPProvider(provider) { + let resolution = OpenFeatureResolution( + value: "default", + error: OpenFeatureResolutionError( + code: .typeMismatch, + message: #"Expected flag value of type "String" but received "Bool"."# + ), + reason: .error, + variant: "on" + ) + let flag = "static-on" + await #expect(provider.resolution(of: flag, defaultValue: "default", context: nil) == resolution) + } + } + } + + @Suite("Int Flag Resolution") + struct IntResolutionTests { + @Test("Static", arguments: [("static-negative-42", "a", -42), ("static-positive-42", "b", 42)]) + func staticBool(flag: String, variant: String, expectedValue: Int) async throws { + let provider = OFREPProvider(serverURL: URL(string: "http://localhost:8016")!) + + try await withOFREPProvider(provider) { + let resolution = OpenFeatureResolution( + value: expectedValue, + error: nil, + reason: .static, + variant: variant, + flagMetadata: [:] + ) + await #expect(provider.resolution(of: flag, defaultValue: 0, context: nil) == resolution) + } + } + + @Test("Targeting Match") + func targetingMatch() async throws { + let provider = OFREPProvider(serverURL: URL(string: "http://localhost:8016")!) + + try await withOFREPProvider(provider) { + let resolution = OpenFeatureResolution( + value: 42, + error: nil, + reason: .targetingMatch, + variant: "b", + flagMetadata: [:] + ) + let flag = "targeting-42" + let context = OpenFeatureEvaluationContext(targetingKey: "swift") + await #expect(provider.resolution(of: flag, defaultValue: 0, context: context) == resolution) + } + } + + @Test("No Targeting Match") + func noTargetingMatch() async throws { + let provider = OFREPProvider(serverURL: URL(string: "http://localhost:8016")!) + + try await withOFREPProvider(provider) { + let resolution = OpenFeatureResolution( + value: -42, + error: nil, + reason: .default, + variant: "a", + flagMetadata: [:] + ) + let flag = "targeting-42" + await #expect(provider.resolution(of: flag, defaultValue: 0, context: nil) == resolution) + } + } + + @Test("Type mismatch") + func typeMismatch() async throws { + let provider = OFREPProvider(serverURL: URL(string: "http://localhost:8016")!) + + try await withOFREPProvider(provider) { + let resolution = OpenFeatureResolution( + value: 42, + error: OpenFeatureResolutionError( + code: .typeMismatch, + message: #"Expected flag value of type "Int" but received "String"."# + ), + reason: .error, + variant: "a" + ) + let flag = "static-a" + await #expect(provider.resolution(of: flag, defaultValue: 42, context: nil) == resolution) + } + } + } + + @Suite("Double Flag Resolution") + struct DoubleResolutionTests { + @Test("Static", arguments: [("static-negative-42.123", "a", -42.123), ("static-positive-42.123", "b", 42.123)]) + func staticBool(flag: String, variant: String, expectedValue: Double) async throws { + let provider = OFREPProvider(serverURL: URL(string: "http://localhost:8016")!) + + try await withOFREPProvider(provider) { + let resolution = OpenFeatureResolution( + value: expectedValue, + error: nil, + reason: .static, + variant: variant, + flagMetadata: [:] + ) + await #expect(provider.resolution(of: flag, defaultValue: 0.0, context: nil) == resolution) + } + } + + @Test("Targeting Match") + func targetingMatch() async throws { + let provider = OFREPProvider(serverURL: URL(string: "http://localhost:8016")!) + + try await withOFREPProvider(provider) { + let resolution = OpenFeatureResolution( + value: 42.123, + error: nil, + reason: .targetingMatch, + variant: "b", + flagMetadata: [:] + ) + let flag = "targeting-42.123" + let context = OpenFeatureEvaluationContext(targetingKey: "swift") + await #expect(provider.resolution(of: flag, defaultValue: 0.0, context: context) == resolution) + } + } + + @Test("No Targeting Match") + func noTargetingMatch() async throws { + let provider = OFREPProvider(serverURL: URL(string: "http://localhost:8016")!) + + try await withOFREPProvider(provider) { + let resolution = OpenFeatureResolution( + value: -42.123, + error: nil, + reason: .default, + variant: "a", + flagMetadata: [:] + ) + let flag = "targeting-42.123" + await #expect(provider.resolution(of: flag, defaultValue: 0.0, context: nil) == resolution) + } + } + + @Test("Type mismatch") + func typeMismatch() async throws { + let provider = OFREPProvider(serverURL: URL(string: "http://localhost:8016")!) + + try await withOFREPProvider(provider) { + let resolution = OpenFeatureResolution( + value: 42.123, + error: OpenFeatureResolutionError( + code: .typeMismatch, + message: #"Expected flag value of type "Double" but received "String"."# + ), + reason: .error, + variant: "a" + ) + let flag = "static-a" + await #expect(provider.resolution(of: flag, defaultValue: 42.123, context: nil) == resolution) + } + } + } + @Test("Flag not found", arguments: [true, false]) func flagNotFound(defaultValue: Bool) async throws { let provider = OFREPProvider(serverURL: URL(string: "http://localhost:8016")!) diff --git a/IntegrationTests/integration.flagd.json b/IntegrationTests/integration.flagd.json index 46f64dd..713a1e5 100644 --- a/IntegrationTests/integration.flagd.json +++ b/IntegrationTests/integration.flagd.json @@ -38,13 +38,116 @@ ] } }, - "static-a-b": { + "static-a": { "state": "ENABLED", "variants": { - "a": "a", - "b": "b" + "a": "value-a", + "b": "value-b" }, "defaultVariant": "a" + }, + "static-b": { + "state": "ENABLED", + "variants": { + "a": "value-a", + "b": "value-b" + }, + "defaultVariant": "b" + }, + "targeting-b": { + "state": "ENABLED", + "variants": { + "a": "value-a", + "b": "value-b" + }, + "defaultVariant": "a", + "targeting": { + "if": [ + { + "===": [ + { + "var": "targetingKey" + }, + "swift" + ] + }, + "b" + ] + } + }, + "static-negative-42": { + "state": "ENABLED", + "variants": { + "a": -42, + "b": 42 + }, + "defaultVariant": "a" + }, + "static-positive-42": { + "state": "ENABLED", + "variants": { + "a": -42, + "b": 42 + }, + "defaultVariant": "b" + }, + "targeting-42": { + "state": "ENABLED", + "variants": { + "a": -42, + "b": 42 + }, + "defaultVariant": "a", + "targeting": { + "if": [ + { + "===": [ + { + "var": "targetingKey" + }, + "swift" + ] + }, + "b" + ] + } + }, + "static-negative-42.123": { + "state": "ENABLED", + "variants": { + "a": -42.123, + "b": 42.123 + }, + "defaultVariant": "a" + }, + "static-positive-42.123": { + "state": "ENABLED", + "variants": { + "a": -42.123, + "b": 42.123 + }, + "defaultVariant": "b" + }, + "targeting-42.123": { + "state": "ENABLED", + "variants": { + "a": -42.123, + "b": 42.123 + }, + "defaultVariant": "a", + "targeting": { + "if": [ + { + "===": [ + { + "var": "targetingKey" + }, + "swift" + ] + }, + "b" + ] + } } } } diff --git a/Sources/OFREP/OFREPProvider.swift b/Sources/OFREP/OFREPProvider.swift index 4ea6a0a..5be5a7c 100644 --- a/Sources/OFREP/OFREPProvider.swift +++ b/Sources/OFREP/OFREPProvider.swift @@ -34,6 +34,54 @@ public struct OFREPProvider: OpenFeatureProvide defaultValue: Bool, context: OpenFeatureEvaluationContext? ) async -> OpenFeatureResolution { + await resolution(of: flag, defaultValue: defaultValue, context: context) { output in + OpenFeatureResolution(output, defaultValue: defaultValue) + } + } + + public func resolution( + of flag: String, + defaultValue: String, + context: OpenFeatureEvaluationContext? + ) async -> OpenFeatureResolution { + await resolution(of: flag, defaultValue: defaultValue, context: context) { output in + OpenFeatureResolution(output, defaultValue: defaultValue) + } + } + + public func resolution( + of flag: String, + defaultValue: Int, + context: OpenFeature.OpenFeatureEvaluationContext? + ) async -> OpenFeature.OpenFeatureResolution { + await resolution(of: flag, defaultValue: defaultValue, context: context) { output in + OpenFeatureResolution(output, defaultValue: defaultValue) + } + } + + public func resolution( + of flag: String, + defaultValue: Double, + context: OpenFeature.OpenFeatureEvaluationContext? + ) async -> OpenFeature.OpenFeatureResolution { + await resolution(of: flag, defaultValue: defaultValue, context: context) { output in + OpenFeatureResolution(output, defaultValue: defaultValue) + } + } + + public func run() async throws { + try await gracefulShutdown() + logger.debug("Shutting down.") + try await transport.shutdownGracefully() + logger.debug("Shut down.") + } + + private func resolution( + of flag: String, + defaultValue: Value, + context: OpenFeatureEvaluationContext?, + transformServerResponse: (Operations.EvaluateFlag.Output) -> OpenFeatureResolution + ) async -> OpenFeatureResolution { let request: Components.Schemas.EvaluationRequest do { request = try Components.Schemas.EvaluationRequest(flag: flag, defaultValue: defaultValue, context: context) @@ -48,7 +96,7 @@ public struct OFREPProvider: OpenFeatureProvide headers: .init(accept: [.init(contentType: .json)]), body: .json(request) ) - return OpenFeatureResolution(response, defaultValue: defaultValue) + return transformServerResponse(response) } catch let error as ClientError { throw error.underlyingError } @@ -60,11 +108,4 @@ public struct OFREPProvider: OpenFeatureProvide ) } } - - public func run() async throws { - try await gracefulShutdown() - logger.debug("Shutting down.") - try await transport.shutdownGracefully() - logger.debug("Shut down.") - } } diff --git a/Sources/OFREP/OpenFeatureResolution+OFREP.swift b/Sources/OFREP/OpenFeatureResolution+OFREP.swift index c69f836..d281e1f 100644 --- a/Sources/OFREP/OpenFeatureResolution+OFREP.swift +++ b/Sources/OFREP/OpenFeatureResolution+OFREP.swift @@ -15,11 +15,73 @@ import OpenFeature extension OpenFeatureResolution { package init(_ response: Operations.EvaluateFlag.Output, defaultValue: Bool) { + self.init(response, defaultValue: defaultValue) { response in + guard case .BooleanFlag(let flag) = response else { return nil } + return flag.value + } + } +} + +extension OpenFeatureResolution { + package init(_ response: Operations.EvaluateFlag.Output, defaultValue: String) { + self.init(response, defaultValue: defaultValue) { response in + guard case .StringFlag(let flag) = response else { return nil } + return flag.value + } + } +} + +extension OpenFeatureResolution { + package init(_ response: Operations.EvaluateFlag.Output, defaultValue: Int) { + self.init(response, defaultValue: defaultValue) { response in + guard case .IntegerFlag(let flag) = response else { return nil } + return flag.value + } + } +} + +extension OpenFeatureResolution { + package init(_ response: Operations.EvaluateFlag.Output, defaultValue: Double) { + self.init(response, defaultValue: defaultValue) { response in + guard case .FloatFlag(let flag) = response else { return nil } + return flag.value + } + } +} + +extension OpenFeatureResolution { + package init( + _ response: Operations.EvaluateFlag.Output, + defaultValue: Value, + transformSuccessfulResponse: (Components.Schemas.EvaluationSuccess.Value2Payload) -> Value? + ) { switch response { case .ok(let ok): switch ok.body { - case .json(let responsePayload): - self = OpenFeatureResolution(responsePayload, defaultValue: defaultValue) + case .json(let jsonPayload): + let variant = jsonPayload.value1.value1.variant + let flagMetadata = jsonPayload.value1.value1.metadata.toFlagMetadata() + + if let value = transformSuccessfulResponse(jsonPayload.value1.value2) { + self = OpenFeatureResolution( + value: value, + error: nil, + reason: jsonPayload.value1.value1.reason.map(OpenFeatureResolutionReason.init), + variant: variant, + flagMetadata: flagMetadata + ) + } else { + self = OpenFeatureResolution( + value: defaultValue, + error: OpenFeatureResolutionError( + code: .typeMismatch, + message: jsonPayload.value1.value2.typeMismatchErrorMessage(expectedType: "\(Value.self)") + ), + reason: .error, + variant: variant, + flagMetadata: flagMetadata + ) + } } case .badRequest(let badRequest): switch badRequest.body { @@ -92,38 +154,6 @@ extension OpenFeatureResolution { } } -extension OpenFeatureResolution { - 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?.value1.additionalProperties.mapValues(OpenFeatureFlagMetadataValue.init) ?? [:] diff --git a/Tests/OFREPTests/OFREPProviderTests.swift b/Tests/OFREPTests/OFREPProviderTests.swift index 3808945..16e826b 100644 --- a/Tests/OFREPTests/OFREPProviderTests.swift +++ b/Tests/OFREPTests/OFREPProviderTests.swift @@ -109,30 +109,123 @@ final class OFREPProviderTests { try #expect(JSONDecoder().decode(Components.Schemas.EvaluationRequest.self, from: bodyBytes) == payload) } - @Test("Returns successful server evaluation", arguments: [true, false]) - func returnsSuccessfulServerEvaluation(value: Bool) async throws { - let transport = ClosureOFREPClientTransport { - ( - HTTPResponse(status: .ok), - HTTPBody( - """ - { - "value": \(value), - "reason": "STATIC", - "variant": "a" - } - """ + @Suite("Bool Evaluation") + struct BoolEvaluationTests { + @Test("Returns successful server evaluation", arguments: [true, false]) + func success(value: Bool) async throws { + let transport = ClosureOFREPClientTransport { + ( + HTTPResponse(status: .ok), + HTTPBody( + """ + { + "value": \(value), + "reason": "STATIC", + "variant": "a" + } + """ + ) ) - ) + } + let provider = OFREPProvider(transport: transport) + + let resolution = await provider.resolution(of: "test-flag", defaultValue: !value, context: nil) + + #expect(resolution.value == value) + #expect(resolution.error == nil) + #expect(resolution.reason == .static) + #expect(resolution.variant == "a") } - let provider = OFREPProvider(transport: transport) + } - let resolution = await provider.resolution(of: "test-flag", defaultValue: !value, context: nil) + @Suite("String Evaluation") + struct StringEvaluationTests { + @Test("Returns successful server evaluation", arguments: ["Hello", ""]) + func success(value: String) async throws { + let transport = ClosureOFREPClientTransport { + ( + HTTPResponse(status: .ok), + HTTPBody( + """ + { + "value": "\(value)", + "reason": "STATIC", + "variant": "a" + } + """ + ) + ) + } + let provider = OFREPProvider(transport: transport) - #expect(resolution.value == value) - #expect(resolution.error == nil) - #expect(resolution.reason == .static) - #expect(resolution.variant == "a") + let resolution = await provider.resolution(of: "test-flag", defaultValue: "default", context: nil) + + #expect(resolution.value == value) + #expect(resolution.error == nil) + #expect(resolution.reason == .static) + #expect(resolution.variant == "a") + } + } + + @Suite("Int Evaluation") + struct IntEvaluationTests { + @Test("Returns successful server evaluation", arguments: [Int.min, 0, Int.max]) + func success(value: Int) async throws { + let transport = ClosureOFREPClientTransport { + ( + HTTPResponse(status: .ok), + HTTPBody( + """ + { + "value": \(value), + "reason": "STATIC", + "variant": "a" + } + """ + ) + ) + } + let provider = OFREPProvider(transport: transport) + + let resolution = await provider.resolution(of: "test-flag", defaultValue: 42, context: nil) + + #expect(resolution.value == value) + #expect(resolution.error == nil) + #expect(resolution.reason == .static) + #expect(resolution.variant == "a") + } + } + + @Suite("Double Evaluation") + struct DoubleEvaluationTests { + @Test( + "Returns successful server evaluation", + arguments: [-Double.greatestFiniteMagnitude, 42.123, Double.greatestFiniteMagnitude] + ) + func success(value: Double) async throws { + let transport = ClosureOFREPClientTransport { + ( + HTTPResponse(status: .ok), + HTTPBody( + """ + { + "value": \(value), + "reason": "STATIC", + "variant": "a" + } + """ + ) + ) + } + let provider = OFREPProvider(transport: transport) + + let resolution = await provider.resolution(of: "test-flag", defaultValue: 42.0, context: nil) + + #expect(resolution.value == value) + #expect(resolution.error == nil) + #expect(resolution.reason == .static) + #expect(resolution.variant == "a") + } } @Test("Returns default value when transport fails", arguments: [true, false]) diff --git a/Tests/OFREPTests/OpenFeatureResolutionDecodingTests.swift b/Tests/OFREPTests/OpenFeatureResolutionDecodingTests.swift index c9fca11..ad62216 100644 --- a/Tests/OFREPTests/OpenFeatureResolutionDecodingTests.swift +++ b/Tests/OFREPTests/OpenFeatureResolutionDecodingTests.swift @@ -57,142 +57,239 @@ struct OpenFeatureResolutionDecodingTests { #expect(OpenFeatureResolution(response, defaultValue: !value) == resolution) } - @Test("Bad request", arguments: ["Targeting key is required.", nil]) - func badRequest(message: String?) { - let response = Operations.EvaluateFlag.Output.badRequest( - .init( - body: .json( - .init( - key: "flag", - errorCode: .targetingKeyMissing, - errorDetails: message + @Test("Type mismatch") + func typeMismatch() { + let response = Components.Schemas.ServerEvaluationSuccess( + value1: .init( + value1: .init( + key: "flag", + reason: "TARGETING_MATCH", + variant: "b", + metadata: .init( + value1: .init(additionalProperties: ["foo": .case2("bar")]), + value2: "", + value3: "" ) - ) - ) + ), + value2: .StringFlag(.init(value: "💩")) + ), + value2: .init(cacheable: nil) ) - let resolution = OpenFeatureResolution( + let expectedResolution = OpenFeatureResolution( value: true, - error: OpenFeatureResolutionError(code: .targetingKeyMissing, message: message), - reason: .error + error: OpenFeatureResolutionError( + code: .typeMismatch, + message: #"Expected flag value of type "Bool" but received "String"."# + ), + reason: .error, + variant: "b", + flagMetadata: ["foo": .string("bar")] ) - #expect(OpenFeatureResolution(response, defaultValue: true) == resolution) + let resolution = OpenFeatureResolution( + Operations.EvaluateFlag.Output.ok(.init(body: .json(response))), + defaultValue: true + ) + + #expect(resolution == expectedResolution) } + } - @Test("Not found", arguments: ["Flag not found.", nil]) - func notFound(message: String?) { - let response = Operations.EvaluateFlag.Output.notFound( + @Suite("String") + struct StringResolutionDecodingTests { + @Test("Success", arguments: ["Hello", ""]) + func success(value: String) { + let response = Operations.EvaluateFlag.Output.ok( .init( body: .json( - .init( - key: "flag", - errorCode: .flagNotFound, - errorDetails: message + Components.Schemas.ServerEvaluationSuccess( + value1: .init( + value1: .init( + key: "flag", + reason: "TARGETING_MATCH", + variant: "b", + metadata: .init( + value1: .init(additionalProperties: ["foo": .case2("bar")]), + value2: "", + value3: "" + ) + ), + value2: .StringFlag(.init(value: value)) + ), + value2: .init(cacheable: nil) ) ) ) ) let resolution = OpenFeatureResolution( - value: false, - error: OpenFeatureResolutionError(code: .flagNotFound, message: message), - reason: .error + value: value, + error: nil, + reason: .targetingMatch, + variant: "b", + flagMetadata: ["foo": .string("bar")] ) - #expect(OpenFeatureResolution(response, defaultValue: false) == resolution) + #expect(OpenFeatureResolution(response, defaultValue: "default") == resolution) } - @Test("Unauthorized") - func unauthorized() throws { - let response = Operations.EvaluateFlag.Output.unauthorized(.init()) - - let resolution = OpenFeatureResolution( - value: false, - error: OpenFeatureResolutionError(code: .general, message: "Unauthorized."), - reason: .error + @Test("Type mismatch") + func typeMismatch() { + let response = Components.Schemas.ServerEvaluationSuccess( + value1: .init( + value1: .init( + key: "flag", + reason: "TARGETING_MATCH", + variant: "b", + metadata: .init( + value1: .init(additionalProperties: ["foo": .case2("bar")]), + value2: "", + value3: "" + ) + ), + value2: .BooleanFlag(.init(value: true)) + ), + value2: .init(cacheable: nil) ) - #expect(OpenFeatureResolution(response, defaultValue: false) == resolution) - } - - @Test("Forbidden") - func forbidden() throws { - let response = Operations.EvaluateFlag.Output.forbidden(.init()) + let expectedResolution = OpenFeatureResolution( + value: "Hello", + error: OpenFeatureResolutionError( + code: .typeMismatch, + message: #"Expected flag value of type "String" but received "Bool"."# + ), + reason: .error, + variant: "b", + flagMetadata: ["foo": .string("bar")] + ) let resolution = OpenFeatureResolution( - value: false, - error: OpenFeatureResolutionError(code: .general, message: "Forbidden."), - reason: .error + Operations.EvaluateFlag.Output.ok(.init(body: .json(response))), + defaultValue: "Hello" ) - #expect(OpenFeatureResolution(response, defaultValue: false) == resolution) + #expect(resolution == expectedResolution) } + } - @Test("Too many requests without retry date") - func tooManyRequests() throws { - let response = Operations.EvaluateFlag.Output.tooManyRequests(.init()) + @Suite("Int") + struct IntResolutionDecodingTests { + @Test("Success", arguments: [Int.min, 0, Int.max]) + func success(value: Int) { + let response = Operations.EvaluateFlag.Output.ok( + .init( + body: .json( + Components.Schemas.ServerEvaluationSuccess( + value1: .init( + value1: .init( + key: "flag", + reason: "TARGETING_MATCH", + variant: "b", + metadata: .init( + value1: .init(additionalProperties: ["foo": .case2("bar")]), + value2: "", + value3: "" + ) + ), + value2: .IntegerFlag(.init(value: value)) + ), + value2: .init(cacheable: nil) + ) + ) + ) + ) let resolution = OpenFeatureResolution( - value: false, - error: OpenFeatureResolutionError(code: .general, message: "Too many requests."), - reason: .error + value: value, + error: nil, + reason: .targetingMatch, + variant: "b", + flagMetadata: ["foo": .string("bar")] ) - #expect(OpenFeatureResolution(response, defaultValue: false) == resolution) + #expect(OpenFeatureResolution(response, defaultValue: 42) == resolution) } - @Test("Too many requests with retry date") - func tooManyRequestsWithRetryDate() throws { - let response = Operations.EvaluateFlag.Output.tooManyRequests( - .init(headers: .init(retryAfter: Date(timeIntervalSince1970: 1_737_935_656))) + @Test("Type mismatch") + func typeMismatch() { + let response = Components.Schemas.ServerEvaluationSuccess( + value1: .init( + value1: .init( + key: "flag", + reason: "TARGETING_MATCH", + variant: "b", + metadata: .init( + value1: .init(additionalProperties: ["foo": .case2("bar")]), + value2: "", + value3: "" + ) + ), + value2: .StringFlag(.init(value: "💩")) + ), + value2: .init(cacheable: nil) ) - let resolution = OpenFeatureResolution( - value: false, + let expectedResolution = OpenFeatureResolution( + value: 42, error: OpenFeatureResolutionError( - code: .general, - message: #"Too many requests. Retry after "2025-01-26T23:54:16Z"."# + code: .typeMismatch, + message: #"Expected flag value of type "Int" but received "String"."# ), - reason: .error - ) - - #expect(OpenFeatureResolution(response, defaultValue: false) == resolution) - } - - @Test("Internal server error", arguments: ["Database connection failed.", nil]) - func internalServerError(message: String?) throws { - let response = Operations.EvaluateFlag.Output.internalServerError( - .init(body: .json(.init(errorDetails: message))) + reason: .error, + variant: "b", + flagMetadata: ["foo": .string("bar")] ) let resolution = OpenFeatureResolution( - value: false, - error: OpenFeatureResolutionError(code: .general, message: message), - reason: .error + Operations.EvaluateFlag.Output.ok(.init(body: .json(response))), + defaultValue: 42 ) - #expect(OpenFeatureResolution(response, defaultValue: false) == resolution) + #expect(resolution == expectedResolution) } + } - @Test("Unknown status code") - func internalServerError() throws { - let response = Operations.EvaluateFlag.Output.undocumented(statusCode: 418, .init()) + @Suite("Double") + struct DoubleResolutionDecodingTests { + @Test("Success", arguments: [-Double.greatestFiniteMagnitude, 42.123, Double.greatestFiniteMagnitude]) + func success(value: Double) { + let response = Operations.EvaluateFlag.Output.ok( + .init( + body: .json( + Components.Schemas.ServerEvaluationSuccess( + value1: .init( + value1: .init( + key: "flag", + reason: "TARGETING_MATCH", + variant: "b", + metadata: .init( + value1: .init(additionalProperties: ["foo": .case2("bar")]), + value2: "", + value3: "" + ) + ), + value2: .FloatFlag(.init(value: value)) + ), + value2: .init(cacheable: nil) + ) + ) + ) + ) let resolution = OpenFeatureResolution( - value: false, - error: OpenFeatureResolutionError( - code: .general, - message: #"Received unexpected response status code "418"."# - ), - reason: .error + value: value, + error: nil, + reason: .targetingMatch, + variant: "b", + flagMetadata: ["foo": .string("bar")] ) - #expect(OpenFeatureResolution(response, defaultValue: false) == resolution) + #expect(OpenFeatureResolution(response, defaultValue: 0.0) == resolution) } - @Test("Type mismatch", arguments: [true, false]) - func typeMismatch(defaultValue: Bool) { + @Test("Type mismatch") + func typeMismatch() { let response = Components.Schemas.ServerEvaluationSuccess( value1: .init( value1: .init( @@ -210,18 +307,23 @@ struct OpenFeatureResolutionDecodingTests { value2: .init(cacheable: nil) ) - let resolution = OpenFeatureResolution( - value: defaultValue, + let expectedResolution = OpenFeatureResolution( + value: 42.0, error: OpenFeatureResolutionError( code: .typeMismatch, - message: #"Expected flag value of type "Bool" but received "String"."# + message: #"Expected flag value of type "Double" but received "String"."# ), reason: .error, variant: "b", flagMetadata: ["foo": .string("bar")] ) - #expect(OpenFeatureResolution(response, defaultValue: defaultValue) == resolution) + let resolution = OpenFeatureResolution( + Operations.EvaluateFlag.Output.ok(.init(body: .json(response))), + defaultValue: 42.0 + ) + + #expect(resolution == expectedResolution) } } @@ -241,6 +343,140 @@ struct OpenFeatureResolutionDecodingTests { #expect(value.typeDescription == key) } + @Test("Bad request", arguments: ["Targeting key is required.", nil]) + func badRequest(message: String?) { + let response = Operations.EvaluateFlag.Output.badRequest( + .init( + body: .json( + .init( + key: "flag", + errorCode: .targetingKeyMissing, + errorDetails: message + ) + ) + ) + ) + + let resolution = OpenFeatureResolution( + value: true, + error: OpenFeatureResolutionError(code: .targetingKeyMissing, message: message), + reason: .error + ) + + #expect(OpenFeatureResolution(response, defaultValue: true) == resolution) + } + + @Test("Not found", arguments: ["Flag not found.", nil]) + func notFound(message: String?) { + let response = Operations.EvaluateFlag.Output.notFound( + .init( + body: .json( + .init( + key: "flag", + errorCode: .flagNotFound, + errorDetails: message + ) + ) + ) + ) + + let resolution = OpenFeatureResolution( + value: false, + error: OpenFeatureResolutionError(code: .flagNotFound, message: message), + reason: .error + ) + + #expect(OpenFeatureResolution(response, defaultValue: false) == resolution) + } + + @Test("Unauthorized") + func unauthorized() throws { + let response = Operations.EvaluateFlag.Output.unauthorized(.init()) + + let resolution = OpenFeatureResolution( + value: false, + error: OpenFeatureResolutionError(code: .general, message: "Unauthorized."), + reason: .error + ) + + #expect(OpenFeatureResolution(response, defaultValue: false) == resolution) + } + + @Test("Forbidden") + func forbidden() throws { + let response = Operations.EvaluateFlag.Output.forbidden(.init()) + + let resolution = OpenFeatureResolution( + value: false, + error: OpenFeatureResolutionError(code: .general, message: "Forbidden."), + reason: .error + ) + + #expect(OpenFeatureResolution(response, defaultValue: false) == resolution) + } + + @Test("Too many requests without retry date") + func tooManyRequests() throws { + let response = Operations.EvaluateFlag.Output.tooManyRequests(.init()) + + let resolution = OpenFeatureResolution( + value: false, + error: OpenFeatureResolutionError(code: .general, message: "Too many requests."), + reason: .error + ) + + #expect(OpenFeatureResolution(response, defaultValue: false) == resolution) + } + + @Test("Too many requests with retry date") + func tooManyRequestsWithRetryDate() throws { + let response = Operations.EvaluateFlag.Output.tooManyRequests( + .init(headers: .init(retryAfter: Date(timeIntervalSince1970: 1_737_935_656))) + ) + + let resolution = OpenFeatureResolution( + value: false, + error: OpenFeatureResolutionError( + code: .general, + message: #"Too many requests. Retry after "2025-01-26T23:54:16Z"."# + ), + reason: .error + ) + + #expect(OpenFeatureResolution(response, defaultValue: false) == resolution) + } + + @Test("Internal server error", arguments: ["Database connection failed.", nil]) + func internalServerError(message: String?) throws { + let response = Operations.EvaluateFlag.Output.internalServerError( + .init(body: .json(.init(errorDetails: message))) + ) + + let resolution = OpenFeatureResolution( + value: false, + error: OpenFeatureResolutionError(code: .general, message: message), + reason: .error + ) + + #expect(OpenFeatureResolution(response, defaultValue: false) == resolution) + } + + @Test("Unknown status code") + func internalServerError() throws { + let response = Operations.EvaluateFlag.Output.undocumented(statusCode: 418, .init()) + + let resolution = OpenFeatureResolution( + value: false, + error: OpenFeatureResolutionError( + code: .general, + message: #"Received unexpected response status code "418"."# + ), + reason: .error + ) + + #expect(OpenFeatureResolution(response, defaultValue: false) == resolution) + } + @Suite("Flag metadata") struct FlagMetadataDecodingTests { @Test("Bool value", arguments: [true, false])