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 @@
+
+
+
+
+
+
+
+# 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.