From c8c11fb3bebb91d7e58ec649f423ccde62b5daeb Mon Sep 17 00:00:00 2001 From: maximkrouk Date: Sun, 18 Oct 2020 03:13:16 +0300 Subject: [PATCH] feat: Extended support for optionals & general improvements --- README.md | 99 ++++++++++------ Sources/FunctionalBuilder/Builder.swift | 82 +++++++++++-- .../ConfigIntializable.swift | 8 +- .../FunctionalConfigurator/Configurator.swift | 110 ++++++++++++++---- .../CustomConfigurable.swift | 2 +- .../FunctionalConfigurator/Modification.swift | 12 ++ .../FunctionalKeyPath/FunctionalKeyPath.swift | 35 +++++- .../ConfiguratorTests.swift | 46 ++++++-- 8 files changed, 312 insertions(+), 82 deletions(-) create mode 100644 Sources/FunctionalConfigurator/Modification.swift diff --git a/README.md b/README.md index 2574c7b..dec2508 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Swift Declarative Configuration -[![Swift 5.3](https://img.shields.io/badge/swift-5.3-ED523F.svg?style=flat)](https://swift.org/download/) [![SwiftPM](https://img.shields.io/badge/SwiftPM-ED523F.svg?style=flat)](https://swift.org/package-manager/) [![@maximkrouk](https://img.shields.io/badge/contact-@maximkrouk-ED523F.svg?style=flat)](https://twitter.com/maximkrouk) +[![Swift 5.3](https://img.shields.io/badge/swift-5.3-ED523F.svg?style=flat)](https://swift.org/download/) [![SwiftPM](https://img.shields.io/badge/SwiftPM-success.svg?style=flat)](https://swift.org/package-manager/) [![@maximkrouk](https://img.shields.io/badge/contact-@maximkrouk-#1DA1F2.svg?style=flat&logo=twitter)](https://twitter.com/maximkrouk) Swift Declarative Configuration (SDC, for short) is a tiny library, that enables you to configure your objects in a declarative, consistent and understandable way, with ergonomics in mind. It can be used to configure any objects on any platform, including server-side-swift. @@ -12,15 +12,17 @@ Swift Declarative Configuration (SDC, for short) is a tiny library, that enables - **[FunctionalKeyPath](./Sources/FunctionalKeyPath)** & **[CasePaths](https://github.com/pointfreeco/swift-case-paths)** - KeyPath functional wrappers, one is generalized and the other is for enums. [CasePath is a dependency](https://github.com/pointfreeco/swift-case-paths). + KeyPath functional wrappers, one is generalized and the other is for enums. _[CasePath is a dependency](https://github.com/pointfreeco/swift-case-paths)_. - **[FunctionalConfigurator](./Sources/FunctionalConfigurator)** Funtional configurator for anything, enables you to specify modification of an object and to apply the modification later. + Also contains self-implementing protocols (`ConfigInitializable`, `CustomConfigurable`) to enable you add custom configuration support for your types (`NSObject` already conforms to it for you). + - **[FunctionalBuilder](./Sources/FunctionalBuilder)** - Functional builder for anything, enables you to modify object instances in a declarative way. Also contains BuilderProvider protocol with a computed `builder` property and implements that protocol on NSObject type. + Functional builder for anything, enables you to modify object instances in a declarative way. Also contains `BuilderProvider` protocol with a computed `builder` property and implements that protocol on `NSObject` type. - **[DeclarativeConfiguration](./Sources/DeclarativeConfiguration)** @@ -28,31 +30,51 @@ Swift Declarative Configuration (SDC, for short) is a tiny library, that enables ## Basic Usage -### UIKit & FunctionalConfigurator +### UIKit & No SDC -Maybe it worth to make another abstraction over configurator for UI setup, but for example I'll be using pure version. +```swift +class ImageViewController: UIViewController { + let imageView = UIImageView() + + override func loadView() { + self.view = imageView + } + + override func viewDidLoad() { + super.viewDidLoad() + imageView.contentMode = .scaleAspectFit + imageView.backgroundColor = .black + imageView.layer.masksToBounds = true + imageView.layer.cornerRadius = 10 + } +} +``` + +### UIKit & FunctionalConfigurator ```swift import FunctionalConfigurator class ImageViewController: UIViewController { - enum StyleSheet { - static let imageView = Configurator - .contentMode(.scaleAspectFit) - .backgroundColor(.black) - .layer.masksToBounds(true) - .layer.cornerRadius(10) - } - let imageView = UIImageView(config: StyleSheet.imageView) + let imageView = UIImageView { $0 + .contentMode(.scaleAspectFit) + .backgroundColor(.black) + .layer.masksToBounds(true) + .layer.cornerRadius(10) + } override func loadView() { self.view = imageView } + } ``` +**Note:** This way is **recommended**, but remember, that custom types **MUST** implement initializer with no parameters even if the superclass already has it or you will get a crash otherwise. + ### UIKit & FunctionalBuilder + ```swift import FunctionalBuilder @@ -70,32 +92,43 @@ class ImageViewController: UIViewController { } ``` -### Modification +Note: This way is recommended too, and it is more **safe**, because it modifies existing objects. + +### Other usecases + +#### Builder + +Customize any object by passing initial value to a builder ```swift -import FunctionalModification +let object = Builder(Object()) + .property.subproperty(value) + .build() // Returns modified object +``` -struct MyModel { - var value1 = 0 - init() {} -} +For classes you can avoid returning a value by calling `apply` method, instead of `build` -let model_0 = MyModel() -let model_1 = modification(of: model_0) { $0.value = 1 } +```swift +let _class = _Class() +Builder(_class) + .property.subproperty(value) + .apply() // Returns Void +``` -import UIKit +Conform your own types to `BuilderProvider` protocol to access builder property. -extension UIView { - @discardableResult - func cornerRadius(_ value: CGFloat) -> Self { - modification(of: self) { view in - view.layer.cornerRadius = value - view.layer.masksToBounds = true - } - } -} +```swift +import CoreLocation +import DeclarativeConfiguration + +extension CLLocationCoordinate2D: BuilderProvider {} +// Now you can access `location.builder.latitude(0).build()` ``` +#### Configurator + +> README PLACEHOLDER (Not yet written 😅) + ## Installation ### Basic @@ -103,7 +136,7 @@ extension UIView { You can add DeclarativeConfiguration to an Xcode project by adding it as a package dependency. 1. From the **File** menu, select **Swift Packages › Add Package Dependency…** -2. Enter "https://github.com/makeupstudio/swift-declarative-configuration" into the package repository URL text field +2. Enter [`"https://github.com/makeupstudio/swift-declarative-configuration"`](https://github.com/makeupstudio/swift-declarative-configuration) into the package repository URL text field 3. Choose products you need to link them to your project. ### Recommended @@ -113,7 +146,7 @@ If you use SwiftPM for your project, you can add DeclarativeConfiguration to you ```swift .package( url: "git@github.com:makeupstudio/swift-declarative-configuration.git", - from: "0.0.2" + from: "0.0.4" ) ``` diff --git a/Sources/FunctionalBuilder/Builder.swift b/Sources/FunctionalBuilder/Builder.swift index 01d5701..e2e5a2f 100644 --- a/Sources/FunctionalBuilder/Builder.swift +++ b/Sources/FunctionalBuilder/Builder.swift @@ -6,7 +6,7 @@ public struct Builder { private var _initialValue: () -> Base private var _configurator: Configurator - public func build() -> Base { _configurator.configure(_initialValue()) } + public func build() -> Base { _configurator.configured(_initialValue()) } @inlinable public func apply() where Base: AnyObject { _ = build() } @@ -69,21 +69,39 @@ public struct Builder { public subscript( dynamicMember keyPath: WritableKeyPath ) -> CallableBlock { - .init( + CallableBlock( builder: self, - keyPath: .init(keyPath) + keyPath: FunctionalKeyPath(keyPath) ) } public subscript( dynamicMember keyPath: KeyPath - ) -> NonCallableBlock where Base: AnyObject, Value: AnyObject { - .init( + ) -> NonCallableBlock { + NonCallableBlock( builder: self, keyPath: .getonly(keyPath) ) } + public subscript( + dynamicMember keyPath: WritableKeyPath + ) -> CallableBlock where Base == Optional { + CallableBlock( + builder: self, + keyPath: FunctionalKeyPath(keyPath).optional() + ) + } + + public subscript( + dynamicMember keyPath: KeyPath + ) -> NonCallableBlock where Base == Optional { + NonCallableBlock( + builder: self, + keyPath: FunctionalKeyPath.getonly(keyPath).optional() + ) + } + } extension Builder { @@ -122,7 +140,7 @@ extension Builder { public subscript( dynamicMember keyPath: WritableKeyPath ) -> CallableBlock { - .init( + CallableBlock( builder: _block.builder, keyPath: _block.keyPath.appending(path: .init(keyPath)) ) @@ -130,9 +148,31 @@ extension Builder { public subscript( dynamicMember keyPath: KeyPath - ) -> NonCallableBlock where Value: AnyObject, LocalValue: AnyObject { + ) -> NonCallableBlock { _block[dynamicMember: keyPath] } + + public subscript( + dynamicMember keyPath: WritableKeyPath + ) -> CallableBlock where Value == Optional { + CallableBlock( + builder: _block.builder, + keyPath: _block.keyPath.appending( + path: FunctionalKeyPath(keyPath).optional() + ) + ) + } + + public subscript( + dynamicMember keyPath: KeyPath + ) -> NonCallableBlock where Value == Optional { + NonCallableBlock( + builder: _block.builder, + keyPath: _block.keyPath.appending( + path: FunctionalKeyPath.getonly(keyPath).optional() + ) + ) + } } @dynamicMemberLookup @@ -143,7 +183,7 @@ extension Builder { public subscript( dynamicMember keyPath: WritableKeyPath ) -> CallableBlock where Value: AnyObject { - .init( + CallableBlock( builder: self.builder, keyPath: self.keyPath.appending(path: .init(keyPath)) ) @@ -151,11 +191,33 @@ extension Builder { public subscript( dynamicMember keyPath: KeyPath - ) -> NonCallableBlock where Value: AnyObject, LocalValue: AnyObject { - .init( + ) -> NonCallableBlock { + NonCallableBlock( builder: self.builder, keyPath: self.keyPath.appending(path: .getonly(keyPath)) ) } + + public subscript( + dynamicMember keyPath: WritableKeyPath + ) -> CallableBlock where Wrapped: AnyObject, Value == Optional { + CallableBlock( + builder: self.builder, + keyPath: self.keyPath.appending( + path: FunctionalKeyPath(keyPath).optional() + ) + ) + } + + public subscript( + dynamicMember keyPath: KeyPath + ) -> NonCallableBlock where Value == Optional { + NonCallableBlock( + builder: self.builder, + keyPath: self.keyPath.appending( + path: FunctionalKeyPath.getonly(keyPath).optional() + ) + ) + } } } diff --git a/Sources/FunctionalConfigurator/ConfigIntializable.swift b/Sources/FunctionalConfigurator/ConfigIntializable.swift index 9daa912..266e12c 100644 --- a/Sources/FunctionalConfigurator/ConfigIntializable.swift +++ b/Sources/FunctionalConfigurator/ConfigIntializable.swift @@ -11,15 +11,15 @@ extension ConfigInitializable { /// Instantiates a new object with specified configuration /// /// Note: Type must implement custom intializer with no parameters, even if it inherits from NSObject - public init(_ configuration: (Config) -> Config) { - self.init(configuration(Config())) + public init(config configuration: (Config) -> Config) { + self.init(config: configuration(Config())) } /// Instantiates a new object with specified configuration /// /// Note: Type must implement custom intializer with no parameters, even if it inherits from NSObject - public init(_ configurator: Config) { - self = configurator.configure(.init()) + public init(config configurator: Config) { + self = configurator.configured(.init()) } } diff --git a/Sources/FunctionalConfigurator/Configurator.swift b/Sources/FunctionalConfigurator/Configurator.swift index 8e6b8b6..9569d39 100644 --- a/Sources/FunctionalConfigurator/Configurator.swift +++ b/Sources/FunctionalConfigurator/Configurator.swift @@ -4,26 +4,33 @@ import FunctionalModification @dynamicMemberLookup public struct Configurator { private var _configure: (Base) -> Base + public init() { _configure = { $0 } } + + public init(config configuration: (Configurator) -> Configurator) { + self = configuration(.init()) + } - @discardableResult - public func configure(_ base: Base) -> Base { - _configure(base) + public func configure(_ base: inout Base) { + _ = _configure(base) } public func configure(_ base: Base) where Base: AnyObject { _ = _configure(base) } - + + public func configured(_ base: Base) -> Base { + _configure(base) + } + public func set(_ transform: @escaping (inout Base) -> Void) -> Configurator { appendingConfiguration { base in modification(of: _configure(base), with: transform) } } - @inlinable public func appending(_ configurator: Configurator) -> Configurator { - appendingConfiguration(configurator.configure) + appendingConfiguration(configurator._configure) } public func appendingConfiguration(_ configuration: @escaping (Base) -> Base) -> Configurator { @@ -35,7 +42,7 @@ public struct Configurator { public subscript( dynamicMember keyPath: WritableKeyPath ) -> CallableBlock { - .init( + CallableBlock( configurator: self, keyPath: .init(keyPath) ) @@ -43,29 +50,53 @@ public struct Configurator { public subscript( dynamicMember keyPath: KeyPath - ) -> NonCallableBlock where Base: AnyObject { - .init( + ) -> NonCallableBlock { + NonCallableBlock( configurator: self, keyPath: .getonly(keyPath) ) } + public subscript( + dynamicMember keyPath: WritableKeyPath + ) -> CallableBlock where Base == Optional { + CallableBlock( + configurator: self, + keyPath: FunctionalKeyPath(keyPath).optional() + ) + } + + public subscript( + dynamicMember keyPath: KeyPath + ) -> NonCallableBlock where Base == Optional { + NonCallableBlock( + configurator: self, + keyPath: FunctionalKeyPath.getonly(keyPath).optional() + ) + } + public static subscript( dynamicMember keyPath: WritableKeyPath ) -> CallableBlock { - .init( - configurator: .init(), - keyPath: .init(keyPath) - ) + Configurator()[dynamicMember: keyPath] } public static subscript( dynamicMember keyPath: KeyPath - ) -> NonCallableBlock where Base: AnyObject, Value: AnyObject { - .init( - configurator: .init(), - keyPath: .getonly(keyPath) - ) + ) -> NonCallableBlock { + Configurator()[dynamicMember: keyPath] + } + + public static subscript( + dynamicMember keyPath: WritableKeyPath + ) -> CallableBlock where Base == Optional { + Configurator()[dynamicMember: keyPath] + } + + public static subscript( + dynamicMember keyPath: KeyPath + ) -> NonCallableBlock where Base == Optional { + Configurator()[dynamicMember: keyPath] } } @@ -92,15 +123,30 @@ extension Configurator { public subscript( dynamicMember keyPath: WritableKeyPath ) -> CallableBlock { - .init( + CallableBlock( configurator: _block.configurator, - keyPath: _block.keyPath.appending(path: .init(keyPath)) + keyPath: _block.keyPath.appending(path: FunctionalKeyPath(keyPath)) ) } public subscript( dynamicMember keyPath: KeyPath - ) -> NonCallableBlock where Value: AnyObject, LocalValue: AnyObject { + ) -> NonCallableBlock { + _block[dynamicMember: keyPath] + } + + public subscript( + dynamicMember keyPath: WritableKeyPath + ) -> CallableBlock where Value == Optional { + CallableBlock( + configurator: _block.configurator, + keyPath: _block.keyPath.appending(path: FunctionalKeyPath(keyPath).optional()) + ) + } + + public subscript( + dynamicMember keyPath: KeyPath + ) -> NonCallableBlock where Value == Optional { _block[dynamicMember: keyPath] } } @@ -115,17 +161,35 @@ extension Configurator { ) -> CallableBlock where Value: AnyObject { .init( configurator: self.configurator, - keyPath: self.keyPath.appending(path: .init(keyPath)) + keyPath: self.keyPath.appending(path: FunctionalKeyPath(keyPath)) ) } public subscript( dynamicMember keyPath: KeyPath - ) -> NonCallableBlock where Value: AnyObject, LocalValue: AnyObject { + ) -> NonCallableBlock { .init( configurator: self.configurator, keyPath: self.keyPath.appending(path: .getonly(keyPath)) ) } + + public subscript( + dynamicMember keyPath: WritableKeyPath + ) -> CallableBlock where Wrapped: AnyObject, Value == Optional { + CallableBlock( + configurator: self.configurator, + keyPath: self.keyPath.appending(path: FunctionalKeyPath(keyPath)) + ) + } + + public subscript( + dynamicMember keyPath: KeyPath + ) -> NonCallableBlock where Value == Optional { + NonCallableBlock( + configurator: self.configurator, + keyPath: self.keyPath.appending(path: .getonly(keyPath)) + ) + } } } diff --git a/Sources/FunctionalConfigurator/CustomConfigurable.swift b/Sources/FunctionalConfigurator/CustomConfigurable.swift index 82b9832..e3b3d7f 100644 --- a/Sources/FunctionalConfigurator/CustomConfigurable.swift +++ b/Sources/FunctionalConfigurator/CustomConfigurable.swift @@ -10,7 +10,7 @@ extension CustomConfigurable { } public func configured(using configurator: Config) -> Self { - configurator.configure(self) + configurator.configured(self) } } diff --git a/Sources/FunctionalConfigurator/Modification.swift b/Sources/FunctionalConfigurator/Modification.swift new file mode 100644 index 0000000..0ddee72 --- /dev/null +++ b/Sources/FunctionalConfigurator/Modification.swift @@ -0,0 +1,12 @@ +/// Modifies an object. +/// +/// Returns a new instance for value types +/// Returns modified reference for reference types +@inlinable +public func modification( + of object: Object, + with configuration: (Configurator) -> Configurator +) -> Object { + return Configurator(config: configuration) + .configured(object) +} diff --git a/Sources/FunctionalKeyPath/FunctionalKeyPath.swift b/Sources/FunctionalKeyPath/FunctionalKeyPath.swift index 6049c1d..bc4dda6 100644 --- a/Sources/FunctionalKeyPath/FunctionalKeyPath.swift +++ b/Sources/FunctionalKeyPath/FunctionalKeyPath.swift @@ -36,7 +36,7 @@ public struct FunctionalKeyPath { /// /// Ignores embed function call @inlinable - public static func getonly(_ keyPath: KeyPath) -> FunctionalKeyPath { + public static func getonly(_ keyPath: KeyPath) -> FunctionalKeyPath { .init(embed: { value, root in return root }, extract: { root in @@ -44,6 +44,17 @@ public struct FunctionalKeyPath { }) } + /// Makes path optional + public func optional() -> FunctionalKeyPath { + FunctionalKeyPath(embed: { value, root in + guard let root = root, let value = value else { return nil } + return self._embed(value, root) + }, extract: { root in + guard let root = root else { return nil } + return self._extract(root) + }) + } + /// Returns a root by embedding a value. /// /// Note: Value will not be embed if FunctionalKeyPath was initialized by default (non-writable) `KeyPath` via `getonly` function @@ -54,6 +65,15 @@ public struct FunctionalKeyPath { self._embed(value, root) } + /// Returns a root by embedding a value. + /// + /// Note: Value will not be embed if FunctionalKeyPath was initialized by default (non-writable) `KeyPath` via `getonly` function + /// + /// - Parameter value: A value to embed. + public func embed(_ value: Value, in root: inout Root) { + root = self.embed(value, in: root) + } + /// Attempts to extract a value from a root. /// /// - Parameter root: A root to extract from. @@ -87,4 +107,17 @@ public struct FunctionalKeyPath { } ) } + + /// Returns a new case path created by appending the given case path to this one. + /// + /// Use this method to extend this case path to the value type of another case path. + /// + /// - Parameter path: The case path to append. + /// - Returns: A case path from the root of this case path to the value type of `path`. + @inlinable + public func appending( + path: FunctionalKeyPath + ) -> FunctionalKeyPath where Value == Optional { + appending(path: path.optional()) + } } diff --git a/Tests/FunctionalConfigurationTests/ConfiguratorTests.swift b/Tests/FunctionalConfigurationTests/ConfiguratorTests.swift index 2924967..9043b42 100644 --- a/Tests/FunctionalConfigurationTests/ConfiguratorTests.swift +++ b/Tests/FunctionalConfigurationTests/ConfiguratorTests.swift @@ -7,24 +7,24 @@ final class ConfiguratorTests: XCTestCase { struct Wrapped: Equatable { var value = 0 } - + var value = false var wrapped = Wrapped() } - - let wrappedConfiguator = Configurator + + let wrappedConfiguator = Configurator() .wrapped.value(1) - - let valueConfigurator = Configurator + + let valueConfigurator = Configurator() .value(true) - + let configurator = wrappedConfiguator .appending(valueConfigurator) - + let initial = TestConfigurable() let expected = TestConfigurable(value: true, wrapped: .init(value: 1)) - let actual = configurator.configure(initial) - + let actual = configurator.configured(initial) + XCTAssertNotEqual(actual, initial) XCTAssertEqual(actual, expected) } @@ -56,7 +56,7 @@ final class ConfiguratorTests: XCTestCase { .wrapped(.init(value: 1)) } let actual2 = TestConfigurable( - TestConfigurable.Config + config: TestConfigurable.Config .value(true) .wrapped(.init(value: 1)) ) @@ -91,4 +91,30 @@ final class ConfiguratorTests: XCTestCase { XCTAssertEqual(actual.value, expected.value) XCTAssertEqual(actual.wrapped, expected.wrapped) } + + func testOptional() { + struct TestConfigurable: CustomConfigurable { + struct Wrapped: Equatable { + var value = 0 + init(value: Int = 0) { + self.value = value + } + } + + var value = false + var wrapped: Wrapped? = Wrapped() + } + + let initial = TestConfigurable() + let expected = TestConfigurable(value: true, wrapped: .init(value: 1)) + let actual = TestConfigurable().configured { $0 + .value(true) + .wrapped.value(1) + } + + XCTAssertNotEqual(actual.value, initial.value) + XCTAssertNotEqual(actual.wrapped, initial.wrapped) + XCTAssertEqual(actual.value, expected.value) + XCTAssertEqual(actual.wrapped, expected.wrapped) + } }