From cd42f5073fe459837972f6e1d876b3f6c72ff1da Mon Sep 17 00:00:00 2001 From: Vahan Harutyunyan Date: Wed, 29 Jan 2025 09:54:38 -0500 Subject: [PATCH 1/2] Add parsing default form values --- Davinci/CONCEPT.md | 131 ++++++++++++++++++ .../Davinci/collector/FieldCollector.swift | 8 ++ Davinci/Davinci/collector/Form.swift | 11 ++ .../collector/MultiSelectCollector.swift | 10 ++ .../collector/SingleValueCollector.swift | 8 ++ Davinci/DavinciTests/DaVinciTests.swift | 12 +- .../MultiSelectCollectorTests.swift | 13 +- .../DavinciTests/PasswordCollectorTests.swift | 7 + .../SingleSelectCollectorTests.swift | 2 +- Davinci/DavinciTests/TextCollectorTests.swift | 8 ++ 10 files changed, 202 insertions(+), 8 deletions(-) create mode 100644 Davinci/CONCEPT.md diff --git a/Davinci/CONCEPT.md b/Davinci/CONCEPT.md new file mode 100644 index 0000000..f4fda45 --- /dev/null +++ b/Davinci/CONCEPT.md @@ -0,0 +1,131 @@ +

+ + Logo + +


+

+ +# Design Concept + +## How Collector map with Field Type + +CollectorFactory is a factory class that maps the Field Type to the Collector, to register a new Collector, provide the +Field Typeas String and the Collector's Type. With the Collector's Type, the CollectorFactory can create +the Collector during parsing the DaVinci Response JSON. + +``` +CollectorFactory().register(, ) +``` + +For example: + +```swift +// Map Password Type to PasswordCollector +CollectorFactory.shared.register(type: "PASSWORD", collector: PasswordCollector.self) + +CollectorFactory.shared.register(type: "SUBMIT_BUTTON", collector: SubmitCollector.self) + +// Allow to map multiple Field Type to the same Collector +CollectorFactory.shared.register(type: "FLOW_BUTTON", collector: FlowCollector.self) +CollectorFactory.shared.register(type: "FLOW_LINK", collector: FlowCollector.self) + +``` + +## How Collector is created & initialized + +DaVinci Response JSON: + +```json +{ + "form": { + "components": { + "fields": [ + { + "type": "TEXT", + "key": "user.username", + "label": "Username", + "required": true, + "validation": { + "regex": "^[^@]+@[^@]+\\.[^@]+$", + "errorMessage": "Must be alphanumeric" + } + }, + { + "type": "PASSWORD", + "key": "password", + "label": "Password", + "required": true + }, + ... + ] + } + } +} +``` + +```mermaid +sequenceDiagram + Form ->> CollectorFactory: collector(fields) + loop ForEach Field in Fields + CollectorFactory ->> Collector: create() + CollectorFactory ->> Collector: init(field) + Collector ->> Collector: populate the instance properties with field Json + end + CollectorFactory ->> Form: collectors +``` + +## How Collector populate default value + +The Collector populates the default value from the `formData` JSON: + +```json +{ + "formData": { + "value": { + "user.username": "", + "password": "", + "dropdown-field": "", + "combobox-field": [], + "radio-field": "", + "checkbox-field": [] + } + } +} +``` + +```mermaid +sequenceDiagram + loop ForEach Collector in Collectors + Form ->> Collector: getKey() + alt key in formData + Form ->> Collector: init(formData[key]) + Collector ->> Collector: "populate the default value with formData + end + end +``` + +## How Collector access ContinueNode + +The Collector is self-contained and does not have access to the ContinueNode by default. The Collector itself handle +how to collect data from the user and how to validate the data. However, in some scenarios the Collector needs to access +the ContinueNode. For example, the Collector needs to access the root JSON to get the `passwordPolicy` to validate the +password. + +To allow the Collector to access the ContinueNode, the Collector can implement the `ContinueNodeAware` protocol. +The `ContinueNodeAware` interface provides the `continueNode` property to access the ContinueNode. After Collector is +created, the ContinueNode will be injected to the Collector. + +```swift +public class PasswordCollector: ContinueNodeAware { + public var continueNode: ContinueNode? +} +``` + +```mermaid +sequenceDiagram + loop ForEach Collector in ContinueNode.collectors + alt Collector is ContinueNodeAware + CollectorFactory ->> Collector: setContinueNode(continueNode) + end + end +``` diff --git a/Davinci/Davinci/collector/FieldCollector.swift b/Davinci/Davinci/collector/FieldCollector.swift index b649700..e65027d 100644 --- a/Davinci/Davinci/collector/FieldCollector.swift +++ b/Davinci/Davinci/collector/FieldCollector.swift @@ -32,6 +32,14 @@ open class FieldCollector: Collector { key = json[Constants.key] as? String ?? "" label = json[Constants.label] as? String ?? "" } + + /// Initializes `FieldCollector` with the given value. + /// This implementation does nothing. + /// Subclasses should override this method as needed. + /// - Parameter input: The value to initialize with. + public func initialize(with value: Any) { + // To be implemented by subclasses + } } diff --git a/Davinci/Davinci/collector/Form.swift b/Davinci/Davinci/collector/Form.swift index a73b2ca..f30ce24 100644 --- a/Davinci/Davinci/collector/Form.swift +++ b/Davinci/Davinci/collector/Form.swift @@ -26,6 +26,17 @@ class Form { let fields = components[Constants.fields] as? [[String: Any]] { collectors = CollectorFactory().collector(from: fields) } + + // Populate default values for collectors + if let formData = json[Constants.formData] as? [String: Any], + let value = formData[Constants.value] as? [String: Any] { + collectors.compactMap { $0 as? FieldCollector }.compactMap{ $0 }.forEach { collector in + if let fieldValue = value[collector.key] { + collector.initialize(with: fieldValue) + } + } + } + return collectors } } diff --git a/Davinci/Davinci/collector/MultiSelectCollector.swift b/Davinci/Davinci/collector/MultiSelectCollector.swift index 47adf08..8d7fb4a 100644 --- a/Davinci/Davinci/collector/MultiSelectCollector.swift +++ b/Davinci/Davinci/collector/MultiSelectCollector.swift @@ -30,6 +30,16 @@ open class MultiSelectCollector: FieldCollector { options = Option.parseOptions(from: json) } + /// Initializes the `MultiSelectCollector` with the given value. + /// - Parameter input: The value to initialize the collector with. + public override func initialize(with value: Any) { + if let stringValue = value as? String, !stringValue.isEmpty { + self.value.append(stringValue) + } else if let arrayValue = value as? [String], !arrayValue.isEmpty { + self.value.append(contentsOf: arrayValue) + } + } + /// Validates this collector, returning a list of validation errors if any. /// - Returns: An array of `ValidationError`. open func validate() -> [ValidationError] { diff --git a/Davinci/Davinci/collector/SingleValueCollector.swift b/Davinci/Davinci/collector/SingleValueCollector.swift index 727ecfa..fb01478 100644 --- a/Davinci/Davinci/collector/SingleValueCollector.swift +++ b/Davinci/Davinci/collector/SingleValueCollector.swift @@ -26,4 +26,12 @@ open class SingleValueCollector: FieldCollector { value = stringValue } } + + /// Initializes the `SingleValueCollector` with the given value. + /// - Parameter value: The value to initialize the collector with. + public override func initialize(with value: Any) { + if let stringValue = value as? String { + self.value = stringValue + } + } } diff --git a/Davinci/DavinciTests/DaVinciTests.swift b/Davinci/DavinciTests/DaVinciTests.swift index ee0e866..f76d4cf 100644 --- a/Davinci/DavinciTests/DaVinciTests.swift +++ b/Davinci/DavinciTests/DaVinciTests.swift @@ -365,7 +365,7 @@ final class DaVinciTests: XCTestCase { XCTAssertEqual(collector3.required, true) XCTAssertEqual(collector3.validation?.regex?.pattern, ".") XCTAssertEqual(collector3.validation?.errorMessage, "Must be valid email address") - //XCTAssertEqual(collector3.value, "default-username") + XCTAssertEqual(collector3.value, "default-username") guard let collector4 = (continueNode.collectors[3] as? PasswordCollector) else { XCTFail("PasswordCollector is nil") @@ -375,7 +375,7 @@ final class DaVinciTests: XCTestCase { XCTAssertEqual(collector4.key, "password") XCTAssertEqual(collector4.label, "Password") XCTAssertEqual(collector4.required, true) - //XCTAssertEqual(collector4.value, "default-password") + XCTAssertEqual(collector4.value, "default-password") guard let collector5 = (continueNode.collectors[4] as? SubmitCollector) else { XCTFail("SubmitCollector is nil") @@ -410,7 +410,7 @@ final class DaVinciTests: XCTestCase { XCTAssertEqual(collector8.label, "Dropdown") XCTAssertEqual(collector8.required, true) XCTAssertEqual(collector8.options.count, 3) - //XCTAssertEqual(collector8.value, "default-dropdown") + XCTAssertEqual(collector8.value, "default-dropdown") guard let collector9 = (continueNode.collectors[8] as? MultiSelectCollector) else { XCTFail("MultiSelectCollector is nil") @@ -421,7 +421,7 @@ final class DaVinciTests: XCTestCase { XCTAssertEqual(collector9.label, "Combobox") XCTAssertEqual(collector9.required, true) XCTAssertEqual(collector9.options.count, 2) - //XCTAssertEqual(collector9.value, "default-combobox") + XCTAssertEqual(collector9.value, ["default-combobox"]) guard let collector10 = (continueNode.collectors[9] as? SingleSelectCollector) else { XCTFail("MultiSelectCollector is nil") @@ -432,7 +432,7 @@ final class DaVinciTests: XCTestCase { XCTAssertEqual(collector10.label, "Radio") XCTAssertEqual(collector10.required, true) XCTAssertEqual(collector10.options.count, 2) - //XCTAssertEqual(collector10.value, "default-radio") + XCTAssertEqual(collector10.value, "default-radio") guard let collector11 = (continueNode.collectors[10] as? MultiSelectCollector) else { XCTFail("SingleSelectCollector is nil") @@ -443,6 +443,6 @@ final class DaVinciTests: XCTestCase { XCTAssertEqual(collector11.label, "Checkbox") XCTAssertEqual(collector11.required, true) XCTAssertEqual(collector11.options.count, 2) - //XCTAssertEqual(collector3.value, "default-checkbox") + XCTAssertEqual(collector11.value, ["default-checkbox"]) } } diff --git a/Davinci/DavinciTests/MultiSelectCollectorTests.swift b/Davinci/DavinciTests/MultiSelectCollectorTests.swift index 9fe7fb9..300c861 100644 --- a/Davinci/DavinciTests/MultiSelectCollectorTests.swift +++ b/Davinci/DavinciTests/MultiSelectCollectorTests.swift @@ -63,7 +63,18 @@ final class MultiSelectCollectorTests: XCTestCase { ] let inputDefault = "Selected Option" let collector = MultiSelectCollector(with: input) - collector.value = [inputDefault] + collector.initialize(with: inputDefault) + + XCTAssertEqual(collector.validate(), []) + } + + func testDoesNotAddRequiredErrorWhenValueIsArrayAndRequired() { + let input: [String: Any] = [ + "required": true + ] + let inputDefault = ["Selected Option"] + let collector = MultiSelectCollector(with: input) + collector.initialize(with: inputDefault) XCTAssertEqual(collector.validate(), []) } diff --git a/Davinci/DavinciTests/PasswordCollectorTests.swift b/Davinci/DavinciTests/PasswordCollectorTests.swift index 68a1614..89cae91 100644 --- a/Davinci/DavinciTests/PasswordCollectorTests.swift +++ b/Davinci/DavinciTests/PasswordCollectorTests.swift @@ -141,6 +141,13 @@ final class PasswordCollectorTests: XCTestCase { XCTAssertTrue(collector.validate().isEmpty) } + func testShouldInitializeDefaultValue() { + let input = "test" + let collector = PasswordCollector(with: [:]) + collector.initialize(with: input) + XCTAssertEqual("test", collector.value) + } + } class MockContinueNode: ContinueNode { } diff --git a/Davinci/DavinciTests/SingleSelectCollectorTests.swift b/Davinci/DavinciTests/SingleSelectCollectorTests.swift index dd72bca..bf50d85 100644 --- a/Davinci/DavinciTests/SingleSelectCollectorTests.swift +++ b/Davinci/DavinciTests/SingleSelectCollectorTests.swift @@ -44,7 +44,7 @@ final class SingleSelectCollectorTests: XCTestCase { func testInitializesValueWithProvidedJsonElement() { let input = "Selected Option" let collector = SingleSelectCollector(with: [:]) - collector.value = input + collector.initialize(with: input) XCTAssertEqual(collector.value, "Selected Option") } diff --git a/Davinci/DavinciTests/TextCollectorTests.swift b/Davinci/DavinciTests/TextCollectorTests.swift index a69ab3c..ffbcd1e 100644 --- a/Davinci/DavinciTests/TextCollectorTests.swift +++ b/Davinci/DavinciTests/TextCollectorTests.swift @@ -38,4 +38,12 @@ final class TextCollectorTests: XCTestCase { XCTAssertEqual(textCollector.value, "test") } + + func testShouldInitializeDefaultValue() { + let input = "test" + let textCollector = TextCollector(with: [:]) + textCollector.initialize(with: input) + + XCTAssertEqual(textCollector.value, "test") + } } From ffe3d4ed71b3938dfc7251fbbcce96bd386b0ae7 Mon Sep 17 00:00:00 2001 From: Vahan Harutyunyan Date: Thu, 30 Jan 2025 12:40:22 -0500 Subject: [PATCH 2/2] Update copyright --- Davinci/Davinci/collector/Form.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Davinci/Davinci/collector/Form.swift b/Davinci/Davinci/collector/Form.swift index f30ce24..265dfa5 100644 --- a/Davinci/Davinci/collector/Form.swift +++ b/Davinci/Davinci/collector/Form.swift @@ -2,7 +2,7 @@ // Form.swift // PingDavinci // -// Copyright (c) 2024 Ping Identity. All rights reserved. +// Copyright (c) 2024 - 2025 Ping Identity. All rights reserved. // // This software may be modified and distributed under the terms // of the MIT license. See the LICENSE file for details.