Created
April 28, 2021 16:34
-
-
Save jessegrosjean/243102a1153dc8c7a7217c5abafd9cbf to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Combine | |
import Foundation | |
import CoreGraphics | |
protocol Store { | |
associatedtype Object | |
func insert(_ object: Object) -> Future<Object, Error> | |
func update(_ object: Object) -> Future<Object, Error> | |
func delete(_ object: Object) -> Future<Object, Error> | |
func execute(_ query: Query<Self>) -> Future<[Object], Error> | |
} | |
extension Store { | |
func filter(where predicate: Predicate<Object>) -> Future<[Object], Error> { | |
execute(Query(store: self, predicate: predicate, sortCriteria: [])) | |
} | |
} | |
struct SortCriterion<T> { | |
let property: PartialKeyPath<T> | |
let order: Order | |
enum Order { | |
case ascending | |
case descending | |
} | |
} | |
struct Query<T: Store> { | |
let store: T | |
let predicate: Predicate<T.Object> | |
let sortCriteria: [SortCriterion<T.Object>] | |
func result() -> Future<[T.Object], Error> { | |
store.execute(self) | |
} | |
func sorted<U: Comparable>( | |
by property: KeyPath<T.Object, U>, | |
inOrder order: SortCriterion<T.Object>.Order = .ascending | |
) -> Query<T> { | |
Query( | |
store: store, | |
predicate: predicate, | |
sortCriteria: sortCriteria + [SortCriterion( | |
property: property, | |
order: order | |
)] | |
) | |
} | |
} | |
indirect enum Predicate<T> { | |
case comparison(PartialKeyPath<T>, Operator, Primitive) | |
case and(Predicate<T>, Predicate<T>) | |
case or(Predicate<T>, Predicate<T>) | |
case not(Predicate<T>) | |
} | |
func == <T, U: Equatable & Primitive> (lhs: KeyPath<T, U>, rhs: U) -> Predicate<T> { | |
return .comparison(lhs, .equalTo, rhs) | |
} | |
func < <T, U: Comparable & Primitive> (lhs: KeyPath<T, U>, rhs: U) -> Predicate<T> { | |
.comparison(lhs, .lessThan, rhs) | |
} | |
func <= <T, U: Comparable & Primitive> (lhs: KeyPath<T, U>, rhs: U) -> Predicate<T> { | |
.comparison(lhs, .lessThanOrEqualTo, rhs) | |
} | |
func > <T, U: Comparable & Primitive> (lhs: KeyPath<T, U>, rhs: U) -> Predicate<T> { | |
.comparison(lhs, .greaterThan, rhs) | |
} | |
func >= <T, U: Comparable & Primitive> (lhs: KeyPath<T, U>, rhs: U) -> Predicate<T> { | |
.comparison(lhs, .greaterThanOrEqualTo, rhs) | |
} | |
func && <T> (lhs: Predicate<T>, rhs: Predicate<T>) -> Predicate<T> { | |
.and(lhs, rhs) | |
} | |
func || <T> (lhs: Predicate<T>, rhs: Predicate<T>) -> Predicate<T> { | |
.or(lhs, rhs) | |
} | |
prefix func ! <T> (predicate: Predicate<T>) -> Predicate<T> { | |
.not(predicate) | |
} | |
enum Operator { | |
case lessThan | |
case lessThanOrEqualTo | |
case equalTo | |
case greaterThanOrEqualTo | |
case greaterThan | |
} | |
protocol Primitive { | |
var type: Type { get } | |
} | |
indirect enum Type { | |
case bool | |
case int | |
case int8 | |
case int16 | |
case int32 | |
case int64 | |
case uint | |
case uint8 | |
case uint16 | |
case uint32 | |
case uint64 | |
case double | |
case float | |
case cgFloat | |
case string | |
case date | |
case wrapped(Any, Type) | |
} | |
extension Bool: Primitive { | |
var type: Type { .bool } | |
} | |
extension Int: Primitive { | |
var type: Type { .int } | |
} | |
extension Int8: Primitive { | |
var type: Type { .int8 } | |
} | |
extension Int16: Primitive { | |
var type: Type { .int16 } | |
} | |
extension Int32: Primitive { | |
var type: Type { .int32 } | |
} | |
extension Int64: Primitive { | |
var type: Type { .int64 } | |
} | |
extension UInt: Primitive { | |
var type: Type { .uint } | |
} | |
extension UInt8: Primitive { | |
var type: Type { .uint8 } | |
} | |
extension UInt16: Primitive { | |
var type: Type { .uint16 } | |
} | |
extension UInt32: Primitive { | |
var type: Type { .uint32 } | |
} | |
extension UInt64: Primitive { | |
var type: Type { .uint64 } | |
} | |
extension Double: Primitive { | |
var type: Type { .double } | |
} | |
extension Float: Primitive { | |
var type: Type { .float } | |
} | |
extension CGFloat: Primitive { | |
var type: Type { .cgFloat } | |
} | |
extension String: Primitive { | |
var type: Type { .string } | |
} | |
extension Date: Primitive { | |
var type: Type { .date } | |
} | |
extension Primitive where Self: RawRepresentable, RawValue == Int { | |
var type: Type { .wrapped(rawValue, .int) } | |
} | |
extension Primitive where Self: RawRepresentable, RawValue == Int8 { | |
var type: Type { .wrapped(rawValue, .int8) } | |
} | |
extension Primitive where Self: RawRepresentable, RawValue == Int16 { | |
var type: Type { .wrapped(rawValue, .int16) } | |
} | |
extension Primitive where Self: RawRepresentable, RawValue == Int32 { | |
var type: Type { .wrapped(rawValue, .int32) } | |
} | |
extension Primitive where Self: RawRepresentable, RawValue == Int64 { | |
var type: Type { .wrapped(rawValue, .int64) } | |
} | |
extension Primitive where Self: RawRepresentable, RawValue == UInt { | |
var type: Type { .wrapped(rawValue, .uint) } | |
} | |
extension Primitive where Self: RawRepresentable, RawValue == UInt8 { | |
var type: Type { .wrapped(rawValue, .uint8) } | |
} | |
extension Primitive where Self: RawRepresentable, RawValue == UInt16 { | |
var type: Type { .wrapped(rawValue, .uint16) } | |
} | |
extension Primitive where Self: RawRepresentable, RawValue == UInt32 { | |
var type: Type { .wrapped(rawValue, .uint32) } | |
} | |
extension Primitive where Self: RawRepresentable, RawValue == UInt64 { | |
var type: Type { .wrapped(rawValue, .uint64) } | |
} | |
extension Primitive where Self: RawRepresentable, RawValue == Double { | |
var type: Type { .wrapped(rawValue, .double) } | |
} | |
extension Primitive where Self: RawRepresentable, RawValue == Float { | |
var type: Type { .wrapped(rawValue, .float) } | |
} | |
extension Primitive where Self: RawRepresentable, RawValue == CGFloat { | |
var type: Type { .wrapped(rawValue, .cgFloat) } | |
} | |
extension Primitive where Self: RawRepresentable, RawValue == String { | |
var type: Type { .wrapped(rawValue, .string) } | |
} | |
extension Predicate { | |
func isIncluded() -> (T) -> Bool { | |
switch self { | |
case let .comparison(keyPath, .greaterThan, value): | |
return { $0[keyPath: keyPath] > value } | |
case let .comparison(keyPath, .greaterThanOrEqualTo, value): | |
return { $0[keyPath: keyPath] >= value } | |
case let .comparison(keyPath, .equalTo, value): | |
return { $0[keyPath: keyPath] == value } | |
case let .comparison(keyPath, .lessThanOrEqualTo, value): | |
return { $0[keyPath: keyPath] <= value } | |
case let .comparison(keyPath, .lessThan, value): | |
return { $0[keyPath: keyPath] < value } | |
case let .and(firstPredicate, secondPredicate): | |
return { firstPredicate.isIncluded()($0) && secondPredicate.isIncluded()($0) } | |
case let .or(firstPredicate, secondPredicate): | |
return { firstPredicate.isIncluded()($0) || secondPredicate.isIncluded()($0) } | |
case let .not(predicate): | |
return { predicate.isIncluded()($0) == false } | |
} | |
} | |
} | |
struct Movie { | |
let id: Id | |
let title: String | |
let genre: String | |
let budget: Double | |
typealias Id = String | |
} | |
final class MovieStoreMock: Store { | |
private(set) var movies: [Movie] = [] | |
func insert(_ object: Movie) -> Future<Movie, Error> { | |
return Future { completion in | |
self.movies.append(object) | |
completion(.success(object)) | |
} | |
} | |
func update(_ object: Movie) -> Future<Movie, Error> { | |
return Future { completion in | |
if let index = self.movies.firstIndex(where: { $0.id == object.id }) { | |
self.movies[index] = object | |
} | |
completion(.success(object)) | |
} | |
} | |
func delete(_ object: Movie) -> Future<Movie, Error> { | |
return Future { completion in | |
if let index = self.movies.firstIndex(where: { $0.id == object.id }) { | |
self.movies.remove(at: index) | |
} | |
completion(.success(object)) | |
} | |
} | |
func execute(_ query: Query<MovieStoreMock>) -> Future<[Movie], Error> { | |
return Future { completion in | |
let includedMovies = self.movies.filter(query.predicate.isIncluded()) | |
completion(.success(includedMovies)) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment