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

feat: Event Uploader #91

Merged
merged 49 commits into from
Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
1706531
feat: add EventStorage
nickybondarenko Apr 5, 2024
d7248c3
refactor: make getFolderURL() static
nickybondarenko Apr 8, 2024
cc239d5
fix: initialize URLs in init()
nicklasl Apr 8, 2024
91bdd52
feat: extend EventStorage API
nickybondarenko Apr 9, 2024
36f8dd4
refactor: add error handling
nickybondarenko Apr 9, 2024
848be71
fix: handle continuing writing to batch from disk
nickybondarenko Apr 9, 2024
9dcabd7
fix: handle continuing writing to batch from disk, remove force unwra…
nickybondarenko Apr 9, 2024
6bb80c4
fix: simplify batching
nickybondarenko Apr 9, 2024
fd24656
refactor: change handling of file creation
nickybondarenko Apr 10, 2024
845b10e
refactor: create a new file if no file left after previous session
nickybondarenko Apr 10, 2024
9d711a2
fix creating file, folder and use file handler, fix appending events …
vahidlazio Apr 10, 2024
9df105c
always seek to the end of the file to append
vahidlazio Apr 10, 2024
3b6d2e7
close file handler before moving it to ready
vahidlazio Apr 10, 2024
3a47146
move private funcs down
vahidlazio Apr 10, 2024
5c34822
cache folder url
vahidlazio Apr 10, 2024
258d8d0
test appending events
vahidlazio Apr 10, 2024
b2f06be
tear down test
vahidlazio Apr 10, 2024
64687ca
fix: update .gitignore
nickybondarenko Apr 11, 2024
6293666
refactor: refactor EventStorage
nickybondarenko Apr 11, 2024
1497115
test: test EventStorage
nickybondarenko Apr 11, 2024
13b79f2
fix: unalignment with main
nickybondarenko Apr 11, 2024
c3418ff
fix: fix lint issues
nickybondarenko Apr 12, 2024
0698e45
fix: align links and add comments
nickybondarenko Apr 15, 2024
b6669d0
Add RemoteClient to Confidence
fabriziodemaria Apr 10, 2024
24bb35c
Make NetworkClient endpoint independent
fabriziodemaria Apr 11, 2024
bf75c20
Generalie and reuse HTTP module
fabriziodemaria Apr 11, 2024
dae83c2
Add Common target for shared internal code
fabriziodemaria Apr 11, 2024
a6445e8
Finalize the network layer for events
fabriziodemaria Apr 11, 2024
f6c894d
Smaller refactoring
fabriziodemaria Apr 11, 2024
edd9857
Move StructValue to Common
fabriziodemaria Apr 11, 2024
a7c5a37
[WIP] One struct for all network
fabriziodemaria Apr 11, 2024
d3b49a6
[WIP] Rename Struct
fabriziodemaria Apr 11, 2024
79bc054
[WIP] Finish implementing NetworkTypeMapper
fabriziodemaria Apr 11, 2024
c8ed29e
Remove generic number type
fabriziodemaria Apr 11, 2024
5cfc9c6
NetworkValue works with number
fabriziodemaria Apr 11, 2024
29e1c14
Network model better represents JSON types
fabriziodemaria Apr 11, 2024
b35185e
Fix file name
fabriziodemaria Apr 11, 2024
67c8285
Fix CI build
fabriziodemaria Apr 11, 2024
f5c570b
Rename NetworkValue
fabriziodemaria Apr 12, 2024
f7b9735
Create testbed with URLProtocol mock
fabriziodemaria Apr 12, 2024
75a4153
Add ConfidenceClient tests
fabriziodemaria Apr 12, 2024
85cedf7
ConfidenceClient error handling/testing
fabriziodemaria Apr 12, 2024
594b49e
Rebase on top of EventSenderEngine
fabriziodemaria Apr 12, 2024
9415ae9
Formatting
fabriziodemaria Apr 12, 2024
49d86d0
Name alignments and visibility tweaks
fabriziodemaria Apr 15, 2024
85220c0
Main rebase
fabriziodemaria Apr 15, 2024
d31b17f
Send context in events and align naming
fabriziodemaria Apr 15, 2024
bee8208
Fix demo app and def policy
fabriziodemaria Apr 15, 2024
d6dbe0a
Test more types in demo app
fabriziodemaria Apr 15, 2024
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
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: "git@github.com: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
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!
}
}
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
Loading