Skip to content

Commit

Permalink
Add СacheService
Browse files Browse the repository at this point in the history
  • Loading branch information
aromanov91 committed Nov 28, 2024
1 parent c246212 commit 3528f8c
Show file tree
Hide file tree
Showing 12 changed files with 852 additions and 40 deletions.
4 changes: 4 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import PackageDescription
let commonDependencies: [PackageDescription.Package.Dependency] = [
.package(url: "https://github.com/aaronsky/asc-swift.git", .upToNextMajor(from: "1.0.1")),
.package(url: "https://github.com/hmlongco/Factory.git", .upToNextMajor(from: "2.1.3")),
.package(url: "https://github.com/1024jp/GzipSwift", .upToNextMajor(from: "6.1.0")),
.package(url: "https://github.com/dehesa/CodableCSV.git", .upToNextMajor(from: "0.6.7"))
]

let remoteDependencies: [PackageDescription.Package.Dependency] = commonDependencies + [
Expand Down Expand Up @@ -51,6 +53,8 @@ let package = Package(
.product(name: "OversizeCore", package: "OversizeCore"),
.product(name: "OversizeModels", package: "OversizeModels"),
.product(name: "OversizeServices", package: "OversizeServices"),
.product(name: "Gzip", package: "GzipSwift"),
.product(name: "CodableCSV", package: "CodableCSV")
]
),
.testTarget(
Expand Down
70 changes: 70 additions & 0 deletions Sources/OversizeAppStoreServices/Cache/СacheService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//
// Copyright © 2024 Alexander Romanov
// FileCache.swift, created on 28.11.2024
//

import Foundation
import OversizeCore

public final class СacheService {
private let cacheDirectory: URL
private let cacheExpiration: TimeInterval

public init(expiration: TimeInterval = 300) { // Default expiration: 5 minutes
let path = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first
cacheDirectory = path ?? FileManager.default.temporaryDirectory.appendingPathComponent("Default")
cacheExpiration = expiration
}

private func cacheFilePath(for key: String) -> URL {
cacheDirectory.appendingPathComponent(key)
}

func save(_ data: some Encodable, for key: String = #function) {
let fileURL = cacheFilePath(for: key)
do {
let jsonData = try JSONEncoder().encode(data)
try jsonData.write(to: fileURL, options: .atomic)
logData("Saved cache: \(key)")
} catch {
logError("Failed to save cache for key \(key): \(error)")
}
}

func load<T: Decodable>(for key: String = #function, as _: T.Type) -> T? {
let fileURL = cacheFilePath(for: key)

guard FileManager.default.fileExists(atPath: fileURL.path),
isCacheValid(for: fileURL)
else {
return nil
}

do {
let data = try Data(contentsOf: fileURL)
logNotice("Readed cache: \(key)")
return try JSONDecoder().decode(T.self, from: data)
} catch {
logError("Failed to load cache for key \(key): \(error)")
return nil
}
}

func remove(for key: String = #function) {
let fileURL = cacheFilePath(for: key)
try? FileManager.default.removeItem(at: fileURL)
}

func clearAll() {
try? FileManager.default.removeItem(at: cacheDirectory)
}

private func isCacheValid(for fileURL: URL) -> Bool {
guard let attributes = try? FileManager.default.attributesOfItem(atPath: fileURL.path),
let modificationDate = attributes[.modificationDate] as? Date
else {
return false
}
return Date().timeIntervalSince(modificationDate) < cacheExpiration
}
}
32 changes: 32 additions & 0 deletions Sources/OversizeAppStoreServices/Models/AnalyticsReport.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// Copyright © 2024 Alexander Romanov
// AnalyticsReport.swift, created on 25.11.2024
//

import AppStoreAPI
import AppStoreConnect
import OversizeCore

public struct AnalyticsReport: Identifiable, Hashable, Sendable {
public let id: String
public var name: String
public var category: Category

init?(schema: AppStoreAPI.AnalyticsReport) {
guard let categoryRawValue = schema.attributes?.category?.rawValue,
let category: Category = .init(rawValue: categoryRawValue),
let name: String = schema.attributes?.name
else { return nil }
id = schema.id
self.category = category
self.name = name
}

public enum Category: String, CaseIterable, Codable, Sendable {
case appUsage = "APP_USAGE"
case appStoreEngagement = "APP_STORE_ENGAGEMENT"
case commerce = "COMMERCE"
case frameworkUsage = "FRAMEWORK_USAGE"
case performance = "PERFORMANCE"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
// Copyright © 2024 Alexander Romanov
// AnalyticsReportRequest.swift, created on 25.11.2024
//

import AppStoreAPI
import AppStoreConnect
import Foundation
import OversizeCore

public struct AnalyticsReportRequest: Identifiable, Sendable {
public let id: String
public var accessType: AccessType?
public var isStoppedDueToInactivity: Bool?

public let included: Included?

init?(schema: AppStoreAPI.AnalyticsReportRequest, included: [AppStoreAPI.AnalyticsReport]? = nil) {
guard let accessTypeRawValue = schema.attributes?.accessType?.rawValue,
let accessType: AccessType = .init(rawValue: accessTypeRawValue) else { return nil }
id = schema.id
self.accessType = accessType
isStoppedDueToInactivity = schema.attributes?.isStoppedDueToInactivity

if let analyticsReport = included?.filter { $0.id == schema.relationships?.reports?.data?.first?.id } {
self.included = .init(analyticsReports: analyticsReport.compactMap { .init(schema: $0) })
} else {
self.included = nil
}
}

public enum AccessType: String, CaseIterable, Codable, Sendable {
case oneTimeSnapshot = "ONE_TIME_SNAPSHOT"
case ongoing = "ONGOING"
}

public struct Included: Sendable {
public let analyticsReports: [AnalyticsReport]?
}
}
152 changes: 152 additions & 0 deletions Sources/OversizeAppStoreServices/Models/FinanceReport.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import CodableCSV
import Foundation
import OversizeCore

public struct FinanceReports: Sendable {
public let reports: [Report]
public let totalRows: Int?
public let totalAmount: Double?
public let totalUnits: Int?

public struct Report: Sendable {
public let startDate: String
public let endDate: String
public let upc: String?
public let isrcIsbn: String?
public let vendorIdentifier: String
public let quantity: String?
public let partnerShare: String?
public let extendedPartnerShare: String?
public let partnerShareCurrency: String?
public let salesOrReturn: String?
public let appleIdentifier: String?
public let artistShowDeveloperAuthor: String?
public let title: String?
public let labelStudioNetworkPublisher: String?
public let grid: String?
public let productTypeIdentifier: String?
public let isanOtherIdentifier: String?
public let countryOfSale: String?
public let preOrderFlag: String?
public let promoCode: String?
public let customerPrice: String
public let customerCurrency: String?

public init(
startDate: String,
endDate: String,
upc: String? = nil,
isrcIsbn: String? = nil,
vendorIdentifier: String,
quantity: String? = nil,
partnerShare: String? = nil,
extendedPartnerShare: String? = nil,
partnerShareCurrency: String? = nil,
salesOrReturn: String? = nil,
appleIdentifier: String? = nil,
artistShowDeveloperAuthor: String? = nil,
title: String? = nil,
labelStudioNetworkPublisher: String? = nil,
grid: String? = nil,
productTypeIdentifier: String? = nil,
isanOtherIdentifier: String? = nil,
countryOfSale: String? = nil,
preOrderFlag: String? = nil,
promoCode: String? = nil,
customerPrice: String,
customerCurrency: String? = nil
) {
self.startDate = startDate
self.endDate = endDate
self.upc = upc
self.isrcIsbn = isrcIsbn
self.vendorIdentifier = vendorIdentifier
self.quantity = quantity
self.partnerShare = partnerShare
self.extendedPartnerShare = extendedPartnerShare
self.partnerShareCurrency = partnerShareCurrency
self.salesOrReturn = salesOrReturn
self.appleIdentifier = appleIdentifier
self.artistShowDeveloperAuthor = artistShowDeveloperAuthor
self.title = title
self.labelStudioNetworkPublisher = labelStudioNetworkPublisher
self.grid = grid
self.productTypeIdentifier = productTypeIdentifier
self.isanOtherIdentifier = isanOtherIdentifier
self.countryOfSale = countryOfSale
self.preOrderFlag = preOrderFlag
self.promoCode = promoCode
self.customerPrice = customerPrice
self.customerCurrency = customerCurrency
}
}

init?(data: Data) {
guard let csvString = String(data: data, encoding: .utf8) else { return nil }

do {
let unwantedKeywords = ["Total_Rows", "Total_Amount", "Total_Units"]

let lines = csvString.components(separatedBy: "\n")

var totalRows: Int? = nil
var totalAmount: Double? = nil
var totalUnits: Int? = nil

var filteredLines: [String] = []
for line in lines {
if line.contains("Total_Rows") {
totalRows = Int(line.split(separator: "\t")[1].trimmingCharacters(in: .whitespacesAndNewlines))
} else if line.contains("Total_Amount") {
totalAmount = Double(line.split(separator: "\t")[1].trimmingCharacters(in: .whitespacesAndNewlines))
} else if line.contains("Total_Units") {
totalUnits = Int(line.split(separator: "\t")[1].trimmingCharacters(in: .whitespacesAndNewlines))
} else {
filteredLines.append(line)
}
}

let filteredCSV = filteredLines.joined(separator: "\n")

let reader = try CSVReader(input: filteredCSV) {
$0.headerStrategy = .firstLine
$0.delimiters.field = "\t"
$0.presample = true
}

reports = reader.compactMap {
.init(
startDate: $0.element(0).valueOrEmpty,
endDate: $0.element(1).valueOrEmpty,
upc: $0.element(2),
isrcIsbn: $0.element(3),
vendorIdentifier: $0.element(4).valueOrEmpty,
quantity: $0.element(5),
partnerShare: $0.element(6),
extendedPartnerShare: $0.element(7),
partnerShareCurrency: $0.element(8),
salesOrReturn: $0.element(9),
appleIdentifier: $0.element(10),
artistShowDeveloperAuthor: $0.element(11),
title: $0.element(12),
labelStudioNetworkPublisher: $0.element(13),
grid: $0.element(14),
productTypeIdentifier: $0.element(15),
isanOtherIdentifier: $0.element(16),
countryOfSale: $0.element(17),
preOrderFlag: $0.element(18),
promoCode: $0.element(19),
customerPrice: $0.element(20).valueOrEmpty,
customerCurrency: $0.element(21)
)
}

self.totalRows = totalRows
self.totalAmount = totalAmount
self.totalUnits = totalUnits

} catch {
return nil
}
}
}
Loading

0 comments on commit 3528f8c

Please sign in to comment.