Skip to content

Commit

Permalink
feat: OpenFeature provider for GO Feature Flag (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
thomaspoignant authored Jul 5, 2024
1 parent 908cea3 commit b911046
Show file tree
Hide file tree
Showing 18 changed files with 2,078 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/swift.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@ jobs:
run: swift build
- name: Run tests
run: swift test --enable-code-coverage
- name: Prepare Code Coverage
run: xcrun llvm-cov export -format="lcov" .build/debug/go-feature-flag-providerPackageTests.xctest/Contents/MacOS/go-feature-flag-providerPackageTests -instr-profile .build/debug/codecov/default.profdata > info.lcov
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v4
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
files: info.lcov
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
lint:
Expand Down
75 changes: 75 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# By default, SwiftLint uses a set of sensible default rules you can adjust:
disabled_rules: # rule identifiers turned on by default to exclude from running
- colon
- comma
- control_statement
opt_in_rules: # some rules are turned off by default, so you need to opt-in
- empty_count # find all the available rules by running: `swiftlint rules`

# Alternatively, specify all rules explicitly by uncommenting this option:
# only_rules: # delete `disabled_rules` & `opt_in_rules` if using this
# - empty_parameters
# - vertical_whitespace

analyzer_rules: # rules run by `swiftlint analyze`
- explicit_self

# Case-sensitive paths to include during linting. Directory paths supplied on the
# command line will be ignored.
included:
- Sources
excluded: # case-sensitive paths to ignore during linting. Takes precedence over `included`
- Carthage
- Pods
- Sources/ExcludedFolder
- Sources/ExcludedFile.swift
- Sources/*/ExcludedFile.swift # exclude files with a wildcard

# If true, SwiftLint will not fail if no lintable files are found.
allow_zero_lintable_files: false

# If true, SwiftLint will treat all warnings as errors.
strict: false

# The path to a baseline file, which will be used to filter out detected violations.
baseline: Baseline.json

# The path to save detected violations to as a new baseline.
write_baseline: Baseline.json

# configurable rules can be customized from this configuration file
# binary rules can set their severity level
force_cast: warning # implicitly
force_try:
severity: warning # explicitly
# rules that have both warning and error levels, can set just the warning level
# implicitly
line_length: 120
# they can set both implicitly with an array
type_body_length:
- 300 # warning
- 400 # error
# or they can set both explicitly
file_length:
warning: 500
error: 1200
# naming rules can set warnings/errors for min_length and max_length
# additionally they can set excluded names
type_name:
min_length: 4 # only warning
max_length: # warning and error
warning: 40
error: 50
excluded: iPhone # excluded via string
allowed_symbols: ["_"] # these are allowed in type names
identifier_name:
min_length: # only min_length
error: 4 # only error
excluded: # excluded via string array
- id
- URL
- url
- GlobalAPIKey
- key
- dto
reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, codeclimate, junit, html, emoji, sonarqube, markdown, github-actions-logging, summary)
7 changes: 7 additions & 0 deletions .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"object": {
"pins": [
{
"package": "OpenFeature",
"repositoryURL": "https://github.com/open-feature/swift-sdk.git",
"state": {
"branch": null,
"revision": "02b033c954766e86d5706bfc8ee5248244c11e77",
"version": "0.1.0"
}
}
]
},
"version": 1
}
35 changes: 35 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// swift-tools-version: 5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "go-feature-flag-provider",
platforms: [
.iOS(.v14),
.macOS(.v12)
],
products: [
.library(
name: "go-feature-flag-provider",
targets: ["go-feature-flag-provider"])
],
dependencies: [
.package(url: "https://github.com/open-feature/swift-sdk.git", from: "0.1.0")
],
targets: [
.target(
name: "go-feature-flag-provider",
dependencies: [
.product(name: "OpenFeature", package: "swift-sdk")
],
plugins:[]
),
.testTarget(
name: "go-feature-flag-providerTests",
dependencies: [
"go-feature-flag-provider"
]
)
]
)
79 changes: 79 additions & 0 deletions Sources/go-feature-flag-provider/controller/ofrep_api.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import Foundation
import OpenFeature

class OfrepAPI {
private let networkingService: NetworkingService
private var etag: String = ""
private let options: GoFeatureFlagProviderOptions

init(networkingService: NetworkingService, options: GoFeatureFlagProviderOptions) {
self.networkingService = networkingService
self.options = options
}

func postBulkEvaluateFlags(context: EvaluationContext?) async throws -> (OfrepEvaluationResponse, HTTPURLResponse) {
guard let context = context else {
throw OpenFeatureError.invalidContextError
}
try validateContext(context: context)

guard let url = URL(string: options.endpoint) else {
throw InvalidOptions.invalidEndpoint(message: "endpoint [" + options.endpoint + "] is not valid")
}
let ofrepURL = url.appendingPathComponent("ofrep/v1/evaluate/flags")
var request = URLRequest(url: ofrepURL)
request.httpMethod = "POST"
request.httpBody = try EvaluationRequest.convertEvaluationContext(context: context).asJSONData()
request.setValue(
"application/json",
forHTTPHeaderField: "Content-Type"
)

if etag != "" {
request.setValue(etag, forHTTPHeaderField: "If-None-Match")
}

let (data, response) = try await networkingService.doRequest(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw OfrepError.httpResponseCastError
}

if httpResponse.statusCode == 401 {
throw OfrepError.apiUnauthorizedError(response: httpResponse)
}
if httpResponse.statusCode == 403 {
throw OfrepError.forbiddenError(response: httpResponse)
}
if httpResponse.statusCode == 429 {
throw OfrepError.apiTooManyRequestsError(response: httpResponse)
}
if httpResponse.statusCode > 400 {
throw OfrepError.unexpectedResponseError(response: httpResponse)
}
if httpResponse.statusCode == 304 {
return (OfrepEvaluationResponse(flags: [], errorCode: nil, errorDetails: nil), httpResponse)
}

// Store ETag to use it in the next request
if let etagHeaderValue = httpResponse.value(forHTTPHeaderField: "ETag") {
if etagHeaderValue != "" && httpResponse.statusCode == 200 {
etag = etagHeaderValue
}
}

do {
let dto = try JSONDecoder().decode(EvaluationResponseDTO.self, from: data)
let evaluationResponse = OfrepEvaluationResponse.fromEvaluationResponseDTO(dto: dto)
return (evaluationResponse, httpResponse)
} catch {
throw OfrepError.unmarshallError(error: error)
}
}

private func validateContext(context: EvaluationContext) throws {
let targetingKey = context.getTargetingKey()
if targetingKey.isEmpty {
throw OpenFeatureError.targetingKeyMissingError
}
}
}
18 changes: 18 additions & 0 deletions Sources/go-feature-flag-provider/exception/ofrep_exceptions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// File.swift
//
//
// Created by thomas.poignant on 27/06/2024.
//

import Foundation

enum OfrepError: Error {
case httpResponseCastError
case unmarshallError(error: Error)
case apiUnauthorizedError(response: HTTPURLResponse)
case forbiddenError(response: HTTPURLResponse)
case apiTooManyRequestsError(response: HTTPURLResponse)
case unexpectedResponseError(response: HTTPURLResponse)
case waitingRetryLater(date: Date?)
}
13 changes: 13 additions & 0 deletions Sources/go-feature-flag-provider/exception/option_exceptions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// File.swift
//
//
// Created by thomas.poignant on 27/06/2024.
//

import Foundation

enum InvalidOptions: Error {
case invalidEndpoint(message: String)

}
Loading

0 comments on commit b911046

Please sign in to comment.