From 5a1102bf14275480a0478046b3e5f136e2e22762 Mon Sep 17 00:00:00 2001 From: Emily Toop Date: Tue, 24 Apr 2018 11:50:24 +0100 Subject: [PATCH] Add wrapper classes for Rust FFI --- .../Mentat/Mentat.xcodeproj/project.pbxproj | 12 - .../swift/Mentat/Mentat/Core/TypedValue.swift | 181 ++++++++++ sdks/swift/Mentat/Mentat/Errors/Errors.swift | 30 ++ .../Mentat/Mentat/Extensions/Date+Int64.swift | 16 + .../Mentat/Extensions/Result+Unwrap.swift | 50 +++ sdks/swift/Mentat/Mentat/Mentat.swift | 170 +++++++++ sdks/swift/Mentat/Mentat/Query/Query.swift | 337 ++++++++++++++++++ .../swift/Mentat/Mentat/Query/RelResult.swift | 106 ++++++ .../Mentat/Mentat/Query/TupleResult.swift | 217 +++++++++++ .../Mentat/Rust/OptionalRustObject.swift | 68 ++++ .../swift/Mentat/Mentat/Rust/RustObject.swift | 49 +++ .../Mentat/Mentat/Transact/TxReport.swift | 61 ++++ 12 files changed, 1285 insertions(+), 12 deletions(-) create mode 100644 sdks/swift/Mentat/Mentat/Core/TypedValue.swift create mode 100644 sdks/swift/Mentat/Mentat/Errors/Errors.swift create mode 100644 sdks/swift/Mentat/Mentat/Extensions/Date+Int64.swift create mode 100644 sdks/swift/Mentat/Mentat/Extensions/Result+Unwrap.swift create mode 100644 sdks/swift/Mentat/Mentat/Mentat.swift create mode 100644 sdks/swift/Mentat/Mentat/Query/Query.swift create mode 100644 sdks/swift/Mentat/Mentat/Query/RelResult.swift create mode 100644 sdks/swift/Mentat/Mentat/Query/TupleResult.swift create mode 100644 sdks/swift/Mentat/Mentat/Rust/OptionalRustObject.swift create mode 100644 sdks/swift/Mentat/Mentat/Rust/RustObject.swift create mode 100644 sdks/swift/Mentat/Mentat/Transact/TxReport.swift diff --git a/sdks/swift/Mentat/Mentat.xcodeproj/project.pbxproj b/sdks/swift/Mentat/Mentat.xcodeproj/project.pbxproj index 62055d08..37886d23 100644 --- a/sdks/swift/Mentat/Mentat.xcodeproj/project.pbxproj +++ b/sdks/swift/Mentat/Mentat.xcodeproj/project.pbxproj @@ -7,9 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 7B744837208DF20D006CFFB0 /* EntityBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B744836208DF20D006CFFB0 /* EntityBuilder.swift */; }; - 7B744839208DF2E1006CFFB0 /* InProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B744838208DF2E1006CFFB0 /* InProgress.swift */; }; - 7B74483B208DF2F9006CFFB0 /* InProgressBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B74483A208DF2F9006CFFB0 /* InProgressBuilder.swift */; }; 7B74483D208DF667006CFFB0 /* Result+Unwrap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B74483C208DF667006CFFB0 /* Result+Unwrap.swift */; }; 7BAE75A22089020E00895D37 /* libmentat_ffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7BEB7D23207BE2AF000369AD /* libmentat_ffi.a */; }; 7BAE75A42089022B00895D37 /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 7BAE75A32089022B00895D37 /* libsqlite3.tbd */; }; @@ -42,9 +39,6 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 7B744836208DF20D006CFFB0 /* EntityBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntityBuilder.swift; sourceTree = ""; }; - 7B744838208DF2E1006CFFB0 /* InProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InProgress.swift; sourceTree = ""; }; - 7B74483A208DF2F9006CFFB0 /* InProgressBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InProgressBuilder.swift; sourceTree = ""; }; 7B74483C208DF667006CFFB0 /* Result+Unwrap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "Result+Unwrap.swift"; path = "Mentat/Extensions/Result+Unwrap.swift"; sourceTree = SOURCE_ROOT; }; 7B911E1A2085081D000998CB /* libtoodle.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libtoodle.a; path = "../../../../sync-storage-prototype/rust/target/universal/release/libtoodle.a"; sourceTree = ""; }; 7BAE75A32089022B00895D37 /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = usr/lib/libsqlite3.tbd; sourceTree = SDKROOT; }; @@ -203,9 +197,6 @@ isa = PBXGroup; children = ( 7BEB7D2B207D03DA000369AD /* TxReport.swift */, - 7B744836208DF20D006CFFB0 /* EntityBuilder.swift */, - 7B744838208DF2E1006CFFB0 /* InProgress.swift */, - 7B74483A208DF2F9006CFFB0 /* InProgressBuilder.swift */, ); path = Transact; sourceTree = ""; @@ -324,15 +315,12 @@ 7BDB96B32077C38E009D0651 /* RustObject.swift in Sources */, 7BDB96C62077D347009D0651 /* Date+Int64.swift in Sources */, 7BEB7D2C207D03DA000369AD /* TxReport.swift in Sources */, - 7B744839208DF2E1006CFFB0 /* InProgress.swift in Sources */, 7BDB96B42077C38E009D0651 /* OptionalRustObject.swift in Sources */, - 7B74483B208DF2F9006CFFB0 /* InProgressBuilder.swift in Sources */, 7BDB96B22077C38E009D0651 /* RelResult.swift in Sources */, 7BDB96AF2077C38E009D0651 /* Query.swift in Sources */, 7BDB96CC207B7684009D0651 /* Errors.swift in Sources */, 7BDB96B02077C38E009D0651 /* Mentat.swift in Sources */, 7BDB96B72077C38E009D0651 /* TypedValue.swift in Sources */, - 7B744837208DF20D006CFFB0 /* EntityBuilder.swift in Sources */, 7BDB96B52077C38E009D0651 /* TupleResult.swift in Sources */, 7B74483D208DF667006CFFB0 /* Result+Unwrap.swift in Sources */, ); diff --git a/sdks/swift/Mentat/Mentat/Core/TypedValue.swift b/sdks/swift/Mentat/Mentat/Core/TypedValue.swift new file mode 100644 index 00000000..ba980a6f --- /dev/null +++ b/sdks/swift/Mentat/Mentat/Core/TypedValue.swift @@ -0,0 +1,181 @@ +/* Copyright 2018 Mozilla + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +import Foundation +import MentatStore + +/** + A wrapper around Mentat's `TypedValue` Rust object. This class wraps a raw pointer to a Rust `TypedValue` + struct and provides accessors to the values according to expected result type. + + As the FFI functions for fetching values are consuming, this class keeps a copy of the result internally after + fetching so that the value can be referenced several times. + + Also, due to the consuming nature of the FFI layer, this class also manages it's raw pointer, nilling it after calling the + FFI conversion function so that the underlying base class can manage cleanup. + */ +class TypedValue: OptionalRustObject { + + private var value: Any? + + /** + The `ValueType` for this `TypedValue`. + - Returns: The `ValueType` for this `TypedValue`. + */ + var valueType: ValueType { + return typed_value_value_type(self.raw!) + } + + private func isConsumed() -> Bool { + return self.raw == nil + } + + /** + This value as a `Int64`. This function will panic if the `ValueType` of this `TypedValue` + is not a `Long` + + - Returns: the value of this `TypedValue` as a `Int64` + */ + func asLong() -> Int64 { + defer { + self.raw = nil + } + if !self.isConsumed() { + self.value = typed_value_as_long(self.raw!) + } + return self.value as! Int64 + } + + /** + This value as an `Entid`. This function will panic if the `ValueType` of this `TypedValue` + is not a `Ref` + + - Returns: the value of this `TypedValue` as an `Entid` + */ + func asEntid() -> Entid { + defer { + self.raw = nil + } + + if !self.isConsumed() { + self.value = typed_value_as_entid(self.raw!) + } + return self.value as! Entid + } + + /** + This value as a keyword `String`. This function will panic if the `ValueType` of this `TypedValue` + is not a `Keyword` + + - Returns: the value of this `TypedValue` as a keyword `String` + */ + func asKeyword() -> String { + defer { + self.raw = nil + } + + if !self.isConsumed() { + self.value = String(cString: typed_value_as_kw(self.raw!)) + } + return self.value as! String + } + + /** + This value as a `Bool`. This function will panic if the `ValueType` of this `TypedValue` + is not a `Boolean` + + - Returns: the value of this `TypedValue` as a `Bool` + */ + func asBool() -> Bool { + defer { + self.raw = nil + } + + if !self.isConsumed() { + let v = typed_value_as_boolean(self.raw!) + self.value = v > 0 + } + return self.value as! Bool + } + + /** + This value as a `Double`. This function will panic if the `ValueType` of this `TypedValue` + is not a `Double` + + - Returns: the value of this `TypedValue` as a `Double` + */ + func asDouble() -> Double { + defer { + self.raw = nil + } + + if !self.isConsumed() { + self.value = typed_value_as_double(self.raw!) + } + return self.value as! Double + } + + /** + This value as a `Date`. This function will panic if the `ValueType` of this `TypedValue` + is not a `Instant` + + - Returns: the value of this `TypedValue` as a `Date` + */ + func asDate() -> Date { + defer { + self.raw = nil + } + + if !self.isConsumed() { + let timestamp = typed_value_as_timestamp(self.raw!) + self.value = Date(timeIntervalSince1970: TimeInterval(timestamp)) + } + return self.value as! Date + } + + /** + This value as a `String`. This function will panic if the `ValueType` of this `TypedValue` + is not a `String` + + - Returns: the value of this `TypedValue` as a `String` + */ + func asString() -> String { + defer { + self.raw = nil + } + + if !self.isConsumed() { + self.value = String(cString: typed_value_as_string(self.raw!)) + } + return self.value as! String + } + + /** + This value as a `UUID`. This function will panic if the `ValueType` of this `TypedValue` + is not a `Uuid` + + - Returns: the value of this `TypedValue` as a `UUID?`. If the `UUID` is not valid then this function returns nil. + */ + func asUUID() -> UUID? { + defer { + self.raw = nil + } + + if !self.isConsumed() { + let bytes = typed_value_as_uuid(self.raw!).pointee + self.value = UUID(uuid: bytes) + } + return self.value as! UUID? + } + + override func cleanup(pointer: OpaquePointer) { + typed_value_destroy(pointer) + } +} diff --git a/sdks/swift/Mentat/Mentat/Errors/Errors.swift b/sdks/swift/Mentat/Mentat/Errors/Errors.swift new file mode 100644 index 00000000..9091219b --- /dev/null +++ b/sdks/swift/Mentat/Mentat/Errors/Errors.swift @@ -0,0 +1,30 @@ +// +/* Copyright 2018 Mozilla + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +import Foundation + +public enum QueryError: Error { + case invalidKeyword(message: String) + case executionFailed(message: String) +} + +public struct MentatError: Error { + let message: String +} + +public enum PointerError: Error { + case pointerConsumed +} + +public enum ResultError: Error { + case error(message: String) + case empty +} diff --git a/sdks/swift/Mentat/Mentat/Extensions/Date+Int64.swift b/sdks/swift/Mentat/Mentat/Extensions/Date+Int64.swift new file mode 100644 index 00000000..a292d47c --- /dev/null +++ b/sdks/swift/Mentat/Mentat/Extensions/Date+Int64.swift @@ -0,0 +1,16 @@ +///* This Source Code Form is subject to the terms of the Mozilla Public +// * License, v. 2.0. If a copy of the MPL was not distributed with this +// * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import Foundation + +extension Date { + /** + This `Date` as microseconds. + + - Returns: The `timeIntervalSince1970` in microseconds + */ + func toMicroseconds() -> Int64 { + return Int64(self.timeIntervalSince1970 * 1_000_000) + } +} diff --git a/sdks/swift/Mentat/Mentat/Extensions/Result+Unwrap.swift b/sdks/swift/Mentat/Mentat/Extensions/Result+Unwrap.swift new file mode 100644 index 00000000..3d94903a --- /dev/null +++ b/sdks/swift/Mentat/Mentat/Extensions/Result+Unwrap.swift @@ -0,0 +1,50 @@ +/* Copyright 2018 Mozilla + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +import Foundation +import MentatStore + +extension Result { + /** + Force unwraps a result. + Expects there to be a value attached and throws an error is there is not. + + - Throws: `ResultError.error` if the result contains an error + - Throws: `ResultError.empty` if the result contains no error but also no result. + + - Returns: The pointer to the successful result value. + */ + @discardableResult public func unwrap() throws -> UnsafeMutableRawPointer { + guard let success = self.ok else { + if let error = self.err { + throw ResultError.error(message: String(cString: error)) + } + throw ResultError.empty + } + return success + } + + /** + Unwraps an optional result, yielding either a successful value or a nil. + + - Throws: `ResultError.error` if the result contains an error + + - Returns: The pointer to the successful result value, or nil if no value is present. + */ + @discardableResult public func tryUnwrap() throws -> UnsafeMutableRawPointer? { + guard let success = self.ok else { + if let error = self.err { + throw ResultError.error(message: String(cString: error)) + } + return nil + } + return success + } +} diff --git a/sdks/swift/Mentat/Mentat/Mentat.swift b/sdks/swift/Mentat/Mentat/Mentat.swift new file mode 100644 index 00000000..43dacb5c --- /dev/null +++ b/sdks/swift/Mentat/Mentat/Mentat.swift @@ -0,0 +1,170 @@ +/* Copyright 2018 Mozilla + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +import Foundation + +import MentatStore + +typealias Entid = Int64 + +/** + Protocol to be implemenented by any object that wishes to register for transaction observation + */ +protocol Observing { + func transactionDidOccur(key: String, reports: [TxChange]) +} + +/** + Protocol to be implemented by any object that provides an interface to Mentat's transaction observers. + */ +protocol Observable { + func register(key: String, observer: Observing, attributes: [String]) + func unregister(key: String) +} + +/** + The primary class for accessing Mentat's API. + This class provides all of the basic API that can be found in Mentat's Store struct. + The raw pointer it holds is a pointer to a Store. +*/ +class Mentat: RustObject { + fileprivate static var observers = [String: Observing]() + + /** + Create a new Mentat with the provided pointer to a Mentat Store + - Parameter raw: A pointer to a Mentat Store. + */ + required override init(raw: OpaquePointer) { + super.init(raw: raw) + } + + /** + Open a connection to a Store in a given location. + If the store does not already exist, one will be created. + + - Parameter storeURI: The URI as a String of the store to open. + If no store URI is provided, an in-memory store will be opened. + */ + convenience init(storeURI: String = "") { + self.init(raw: store_open(storeURI)) + } + + /** + Simple transact of an EDN string. + - Parameter transaction: The string, as EDN, to be transacted + + - Throws: `MentatError` if the an error occured during the transaction, or the TxReport is nil. + + - Returns: The `TxReport` of the completed transaction + */ + func transact(transaction: String) throws -> TxReport { + let result = store_transact(self.raw, transaction).pointee + return TxReport(raw: try result.unwrap()) + } + + /** + Get the the `Entid` of the attribute. + - Parameter attribute: The string represeting the attribute whose `Entid` we are after. + The string is represented as `:namespace/name`. + + - Returns: The `Entid` associated with the attribute. + */ + func entidForAttribute(attribute: String) -> Entid { + return Entid(store_entid_for_attribute(self.raw, attribute)) + } + + /** + Start a query. + - Parameter query: The string represeting the the query to be executed. + + - Returns: The `Query` representing the query that can be executed. + */ + func query(query: String) -> Query { + return Query(raw: store_query(self.raw, query)) + } + + /** + Retrieve a single value of an attribute for an Entity + - Parameter attribute: The string the attribute whose value is to be returned. + The string is represented as `:namespace/name`. + - Parameter entid: The `Entid` of the entity we want the value from. + + - Returns: The `TypedValue` containing the value of the attribute for the entity. + */ + func value(forAttribute attribute: String, ofEntity entid: Entid) throws -> TypedValue? { + let result = store_value_for_attribute(self.raw, entid, attribute).pointee + return TypedValue(raw: try result.unwrap()) + } + + // Destroys the pointer by passing it back into Rust to be cleaned up + override func cleanup(pointer: OpaquePointer) { + store_destroy(pointer) + } +} + +/** + Set up `Mentat` to provide an interface to Mentat's transaction observation + */ +extension Mentat: Observable { + /** + Register an `Observing` and a set of attributes to observer for transaction observation. + The `transactionDidOccur(String: [TxChange]:)` function is called when a transaction + occurs in the `Store` that this `Mentat` is connected to that affects the attributes that an + `Observing` has registered for. + + - Parameter key: `String` representing an identifier for the `Observing`. + - Parameter observer: The `Observing` to be notified when a transaction occurs. + - Parameter attributes: An `Array` of `Strings` representing the attributes that the `Observing` + wishes to be notified about if they are referenced in a transaction. + */ + func register(key: String, observer: Observing, attributes: [String]) { + let attrEntIds = attributes.map({ (kw) -> Entid in + let entid = Entid(self.entidForAttribute(attribute: kw)); + return entid + }) + + let ptr = UnsafeMutablePointer.allocate(capacity: attrEntIds.count) + let entidPointer = UnsafeMutableBufferPointer(start: ptr, count: attrEntIds.count) + var _ = entidPointer.initialize(from: attrEntIds) + + guard let firstElement = entidPointer.baseAddress else { + return + } + Mentat.observers[key] = observer + store_register_observer(self.raw, key, firstElement, Entid(attributes.count), transactionObserverCallback) + + } + + /** + Unregister the `Observing` that was registered with the provided key such that it will no longer be called + if a transaction occurs that affects the attributes that `Observing` was registered to observe. + + The `Observing` will need to re-register if it wants to start observing again. + + - Parameter key: `String` representing an identifier for the `Observing`. + */ + func unregister(key: String) { + Mentat.observers.removeValue(forKey: key) + store_unregister_observer(self.raw, key) + } +} + + +/** + This function needs to be static as callbacks passed into Rust from Swift cannot contain state. Therefore the observers are static, as is + the function that we pass into Rust to receive the callback. + */ +private func transactionObserverCallback(key: UnsafePointer, reports: UnsafePointer) { + let key = String(cString: key) + guard let observer = Mentat.observers[key] else { return } + DispatchQueue.global(qos: .background).async { + observer.transactionDidOccur(key: key, reports: [TxChange]()) + } +} diff --git a/sdks/swift/Mentat/Mentat/Query/Query.swift b/sdks/swift/Mentat/Mentat/Query/Query.swift new file mode 100644 index 00000000..883e9ea6 --- /dev/null +++ b/sdks/swift/Mentat/Mentat/Query/Query.swift @@ -0,0 +1,337 @@ +/* Copyright 2018 Mozilla + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +import Foundation +import MentatStore + + +/** + This class allows you to contruct a query, bind values to variables and run those queries against a mentat DB. + + This class cannot be created directly, but must be created through `Mentat.query(String:)`. + + The types of values you can bind are + - `Int64` + - `Entid` + - `Keyword` + - `Bool` + - `Double` + - `Date` + - `String` + - `UUID`. + + Each bound variable must have a corresponding value in the query string used to create this query. + + ``` + let query = """ + [:find ?name ?cat + :in ?type + :where + [?c :community/name ?name] + [?c :community/type ?type] + [?c :community/category ?cat]] + """ + mentat.query(query: query) + .bind(varName: "?type", toKeyword: ":community.type/website") + .run { result in + ... + } + ``` + + Queries can be run and the results returned in a number of different formats. Individual result values are returned as `TypedValues` and + the format differences relate to the number and structure of those values. The result format is related to the format provided in the query string. + + - `Rel` - This is the default `run` function and returns a list of rows of values. Queries that wish to have `Rel` results should format their query strings: + ``` + let query = """ + [: find ?a ?b ?c + : where ... ] + """ + mentat.query(query: query) + .run { result in + ... + } + ``` + - `Scalar` - This returns a single value as a result. This can be optional, as the value may not be present. Queries that wish to have `Scalar` results should format their query strings: + ``` + let query = """ + [: find ?a . + : where ... ] + """ + mentat.query(query: query) + .runScalar { result in + ... + } + ``` + - `Coll` - This returns a list of single values as a result. Queries that wish to have `Coll` results should format their query strings: + ``` + let query = """ + [: find [?a ...] + : where ... ] + """ + mentat.query(query: query) + .runColl { result in + ... + } + ``` + - `Tuple` - This returns a single row of values. Queries that wish to have `Tuple` results should format their query strings: + ``` + let query = """ + [: find [?a ?b ?c] + : where ... ] + """ + mentat.query(query: query) + .runTuple { result in + ... + } + ``` + */ +class Query: OptionalRustObject { + + /** + Binds a `Int64` value to the provided variable name. + + - Parameter varName: The name of the variable in the format `?name`. + - Parameter value: The value to be bound + + - Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has already been executed. + + - Returns: This `Query` such that further function can be called. + */ + func bind(varName: String, toLong value: Int64) throws -> Query { + query_builder_bind_long(try! self.validPointer(), varName, value) + return self + } + + /** + Binds a `Entid` value to the provided variable name. + + - Parameter varName: The name of the variable in the format `?name`. + - Parameter value: The value to be bound + + - Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has already been executed. + + - Returns: This `Query` such that further function can be called. + */ + func bind(varName: String, toReference value: Entid) throws -> Query { + query_builder_bind_ref(try! self.validPointer(), varName, value) + return self + } + + /** + Binds a `String` value representing a keyword for an attribute to the provided variable name. + Keywords take the format `:namespace/name`. + + - Parameter varName: The name of the variable in the format `?name`. + - Parameter value: The value to be bound + + - Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has already been executed. + + - Returns: This `Query` such that further function can be called. + */ + func bind(varName: String, toReference value: String) throws -> Query { + query_builder_bind_ref_kw(try! self.validPointer(), varName, value) + return self + } + + /** + Binds a keyword `String` value to the provided variable name. + Keywords take the format `:namespace/name`. + + - Parameter varName: The name of the variable in the format `?name`. + - Parameter value: The value to be bound + + - Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has already been executed. + + - Returns: This `Query` such that further function can be called. + */ + func bind(varName: String, toKeyword value: String) throws -> Query { + query_builder_bind_kw(try! self.validPointer(), varName, value) + return self + } + + /** + Binds a `Bool` value to the provided variable name. + + - Parameter varName: The name of the variable in the format `?name`. + - Parameter value: The value to be bound + + - Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has already been executed. + + - Returns: This `Query` such that further function can be called. + */ + func bind(varName: String, toBoolean value: Bool) throws -> Query { + query_builder_bind_boolean(try! self.validPointer(), varName, value ? 1 : 0) + return self + } + + /** + Binds a `Double` value to the provided variable name. + + - Parameter varName: The name of the variable in the format `?name`. + - Parameter value: The value to be bound + + - Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has already been executed. + + - Returns: This `Query` such that further function can be called. + */ + func bind(varName: String, toDouble value: Double) throws -> Query { + query_builder_bind_double(try! self.validPointer(), varName, value) + return self + } + + /** + Binds a `Date` value to the provided variable name. + + - Parameter varName: The name of the variable in the format `?name`. + - Parameter value: The value to be bound + + - Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has already been executed. + + - Returns: This `Query` such that further function can be called. + */ + func bind(varName: String, toDate value: Date) throws -> Query { + query_builder_bind_timestamp(try! self.validPointer(), varName, value.toMicroseconds()) + return self + } + + /** + Binds a `String` value to the provided variable name. + + - Parameter varName: The name of the variable in the format `?name`. + - Parameter value: The value to be bound + + - Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has already been executed. + + - Returns: This `Query` such that further function can be called. + */ + func bind(varName: String, toString value: String) throws -> Query { + query_builder_bind_string(try! self.validPointer(), varName, value) + return self + } + + /** + Binds a `UUID` value to the provided variable name. + + - Parameter varName: The name of the variable in the format `?name`. + - Parameter value: The value to be bound + + - Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has already been executed. + + - Returns: This `Query` such that further function can be called. + */ + func bind(varName: String, toUuid value: UUID) throws -> Query { + var rawUuid = value.uuid + withUnsafePointer(to: &rawUuid) { uuidPtr in + query_builder_bind_uuid(try! self.validPointer(), varName, uuidPtr) + } + return self + } + + /** + Execute the query with the values bound associated with this `Query` and call the provided callback function with the results as a list of rows of `TypedValues`. + + - Parameter callback: the function to call with the results of this query + + - Throws: `QueryError.executionFailed` if the query fails to execute. This could be because the provided query did not parse, or that + variable we incorrectly bound, or that the query provided was not `Rel`. + - Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has previously been executed. + */ + func run(callback: @escaping (RelResult?) -> Void) throws { + let result = query_builder_execute(try! self.validPointer()) + self.raw = nil + + if let err = result.pointee.err { + let message = String(cString: err) + throw QueryError.executionFailed(message: message) + } + guard let results = result.pointee.ok else { + callback(nil) + return + } + callback(RelResult(raw: results)) + } + + /** + Execute the query with the values bound associated with this `Query` and call the provided callback function with the result as a single `TypedValue`. + + - Parameter callback: the function to call with the results of this query + + - Throws: `QueryError.executionFailed` if the query fails to execute. This could be because the provided query did not parse, that + variable we incorrectly bound, or that the query provided was not `Scalar`. + - Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has previously been executed. + */ + func runScalar(callback: @escaping (TypedValue?) -> Void) throws { + let result = query_builder_execute_scalar(try! self.validPointer()) + self.raw = nil + + if let err = result.pointee.err { + let message = String(cString: err) + throw QueryError.executionFailed(message: message) + } + guard let results = result.pointee.ok else { + callback(nil) + return + } + callback(TypedValue(raw: OpaquePointer(results))) + } + + /** + Execute the query with the values bound associated with this `Query` and call the provided callback function with the result as a list of single `TypedValues`. + + - Parameter callback: the function to call with the results of this query + + - Throws: `QueryError.executionFailed` if the query fails to execute. This could be because the provided query did not parse, that + variable we incorrectly bound, or that the query provided was not `Coll`. + - Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has previously been executed. + */ + func runColl(callback: @escaping (ColResult?) -> Void) throws { + let result = query_builder_execute_coll(try! self.validPointer()) + self.raw = nil + + if let err = result.pointee.err { + let message = String(cString: err) + throw QueryError.executionFailed(message: message) + } + guard let results = result.pointee.ok else { + callback(nil) + return + } + callback(ColResult(raw: results)) + } + + /** + Execute the query with the values bound associated with this `Query` and call the provided callback function with the result as a list of single `TypedValues`. + + - Parameter callback: the function to call with the results of this query + + - Throws: `QueryError.executionFailed` if the query fails to execute. This could be because the provided query did not parse, that + variable we incorrectly bound, or that the query provided was not `Tuple`. + - Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has previously been executed. + */ + func runTuple(callback: @escaping (TupleResult?) -> Void) throws { + let result = query_builder_execute_tuple(try! self.validPointer()) + self.raw = nil + + if let err = result.pointee.err { + let message = String(cString: err) + throw QueryError.executionFailed(message: message) + } + guard let results = result.pointee.ok else { + callback(nil) + return + } + callback(TupleResult(raw: OpaquePointer(results))) + } + + override func cleanup(pointer: OpaquePointer) { + query_builder_destroy(pointer) + } +} diff --git a/sdks/swift/Mentat/Mentat/Query/RelResult.swift b/sdks/swift/Mentat/Mentat/Query/RelResult.swift new file mode 100644 index 00000000..b9443b38 --- /dev/null +++ b/sdks/swift/Mentat/Mentat/Query/RelResult.swift @@ -0,0 +1,106 @@ +/* Copyright 2018 Mozilla + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +import Foundation +import MentatStore + +/** + Wraps a `Rel` result from a Mentat query. + A `Rel` result is a list of rows of `TypedValues`. + Individual rows can be fetched or the set can be iterated. + + To fetch individual rows from a `RelResult` use `row(Int32)`. + + ``` + query.run { rows in + let row1 = rows.row(0) + let row2 = rows.row(1) + } + ``` + + To iterate over the result set use standard iteration flows. + ``` + query.run { rows in + rows.forEach { row in + ... + } + } + ``` + + Note that iteration is consuming and can only be done once. + */ +class RelResult: OptionalRustObject { + + /** + Fetch the row at the requested index. + + - Parameter index: the index of the row to be fetched + + - Throws: `PointerError.pointerConsumed` if the result set has already been iterated. + + - Returns: The row at the requested index as a `TupleResult`, if present, or nil if there is no row at that index. + */ + func row(index: Int32) throws -> TupleResult? { + guard let row = row_at_index(try self.validPointer(), index) else { + return nil + } + return TupleResult(raw: row) + } + + override func cleanup(pointer: OpaquePointer) { + destroy(UnsafeMutableRawPointer(pointer)) + } +} + +/** + Iterator for `RelResult`. + + To iterate over the result set use standard iteration flows. + ``` + query.run { result in + rows.forEach { row in + ... + } + } + ``` + + Note that iteration is consuming and can only be done once. + */ +class RelResultIterator: OptionalRustObject, IteratorProtocol { + typealias Element = TupleResult + + init(iter: OpaquePointer?) { + super.init(raw: iter) + } + + func next() -> Element? { + guard let iter = self.raw, + let rowPtr = rows_iter_next(iter) else { + return nil + } + return TupleResult(raw: rowPtr) + } + + override func cleanup(pointer: OpaquePointer) { + typed_value_result_set_iter_destroy(pointer) + } +} + +extension RelResult: Sequence { + func makeIterator() -> RelResultIterator { + do { + let rowIter = rows_iter(try self.validPointer()) + self.raw = nil + return RelResultIterator(iter: rowIter) + } catch { + return RelResultIterator(iter: nil) + } + } +} diff --git a/sdks/swift/Mentat/Mentat/Query/TupleResult.swift b/sdks/swift/Mentat/Mentat/Query/TupleResult.swift new file mode 100644 index 00000000..1ba47fc0 --- /dev/null +++ b/sdks/swift/Mentat/Mentat/Query/TupleResult.swift @@ -0,0 +1,217 @@ +/* Copyright 2018 Mozilla + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +import Foundation +import MentatStore + +/** + Wraps a `Tuple` result from a Mentat query. + A `Tuple` result is a list of `TypedValues`. + Individual values can be fetched as `TypedValues` or converted into a requested type. + + Values can be fetched as one of the following types: + - `TypedValue` + - `Int64` + - `Entid` + - `Keyword` + - `Bool` + - `Double` + - `Date` + - `String` + - `UUID`. + */ +class TupleResult: OptionalRustObject { + + /** + Return the `TypedValue` at the specified index. + If the index is greater than the number of values then this function will crash. + + - Parameter index: The index of the value to fetch. + + - Returns: The `TypedValue` at that index. + */ + func get(index: Int) -> TypedValue { + return TypedValue(raw: value_at_index(self.raw!, Int32(index))) + } + + /** + Return the `Int64` at the specified index. + If the index is greater than the number of values then this function will crash. + If the value type if the `TypedValue` at this index is not `Long` then this function will crash. + + - Parameter index: The index of the value to fetch. + + - Returns: The `Int64` at that index. + */ + func asLong(index: Int) -> Int64 { + return value_at_index_as_long(self.raw!, Int32(index)) + } + + /** + Return the `Entid` at the specified index. + If the index is greater than the number of values then this function will crash. + If the value type if the `TypedValue` at this index is not `Ref` then this function will crash. + + - Parameter index: The index of the value to fetch. + + - Returns: The `Entid` at that index. + */ + func asEntid(index: Int) -> Entid { + return value_at_index_as_entid(self.raw!, Int32(index)) + } + + /** + Return the keyword `String` at the specified index. + If the index is greater than the number of values then this function will crash. + If the value type if the `TypedValue` at this index is not `Keyword` then this function will crash. + + - Parameter index: The index of the value to fetch. + + - Returns: The keyword `String` at that index. + */ + func asKeyword(index: Int) -> String { + return String(cString: value_at_index_as_kw(self.raw!, Int32(index))) + } + + /** + Return the `Bool` at the specified index. + If the index is greater than the number of values then this function will crash. + If the value type if the `TypedValue` at this index is not `Boolean` then this function will crash. + + - Parameter index: The index of the value to fetch. + + - Returns: The `Bool` at that index. + */ + func asBool(index: Int) -> Bool { + return value_at_index_as_boolean(self.raw!, Int32(index)) == 0 ? false : true + } + + /** + Return the `Double` at the specified index. + If the index is greater than the number of values then this function will crash. + If the value type if the `TypedValue` at this index is not `Double` then this function will crash. + + - Parameter index: The index of the value to fetch. + + - Returns: The `Double` at that index. + */ + func asDouble(index: Int) -> Double { + return value_at_index_as_double(self.raw!, Int32(index)) + } + + /** + Return the `Date` at the specified index. + If the index is greater than the number of values then this function will crash. + If the value type if the `TypedValue` at this index is not `Instant` then this function will crash. + + - Parameter index: The index of the value to fetch. + + - Returns: The `Date` at that index. + */ + func asDate(index: Int) -> Date { + return Date(timeIntervalSince1970: TimeInterval(value_at_index_as_timestamp(self.raw!, Int32(index)))) + } + + /** + Return the `String` at the specified index. + If the index is greater than the number of values then this function will crash. + If the value type if the `TypedValue` at this index is not `String` then this function will crash. + + - Parameter index: The index of the value to fetch. + + - Returns: The `String` at that index. + */ + func asString(index: Int) -> String { + return String(cString: value_at_index_as_string(self.raw!, Int32(index))) + } + + /** + Return the `UUID` at the specified index. + If the index is greater than the number of values then this function will crash. + If the value type if the `TypedValue` at this index is not `Uuid` then this function will crash. + + - Parameter index: The index of the value to fetch. + + - Returns: The `UUID` at that index. + */ + func asUUID(index: Int) -> UUID? { + return UUID(uuid: value_at_index_as_uuid(self.raw!, Int32(index)).pointee) + } + + override func cleanup(pointer: OpaquePointer) { + typed_value_list_destroy(pointer) + } +} + +/** + Wraps a `Coll` result from a Mentat query. + A `Coll` result is a list of rows of single values of type `TypedValue`. + Values for individual rows can be fetched as `TypedValue` or converted into a requested type. + + Row values can be fetched as one of the following types: + - `TypedValue` + - `Int64` + - `Entid` + - `Keyword` + - `Bool` + - `Double` + - `Date` + - `String` + - `UUID`. + */ +class ColResult: TupleResult { +} + +/** + Iterator for `ColResult`. + + To iterate over the result set use standard iteration flows. + ``` + query.runColl { rows in + rows.forEach { value in + ... + } + } + ``` + + Note that iteration is consuming and can only be done once. + */ +class ColResultIterator: OptionalRustObject, IteratorProtocol { + typealias Element = TypedValue + + init(iter: OpaquePointer?) { + super.init(raw: iter) + } + + func next() -> Element? { + guard let iter = self.raw, + let rowPtr = values_iter_next(iter) else { + return nil + } + return TypedValue(raw: rowPtr) + } + + override func cleanup(pointer: OpaquePointer) { + typed_value_list_iter_destroy(pointer) + } +} + +extension ColResult: Sequence { + func makeIterator() -> ColResultIterator { + defer { + self.raw = nil + } + guard let raw = self.raw else { + return ColResultIterator(iter: nil) + } + let rowIter = values_iter(raw) + return ColResultIterator(iter: rowIter) + } +} diff --git a/sdks/swift/Mentat/Mentat/Rust/OptionalRustObject.swift b/sdks/swift/Mentat/Mentat/Rust/OptionalRustObject.swift new file mode 100644 index 00000000..7f2fe74d --- /dev/null +++ b/sdks/swift/Mentat/Mentat/Rust/OptionalRustObject.swift @@ -0,0 +1,68 @@ +/* Copyright 2018 Mozilla + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +import Foundation +import MentatStore + +/** + Base class that wraps an optional `OpaquePointer` representing a pointer to a Rust object. + This class should be used to wrap Rust pointer that point to consuming structs, that is, calling a function + for that Rust pointer, will cause Rust to destroy the pointer, leaving the Swift pointer dangling. + These classes are responsible for ensuring that their raw `OpaquePointer` are `nil`led after calling a consuming + FFI function. + This class provides cleanup functions on deinit, ensuring that all classes + that inherit from it will have their `OpaquePointer` destroyed when the Swift wrapper is destroyed. + If a class does not override `cleanup` then a `fatalError` is thrown. + The optional pointer is managed here such that is the pointer is nil, then the cleanup function is not called + ensuring that we do not double free the pointer on exit. + */ +class OptionalRustObject: Destroyable { + var raw: OpaquePointer? + lazy var uniqueId: ObjectIdentifier = { + ObjectIdentifier(self) + }() + + init(raw: UnsafeMutableRawPointer) { + self.raw = OpaquePointer(raw) + } + + init(raw: OpaquePointer?) { + self.raw = raw + } + + func intoRaw() -> OpaquePointer? { + return self.raw + } + + deinit { + guard let raw = self.raw else { return } + self.cleanup(pointer: raw) + } + + /** + Provides a non-optional `OpaquePointer` if one exists for this class. + + - Throws: `Pointer.pointerConsumed` if the raw pointer wrapped by this class is nil + + - Returns: the raw `OpaquePointer` wrapped by this class. + */ + func validPointer() throws -> OpaquePointer { + guard let r = self.raw else { + throw PointerError.pointerConsumed + } + + return r + } + + func cleanup(pointer: OpaquePointer) { + fatalError("\(cleanup) is not implemented.") + } +} + diff --git a/sdks/swift/Mentat/Mentat/Rust/RustObject.swift b/sdks/swift/Mentat/Mentat/Rust/RustObject.swift new file mode 100644 index 00000000..7648fadb --- /dev/null +++ b/sdks/swift/Mentat/Mentat/Rust/RustObject.swift @@ -0,0 +1,49 @@ +/* Copyright 2018 Mozilla + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +import Foundation +import MentatStore + +protocol Destroyable { + func cleanup(pointer: OpaquePointer) +} + +/** + Base class that wraps an non-optional `OpaquePointer` representing a pointer to a Rust object. + This class provides cleanup functions on deinit, ensuring that all classes + that inherit from it will have their `OpaquePointer` destroyed when the Swift wrapper is destroyed. + If a class does not override `cleanup` then a `fatalError` is thrown. + */ +public class RustObject: Destroyable { + var raw: OpaquePointer + + init(raw: OpaquePointer) { + self.raw = raw + } + + init(raw: UnsafeMutableRawPointer) { + self.raw = OpaquePointer(raw) + } + + init?(raw: OpaquePointer?) { + guard let r = raw else { + return nil + } + self.raw = r + } + + deinit { + self.cleanup(pointer: self.raw) + } + + func cleanup(pointer: OpaquePointer) { + fatalError("\(cleanup) is not implemented.") + } +} diff --git a/sdks/swift/Mentat/Mentat/Transact/TxReport.swift b/sdks/swift/Mentat/Mentat/Transact/TxReport.swift new file mode 100644 index 00000000..8d0d2528 --- /dev/null +++ b/sdks/swift/Mentat/Mentat/Transact/TxReport.swift @@ -0,0 +1,61 @@ +/* Copyright 2018 Mozilla + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +import Foundation + +import MentatStore + +/** + This class wraps a raw pointer than points to a Rust `TxReport` object. + + The `TxReport` contains information about a successful Mentat transaction. + + This information includes: + - `txId` - the identifier for the transaction. + - `txInstant` - the time that the transaction occured. + - a map of temporary identifiers provided in the transaction and the `Entid`s that they were mapped to, + + Access an `Entid` for a temporary identifier that was provided in the transaction can be done through `entid(String:)`. + + ``` + let report = mentat.transact("[[:db/add "a" :foo/boolean true]]") + let aEntid = report.entid(forTempId: "a") + ``` + */ +class TxReport: RustObject { + + // The identifier for the transaction. + public var txId: Entid { + return tx_report_get_entid(self.raw) + } + + // The time that the transaction occured. + public var txInstant: Date { + return Date(timeIntervalSince1970: TimeInterval(tx_report_get_tx_instant(self.raw))) + } + + /** + Access an `Entid` for a temporary identifier that was provided in the transaction can be done through `entid(String:)`. + + - Parameter tempId: A `String` representing the temporary identifier to fetch the `Entid` for. + + - Returns: The `Entid` for the temporary identifier, if present, otherwise `nil`. + */ + public func entid(forTempId tempId: String) -> Entid? { + guard let entidPtr = tx_report_entity_for_temp_id(self.raw, tempId) else { + return nil + } + return entidPtr.pointee + } + + override func cleanup(pointer: OpaquePointer) { + tx_report_destroy(pointer) + } +}