Skip to content

Commit

Permalink
After years of generating solutions - 🎉 KeyPath queries - at last!
Browse files Browse the repository at this point in the history
  • Loading branch information
Joannis committed May 15, 2022
1 parent cf17e77 commit 091c2d4
Show file tree
Hide file tree
Showing 13 changed files with 508 additions and 112 deletions.
3 changes: 3 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,5 +81,8 @@ let package = Package(
.testTarget(
name: "MongoKittenTests",
dependencies: ["MongoKitten"]),
.testTarget(
name: "MeowTests",
dependencies: ["Meow"]),
]
)
272 changes: 272 additions & 0 deletions Sources/Meow/KeyPathModel/KeyPathModels.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
import BSON

@dynamicMemberLookup
public struct QueryMatcher<M: KeyPathQueryableModel> {
internal init() {}

public subscript<T>(dynamicMember keyPath: KeyPath<M, QueryableField<T>>) -> QuerySubject<M, T> {
let path = try! M.resolveFieldPath(keyPath)
return QuerySubject(path: FieldPath(components: path))
}
}

public struct QuerySubject<M: KeyPathQueryableModel, T> {
internal let path: FieldPath!
}

public protocol KeyPathQueryableModel: BaseModel, Codable {
// static func makeFieldPath<T>(forKeyPath keyPath: KeyPath<Self, T>) -> FieldPath
// static func makePathComponents<T>(forKeyPath keyPath: KeyPath<Self, T>) -> [String]
}

extension KeyPathQueryableModel {
// public static func makeFieldPath<T>(forKeyPath keyPath: KeyPath<Self, T>) -> FieldPath {
// FieldPath(components: makePathComponents(forKeyPath: keyPath))
// }
}

extension CodingUserInfoKey {
static let isDiscoveringQuery = CodingUserInfoKey(rawValue: "__meow__discovery_query")!
}

enum MeowModelDecodingError: Error {
case unknownDecodingKey
}

public typealias Model = KeyPathQueryableModel & MutableModel

extension KeyPathQueryableModel {
internal static func resolveFieldPath<T>(_ field: QueryableField<T>) -> [String] {
return field.parents + [field.key!]
}

public static func resolveFieldPath<T>(_ field: KeyPath<Self, QueryableField<T>>) throws -> [String] {
let model = try Self(from: TestDecoder())
let field = model[keyPath: field]

return resolveFieldPath(field)
}

public static func resolveFieldPath<T>(_ field: KeyPath<Self, Field<T>>) throws -> [String] {
try resolveFieldPath(field.appending(path: \.projectedValue))
}
}

extension KeyPathQueryableModel where Self: MutableModel {
public func makePartialUpdate(_ mutate: (inout ModelUpdater<Self>) async throws -> Void) async throws -> PartialUpdate<Self> {
var updater = ModelUpdater(model: self)
try await mutate(&updater)
return updater.update
}
}

@dynamicMemberLookup
public struct ModelUpdater<M: KeyPathQueryableModel & MutableModel> {
var update: PartialUpdate<M>

internal init(model: M) {
self.update = PartialUpdate(
model: model
)
}

public subscript<P: Primitive>(dynamicMember keyPath: WritableKeyPath<M, QueryableField<P>>) -> P {
get {
update.model[keyPath: keyPath].value!
}
set {
update.model[keyPath: keyPath].value = newValue

do {
let path = try M.resolveFieldPath(keyPath)
update.changes[path] = newValue
} catch {
update.error = error
}
}
}
}

public struct PartialUpdate<M: KeyPathQueryableModel & MutableModel> {
var model: M
var changes = Document()
var error: Error?

public func apply(on collection: MeowCollection<M>) async throws -> M {
if let error = error {
throw error
}

return model
}
}

fileprivate extension Document {
subscript(path: [String]) -> Primitive? {
get {
if path.isEmpty {
return self
}

var path = path
let key = path.removeFirst()

if let document = self[key] as? Document {
return document[path]
} else {
return Document()
}
}
set {
if path.isEmpty {
return
}

var path = path
let key = path.removeFirst()

if path.isEmpty {
self[key] = newValue
} else {
var document = (self[key] as? Document) ?? Document()
document[path] = newValue
self[key] = document
}
}
}
}

@dynamicMemberLookup
public struct QueryableField<Value> {
internal let parents: [String]
internal let key: String?
internal var value: Value?

internal var isInvalid: Bool { key == nil }

public subscript<T>(dynamicMember keyPath: KeyPath<Value, QueryableField<T>>) -> QueryableField<T> {
if let key = key, let value = value {
let subField = value[keyPath: keyPath]

return QueryableField<T>(
parents: parents + [key],
key: subField.key,
value: subField.value
)
} else {
return QueryableField<T>(
parents: [],
key: nil,
value: nil
)
}
}
}

@propertyWrapper
public struct Field<C: Codable>: Codable {
public let key: String?
private var _wrappedValue: C?

public var wrappedValue: C {
get { _wrappedValue! }
set { _wrappedValue = newValue }
}

public var projectedValue: QueryableField<C> {
get { QueryableField(parents: [], key: key, value: _wrappedValue) }
set { _wrappedValue = newValue.value }
}

public init(from decoder: Decoder) throws {
guard let key = decoder.codingPath.last?.stringValue else {
throw MeowModelDecodingError.unknownDecodingKey
}

self.key = key

if decoder is TestDecoder {
self._wrappedValue = try? C(from: decoder)
} else {
self._wrappedValue = try C(from: decoder)
}
}

public init(wrappedValue: C) {
self.key = nil
self._wrappedValue = wrappedValue
}

public func encode(to encoder: Encoder) throws {
try _wrappedValue!.encode(to: encoder)
}
}

@propertyWrapper
public struct ID<Wrapped: MeowIdentifier>: Codable {
public var key: String { "_id" }
private var _wrappedValue: Wrapped?

public var wrappedValue: Wrapped {
get { _wrappedValue! }
set { _wrappedValue = newValue }
}

public var projectedValue: QueryableField<Wrapped> {
QueryableField(parents: [], key: key, value: _wrappedValue)
}

public init(from decoder: Decoder) throws {
if decoder is TestDecoder {
self._wrappedValue = try? Wrapped(from: decoder)
} else {
self._wrappedValue = try Wrapped(from: decoder)
}
}

public init(wrappedValue: Wrapped) {
self._wrappedValue = wrappedValue
}

public func encode(to encoder: Encoder) throws {
try _wrappedValue!.encode(to: encoder)
}
}

@propertyWrapper
public struct ReferenceField<M: BaseModel>: Codable {
public let key: String?
private var _wrappedValue: M.Identifier?

public var wrappedValue: M.Identifier {
get { _wrappedValue! }
set { _wrappedValue = newValue }
}

public var projectedValue: QueryableField<M.Identifier> {
QueryableField(parents: [], key: key, value: _wrappedValue)
}

public init(from decoder: Decoder) throws {
guard let key = decoder.codingPath.last?.stringValue else {
throw MeowModelDecodingError.unknownDecodingKey
}

self.key = key

if decoder is TestDecoder {
self._wrappedValue = try? M.Identifier(from: decoder)
} else {
self._wrappedValue = try M.Identifier(from: decoder)
}
}

public init(wrappedValue: M.Identifier) {
self.key = nil
self._wrappedValue = wrappedValue
}

public func encode(to encoder: Encoder) throws {
try _wrappedValue!.encode(to: encoder)
}
}
54 changes: 54 additions & 0 deletions Sources/Meow/KeyPathModel/MeowCollection+KeyPath.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import MongoKitten
import MongoClient

extension MeowCollection where M: KeyPathQueryableModel {
public func find(
matching: (QueryMatcher<M>) -> Document
) -> MappedCursor<FindQueryBuilder, M> {
let matcher = QueryMatcher<M>()
let filter = matching(matcher)
return raw.find(filter).decode(M.self)
}

public func findOne(
matching: (QueryMatcher<M>) -> Document
) async throws -> M? {
let matcher = QueryMatcher<M>()
let filter = matching(matcher)
return try await raw.findOne(filter, as: M.self)
}

public func count(
matching: (QueryMatcher<M>) -> Document
) async throws -> Int {
let matcher = QueryMatcher<M>()
let filter = matching(matcher)
return try await raw.count(filter)
}

public func watch(options: ChangeStreamOptions = .init()) async throws -> ChangeStream<M> {
return try await raw.watch(options: options, type: M.self)
}
}

extension MeowCollection where M: MutableModel & KeyPathQueryableModel {
@discardableResult
public func deleteOne(where filter: Document, writeConcern: WriteConcern? = nil) async throws -> DeleteReply {
return try await raw.deleteOne(where: filter, writeConcern: writeConcern)
}

@discardableResult
public func deleteOne<Q: MongoKittenQuery>(where filter: Q, writeConcern: WriteConcern? = nil) async throws -> DeleteReply {
return try await self.deleteOne(where: filter.makeDocument(), writeConcern: writeConcern)
}

@discardableResult
public func deleteAll(where filter: Document, writeConcern: WriteConcern? = nil) async throws -> DeleteReply {
return try await raw.deleteAll(where: filter, writeConcern: writeConcern)
}

@discardableResult
public func deleteAll<Q: MongoKittenQuery>(where filter: Q, writeConcern: WriteConcern? = nil) async throws -> DeleteReply {
return try await self.deleteAll(where: filter.makeDocument(), writeConcern: writeConcern)
}
}
Loading

0 comments on commit 091c2d4

Please sign in to comment.