diff --git a/Sources/Partial/PartialConvertible.swift b/Sources/Partial/PartialConvertible.swift index c67bccd6..776b3a71 100644 --- a/Sources/Partial/PartialConvertible.swift +++ b/Sources/Partial/PartialConvertible.swift @@ -10,6 +10,6 @@ public protocol PartialConvertible { /// ``` /// /// - Parameter partial: The partial to retrieve values from - init(partial: Partial) throws + init(partial: PartialType) throws where PartialType.Wrapped == Self } diff --git a/Sources/Partial/PartialProtocol.swift b/Sources/Partial/PartialProtocol.swift index 5ab1a5e8..b84be190 100644 --- a/Sources/Partial/PartialProtocol.swift +++ b/Sources/Partial/PartialProtocol.swift @@ -31,26 +31,66 @@ public protocol PartialProtocol { 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. + /// 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 value: A partial wrapping a value of type `Value`. + /// - Parameter partial: A partial wrapping a value of type `Value`. /// - Parameter keyPath: A key path from `Wrapped` to a property of type `Value`. - mutating func setValue(_ partial: Partial, for keyPath: KeyPath) throws where Value: PartialConvertible { - let value = try partial.unwrappedValue() + /// - Parameter unwrapper: A closure that will be called to unwrap the passed partial. + mutating func setValue( + _ partial: PartialType, + for keyPath: KeyPath, + unwrapper: (_ partial: PartialType) throws -> Value + ) rethrows where PartialType: PartialProtocol, PartialType.Wrapped == Value { + let value = try unwrapper(partial) setValue(value, for: keyPath) } - /// 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. + /// 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 value: A partial wrapping a value of type `Value?`. - /// - Parameter keyPath: A key path from `Wrapped` to a property of type `Value`. - mutating func setValue(_ partial: Partial, for keyPath: KeyPath) throws where Value: PartialConvertible { - let value = try partial.unwrappedValue() + /// - 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: PartialType, + for keyPath: KeyPath, + unwrapper: (_ partial: PartialType) throws -> Value + ) rethrows where PartialType: PartialProtocol, PartialType.Wrapped == Value { + let value = try unwrapper(partial) setValue(value, for: keyPath) } + /// 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. Defaults to the + /// `init(partial:)` function of `Value` + mutating func setValue( + _ partial: PartialType, + for keyPath: KeyPath, + customUnwrapper unwrapper: (_ partial: PartialType) throws -> Value = Value.init(partial:) + ) rethrows where PartialType: PartialProtocol, PartialType.Wrapped == Value, PartialType.Wrapped: PartialConvertible { + try setValue(partial, for: keyPath, unwrapper: unwrapper) + } + + /// 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. Defaults to the + /// `init(partial:)` function of `Value` + mutating func setValue( + _ partial: PartialType, + for keyPath: KeyPath, + customUnwrapper unwrapper: (_ partial: PartialType) throws -> Value = Value.init(partial:) + ) rethrows where PartialType: PartialProtocol, PartialType.Wrapped == Value, PartialType.Wrapped: PartialConvertible { + try setValue(partial, for: keyPath, unwrapper: unwrapper) + } + /// Retrieve or set a value for the given key path. Returns `nil` if the value has not been set. If the value is set /// to nil it will remove the value. /// diff --git a/Tests/PartialTests/Models/StringWrapper.swift b/Tests/PartialTests/Models/StringWrapper.swift index 139357fa..e8bfd8e6 100644 --- a/Tests/PartialTests/Models/StringWrapper.swift +++ b/Tests/PartialTests/Models/StringWrapper.swift @@ -1,13 +1,14 @@ import Partial struct StringWrapper: PartialConvertible, Hashable, ExpressibleByStringLiteral { + let string: String init(stringLiteral value: String) { self.string = value } - init(partial: Partial) throws { + init(partial: PartialType) throws where PartialType.Wrapped == StringWrapper { string = try partial.value(for: \.string) } } diff --git a/Tests/PartialTests/Models/StringWrapperWrapper.swift b/Tests/PartialTests/Models/StringWrapperWrapper.swift index 1bf79683..3e1b39e2 100644 --- a/Tests/PartialTests/Models/StringWrapperWrapper.swift +++ b/Tests/PartialTests/Models/StringWrapperWrapper.swift @@ -10,7 +10,7 @@ struct StringWrapperWrapper: PartialConvertible, Hashable { self.optionalStringWrapper = optionalStringWrapper } - init(partial: Partial) throws { + init(partial: PartialType) throws where PartialType.Wrapped == StringWrapperWrapper { stringWrapper = try partial.value(for: \.stringWrapper) optionalStringWrapper = try partial.value(for: \.optionalStringWrapper) } diff --git a/Tests/PartialTests/Tests/Partial+PartialConvertibleTests.swift b/Tests/PartialTests/Tests/Partial+PartialConvertibleTests.swift index 0802de8d..36d081af 100644 --- a/Tests/PartialTests/Tests/Partial+PartialConvertibleTests.swift +++ b/Tests/PartialTests/Tests/Partial+PartialConvertibleTests.swift @@ -72,6 +72,48 @@ final class Partial_PartialConvertibleTests: QuickSpec { expect(partial[keyPath]) == unwrappedValue } } + + 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") { @@ -83,7 +125,7 @@ final class Partial_PartialConvertibleTests: QuickSpec { partial.setValue(initialValue, for: keyPath) } - context("set no an incomplete Partial") { + context("set to an incomplete Partial") { var thrownError: Error? beforeEach { @@ -132,6 +174,48 @@ final class Partial_PartialConvertibleTests: QuickSpec { expect(partial[keyPath]) == unwrappedValue } } + + 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("unwrappedValue()") { diff --git a/Tests/PartialTests/Tests/PartialBuilder+PartialConvertibleTests.swift b/Tests/PartialTests/Tests/PartialBuilder+PartialConvertibleTests.swift index e71bbec1..01b61fc5 100644 --- a/Tests/PartialTests/Tests/PartialBuilder+PartialConvertibleTests.swift +++ b/Tests/PartialTests/Tests/PartialBuilder+PartialConvertibleTests.swift @@ -72,6 +72,48 @@ final class PartialBuilder_PartialConvertibleTests: QuickSpec { expect(builder[keyPath]) == unwrappedValue } } + + context("set with a custom unwrapper") { + context("that throws an error") { + enum TestError: Error { + case testError + } + + var thrownError: Error! + + beforeEach { + do { + try builder.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" + builder.setValue(Partial(), for: keyPath) { _ in + return returnedValue + } + } + it("should set the key path to the value returned by the unwrapper") { + expect(builder[keyPath]) == returnedValue + } + } + } } context("an optional key path") { @@ -132,6 +174,48 @@ final class PartialBuilder_PartialConvertibleTests: QuickSpec { expect(builder[keyPath]) == unwrappedValue } } + + context("set with a custom unwrapper") { + context("that throws an error") { + enum TestError: Error { + case testError + } + + var thrownError: Error! + + beforeEach { + do { + try builder.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" + builder.setValue(Partial(), for: keyPath) { _ in + return returnedValue + } + } + it("should set the key path to the value returned by the unwrapper") { + expect(builder[keyPath]) == returnedValue + } + } + } } context("unwrappedValue()") {