Skip to content
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

chore: Merging hooks forward #375

Merged
merged 3 commits into from
May 2, 2024
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
6 changes: 6 additions & 0 deletions .github/actions/ci/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ inputs:
ios-sim:
description: 'iOS Simulator to use for testing'
required: true
run-contract-tests:
description: 'Should the contract tests be run?'
required: true

runs:
using: composite
Expand Down Expand Up @@ -71,14 +74,17 @@ runs:
run: swift test -v

- name: Build contract tests
if: ${{ inputs.run-contract-tests == 'true' }}
shell: bash
run: make build-contract-tests

- name: Start contract tests in background
if: ${{ inputs.run-contract-tests == 'true' }}
shell: bash
run: make start-contract-test-service-bg

- name: Run contract tests
if: ${{ inputs.run-contract-tests == 'true' }}
shell: bash
# Add a brief sleep here to ensure the test service is ready to receive
# requests
Expand Down
8 changes: 6 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
name: Run CI
on:
push:
branches: [ v9 ]
branches: [ v9, 'feat/**' ]
paths-ignore:
- '**.md' # Do not need to run CI for markdown changes.
pull_request:
branches: [ v9 ]
branches: [ v9, 'feat/**' ]
paths-ignore:
- '**.md'

Expand All @@ -20,12 +20,15 @@ jobs:
- xcode-version: 15.0.1
ios-sim: 'platform=iOS Simulator,name=iPhone 15,OS=17.2'
os: macos-13
run-contract-tests: true
- xcode-version: 14.3.1
ios-sim: 'platform=iOS Simulator,name=iPhone 14,OS=16.4'
os: macos-13
run-contract-tests: true
- xcode-version: 13.4.1
ios-sim: 'platform=iOS Simulator,name=iPhone 11,OS=15.5'
os: macos-12
run-contract-tests: false

steps:
- uses: actions/checkout@v4
Expand All @@ -36,5 +39,6 @@ jobs:
with:
xcode-version: ${{ matrix.xcode-version }}
ios-sim: ${{ matrix.ios-sim }}
run-contract-tests: ${{ matrix.run-contract-tests }}

- uses: ./.github/actions/build-docs
1 change: 1 addition & 0 deletions .github/workflows/manual-publish-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ jobs:
with:
xcode-version: 14.3.1
ios-sim: 'platform=iOS Simulator,name=iPhone 14,OS=16.4'
run-contract-tests: true

- uses: ./.github/actions/build-docs

Expand Down
1 change: 1 addition & 0 deletions .github/workflows/manual-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ jobs:
with:
xcode-version: 14.3.1
ios-sim: 'platform=iOS Simulator,name=iPhone 14,OS=16.4'
run-contract-tests: true

- uses: ./.github/actions/publish
with:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ jobs:
with:
xcode-version: 14.3.1
ios-sim: 'platform=iOS Simulator,name=iPhone 14,OS=16.4'
run-contract-tests: true

- uses: ./.github/actions/build-docs
if: ${{ steps.release.outputs.releases_created == 'true' }}
Expand Down
15 changes: 14 additions & 1 deletion ContractTests/Source/Controllers/SdkController.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import Vapor
import Foundation
@testable import LaunchDarkly

// swiftlint:disable:next type_body_length
final class SdkController: RouteCollection {
private var clients: [Int: LDClient] = [:]
private var clientCounter = 0
Expand All @@ -26,14 +28,16 @@ final class SdkController: RouteCollection {
"context-comparison",
"etag-caching",
"inline-context",
"anonymous-redaction"
"anonymous-redaction",
"evaluation-hooks"
]

return StatusResponse(
name: "ios-swift-client-sdk",
capabilities: capabilities)
}

// swiftlint:disable:next function_body_length
func createClient(_ req: Request) throws -> Response {
let createInstance = try req.content.decode(CreateInstance.self)
let mobileKey = createInstance.configuration.credential
Expand Down Expand Up @@ -104,6 +108,14 @@ final class SdkController: RouteCollection {
config.applicationInfo = applicationInfo
}

if let hooksConfig = createInstance.configuration.hooks {
let hooks: [Hook] = hooksConfig.hooks.map { hookParameter in
let url = URL(string: hookParameter.callbackUri)!
return TestHook(name: hookParameter.name, callbackUrl: url, data: hookParameter.data ?? [:], errors: hookParameter.errors ?? [:])
}
config.hooks = hooks
}

let clientSide = createInstance.configuration.clientSide

if let evaluationReasons = clientSide.evaluationReasons {
Expand Down Expand Up @@ -151,6 +163,7 @@ final class SdkController: RouteCollection {
return HTTPStatus.accepted
}

// swiftlint:disable:next function_body_length
func executeCommand(_ req: Request) throws -> CommandResponse {
guard let id = req.parameters.get("id", as: Int.self)
else { throw Abort(.badRequest) }
Expand Down
12 changes: 12 additions & 0 deletions ContractTests/Source/Models/client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ struct Configuration: Content {
var events: EventParameters?
var tags: TagParameters?
var clientSide: ClientSideParameters
var hooks: HookParameters?
}

struct StreamingParameters: Content {
Expand Down Expand Up @@ -50,3 +51,14 @@ struct ClientSideParameters: Content {
var useReport: Bool?
var includeEnvironmentAttributes: Bool?
}

struct HookParameters: Content {
var hooks: [HookParameter]
}

struct HookParameter: Content {
var name: String
var callbackUri: String
var data: [String: [String: LDValue]]?
var errors: [String: LDValue]?
}
134 changes: 134 additions & 0 deletions ContractTests/Source/Models/hook.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import Foundation
import LaunchDarkly

class TestHook: Hook {
private let name: String
private let callbackUrl: URL
private let data: [String: [String: Encodable]]
private let errors: [String: LDValue]

init(name: String, callbackUrl: URL, data: [String: [String: Encodable]], errors: [String: LDValue]) {
self.name = name
self.callbackUrl = callbackUrl
self.data = data
self.errors = errors
}

func metadata() -> Metadata {
return Metadata(name: self.name)
}

func beforeEvaluation(seriesContext: EvaluationSeriesContext, seriesData: EvaluationSeriesData) -> LaunchDarkly.EvaluationSeriesData {
return processHook(seriesContext: seriesContext, seriesData: seriesData, evaluationDetail: nil, stage: "beforeEvaluation")
}

func afterEvaluation(seriesContext: EvaluationSeriesContext, seriesData: EvaluationSeriesData, evaluationDetail: LDEvaluationDetail<LDValue>) -> EvaluationSeriesData {
return processHook(seriesContext: seriesContext, seriesData: seriesData, evaluationDetail: evaluationDetail, stage: "afterEvaluation")
}

private func processHook(seriesContext: EvaluationSeriesContext, seriesData: EvaluationSeriesData, evaluationDetail: LDEvaluationDetail<LDValue>?, stage: String) -> EvaluationSeriesData {
guard self.errors[stage] == nil else { return seriesData }

let payload = EvaluationPayload(evaluationSeriesContext: seriesContext, evaluationSeriesData: seriesData, stage: stage, evaluationDetail: evaluationDetail)

// swiftlint:disable:next force_try
let data = try! JSONEncoder().encode(payload)

var request = URLRequest(url: self.callbackUrl)
request.httpMethod = "POST"
request.httpBody = data
request.setValue("application/json", forHTTPHeaderField: "Content-Type")

URLSession.shared.dataTask(with: request) { (_, _, _) in
}.resume()

var updatedData = seriesData
if let be = self.data[stage] {
be.forEach { (key, value) in
updatedData[key] = value
}
}

return updatedData
}
}

struct EvaluationPayload: Encodable {
var evaluationSeriesContext: EvaluationSeriesContext
var evaluationSeriesData: EvaluationSeriesData
var stage: String
var evaluationDetail: LDEvaluationDetail<LDValue>?

init(evaluationSeriesContext: EvaluationSeriesContext, evaluationSeriesData: EvaluationSeriesData, stage: String, evaluationDetail: LDEvaluationDetail<LDValue>? = nil) {
self.evaluationSeriesContext = evaluationSeriesContext
self.evaluationSeriesData = evaluationSeriesData
self.stage = stage
self.evaluationDetail = evaluationDetail
}

private enum CodingKeys: String, CodingKey {
case evaluationSeriesContext
case evaluationSeriesData
case stage
case evaluationDetail
}

struct DynamicKey: CodingKey {
let intValue: Int? = nil
let stringValue: String

init?(intValue: Int) {
return nil
}

init?(stringValue: String) {
self.stringValue = stringValue
}
}

public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)

try container.encode(evaluationSeriesContext, forKey: .evaluationSeriesContext)
try container.encode(stage, forKey: .stage)

try container.encodeIfPresent(evaluationDetail, forKey: .evaluationDetail)

var nested = container.nestedContainer(keyedBy: DynamicKey.self, forKey: .evaluationSeriesData)
try evaluationSeriesData.forEach { (_, _) in
try evaluationSeriesData.forEach { try nested.encode($1, forKey: DynamicKey(stringValue: $0)!) }
}
}
}

extension EvaluationSeriesContext: Encodable {
private enum CodingKeys: String, CodingKey {
case flagKey
case context
case defaultValue
case method
}

public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(flagKey, forKey: .flagKey)
try container.encode(context, forKey: .context)
try container.encode(defaultValue, forKey: .defaultValue)
try container.encode(methodName, forKey: .method)
}
}

extension LDEvaluationDetail: Encodable where T == LDValue {
private enum CodingKeys: String, CodingKey {
case value
case variationIndex
case reason
}

public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(value, forKey: .value)
try container.encode(variationIndex, forKey: .variationIndex)
try container.encode(reason, forKey: .reason)
}
}
Loading