Refactoring

This commit is contained in:
Justin Mazzocchi 2020-10-15 03:03:39 -07:00
parent 66e7b01282
commit 66bd3c78b9
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
4 changed files with 53 additions and 50 deletions

View file

@ -2,14 +2,6 @@
import Foundation import Foundation
// https://en.wikipedia.org/wiki/Bloom_filter
// https://khanlou.com/2018/09/bloom-filters/
// This implementation uses deterministic hashing functions so it can conform to Codable
enum BloomFilterError: Error {
case noHashesProvided
}
public struct BloomFilter<T: DeterministicallyHashable>: Codable { public struct BloomFilter<T: DeterministicallyHashable>: Codable {
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case hashes case hashes
@ -20,12 +12,11 @@ public struct BloomFilter<T: DeterministicallyHashable>: Codable {
private var bits: BitArray private var bits: BitArray
public init(hashes: Set<Hash>, byteCount: Int) throws { public init(hashes: Set<Hash>, byteCount: Int) {
try self.init(hashes: hashes, data: Data(repeating: 0, count: byteCount)) self.init(hashes: hashes, data: Data(repeating: 0, count: byteCount))
} }
public init(hashes: Set<Hash>, data: Data) throws { public init(hashes: Set<Hash>, data: Data) {
guard !hashes.isEmpty else { throw BloomFilterError.noHashesProvided }
// Sort the hashes for consistent decoding output // Sort the hashes for consistent decoding output
self.hashes = Array(hashes.sorted { $0.rawValue < $1.rawValue }) self.hashes = Array(hashes.sorted { $0.rawValue < $1.rawValue })
bits = BitArray(data: data) bits = BitArray(data: data)

View file

@ -12,18 +12,8 @@ final class CodableBloomFilterTests: XCTestCase {
XCTAssertEqual(Hash.fnv1a32.apply("hash"), 3469047761) XCTAssertEqual(Hash.fnv1a32.apply("hash"), 3469047761)
} }
func noHashesProvided() throws {
XCTAssertThrowsError(try BloomFilter<String>(hashes: [], byteCount: 8)) {
guard case BloomFilterError.noHashesProvided = $0 else {
XCTFail("Expected no hashers provided error")
return
}
}
}
func testContains() throws { func testContains() throws {
var sut = try BloomFilter<String>(hashes: [.sdbm32, .djb232], byteCount: 8) var sut = BloomFilter<String>(hashes: [.sdbm32, .djb232], byteCount: 8)
sut.insert("lol") sut.insert("lol")
sut.insert("ok") sut.insert("ok")
@ -35,7 +25,7 @@ final class CodableBloomFilterTests: XCTestCase {
} }
func testData() throws { func testData() throws {
var sut = try BloomFilter<String>(hashes: [.sdbm32, .djb232], byteCount: 8) var sut = BloomFilter<String>(hashes: [.sdbm32, .djb232], byteCount: 8)
sut.insert("lol") sut.insert("lol")
sut.insert("ok") sut.insert("ok")
@ -44,7 +34,7 @@ final class CodableBloomFilterTests: XCTestCase {
} }
func testFromData() throws { func testFromData() throws {
let sut = try BloomFilter<String>(hashes: [.sdbm32, .djb232], data: Data([0, 16, 0, 0, 0, 2, 0, 144])) let sut = BloomFilter<String>(hashes: [.sdbm32, .djb232], data: Data([0, 16, 0, 0, 0, 2, 0, 144]))
XCTAssert(sut.contains("lol")) XCTAssert(sut.contains("lol"))
XCTAssert(sut.contains("ok")) XCTAssert(sut.contains("ok"))
@ -53,7 +43,7 @@ final class CodableBloomFilterTests: XCTestCase {
} }
func testCoding() throws { func testCoding() throws {
var sut = try BloomFilter<String>(hashes: [.sdbm32, .djb232], byteCount: 8) var sut = BloomFilter<String>(hashes: [.sdbm32, .djb232], byteCount: 8)
let expectedData = Data(#"{"data":"ABAAAAACAJA=","hashes":["djb232","sdbm32"]}"#.utf8) let expectedData = Data(#"{"data":"ABAAAAACAJA=","hashes":["djb232","sdbm32"]}"#.utf8)
sut.insert("lol") sut.insert("lol")
@ -88,7 +78,7 @@ final class CodableBloomFilterTests: XCTestCase {
} }
func testDataEncodingStrategy() throws { func testDataEncodingStrategy() throws {
var sut = try BloomFilter<String>(hashes: [.sdbm32, .djb232], byteCount: 8) var sut = BloomFilter<String>(hashes: [.sdbm32, .djb232], byteCount: 8)
let expectedData = Data(#"{"data":"0010000000020090","hashes":["djb232","sdbm32"]}"#.utf8) let expectedData = Data(#"{"data":"0010000000020090","hashes":["djb232","sdbm32"]}"#.utf8)
sut.insert("lol") sut.insert("lol")

View file

@ -61,7 +61,7 @@ public extension InstanceURLService {
func updateFilter() -> AnyPublisher<Never, Error> { func updateFilter() -> AnyPublisher<Never, Error> {
httpClient.request(UpdatedFilterTarget()) httpClient.request(UpdatedFilterTarget())
.handleEvents(receiveOutput: { userDefaultsClient.updatedInstanceFilter = $0 }) .handleEvents(receiveOutput: { userDefaultsClient.updateInstanceFilter($0) })
.ignoreOutput() .ignoreOutput()
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
@ -81,13 +81,43 @@ private struct UpdatedFilterTarget: DecodableTarget {
private extension InstanceURLService { private extension InstanceURLService {
static let httpsPrefix = "https://" static let httpsPrefix = "https://"
static let shortestPossibleURLLength = 4 static let shortestPossibleURLLength = 4
// swiftlint:disable line_length static let defaultFilter = BloomFilter<String>(
static let defaultFilterData = #"{"hashes":["djb232","djb2a32","fnv132","fnv1a32","sdbm32"],"data":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAIAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAgAAAAAQAAAAAABAAACAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAABAAAEAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAIAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAIAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAIAAAQAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAQAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAADAAAAAAAAAAAAA=="}"# hashes: [.djb232, .djb2a32, .sdbm32, .fnv132, .fnv1a32],
.data(using: .utf8)! data: Data([
// swiftlint:enable line_length 0, 0, 0, 16, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 8, 0, 0, 0, 0, 0, 0, 0, 0,
// swiftlint:disable force_try 0, 2, 2, 8, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 0, 0,
static let defaultFilter = try! JSONDecoder().decode(BloomFilter<String>.self, from: defaultFilterData) 128, 0, 0, 32, 0, 128, 0, 0, 0, 4, 16, 4, 32, 0, 0, 16, 16, 4, 32, 0, 0, 128, 0, 16, 0, 0, 0, 0, 0, 0, 0, 4,
// swiftlint:enable force_try 0, 0, 0, 0, 4, 0, 0, 3, 2, 0, 0, 0, 4, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 132, 0, 0, 64, 0, 0, 0, 2,
0, 0, 0, 0, 0, 0, 64, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 1, 0, 0,
0, 0, 0, 96, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 8, 0, 1, 0, 8, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 160,
0, 0, 0, 8, 64, 0, 1, 32, 0, 0, 1, 0, 0, 0, 0, 64, 8, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 32, 0, 0, 0, 0, 0, 130, 65, 0, 4, 0, 0, 0, 0,
0, 0, 0, 8, 0, 0, 0, 0, 128, 65, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 64, 0, 0, 64, 0, 128, 0, 0, 0, 16,
0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 2, 128, 0, 1, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 8, 0, 0, 0, 0, 0, 0, 0, 16, 0, 1, 48, 0, 0, 0, 2, 0, 0, 0, 0, 48, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0,
0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 16, 0, 0, 0, 16, 0, 16, 0, 2, 64,
0, 0, 0, 128, 0, 0, 0, 64, 16, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 8, 0, 0, 0, 4, 8, 0, 64, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 66,
0, 64, 0, 16, 8, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 64, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 10, 0,
0, 4, 0, 0, 0, 1, 24, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 128, 4, 64, 0, 128, 0, 0, 0,
0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 8, 0, 8, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 16, 0, 2, 0, 0, 0,
0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 0, 129, 8, 0, 8, 0, 0, 0, 8, 0, 0,
0, 2, 0, 0, 128, 8, 36, 32, 0, 64, 0, 0, 0, 4, 0, 32, 0, 0, 0, 0, 0, 16, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 8, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 16, 0, 0, 0, 136, 128, 0, 0, 0, 0, 0, 0,
0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 64, 0, 64, 0, 0, 0, 16, 0, 0, 0, 8, 0, 0, 0, 0, 16, 0,
0, 0, 0, 32, 0, 4, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 0,
0, 0, 0, 8, 0, 4, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 0, 0, 0, 160, 0, 0, 0, 4, 0, 0, 16, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 64,
16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 16, 0, 0, 0, 4, 0, 1, 0, 0, 0, 16, 0, 0,
0, 0, 0, 0, 0, 0, 128, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 4, 0, 64, 0, 1, 4, 0, 0, 0, 0,
0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 12, 16, 0, 72, 0, 0, 0, 0, 0, 0
]))
var filter: BloomFilter<String> { var filter: BloomFilter<String> {
userDefaultsClient.updatedInstanceFilter ?? Self.defaultFilter userDefaultsClient.updatedInstanceFilter ?? Self.defaultFilter
} }

View file

@ -3,7 +3,7 @@
import CodableBloomFilter import CodableBloomFilter
import Foundation import Foundation
final class UserDefaultsClient { struct UserDefaultsClient {
private let userDefaults: UserDefaults private let userDefaults: UserDefaults
init(userDefaults: UserDefaults) { init(userDefaults: UserDefaults) {
@ -13,23 +13,15 @@ final class UserDefaultsClient {
extension UserDefaultsClient { extension UserDefaultsClient {
var updatedInstanceFilter: BloomFilter<String>? { var updatedInstanceFilter: BloomFilter<String>? {
get { guard let data = self[.updatedFilter] as Data? else {
guard let data = self[.updatedFilter] as Data? else { return nil
return nil
}
return try? JSONDecoder().decode(BloomFilter<String>.self, from: data)
} }
set { return try? JSONDecoder().decode(BloomFilter<String>.self, from: data)
var data: Data? }
if let newValue = newValue { func updateInstanceFilter( _ filter: BloomFilter<String>) {
data = try? JSONEncoder().encode(newValue) userDefaults.set(try? JSONEncoder().encode(filter), forKey: Item.updatedFilter.rawValue)
}
self[.updatedFilter] = data
}
} }
} }