Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add parsing default form values #24

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions Davinci/CONCEPT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<p align="center">
<a href="https://github.com/ForgeRock/ping-android-sdk">
<img src="https://www.pingidentity.com/content/dam/picr/nav/Ping-Logo-2.svg" alt="Logo">
</a>
<hr/>
</p>

# 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(<FieldTypeString>, <Collector.Type>)
```

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
```
8 changes: 8 additions & 0 deletions Davinci/Davinci/collector/FieldCollector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}


Expand Down
11 changes: 11 additions & 0 deletions Davinci/Davinci/collector/Form.swift
vahancouver marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
10 changes: 10 additions & 0 deletions Davinci/Davinci/collector/MultiSelectCollector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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] {
Expand Down
8 changes: 8 additions & 0 deletions Davinci/Davinci/collector/SingleValueCollector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
12 changes: 6 additions & 6 deletions Davinci/DavinciTests/DaVinciTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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")
Expand Down Expand Up @@ -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")
Expand All @@ -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")
Expand All @@ -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")
Expand All @@ -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"])
}
}
13 changes: 12 additions & 1 deletion Davinci/DavinciTests/MultiSelectCollectorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(), [])
}
Expand Down
7 changes: 7 additions & 0 deletions Davinci/DavinciTests/PasswordCollectorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 { }
2 changes: 1 addition & 1 deletion Davinci/DavinciTests/SingleSelectCollectorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
8 changes: 8 additions & 0 deletions Davinci/DavinciTests/TextCollectorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
Loading