Skip to content

Commit

Permalink
feat: Event Uploader (#91)
Browse files Browse the repository at this point in the history
* feat: add EventStorage

* refactor: make getFolderURL() static

* fix: initialize URLs in init()

* feat: extend EventStorage API

* refactor: add error handling

* fix: handle continuing writing to batch from disk

* fix: handle continuing writing to batch from disk, remove force unwrapping

* fix: simplify batching

* refactor: change handling of file creation

* refactor: create a new file if no file left after previous session

* fix creating file, folder and use file handler, fix appending events to the storage. fix tests

* always seek to the end of the file to append

* close file handler before moving it to ready

* move private funcs down

* cache folder url

* test appending events

* tear down test

* fix: update .gitignore

* refactor: refactor EventStorage

* test: test EventStorage

* fix: unalignment with main

* fix: fix lint issues

* fix: align links and add comments

* Add RemoteClient to Confidence

* Make NetworkClient endpoint independent

* Generalie and reuse HTTP module

* Add Common target for shared internal code

* Finalize the network layer for events

* Smaller refactoring

* Move StructValue to Common

* [WIP] One struct for all network

* [WIP] Rename Struct

* [WIP] Finish implementing NetworkTypeMapper

* Remove generic number type

* NetworkValue works with number

* Network model better represents JSON types

* Fix file name

* Fix CI build

* Rename NetworkValue

* Create testbed with URLProtocol mock

* Add ConfidenceClient tests

* ConfidenceClient error handling/testing

* Rebase on top of EventSenderEngine

* Formatting

* Name alignments and visibility tweaks

* Main rebase

* Send context in events and align naming

* Fix demo app and def policy

* Test more types in demo app

---------

Co-authored-by: Nicky Bondarenko <[email protected]>
Co-authored-by: Nicklas Lundin <[email protected]>
Co-authored-by: vahid torkaman <[email protected]>
  • Loading branch information
4 people authored Apr 15, 2024
1 parent fdc7543 commit b5ba3e0
Show file tree
Hide file tree
Showing 52 changed files with 1,049 additions and 410 deletions.
28 changes: 25 additions & 3 deletions ConfidenceDemoApp/ConfidenceDemoApp/ConfidenceDemoApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ extension ConfidenceDemoApp {
}

let confidence = Confidence.Builder(clientSecret: secret)
.withRegion(region: .europe)
.withInitializationstrategy(initializationStrategy: initializationStrategy)
.build()
let provider = ConfidenceFeatureProvider(confidence: confidence)
Expand All @@ -38,9 +39,30 @@ extension ConfidenceDemoApp {
structure: MutableStructure.init(attributes: ["country": .string("SE")]))
Task {
await OpenFeatureAPI.shared.setProviderAndWait(provider: provider, initialContext: ctx)
confidence.send(
definition: "my_event",
payload: ["my_string_field": ConfidenceValue(string: "hello_from_world")])
}
confidence.send(
definition: "all-types",
payload: [
"my_string": ConfidenceValue(string: "hello_from_world"),
"my_timestamp": ConfidenceValue(timestamp: Date()),
"my_bool": ConfidenceValue(boolean: true),
"my_date": ConfidenceValue(date: DateComponents(year: 2024, month: 4, day: 3)),
"my_int": ConfidenceValue(integer: 2),
"my_double": ConfidenceValue(double: 3.14),
"my_list": ConfidenceValue(booleanList: [true, false]),
"my_struct": ConfidenceValue(structure: [
"my_nested_struct": ConfidenceValue(structure: [
"my_nested_nested_struct": ConfidenceValue(structure: [
"my_nested_nested_nested_int": ConfidenceValue(integer: 666)
]),
"my_nested_nested_list": ConfidenceValue(dateList: [
DateComponents(year: 2024, month: 4, day: 4),
DateComponents(year: 2024, month: 4, day: 5)
])
]),
"my_nested_string": ConfidenceValue(string: "nested_hello")
])
]
)
}
}
17 changes: 14 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,29 +21,40 @@ let package = Package(
.package(url: "[email protected]:open-feature/swift-sdk.git", from: "0.1.0"),
],
targets: [
// Internal definitions shared between Confidence and ConfidenceProvider
// These are not exposed to the consumers of Confidence or ConfidenceProvider
.target(
name: "Confidence",
name: "Common",
dependencies: [],
plugins: []
),
.target(
name: "Confidence",
dependencies: [
"Common"
],
plugins: []
),
.target(
name: "ConfidenceProvider",
dependencies: [
.product(name: "OpenFeature", package: "swift-sdk"),
"Confidence"
"Confidence",
"Common"
],
plugins: []
),
.testTarget(
name: "ConfidenceProviderTests",
dependencies: [
"ConfidenceProvider",
"Common",
]
),
.testTarget(
name: "ConfidenceTests",
dependencies: [
"Confidence"
"Confidence",
]
),
]
Expand Down
28 changes: 14 additions & 14 deletions Sources/Confidence/Backport.swift → Sources/Common/Backport.swift
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
import Foundation

public extension URL {
struct Backport {
extension URL {
public struct Backport {
var base: URL

public init(base: URL) {
init(base: URL) {
self.base = base
}
}

var backport: Backport {
public var backport: Backport {
Backport(base: self)
}
}

public extension URL.Backport {
var path: String {
extension URL.Backport {
public var path: String {
if #available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) {
return self.base.path(percentEncoded: false)
} else {
return self.base.path
}
}

func appending<S>(components: S...) -> URL where S: StringProtocol {
public func appending<S>(components: S...) -> URL where S: StringProtocol {
if #available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) {
return components.reduce(self.base) { acc, cur in
return acc.appending(component: cur)
Expand All @@ -36,31 +36,31 @@ public extension URL.Backport {
}
}

public extension Date {
struct Backport {
extension Date {
public struct Backport {
}

static var backport: Backport.Type { Backport.self }
static public var backport: Backport.Type { Backport.self }
}

public extension Date.Backport {
static var now: Date {
extension Date.Backport {
static public var now: Date {
if #available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) {
return Date.now
} else {
return Date()
}
}

static var nowISOString: String {
static public var nowISOString: String {
if #available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) {
return toISOString(date: Date.now)
} else {
return toISOString(date: Date())
}
}

static func toISOString(date: Date) -> String {
static public func toISOString(date: Date) -> String {
if #available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) {
return date.ISO8601Format()
} else {
Expand Down
13 changes: 13 additions & 0 deletions Sources/Common/CaseIterableDefaultsLast.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Foundation

/// Used to default an enum to the last value if none matches, this should respresent unknown
public protocol CaseIterableDefaultsLast: Decodable & CaseIterable & RawRepresentable
where RawValue: Decodable, AllCases: BidirectionalCollection {}

extension CaseIterableDefaultsLast {
public init(from decoder: Decoder) throws {
// All enums should contain at least one item so we allow force unwrap
// swiftlint:disable:next force_unwrapping
self = try Self(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? Self.allCases.last!
}
}
File renamed without changes.
48 changes: 48 additions & 0 deletions Sources/Common/Http/HttpClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import Foundation

public typealias HttpClientResult<T> = Result<HttpClientResponse<T>, Error>

public protocol HttpClient {
func post<T: Decodable>(path: String, data: Encodable) async throws -> HttpClientResult<T>
}

public struct HttpClientResponse<T> {
public init(decodedData: T? = nil, decodedError: HttpError? = nil, response: HTTPURLResponse) {
self.decodedData = decodedData
self.decodedError = decodedError
self.response = response
}
public var decodedData: T?
public var decodedError: HttpError?
public var response: HTTPURLResponse
}

public struct HttpError: Codable {
public init(code: Int, message: String, details: [String]) {
self.code = code
self.message = message
self.details = details
}
public var code: Int
public var message: String
public var details: [String]
}

public enum HttpClientError: Error {
case invalidResponse
case internalError
}

extension HTTPURLResponse {
public func mapStatusToError(error: HttpError?) -> ConfidenceError {
let defaultError = ConfidenceError.internalError(
message: "General error: \(error?.message ?? "Unknown error")")

switch self.status {
case .notFound, .badRequest:
return ConfidenceError.badRequest(message: error?.message ?? "")
default:
return defaultError
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Foundation
/// This is a list of Hypertext Transfer Protocol (HTTP) response status codes.
/// It includes codes from IETF internet standards, other IETF RFCs, other specifications, and some additional commonly used codes.
/// The first digit of the status code specifies one of five classes of response; an HTTP client must recognise these five classes at a minimum.
enum HTTPStatusCode: Int, Error {
public enum HTTPStatusCode: Int, Error {
/// The response class representation of status codes, these get grouped by their first digit.
enum ResponseType {
/// - informational: This class of status code indicates a provisional response, consisting only of the Status-Line and optional headers, and is terminated by an empty line.
Expand Down Expand Up @@ -271,7 +271,7 @@ enum HTTPStatusCode: Int, Error {
}

extension HTTPURLResponse {
var status: HTTPStatusCode? {
public var status: HTTPStatusCode? {
return HTTPStatusCode(rawValue: statusCode)
}
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,15 @@
import Foundation
import Confidence

final class NetworkClient: HttpClient {
final public class NetworkClient: HttpClient {
private let headers: [String: String]
private let retry: Retry
private let timeout: TimeInterval
private let session: URLSession
private let region: ConfidenceRegion

private var baseUrl: String {
switch region {
case .global:
return "https://resolver.confidence.dev/v1/flags"
case .europe:
return "https://resolver.eu.confidence.dev/v1/flags"
case .usa:
return "https://resolver.us.confidence.dev/v1/flags"
}
}
private let baseUrl: String

init(
public init(
session: URLSession? = nil,
region: ConfidenceRegion,
baseUrl: String,
defaultHeaders: [String: String] = [:],
timeout: TimeInterval = 30.0,
retry: Retry = .none
Expand All @@ -39,12 +27,12 @@ final class NetworkClient: HttpClient {
self.headers = defaultHeaders
self.retry = retry
self.timeout = timeout
self.region = region
self.baseUrl = baseUrl
}

func post<T: Decodable>(
public func post<T: Decodable>(
path: String,
data: Codable
data: Encodable
) async throws -> HttpClientResult<T> {
let request = try buildRequest(path: path, data: data)
let requestResult = await perform(request: request, retry: self.retry)
Expand Down Expand Up @@ -109,7 +97,7 @@ extension NetworkClient {
return URL(string: "\(normalisedBase)\(normalisedPath)")
}

private func buildRequest(path: String, data: Codable) throws -> URLRequest {
private func buildRequest(path: String, data: Encodable) throws -> URLRequest {
guard let url = constructURL(base: baseUrl, path: path) else {
throw ConfidenceError.internalError(message: "Could not create service url")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

enum Retry {
public enum Retry {
case none
case exponential(maxBackoff: TimeInterval, maxAttempts: UInt)

Expand All @@ -14,11 +14,11 @@ enum Retry {
}
}

protocol RetryHandler {
public protocol RetryHandler {
func retryIn() -> TimeInterval?
}

class ExponentialBackoffRetryHandler: RetryHandler {
public class ExponentialBackoffRetryHandler: RetryHandler {
private var currentAttempts: UInt = 0
private let maxBackoff: TimeInterval
private let maxAttempts: UInt
Expand All @@ -28,7 +28,7 @@ class ExponentialBackoffRetryHandler: RetryHandler {
self.maxAttempts = maxAttempts
}

func retryIn() -> TimeInterval? {
public func retryIn() -> TimeInterval? {
if currentAttempts >= maxAttempts {
return nil
}
Expand All @@ -40,8 +40,8 @@ class ExponentialBackoffRetryHandler: RetryHandler {
}
}

class NoneRetryHandler: RetryHandler {
func retryIn() -> TimeInterval? {
public class NoneRetryHandler: RetryHandler {
public func retryIn() -> TimeInterval? {
return nil
}
}
Loading

0 comments on commit b5ba3e0

Please sign in to comment.