Skip to content
This repository has been archived by the owner on Jun 3, 2024. It is now read-only.

Commit

Permalink
fix: NSNull treatment (#61)
Browse files Browse the repository at this point in the history
* Fixes problem where sending data in a POST or PUT request would cause a serialization error. Plus: makes the deserialziation of action properties better by using AnyServerDrivenData.

* better error handling

* self review

* Makes nimbus core aware of NSNull + correctly handles potential errors on operations

* self review
  • Loading branch information
Tiagoperes authored Nov 29, 2022
1 parent 8e6a50e commit 62231af
Show file tree
Hide file tree
Showing 13 changed files with 178 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.zup.nimbus.core.deserialization

import com.zup.nimbus.core.tree.ServerDrivenEvent
import com.zup.nimbus.core.tree.dynamic.DynamicEvent
import com.zup.nimbus.core.utils.Null

/**
* This class helps to deserialize data of unknown type. This is very useful for deserializing the data that comes from
Expand All @@ -21,7 +22,7 @@ class AnyServerDrivenData private constructor (
/**
* The actual value wrapped by this.
*/
val value: Any?,
value: Any?,
/**
* The path for the current data access.
*
Expand Down Expand Up @@ -56,6 +57,12 @@ class AnyServerDrivenData private constructor (
value: Any?,
): this(value, "", mutableListOf())

val value: Any?

init {
this.value = Null.sanitize(value)
}

/**
* Holds the values to use when null is found on functions that can't return null.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ package com.zup.nimbus.core.tree.dynamic.node

import com.zup.nimbus.core.Nimbus
import com.zup.nimbus.core.ServerDrivenState
import com.zup.nimbus.core.deserialization.AnyServerDrivenData
import com.zup.nimbus.core.scope.Scope
import com.zup.nimbus.core.scope.StateOnlyScope
import com.zup.nimbus.core.scope.closestScopeWithType
import com.zup.nimbus.core.scope.getPathToScope
import com.zup.nimbus.core.tree.dynamic.container.NodeContainer
import com.zup.nimbus.core.utils.valueOfKey

/**
* A rendered list that can change must somehow identify each of its item. This encapsulates an item with an object
Expand All @@ -18,8 +18,7 @@ private class IdentifiableItem(val value: Any?, index: Int, key: String?) {
val id: String

init {
// fixme: we probably want to change this to valueOfPath, but it would be computationally more expensive
val keyValue: Any? = key?.let { valueOfKey(value, it) }
val keyValue = key?.let { AnyServerDrivenData(value).get(it).asAnyOrNull() }
id = if (keyValue == null) "$index" else "$keyValue"
}

Expand Down Expand Up @@ -89,9 +88,14 @@ class ForEachNode(
propertyContainer?.initialize(this)
propertyContainer?.addDependent(this)
properties = propertyContainer?.read()
iteratorName = valueOfKey(properties, "iteratorName") ?: iteratorName
indexName = valueOfKey(properties, "indexName") ?: indexName
key = valueOfKey(properties, "key")
val deserializer = AnyServerDrivenData(properties)
iteratorName = deserializer.get("iteratorName").asStringOrNull() ?: iteratorName
indexName = deserializer.get("indexName").asStringOrNull() ?: indexName
key = deserializer.get("key").asStringOrNull()
if (deserializer.hasError()) {
nimbus?.logger?.error("Error while deserializing the properties for the component forEach.\nAt: " +
getPathToScope() + deserializer.errorsAsString())
}
hasInitialized = true
update()
hasChanged = false
Expand Down Expand Up @@ -131,11 +135,17 @@ class ForEachNode(

override fun update() {
properties = propertyContainer?.read()
val newItems: List<Any?> = valueOfKey(properties, "items") ?: emptyList()
val newIdentified = newItems.mapIndexed { index, item -> IdentifiableItem(item, index, key) }
if (newIdentified != items) {
val deserializer = AnyServerDrivenData(properties)
val newItems = deserializer.get("items").asListOrNull()?.mapIndexed { index, item ->
IdentifiableItem(item.asAnyOrNull(), index, key)
} ?: emptyList()
if (deserializer.hasError()) {
nimbus?.logger?.error("Error while deserializing the items for the component forEach.\nAt: " +
getPathToScope() + deserializer.errorsAsString())
}
else if (newItems != items) {
warnIfUpdatingWithoutKey()
items = newIdentified
items = newItems
val newChildren = calculateChildren()
hasChanged = true
children = newChildren
Expand Down
24 changes: 17 additions & 7 deletions src/commonMain/kotlin/com/zup/nimbus/core/ui/operations/array.kt
Original file line number Diff line number Diff line change
@@ -1,25 +1,35 @@
package com.zup.nimbus.core.ui.operations

import com.zup.nimbus.core.deserialization.AnyServerDrivenData
import com.zup.nimbus.core.ui.UILibrary

internal fun registerArrayOperations(library: UILibrary) {
library
.addOperation("insert") {
val list = if (it[0] is List<*>) (it[0] as List<*>).toMutableList() else ArrayList()
val item = it[1]
val index = it.getOrNull(2) as Int?
val arguments = AnyServerDrivenData(it)
val list = if (arguments.at(0).isList()) (arguments.at(0).value as List<*>).toMutableList()
else ArrayList()
val item = arguments.at(1).asAnyOrNull()
val index = arguments.at(2).asIntOrNull()
if (arguments.hasError()) throw IllegalArgumentException(arguments.errorsAsString())
if (index == null) list.add(item) else list.add(index, item)
list
}
.addOperation("remove") {
val list = if (it[0] is List<*>) (it[0] as List<*>).toMutableList() else ArrayList()
val item = it[1]
val arguments = AnyServerDrivenData(it)
val list = if (arguments.at(0).isList()) (arguments.at(0).value as List<*>).toMutableList()
else ArrayList()
val item = arguments.at(1).asAnyOrNull()
if (arguments.hasError()) throw IllegalArgumentException(arguments.errorsAsString())
list.remove(item)
list
}
.addOperation("removeIndex") {
val list = if (it[0] is List<*>) (it[0] as List<*>).toMutableList() else ArrayList()
val index = it.getOrNull(1) as Int?
val arguments = AnyServerDrivenData(it)
val list = if (arguments.at(0).isList()) (arguments.at(0).value as List<*>).toMutableList()
else ArrayList()
val index = arguments.at(1).asIntOrNull()
if (arguments.hasError()) throw IllegalArgumentException(arguments.errorsAsString())
if (index == null) list.removeLast() else list.removeAt(index)
list
}
Expand Down
14 changes: 10 additions & 4 deletions src/commonMain/kotlin/com/zup/nimbus/core/ui/operations/logic.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.zup.nimbus.core.ui.operations

import com.zup.nimbus.core.deserialization.AnyServerDrivenData
import com.zup.nimbus.core.ui.UILibrary
import com.zup.nimbus.core.utils.then

Expand All @@ -16,12 +17,17 @@ internal fun registerLogicOperations(library: UILibrary) {
toBooleanList(it).contains(true)
}
.addOperation("not") {
!(it[0] as Boolean)
val arguments = AnyServerDrivenData(it)
val value = arguments.at(0).asBoolean()
if (arguments.hasError()) throw IllegalArgumentException(arguments.errorsAsString())
!value
}
.addOperation("condition") {
val premise = it[0] as Boolean
val trueValue = it[1]
val falseValue = it[2]
val arguments = AnyServerDrivenData(it)
val premise = arguments.at(0).asBoolean()
val trueValue = arguments.at(1).asAnyOrNull()
val falseValue = arguments.at(2).asAnyOrNull()
if (arguments.hasError()) throw IllegalArgumentException(arguments.errorsAsString())
((premise) then trueValue) ?: falseValue
}
}
14 changes: 10 additions & 4 deletions src/commonMain/kotlin/com/zup/nimbus/core/ui/operations/number.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import com.zup.nimbus.core.utils.toNumberOrNull

private fun toNumberList(values: List<Any?>) = values.map { toNumberOrNull(it) }

// examples: left == right; left > right; left <= right
private fun toLeftAndRight(values: List<Any?>): Pair<Number?, Number?> {
val numberList = toNumberList(values)
return numberList.firstOrNull() to numberList.getOrNull(1)
}

@Suppress("ComplexMethod")
internal fun registerNumberOperations(library: UILibrary) {
library
Expand All @@ -26,22 +32,22 @@ internal fun registerNumberOperations(library: UILibrary) {
toNumberList(it).reduce { result, item -> if (result == null || item == null) null else result / item }
}
.addOperation("gt"){
val (left, right) = toNumberList(it)
val (left, right) = toLeftAndRight(it)
if (left == null || right == null) false
else left > right
}
.addOperation("gte"){
val (left, right) = toNumberList(it)
val (left, right) = toLeftAndRight(it)
if (left == null || right == null) false
else left >= right
}
.addOperation("lt"){
val (left, right) = toNumberList(it)
val (left, right) = toLeftAndRight(it)
if (left == null || right == null) false
else left < right
}
.addOperation("lte"){
val (left, right) = toNumberList(it)
val (left, right) = toLeftAndRight(it)
if (left == null || right == null) false
else left <= right
}
Expand Down
17 changes: 10 additions & 7 deletions src/commonMain/kotlin/com/zup/nimbus/core/ui/operations/other.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.zup.nimbus.core.ui.operations

import com.zup.nimbus.core.ui.UILibrary
import com.zup.nimbus.core.utils.Null
import com.zup.nimbus.core.utils.compareTo
import com.zup.nimbus.core.utils.toNumberOrNull

Expand All @@ -14,7 +15,8 @@ private fun areNumbersEqual(left: Any?, right: Any?): Boolean {
internal fun registerOtherOperations(library: UILibrary) {
library
.addOperation("contains"){
val (collection, element) = it
val collection = it.firstOrNull()
val element = it.getOrNull(1)
when (collection) {
is List<*> -> collection.contains(element)
is Map<*, *> -> collection.contains(element)
Expand All @@ -23,7 +25,7 @@ internal fun registerOtherOperations(library: UILibrary) {
}
}
.addOperation("concat"){
when (it[0]) {
when (it.firstOrNull()) {
is List<*> -> {
val result = ArrayList<Any?>()
it.forEach { list ->
Expand All @@ -42,27 +44,28 @@ internal fun registerOtherOperations(library: UILibrary) {
}
}
.addOperation("length"){
when (val collection = it[0]) {
when (val collection = it.firstOrNull()) {
is List<*> -> collection.size
is Map<*, *> -> collection.size
is String -> collection.length
else -> 0
}
}
.addOperation("eq"){
val (left, right) = it
val left = it.firstOrNull()
val right = it.getOrNull(1)
if (left == right) true
else areNumbersEqual(left, right)
}
.addOperation("isNull"){
it[0] == null
Null.isNull(it.firstOrNull())
}
.addOperation("isEmpty"){
when (val collection = it[0]) {
when (val collection = it.firstOrNull()) {
is List<*> -> collection.isEmpty()
is Map<*, *> -> collection.isEmpty()
is String -> collection.isEmpty()
else -> collection == null
else -> Null.isNull(collection)
}
}
}
33 changes: 23 additions & 10 deletions src/commonMain/kotlin/com/zup/nimbus/core/ui/operations/string.kt
Original file line number Diff line number Diff line change
@@ -1,37 +1,50 @@
package com.zup.nimbus.core.ui.operations

import com.zup.nimbus.core.deserialization.AnyServerDrivenData
import com.zup.nimbus.core.ui.UILibrary
import com.zup.nimbus.core.regex.replace
import com.zup.nimbus.core.regex.matches
import com.zup.nimbus.core.regex.toFastRegex

private fun toStringList(values: List<Any?>): List<String> {
return values.filterIsInstance<String>()
private fun getSingleArgument(argumentList: List<Any?>): String {
val arguments = AnyServerDrivenData(argumentList)
val str = arguments.at(0).asString()
if (arguments.hasError()) throw IllegalArgumentException(arguments.errorsAsString())
return str
}

internal fun registerStringOperations(library: UILibrary) {
library
.addOperation("capitalize"){
(it[0] as String).replaceFirstChar { char -> char.uppercaseChar() }
getSingleArgument(it).replaceFirstChar { char -> char.uppercaseChar() }
}
.addOperation("lowercase"){
(it[0] as String).lowercase()
getSingleArgument(it).lowercase()
}
.addOperation("uppercase"){
(it[0] as String).uppercase()
getSingleArgument(it).uppercase()
}
.addOperation("match"){
val (value, regex) = toStringList(it)
val arguments = AnyServerDrivenData(it)
val value = arguments.at(0).asString()
val regex = arguments.at(1).asString()
if (arguments.hasError()) throw IllegalArgumentException(arguments.errorsAsString())
value.matches(regex.toFastRegex())
}
.addOperation("replace"){
val (value, regex, replace) = toStringList(it)
val arguments = AnyServerDrivenData(it)
val value = arguments.at(0).asString()
val regex = arguments.at(1).asString()
val replace = arguments.at(2).asString()
if (arguments.hasError()) throw IllegalArgumentException(arguments.errorsAsString())
value.replace(regex.toFastRegex(), replace)
}
.addOperation("substr"){
val value = it[0] as String
val start = it[1] as Int
val end = it.getOrNull(2) as Int?
val arguments = AnyServerDrivenData(it)
val value = arguments.at(0).asString()
val start = arguments.at(1).asInt()
val end = arguments.at(2).asIntOrNull()
if (arguments.hasError()) throw IllegalArgumentException(arguments.errorsAsString())
if (end == null) value.substring(start) else value.substring(start, end)
}
}
16 changes: 16 additions & 0 deletions src/commonMain/kotlin/com/zup/nimbus/core/utils/Null.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.zup.nimbus.core.utils

expect object Null {
/**
* Verifies if the value is null considering all the null values of the platform.
*
* Example: NSNull on iOS.
*/
fun isNull(value: Any?): Boolean

/**
* If the value passed as parameter corresponds to null in the current platform, this function returns the Kotlin
* Null (`null`). Otherwise, it returns the value.
*/
fun <T>sanitize(value: T): T?
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ class ObservableNavigator(
}

override fun push(request: ViewRequest) {
val view = ServerDrivenView(nimbus, description = request.url) { this }
val states = request.params?.map { ServerDrivenState(it.key, it.value) }
val view = ServerDrivenView(nimbus, states = states, description = request.url) { this }
testScope.launch {
try {
val tree = nimbus.viewClient.fetch(request)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.zup.nimbus.core.network.ResponseError
import com.zup.nimbus.core.network.ViewRequest
import com.zup.nimbus.core.scope.closestScopeWithType
import com.zup.nimbus.core.tree.dynamic.node.RootNode
import com.zup.nimbus.core.tree.findNodeById
import io.ktor.http.HttpStatusCode
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.ExperimentalCoroutinesApi
Expand Down Expand Up @@ -132,5 +133,15 @@ class NavigationTest {
view = lastPage.closestScopeWithType()
assertEquals("/screen1", view?.description)
}

@Test
fun `should use a navigation state`() = scope.runTest {
navigator.push(ViewRequest("/stateful-navigation-1"))
val page1 = navigator.awaitPushCompletion()
NodeUtils.pressButton(page1, "next")
val page2 = navigator.awaitPushCompletion()
val address = page2.findNodeById("address")?.properties?.get("text")
assertEquals("Rua dos bobos, 0", address)
}
}

Loading

0 comments on commit 62231af

Please sign in to comment.