Skip to content

Commit

Permalink
update: RegionsService
Browse files Browse the repository at this point in the history
update
  • Loading branch information
aaryankotharii committed Mar 17, 2024
1 parent 47b6a80 commit fe1f546
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 54 deletions.
61 changes: 30 additions & 31 deletions OBAKitCore/Location/Regions/RegionsService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public class RegionsService: NSObject, LocationServiceDelegate {
private let apiService: RegionsAPIService?
private let locationService: LocationService
private let userDefaults: UserDefaults
private let fileManager: RegionsServiceFileManagerProtocol
private let bundledRegionsFilePath: String
private let apiPath: String?

Expand All @@ -40,13 +41,15 @@ public class RegionsService: NSObject, LocationServiceDelegate {
/// - apiService: Retrieves new data from the region server and turns it into models.
/// - locationService: A location service object.
/// - userDefaults: The user defaults object.
/// - fileManager: The file manager object.
/// - bundledRegionsFilePath: The path to the bundled regions file. It is probably named "regions.json" or something similar.
/// - apiPath: The path to the remote regions.json file on the server. e.g. /path/to/regions.json
/// - delegate: A delegate object for callbacks.
public init(apiService: RegionsAPIService?, locationService: LocationService, userDefaults: UserDefaults, bundledRegionsFilePath: String, apiPath: String?, delegate: RegionsServiceDelegate? = nil) {
public init(apiService: RegionsAPIService?, locationService: LocationService, userDefaults: UserDefaults, fileManager: RegionsServiceFileManagerProtocol, bundledRegionsFilePath: String, apiPath: String?, delegate: RegionsServiceDelegate? = nil) {
self.apiService = apiService
self.locationService = locationService
self.userDefaults = userDefaults
self.fileManager = fileManager
self.bundledRegionsFilePath = bundledRegionsFilePath
self.apiPath = apiPath

Expand All @@ -55,7 +58,7 @@ public class RegionsService: NSObject, LocationServiceDelegate {
RegionsService.alwaysRefreshRegionsOnLaunchUserDefaultsKey: false
])

if let regions = RegionsService.loadStoredRegions(from: userDefaults), regions.count > 0 {
if let regions = RegionsService.loadStoredRegions(from: fileManager), regions.count > 0 {
self.regions = regions
}
else {
Expand Down Expand Up @@ -204,17 +207,10 @@ public class RegionsService: NSObject, LocationServiceDelegate {

// MARK: - Custom Regions
/// Adds the provided custom region to the RegionsService.
/// If an existing custom region with the same `regionIdentifier` exists, the new region replaces the existing region.
/// - throws: Persistence storage errors.
/// - throws: File system errors.
public func add(customRegion newRegion: Region) async throws {
var regions = customRegions
if let index = regions.firstIndex(where: { $0.regionIdentifier == newRegion.regionIdentifier }) {
regions.remove(at: index)
}

regions.append(newRegion)

try userDefaults.encodeUserDefaultsObjects(regions, key: RegionsService.storedCustomRegionsUserDefaultsKey)
let customRegionPath = RegionsService.getCustomRegionPath(customRegionIdentifier: newRegion.regionIdentifier)
try fileManager.save(newRegion, to: customRegionPath)
}

/// Deletes the custom region. If the region could not be found, this method exits normally.
Expand All @@ -228,28 +224,27 @@ public class RegionsService: NSObject, LocationServiceDelegate {
/// - parameter identifier: The region identifier used to find the custom region to delete.
/// - throws: If a custom region with the provided `identifier` could not be found, or is not a custom region, this method will throw.
public func delete(customRegionIdentifier identifier: RegionIdentifier) async throws {
var regions = customRegions


guard self.currentRegion?.regionIdentifier != identifier else {
throw UnstructuredError(
"Cannot delete the current selected region",
recoverySuggestion: "Choose a different region to be the currently selected region, before deleting this region.")
}

guard let index = regions.firstIndex(where: { $0.regionIdentifier == identifier }) else {
return
}

regions.remove(at: index)
try userDefaults.encodeUserDefaultsObjects(regions, key: RegionsService.storedCustomRegionsUserDefaultsKey)

let customRegionPath = RegionsService.getCustomRegionPath(customRegionIdentifier: identifier)
try fileManager.remove(at: customRegionPath)
}

/// Retrieves an array of custom regions loaded from files in the custom regions directory.
/// - Returns: An array of `Region` objects representing custom regions.
public var customRegions: [Region] {
let regions: [Region]
do {
regions = try userDefaults.decodeUserDefaultsObjects(type: [Region].self, key: RegionsService.storedCustomRegionsUserDefaultsKey) ?? []
} catch {
regions = []
var regions: [Region] = []
if let fileURLs = try? fileManager.urls(at: RegionsService.customRegionsPath) {
for url in fileURLs {
if let region = try? fileManager.load(Region.self, from: url) {
regions.append(region)
}
}
}
return regions
}
Expand All @@ -262,16 +257,20 @@ public class RegionsService: NSObject, LocationServiceDelegate {

public static let alwaysRefreshRegionsOnLaunchUserDefaultsKey = "OBAAlwaysRefreshRegionsOnLaunchUserDefaultsKey"
static let automaticallySelectRegionUserDefaultsKey = "OBAAutomaticallySelectRegionUserDefaultsKey"
static let storedRegionsUserDefaultsKey = "OBAStoredRegionsUserDefaultsKey"
static let storedCustomRegionsUserDefaultsKey = "OBAStoredCustomRegionsUserDefaultsKey"
static let currentRegionUserDefaultsKey = "OBACurrentRegionUserDefaultsKey"
static let regionsUpdatedAtUserDefaultsKey = "OBARegionsUpdatedAtUserDefaultsKey"
static let defaultRegionsPath = URL.applicationSupportDirectory.appendingPathComponent("default-regions.json")
static let customRegionsPath = URL.documentsDirectory.appendingPathComponent("Regions/custom-regions")

private class func getCustomRegionPath(customRegionIdentifier identifier: RegionIdentifier) -> URL {
return customRegionsPath.appendingPathComponent("region-\(identifier).json")
}

// MARK: - Save Regions

private func storeRegions() {
do {
try userDefaults.encodeUserDefaultsObjects(regions, key: RegionsService.storedRegionsUserDefaultsKey)
try fileManager.save(regions, to: RegionsService.defaultRegionsPath)
userDefaults.set(Date(), forKey: RegionsService.regionsUpdatedAtUserDefaultsKey)
}
catch {
Expand All @@ -281,11 +280,11 @@ public class RegionsService: NSObject, LocationServiceDelegate {

// MARK: - Load Stored Regions

private class func loadStoredRegions(from userDefaults: UserDefaults) -> [Region]? {
private class func loadStoredRegions(from fileManager: RegionsServiceFileManagerProtocol) -> [Region]? {
let regions: [Region]

do {
regions = try userDefaults.decodeUserDefaultsObjects(type: [Region].self, key: RegionsService.storedRegionsUserDefaultsKey) ?? []
regions = try fileManager.load([Region].self, from: RegionsService.defaultRegionsPath)
} catch {
return nil
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//
// RegionsServiceFileManagerProtocol.swift
// OBAKit
//
// Copyright © Open Transit Software Foundation
// This source code is licensed under the Apache 2.0 license found in the
// LICENSE file in the root directory of this source tree.
//

import Foundation

/// A protocol defining file management operations.
public protocol RegionsServiceFileManagerProtocol: AnyObject {

/// Saves the Codable object to the specified directory with the given name.
///
/// - Parameters:
/// - object: The Codable object to save.
/// - destination: The destination specifying where to save the object.
func save<T: Codable>(_ object: T, to destination: URL) throws

/// Loads a Codable object of the specified type from the file with the given URL.
///
/// - Parameters:
/// - type: The type of the Codable object to load.
/// - fileURL: The URL of the file to load the object from.
/// - Returns: The decoded Codable object.
func load<T: Codable>(_ type: T.Type, from fileURL: URL) throws -> T

/// Removes the file at the specified destination.
///
/// - Parameters:
/// - destination: The destination specifying which file to remove.
func remove(at destination: URL) throws

/// Returns an array of URLs for the items at the specified URL.
///
/// - Parameters:
/// - destination: The destination specifying where to load contents of.
/// - Returns: An array of URLs for the items in the directory.
func urls(at destination: URL) throws -> [URL]

}

extension FileManager: RegionsServiceFileManagerProtocol {

/// Creates a directory at the specified URL if it doesn't already exist.
private func createDirectoryIfNeeded(at url: URL) throws {
guard !fileExists(atPath: url.path) else { return }
try createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
}

public func urls(at destination: URL) throws -> [URL] {
return try contentsOfDirectory(at: destination, includingPropertiesForKeys: nil)
}

public func load<T: Codable>(_ type: T.Type, from fileURL: URL) throws -> T {
let data = try Data(contentsOf: fileURL)
return try JSONDecoder().decode(T.self, from: data)
}

public func save<T: Codable>(_ object: T, to destination: URL) throws {
let directoryURL = destination.deletingLastPathComponent()
try createDirectoryIfNeeded(at: directoryURL)
let encoded = try JSONEncoder().encode(object)
try encoded.write(to: destination)
}

public func remove(at destination: URL) throws {
guard fileExists(atPath: destination.path) else { return }
try removeItem(at: destination)
}

}
6 changes: 5 additions & 1 deletion OBAKitCore/Orchestration/CoreApplication.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ open class CoreApplication: NSObject,
/// Shared user defaults
@objc public let userDefaults: UserDefaults

/// Default file manager.
private let fileManager: RegionsServiceFileManagerProtocol

/// The underlying implementation of our data stores.
private let userDefaultsStore: UserDefaultsStore

Expand All @@ -47,7 +50,7 @@ open class CoreApplication: NSObject,
@objc public let locationService: LocationService

/// Responsible for managing `Region`s and determining the correct `Region` for the user.
@objc public lazy var regionsService = RegionsService(apiService: regionsAPIService, locationService: locationService, userDefaults: userDefaults, bundledRegionsFilePath: self.config.bundledRegionsFilePath, apiPath: self.config.regionsAPIPath)
@objc public lazy var regionsService = RegionsService(apiService: regionsAPIService, locationService: locationService, userDefaults: userDefaults, fileManager: fileManager, bundledRegionsFilePath: self.config.bundledRegionsFilePath, apiPath: self.config.regionsAPIPath)

/// Helper property that returns `regionsService.currentRegion`.
@objc public var currentRegion: Region? {
Expand All @@ -73,6 +76,7 @@ open class CoreApplication: NSObject,
self.config = config

userDefaults = config.userDefaults
fileManager = FileManager.default
userDefaultsStore = UserDefaultsStore(userDefaults: userDefaults)

locationService = config.locationService
Expand Down
35 changes: 35 additions & 0 deletions OBAKitTests/Helpers/Mocks/FileManagerMock.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// FileManagerMock.swift
// OBAKitTests
//
// Copyright © Open Transit Software Foundation
// This source code is licensed under the Apache 2.0 license found in the
// LICENSE file in the root directory of this source tree.
//

import Foundation
import OBAKitCore

class RegionsServiceFileManagerMock: RegionsServiceFileManagerProtocol {

var savedObjects: [String: Data] = [:]

func save<T>(_ object: T, to destination: URL) throws where T : Decodable, T : Encodable {
let encodedData = try JSONEncoder().encode(object)
savedObjects[destination.path] = encodedData
}

func load<T>(_ type: T.Type, from fileURL: URL) throws -> T where T : Decodable {
let data = savedObjects[fileURL.path, default: Data()]
return try JSONDecoder().decode(T.self, from: data)
}

func remove(at destination: URL) throws {
savedObjects.removeValue(forKey: destination.path)
}

func urls(at destination: URL) throws -> [URL] {
return []
}

}
Loading

0 comments on commit fe1f546

Please sign in to comment.