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: add EventStorage #87

Merged
merged 26 commits into from
Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
1e1e752
feat: add EventStorage
nickybondarenko Apr 5, 2024
fcc0d15
refactor: make getFolderURL() static
nickybondarenko Apr 8, 2024
e846b2f
fix: initialize URLs in init()
nicklasl Apr 8, 2024
e04cfd3
feat: extend EventStorage API
nickybondarenko Apr 9, 2024
8a1f1f0
refactor: add error handling
nickybondarenko Apr 9, 2024
605cae7
fix: handle continuing writing to batch from disk
nickybondarenko Apr 9, 2024
3433906
fix: handle continuing writing to batch from disk, remove force unwra…
nickybondarenko Apr 9, 2024
220bead
fix: simplify batching
nickybondarenko Apr 9, 2024
8b83092
refactor: change handling of file creation
nickybondarenko Apr 10, 2024
c13212b
refactor: create a new file if no file left after previous session
nickybondarenko Apr 10, 2024
4635e28
test: test EventStorage
nickybondarenko Apr 10, 2024
c522918
fix creating file, folder and use file handler, fix appending events …
vahidlazio Apr 10, 2024
e2858b0
always seek to the end of the file to append
vahidlazio Apr 10, 2024
f58ba7f
close file handler before moving it to ready
vahidlazio Apr 10, 2024
5bacd29
move private funcs down
vahidlazio Apr 10, 2024
c9c45cb
cache folder url
vahidlazio Apr 10, 2024
c3a1570
test appending events
vahidlazio Apr 10, 2024
09eb85b
tear down test
vahidlazio Apr 10, 2024
c05e21a
fix: update .gitignore
nickybondarenko Apr 11, 2024
9450360
refactor: refactor EventStorage
nickybondarenko Apr 11, 2024
8ea8cb4
test: test EventStorage
nickybondarenko Apr 11, 2024
ff96d25
fix: unalignment with main
nickybondarenko Apr 11, 2024
c94bf0c
fix: fix lint issues
nickybondarenko Apr 12, 2024
2ce9c77
fix: align links and add comments
nickybondarenko Apr 15, 2024
f43f9c8
Merge branch 'main' into es-storage
nickybondarenko Apr 15, 2024
c3582d3
fix: align with main
nickybondarenko 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
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,6 @@ import XCTest
@testable import ConfidenceDemoApp

final class ConfidenceDemoTests: XCTestCase {
override func setUpWithError() throws {
try super.setUpWithError()
// Put setup code here. This method is called before the invocation of each test method in the class.
}

override func tearDownWithError() throws {
try super.tearDownWithError()
// Put teardown code here. This method is called after the invocation of each test method in the class.
}

func testExample() throws {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,6 @@ final class ConfidenceDemoUITests: XCTestCase {
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
}

override func tearDownWithError() throws {
try super.tearDownWithError()
// Put teardown code here. This method is called after the invocation of each test method in the class.
}

func testExample() throws {
// UI tests must launch the application that they test.
let app = XCUIApplication()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import Foundation

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

init(base: URL) {
public init(base: URL) {
self.base = base
}
}
Expand All @@ -14,7 +14,7 @@ extension URL {
}
}

extension URL.Backport {
public extension URL.Backport {
var path: String {
if #available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) {
return self.base.path(percentEncoded: false)
Expand All @@ -36,14 +36,14 @@ extension URL.Backport {
}
}

extension Date {
public extension Date {
struct Backport {
}

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

extension Date.Backport {
public extension Date.Backport {
static var now: Date {
if #available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) {
return Date.now
Expand All @@ -60,7 +60,7 @@ extension Date.Backport {
}
}

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

internal protocol EventStorage {
func startNewBatch() throws
func writeEvent(event: Event) throws
func batchReadyIds() throws -> [String]
func eventsFrom(id: String) throws -> [Event]
func remove(id: String) throws
}

internal class EventStorageImpl: EventStorage {
private let READYTOSENDEXTENSION = "READY"
private let storageQueue = DispatchQueue(label: "com.confidence.events.storage")
private var folderURL: URL
nickybondarenko marked this conversation as resolved.
Show resolved Hide resolved
private var currentFileUrl: URL?
private var currentFileHandle: FileHandle?

init() throws {
self.folderURL = try EventStorageImpl.getFolderURL()
if !FileManager.default.fileExists(atPath: folderURL.backport.path) {
try FileManager.default.createDirectory(at: folderURL, withIntermediateDirectories: true)
}
try resetCurrentFile()
}

func startNewBatch() throws {
try storageQueue.sync {
guard let currentFileName = self.currentFileUrl else {
return
}
try currentFileHandle?.close()
try FileManager.default.moveItem(at: currentFileName, to: currentFileName.appendingPathExtension(READYTOSENDEXTENSION))

Check warning on line 33 in Sources/Confidence/EventStorage.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Line Length Violation: Line should be 120 characters or less; currently it has 131 characters (line_length)
try resetCurrentFile()
}
}

func writeEvent(event: Event) throws {
try storageQueue.sync {
guard let currentFileHandle = currentFileHandle else {
return
}
let encoder = JSONEncoder()
let serialied = try encoder.encode(event)
let delimiter = "\n".data(using: .utf8)
guard let delimiter else {
return
}
currentFileHandle.seekToEndOfFile()
try currentFileHandle.write(contentsOf: delimiter)
try currentFileHandle.write(contentsOf: serialied)
}
}


func batchReadyIds() throws -> [String] {
try storageQueue.sync {
let fileUrls = try FileManager.default.contentsOfDirectory(at: folderURL, includingPropertiesForKeys: nil)
return fileUrls.filter({ url in url.pathExtension == READYTOSENDEXTENSION }).map({ url in url.lastPathComponent })

Check warning on line 59 in Sources/Confidence/EventStorage.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Line Length Violation: Line should be 120 characters or less; currently it has 126 characters (line_length)

Check warning on line 59 in Sources/Confidence/EventStorage.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing Closure Violation: Trailing closure syntax should be used whenever possible (trailing_closure)

Check warning on line 59 in Sources/Confidence/EventStorage.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing Closure Violation: Trailing closure syntax should be used whenever possible (trailing_closure)
}
}

func eventsFrom(id: String) throws -> [Event] {
try storageQueue.sync {
let decoder = JSONDecoder()
let fileUrl = folderURL.appendingPathComponent(id)
let data = try Data(contentsOf: fileUrl)
let dataString = String(data: data, encoding: .utf8)
return try dataString?.components(separatedBy: "\n")

Check warning on line 69 in Sources/Confidence/EventStorage.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing Closure Violation: Trailing closure syntax should be used whenever possible (trailing_closure)

Check warning on line 69 in Sources/Confidence/EventStorage.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing Closure Violation: Trailing closure syntax should be used whenever possible (trailing_closure)
.filter({ events in !events.isEmpty })
.map({ eventString in try decoder.decode(Event.self, from: eventString.data(using: .utf8)!) }) ?? []

Check warning on line 71 in Sources/Confidence/EventStorage.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Force Unwrapping Violation: Force unwrapping should be avoided (force_unwrapping)
}
}

func remove(id: String) throws {
try storageQueue.sync {
let fileUrl = folderURL.appendingPathComponent(id)

Check warning on line 77 in Sources/Confidence/EventStorage.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Indentation Width Violation: Code should be indented using one tab or 4 spaces (indentation_width)
try FileManager.default.removeItem(at: fileUrl)
}
}

private func getLastWritingFile() throws -> URL? {
let files = try FileManager.default.contentsOfDirectory(at: folderURL, includingPropertiesForKeys: nil)
for fileUrl in files {
if fileUrl.pathExtension != READYTOSENDEXTENSION {

Check warning on line 85 in Sources/Confidence/EventStorage.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Prefer For-Where Violation: `where` clauses are preferred over a single `if` inside a `for` (for_where)
return fileUrl
}
}
return nil
}

private func resetCurrentFile() throws {
if let currentFile = try getLastWritingFile() {
nickybondarenko marked this conversation as resolved.
Show resolved Hide resolved
self.currentFileUrl = currentFile
self.currentFileHandle = try FileHandle(forWritingTo: currentFile)
} else {
let fileUrl = folderURL.appendingPathComponent(String(Date().timeIntervalSince1970))
FileManager.default.createFile(atPath: fileUrl.path, contents: nil)
self.currentFileUrl = fileUrl
self.currentFileHandle = try FileHandle(forWritingTo: fileUrl)
}
}

internal static func getFolderURL() throws -> URL {
guard
let applicationSupportUrl = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)
.last
else {
throw ConfidenceError.cacheError(message: "Could not get URL for application directory")
}

guard let bundleIdentifier = Bundle.main.bundleIdentifier else {
throw ConfidenceError.cacheError(message: "Unable to get bundle identifier")
}

return applicationSupportUrl.backport.appending(
components: "com.confidence.cache", "\(bundleIdentifier)", "events")
nickybondarenko marked this conversation as resolved.
Show resolved Hide resolved
}
nicklasl marked this conversation as resolved.
Show resolved Hide resolved
}

struct Event: Codable {
let eventDefinition: String
let eventTime: Date
// TODO: fix this to be ConfidenceValue
let payload: [String]
let context: [String]
nicklasl marked this conversation as resolved.
Show resolved Hide resolved
}
2 changes: 1 addition & 1 deletion Sources/ConfidenceProvider/Cache/DefaultStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public class DefaultStorage: Storage {

func getConfigUrl() throws -> URL {
guard
let applicationSupportUrl = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)
let applicationSupportUrl: URL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)
.last
else {
throw ConfidenceError.cacheError(message: "Could not get URL for application directory")
Expand Down
46 changes: 46 additions & 0 deletions Tests/ConfidenceTests/EventStorageTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import Foundation
import XCTest

@testable import Confidence

class EventStorageTest: XCTestCase {
override func setUp() async throws {
let folderURL = try! EventStorageImpl.getFolderURL()
if FileManager.default.fileExists(atPath: folderURL.path) {
try! FileManager.default.removeItem(at: folderURL)
}
}

func testCreateNewBatch() throws {
let eventStorage = try EventStorageImpl()
try eventStorage.writeEvent(event: Event(eventDefinition: "some event", eventTime: Date().self, payload: ["pants"], context: ["pants context"]))
try eventStorage.writeEvent(event: Event(eventDefinition: "some event 2", eventTime: Date().self, payload: ["pants"], context: ["pants context"]))
try eventStorage.startNewBatch()
try XCTAssertEqual(eventStorage.batchReadyIds().count, 1)
let events = try eventStorage.eventsFrom(id: try eventStorage.batchReadyIds()[0])
XCTAssertEqual(events[0].eventDefinition, "some event")
XCTAssertEqual(events[1].eventDefinition, "some event 2")
}

func testContinueWritingToOldBatch() throws {
let eventStorage = try EventStorageImpl()
try eventStorage.writeEvent(event: Event(eventDefinition: "some event", eventTime: Date().self, payload: ["pants"], context: ["pants context"]))
// user stops using app, new session after this
let eventStorageNew = try EventStorageImpl()
try eventStorageNew.writeEvent(event: Event(eventDefinition: "some event 2", eventTime: Date().self, payload: ["pants"], context: ["pants context"]))
try eventStorageNew.startNewBatch()
try XCTAssertEqual(eventStorageNew.batchReadyIds().count, 1)
let events = try eventStorageNew.eventsFrom(id: try eventStorageNew.batchReadyIds()[0])
XCTAssertEqual(events[0].eventDefinition, "some event")
XCTAssertEqual(events[1].eventDefinition, "some event 2")
}

func testRemoveFile() throws {
let eventStorage = try EventStorageImpl()
try eventStorage.writeEvent(event: Event(eventDefinition: "some event", eventTime: Date().self, payload: ["pants"], context: ["pants context"]))
try eventStorage.writeEvent(event: Event(eventDefinition: "some event 2", eventTime: Date().self, payload: ["pants"], context: ["pants context"]))
try eventStorage.startNewBatch()
try eventStorage.remove(id: eventStorage.batchReadyIds()[0])
try XCTAssertEqual(eventStorage.batchReadyIds().count, 0)
}
}
Loading