diff --git a/Sources/Partial/Partial.swift b/Sources/Partial/Partial.swift index fa882b3..3f0d690 100644 --- a/Sources/Partial/Partial.swift +++ b/Sources/Partial/Partial.swift @@ -5,7 +5,7 @@ import Foundation public struct Partial: PartialProtocol, CustomStringConvertible { /// An error that can be thrown by the `value(for:)` function. - public enum Error: Swift.Error { + public enum Error: Swift.Error, Equatable { /// The key path has not been set. case keyPathNotSet(KeyPath) } diff --git a/Sources/Partial/PartialConvertible.swift b/Sources/Partial/PartialConvertible.swift index 776b3a7..ff60dfe 100644 --- a/Sources/Partial/PartialConvertible.swift +++ b/Sources/Partial/PartialConvertible.swift @@ -13,3 +13,58 @@ public protocol PartialConvertible { init(partial: PartialType) throws where PartialType.Wrapped == Self } + +//extension Optional: PartialConvertible where Wrapped: PartialConvertible { +// public init(partial: PartialType) throws where PartialType.Wrapped == Self { +// do { +// let optionalWrapper = OptionalPartialWrapper(partial: partial) +// let unwrappedValue = try Wrapped(partial: optionalWrapper) +// self = .some(unwrappedValue) +// } catch let error as OptionalPartialWrapper.Error { +// switch error { +// case .valueIsNil: +// self = .none +// case .valueNotSet: +// throw error +// } +// } catch { +// throw error +// } +// } +//} + +//private struct OptionalPartialWrapper: PartialProtocol { +// mutating func setValue(_ value: Value, for keyPath: KeyPath) { +// fatalError() +// } +// +// init() { +// getValue = { _ in +// throw Error.valueNotSet +// } +// } +// +// fileprivate enum Error: Swift.Error { +// case valueIsNil +// case valueNotSet +// } +// +// fileprivate var knownKeys: [PartialKeyPath: Any] = [:] +// +// fileprivate var requestedKeys: [PartialKeyPath] = [] +// +// init( +// partial: PartialType, +// requestedKeys: [PartialKeyPath] +// ) where PartialType.Wrapped == Wrapped? { +// knownKeys +// } +// +// func value(for keyPath: KeyPath) throws -> Value { +// knownKeys[keyPath] as! Value +// } +// +// mutating func removeValue(for keyPath: KeyPath) { +// knownKeys.removeValue(forKey: keyPath) +// } +//} diff --git a/Tests/PartialTests/Tests/Partial+PartialConvertibleTests.swift b/Tests/PartialTests/Tests/Partial+PartialConvertibleTests.swift index a3567e5..c5a6e7a 100644 --- a/Tests/PartialTests/Tests/Partial+PartialConvertibleTests.swift +++ b/Tests/PartialTests/Tests/Partial+PartialConvertibleTests.swift @@ -14,108 +14,6 @@ final class Partial_PartialConvertibleTests: QuickSpec { partial = Partial() } - context("a non-optional key path") { - let keyPath = \StringWrapperWrapper.stringWrapper - var initialValue: StringWrapper! - - beforeEach { - initialValue = "initial value" - partial.setValue(initialValue, for: keyPath) - } - - context("set no an incomplete Partial") { - var thrownError: Error? - - beforeEach { - do { - try partial.setValue(Partial(), for: keyPath) - } catch { - thrownError = error - } - } - - afterEach { - thrownError = nil - } - - it("should throw a `keyPathNotSet` error") { - let expectedError = Partial.Error.keyPathNotSet(\.string) - expect(thrownError).to(matchError(expectedError)) - } - - it("should not overwrite the value") { - expect(partial[keyPath]) == initialValue - } - } - - context("set to a complete partial") { - var unwrapped: StringWrapper! - var thrownError: Error? - - beforeEach { - unwrapped = "unwrapped value" - - do { - var partialStringWrapper = Partial() - partialStringWrapper[\.string] = unwrapped.string - try partial.setValue(partialStringWrapper, for: keyPath) - } catch { - thrownError = error - } - } - - it("should not throw an error") { - expect(thrownError).to(beNil()) - } - - it("should set the key path to the unwrapped value") { - expect(partial[keyPath]) == unwrapped - } - } - - context("set with a custom unwrapper") { - context("that throws an error") { - enum TestError: Error { - case testError - } - - var thrownError: Error! - - beforeEach { - do { - try partial.setValue(Partial(), for: keyPath) { _ in - throw TestError.testError - } - } catch { - thrownError = error - } - } - - afterEach { - thrownError = nil - } - - it("should throw errors thrown by the unwrapper") { - expect(thrownError).to(matchError(TestError.testError)) - } - } - - context("that returns a value") { - var returnedValue: StringWrapper! - - beforeEach { - returnedValue = "returned value" - partial.setValue(Partial(), for: keyPath) { _ in - return returnedValue - } - } - it("should set the key path to the value returned by the unwrapper") { - expect(partial[keyPath]) == returnedValue - } - } - } - } - context("an optional key path") { let keyPath = \StringWrapperWrapper.optionalStringWrapper var initialValue: StringWrapper! diff --git a/Tests/PartialTests/Tests/PartialTests.swift b/Tests/PartialTests/Tests/PartialTests.swift index 17fafba..291fa30 100644 --- a/Tests/PartialTests/Tests/PartialTests.swift +++ b/Tests/PartialTests/Tests/PartialTests.swift @@ -1,336 +1,178 @@ -import Quick -import Nimble - -@testable -import Partial - -final class PartialTests: QuickSpec { - - override func spec() { - describe("Partial") { - var partial: Partial! - - beforeEach { - partial = Partial() - } - - context("description") { - it("should include the name of the wrapping type") { - expect(String(describing: partial)).to(contain(String(describing: StringWrapperWrapper.self))) - } - } - - context("a non-optional key path that has not been set") { - let keyPath = \StringWrapperWrapper.stringWrapper - - context("value(for:)") { - it("should throw a `keyPathNotSet` error") { - let expectedError = Partial.Error.keyPathNotSet(keyPath) - expect({ try partial.value(for: keyPath) }).to(throwError(expectedError)) - } - } - - context("subscript") { - it("should return `nil`") { - expect(partial[keyPath]).to(beNil()) - } - } - - context("dynamic member lookup") { - it("should return `nil`") { - expect(partial[dynamicMember: keyPath]).to(beNil()) - } - } - } - - context("an optional key path that has not been set") { - let keyPath = \StringWrapperWrapper.optionalStringWrapper - - context("value(for:)") { - it("should throw a `keyPathNotSet` error") { - let expectedError = Partial.Error.keyPathNotSet(keyPath) - expect({ try partial.value(for: keyPath) }).to(throwError(expectedError)) - } - } - - context("subscript") { - it("should return `nil`") { - expect(partial[keyPath]).to(beNil()) - } - } - - context("dynamic member lookup") { - it("should return `nil`") { - expect(partial[dynamicMember: keyPath]).to(beNil()) - } - } - } - - context("a non-optional key path that has been set") { - let keyPath = \StringWrapperWrapper.stringWrapper - var newValue: StringWrapper! - - beforeEach { - newValue = "new value" - partial.setValue(newValue, for: keyPath) - } - - context("value(for:)") { - it("should not throw an error") { - expect({ try partial.value(for: keyPath) }).toNot(throwError()) - } - - it("should return the set value") { - expect({ try partial.value(for: keyPath) }) == newValue - } - } - - context("subscript") { - it("should return the set value") { - expect(partial[keyPath]) == newValue - } - } - - context("dynamic member lookup") { - it("should return the set value") { - expect(partial[dynamicMember: keyPath]) == newValue - } - } - - context("description") { - it("should include description of the set value") { - expect(String(describing: partial)).to(contain(String(describing: newValue!))) - } - } - - context("removeValue(for:)") { - beforeEach { - partial.removeValue(for: keyPath) - } - - it("should cause the value to be unset") { - let expectedError = Partial.Error.keyPathNotSet(keyPath) - expect({ try partial.value(for: keyPath) }).to(throwError(expectedError)) - } - } - } - - context("an optional key path that has been set to a non-nil value") { - let keyPath = \StringWrapperWrapper.optionalStringWrapper - var newValue: StringWrapper! - - beforeEach { - newValue = "new value" - partial.setValue(newValue, for: keyPath) - } - - context("value(for:)") { - it("should not throw an error") { - expect({ try partial.value(for: keyPath) }).toNot(throwError()) - } - - it("should return the set value") { - expect({ try partial.value(for: keyPath) }) == newValue - } - } - - context("subscript") { - it("should return the set value") { - expect(partial[keyPath]) == newValue - } - } - - context("dynamic member lookup") { - it("should return the set value") { - expect(partial[dynamicMember: keyPath]) == newValue - } - } - - context("removeValue(for:)") { - beforeEach { - partial.removeValue(for: keyPath) - } - - it("should cause the value to be unset") { - let expectedError = Partial.Error.keyPathNotSet(keyPath) - expect({ try partial.value(for: keyPath) }).to(throwError(expectedError)) - } - } - } - - context("an optional key path that has been set to `nil`") { - let keyPath = \StringWrapperWrapper.optionalStringWrapper - - beforeEach { - partial.setValue(nil, for: keyPath) - } - - context("value(for:)") { - it("should not throw an error") { - expect({ try partial.value(for: keyPath) }).toNot(throwError()) - } - - it("should return `nil`") { - expect({ try partial.value(for: keyPath) }).to(beNil()) - } - } - - context("subscript") { - it("should return `nil` wrapped in an Optional") { - expect(partial[keyPath]).to(beNilWrappedInOptional()) - } - } - - context("dynamic member lookup") { - it("should return `nil` wrapped in an Optional") { - expect(partial[dynamicMember: keyPath]).to(beNilWrappedInOptional()) - } - } - - context("removeValue(for:)") { - beforeEach { - partial.removeValue(for: keyPath) - } - - it("should cause the value to be unset") { - let expectedError = Partial.Error.keyPathNotSet(keyPath) - expect({ try partial.value(for: keyPath) }).to(throwError(expectedError)) - } - } - } - - context("a non-optional key path set with the subscript") { - let keyPath = \StringWrapperWrapper.stringWrapper - var newValue: StringWrapper! - - beforeEach { - newValue = "new value" - partial[keyPath] = newValue - } - - it("should be returned by value(for:)") { - expect({ try partial.value(for: keyPath) }) == newValue - } - } - - context("an optional key path set to a non-nil with the subscript") { - let keyPath = \StringWrapperWrapper.optionalStringWrapper - var newValue: StringWrapper! - - beforeEach { - newValue = "new value" - partial[keyPath] = newValue - } - - it("should be returned by value(for:)") { - expect({ try partial.value(for: keyPath) }) == newValue - } - } - - context("an optional key path set to `nil` wrapped in an `Optional` with the subscript") { - let keyPath = \StringWrapperWrapper.optionalStringWrapper - - beforeEach { - partial[keyPath] = StringWrapper?.none - } - - it("should be returned by value(for:)") { - expect({ try partial.value(for: keyPath) }).to(beNil()) - } - } - - context("setting a non-optional key path set to `nil` with the subscript") { - let keyPath = \StringWrapperWrapper.stringWrapper - - beforeEach { - partial[keyPath] = "first value" - partial[keyPath] = nil - } - - it("should remove the value") { - expect({ try? partial.value(for: keyPath) }).to(beNil()) - } - } - - context("setting an optional key path set to a `nil` with the subscript") { - let keyPath = \StringWrapperWrapper.optionalStringWrapper - - beforeEach { - partial[keyPath] = "first value" - partial[keyPath] = nil - } - - it("should remove the value") { - expect({ try? partial.value(for: keyPath) }).to(beNil()) - } - } - - context("a non-optional key path set with dynamic member lookup") { - let keyPath = \StringWrapperWrapper.stringWrapper - var newValue: StringWrapper! - - beforeEach { - newValue = "new value" - partial[dynamicMember: keyPath] = newValue - } - - it("should be returned by value(for:)") { - expect({ try partial.value(for: keyPath) }) == newValue - } - } - - context("an optional key path set to a non-nil with dynamic member lookup") { - let keyPath = \StringWrapperWrapper.optionalStringWrapper - var newValue: StringWrapper! - - beforeEach { - newValue = "new value" - partial[dynamicMember: keyPath] = newValue - } - - it("should be returned by value(for:)") { - expect({ try partial.value(for: keyPath) }) == newValue - } - } - - context("an optional key path set to `nil` wrapped in an `Optional` with dynamic member lookup") { - let keyPath = \StringWrapperWrapper.optionalStringWrapper - - beforeEach { - partial[dynamicMember: keyPath] = StringWrapper?.none - } - - it("should be returned by value(for:)") { - expect({ try partial.value(for: keyPath) }).to(beNil()) - } - } - - context("setting a non-optional key path set to `nil` with dynamic member lookup") { - let keyPath = \StringWrapperWrapper.stringWrapper - - beforeEach { - partial[dynamicMember: keyPath] = "first value" - partial[dynamicMember: keyPath] = nil - } - - it("should remove the value") { - expect({ try? partial.value(for: keyPath) }).to(beNil()) - } - } - - context("setting an optional key path set to a `nil` with dynamic member lookup") { - let keyPath = \StringWrapperWrapper.optionalStringWrapper - - beforeEach { - partial[keyPath] = "first value" - partial[keyPath] = nil - } - - it("should remove the value") { - expect({ try? partial.value(for: keyPath) }).to(beNil()) - } - } +@testable import Partial +import XCTest + +final class PartialTests: XCTestCase { + func testDescription() { + let partial = Partial() + XCTAssertTrue(String(describing: partial).contains(String(describing: StringWrapperWrapper.self))) + } + + func testFunctionalAPI() { + var partial = Partial() + XCTAssertNil(try? partial.value(for: \.stringWrapper)) + XCTAssertNil(try? partial.value(for: \.optionalStringWrapper) ?? nil) + + partial.setValue("test string", for: \.stringWrapper) + XCTAssertEqual(try? partial.value(for: \.stringWrapper), "test string") + XCTAssertNil(try? partial.value(for: \.optionalStringWrapper) ?? nil) + XCTAssertNil(try? partial.unwrapped()) + + partial.removeValue(for: \.stringWrapper) + XCTAssertNil(try? partial.value(for: \.stringWrapper)) + XCTAssertNil(try? partial.value(for: \.optionalStringWrapper) ?? nil) + XCTAssertNil(try? partial.unwrapped()) + + partial.setValue("test string", for: \.optionalStringWrapper) + XCTAssertNil(try? partial.value(for: \.stringWrapper)) + XCTAssertEqual(try? partial.value(for: \.optionalStringWrapper), "test string") + XCTAssertNil(try? partial.unwrapped()) + + partial.setValue(nil, for: \.optionalStringWrapper) + XCTAssertNil(try? partial.value(for: \.stringWrapper)) + XCTAssertNil(try? partial.value(for: \.optionalStringWrapper) ?? nil) + XCTAssertNil(try? partial.unwrapped()) + + partial.setValue("test string", for: \.optionalStringWrapper) + partial.removeValue(for: \.optionalStringWrapper) + XCTAssertNil(try? partial.value(for: \.stringWrapper)) + XCTAssertNil(try? partial.value(for: \.optionalStringWrapper) ?? nil) + XCTAssertNil(try? partial.unwrapped()) + + partial.setValue("test string", for: \.stringWrapper) + partial.setValue(StringWrapper?.none, for: \.optionalStringWrapper) + XCTAssertEqual(try? partial.unwrapped(), StringWrapperWrapper(stringWrapper: "test string", optionalStringWrapper: nil)) + } + + func testSubscript() { + var partial = Partial() + XCTAssertNil(partial[\.stringWrapper]) + XCTAssertNil(partial[\.optionalStringWrapper] ?? nil) + + partial[\.stringWrapper] = "test string" + XCTAssertEqual(partial[\.stringWrapper], "test string") + XCTAssertNil(partial[\.optionalStringWrapper] ?? nil) + XCTAssertNil(try? partial.unwrapped()) + + partial[\.stringWrapper] = nil + XCTAssertNil(partial[\.stringWrapper]) + XCTAssertNil(partial[\.optionalStringWrapper] ?? nil) + XCTAssertNil(try? partial.unwrapped()) + + partial[\.optionalStringWrapper] = "test string" + XCTAssertNil(partial[\.stringWrapper]) + XCTAssertEqual(partial[\.optionalStringWrapper], "test string") + XCTAssertNil(try? partial.unwrapped()) + + partial[\.optionalStringWrapper] = nil + XCTAssertNil(partial[\.stringWrapper]) + XCTAssertNil(partial[\.optionalStringWrapper] ?? nil) + XCTAssertNil(try? partial.unwrapped()) + + partial[\.stringWrapper] = "test string" + partial[\.optionalStringWrapper] = StringWrapper?.none + XCTAssertEqual(try? partial.unwrapped(), StringWrapperWrapper(stringWrapper: "test string", optionalStringWrapper: nil)) + } + + func testDynamicMemberLookup() { + var partial = Partial() + XCTAssertNil(partial.stringWrapper) + XCTAssertNil(partial.optionalStringWrapper ?? nil) + + partial.stringWrapper = "test string" + XCTAssertEqual(partial.stringWrapper, "test string") + XCTAssertNil(partial.optionalStringWrapper ?? nil) + XCTAssertNil(try? partial.unwrapped()) + + partial.stringWrapper = nil + XCTAssertNil(partial.stringWrapper) + XCTAssertNil(partial.optionalStringWrapper ?? nil) + XCTAssertNil(try? partial.unwrapped()) + + partial.optionalStringWrapper = "test string" + XCTAssertNil(partial.stringWrapper) + XCTAssertEqual(partial.optionalStringWrapper, "test string") + XCTAssertNil(try? partial.unwrapped()) + + partial.optionalStringWrapper = nil + XCTAssertNil(partial.stringWrapper) + XCTAssertNil(partial.optionalStringWrapper ?? nil) + XCTAssertNil(try? partial.unwrapped()) + + partial.stringWrapper = "test string" + partial.optionalStringWrapper = StringWrapper?.none + XCTAssertEqual(try? partial.unwrapped(), StringWrapperWrapper(stringWrapper: "test string", optionalStringWrapper: nil)) + } + + func testSettingPartialConvertibleValue() throws { + var partial = Partial() + partial[\.stringWrapper] = "Initial value" + partial[\.optionalStringWrapper] = "Initial optional value" + + XCTAssertThrowsError(try partial.setValue(Partial(), for: \.stringWrapper)) { thrownError in + XCTAssertEqual(thrownError as? Partial.Error, .keyPathNotSet(\.string)) + } + XCTAssertEqual(partial[\.stringWrapper], "Initial value") + + XCTAssertThrowsError(try partial.setValue(Partial(), for: \.optionalStringWrapper)) { thrownError in + XCTAssertEqual(thrownError as? Partial.Error, .keyPathNotSet(\.string)) } + XCTAssertEqual(partial[\.optionalStringWrapper], "Initial optional value") + + // It would be nice to support this, but it would require `Optional` to + // be conformed to `PartialConvertible`, which isn't currently possible + // because the initialiser would need to "unwrap" the passed `Partial` + // to allow the use of `StringWrapper.init(partial:)` +// XCTAssertThrowsError(try partial.setValue(Partial(), for: \.optionalStringWrapper)) { thrownError in +// print(thrownError) +// Partial.Error +// XCTAssertEqual(thrownError as? Partial.Error, .keyPathNotSet(\.?.string)) +// } +// XCTAssertEqual(partial[\.optionalStringWrapper], "Initial optional value") + + var partialStringWrapper = Partial() + partialStringWrapper.string = "New value" + try partial.setValue(partialStringWrapper, for: \.stringWrapper) + XCTAssertEqual(partial[\.stringWrapper], "New value") } + func testSettingPartialConvertibleValueWithCustomUnwrapper() throws { + var partial = Partial() + partial[\.stringWrapper] = "Initial value" + + enum TestError: Error { + case testError + } + + XCTAssertThrowsError(try partial.setValue(Partial(), for: \.stringWrapper) { _ in + throw TestError.testError + }) { thrownError in + XCTAssertEqual(thrownError as? TestError, .testError) + } + + XCTAssertEqual(partial[\.stringWrapper], "Initial value") + + partial.setValue(Partial(), for: \.stringWrapper) { _ in + return "Custom value" + } + + XCTAssertEqual(partial[\.stringWrapper], "Custom value") + } } + +//extension PartialProtocol { +// /// Attempts to update the stored value for the given key path by unwrapping the provided partial value. If the +// /// unwrapping fails the error will be thrown. +// /// +// /// - Parameter partial: A partial wrapping a value of type `Value`. +// /// - Parameter keyPath: A key path from `Wrapped` to a property of type `Value?`. +// /// - Parameter unwrapper: A closure that will be called to unwrap the passed partial. +// mutating func setValue( +// _ partial: Partial, +// for keyPath: KeyPath, +// unwrapper: (_ partial: Partial) throws -> StringWrapper? +// ) rethrows { +// if let value = partial. +// let value = try unwrapper(partial) +// setValue(value, for: keyPath) +// } +//} diff --git a/Tests/PartialTests/Tests/PartiallyBuiltTests.swift b/Tests/PartialTests/Tests/PartiallyBuiltTests.swift index db419ec..e5ba9d3 100644 --- a/Tests/PartialTests/Tests/PartiallyBuiltTests.swift +++ b/Tests/PartialTests/Tests/PartiallyBuiltTests.swift @@ -1,49 +1,26 @@ -import Quick -import Nimble +@testable import Partial +import XCTest -@testable -import Partial - -final class PartiallyBuiltTests: QuickSpec { - - override func spec() { - describe("PartiallyBuilt") { - var partiallyBuilt: PartiallyBuilt! - - beforeEach { - partiallyBuilt = PartiallyBuilt() - } - - context("with an incomplete partial value") { - it("wrappedValue should be nil") { - expect(partiallyBuilt.wrappedValue).to(beNil()) - } - } +final class PartiallyBuiltTests: XCTestCase { + func testIncompletePartialValue() { + let partiallyBuilt = PartiallyBuilt() + XCTAssertNil(partiallyBuilt.wrappedValue) + } - context("with a complete partial value") { - let setValue = StringWrapper(stringLiteral: "test") - beforeEach { - partiallyBuilt.projectedValue.string = "test" - } + func testCompletePartialValue() { + var partiallyBuilt = PartiallyBuilt() + let expectedValue = StringWrapper(stringLiteral: "test") + partiallyBuilt.projectedValue.string = expectedValue.string - it("wrappedValue should return the unwrapped value") { - expect(partiallyBuilt.wrappedValue).to(equal(setValue)) - } - } + XCTAssertEqual(partiallyBuilt.wrappedValue, expectedValue) + } - context("initialised with a complete partial") { - let setValue = StringWrapper(stringLiteral: "test") - beforeEach { - var partial = Partial() - partial.string = setValue.string - partiallyBuilt = PartiallyBuilt(partial: partial) - } + func testInitialisedWithACompletePartial() { + let expectedValue = StringWrapper(stringLiteral: "test") + var partial = Partial() + partial.string = expectedValue.string + let partiallyBuilt = PartiallyBuilt(partial: partial) - it("wrappedValue should return the unwrapped value") { - expect(partiallyBuilt.wrappedValue).to(equal(setValue)) - } - } - } + XCTAssertEqual(partiallyBuilt.wrappedValue, expectedValue) } - }