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

feat: navigation callbacks #75

Merged
merged 2 commits into from
Jan 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
The Nimbus SDUI is:

1. A solution for applications that need to have some of its user interface (UI) driven by the backend, i.e. Server Driven UI (SDUI).
1. A protocol for serializing the content and behavior of a UI into JSON so it can be sent by a backend sever and interpreted by the front-end.
1. A protocol for serializing the content and behavior of a UI into JSON so it can be sent by a backend server and interpreted by the front-end.
1. A set of libraries that implements this protocol.

An application that uses Nimbus will have:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package br.com.zup.nimbus.core

import br.com.zup.nimbus.core.scope.CommonScope
import br.com.zup.nimbus.core.tree.ServerDrivenEvent

/**
* A scope for the current view in a navigator.
Expand All @@ -30,6 +31,7 @@ class ServerDrivenView(
* The states in this scope. Useful for creating view parameters in the navigation.
*/
states: List<ServerDrivenState>? = null,
val events: List<ServerDrivenEvent>? = null,
/**
* A description for this view. Suggestion: the URL used to load the content of this view or "json", if a local json
* string was used to load it.
Expand All @@ -44,7 +46,7 @@ class ServerDrivenView(
getNavigator: () -> ServerDrivenNavigator,
): CommonScope(parent = nimbus, states = states) {
constructor(nimbus: Nimbus, getNavigator: () -> ServerDrivenNavigator):
this(nimbus, null, null, getNavigator)
this(nimbus, null, null, null, getNavigator)

val navigator = getNavigator()
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package br.com.zup.nimbus.core.network

import br.com.zup.nimbus.core.tree.ServerDrivenEvent

data class ViewRequest(
/**
* The URL to send the request to. When it starts with "/", it's relative to the BaseUrl.
Expand All @@ -38,7 +40,8 @@ data class ViewRequest(
*/
val fallback: Map<String, Any?>? = null,
/**
* The map of state ids and its values that will be used on the next page.
* The map of states and their values that will be used on the next page.
*/
val params: Map<String, Any?>? = null,
val state: Map<String, Any?>? = null,
val events: List<ServerDrivenEvent>? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,17 @@

package br.com.zup.nimbus.core.tree.dynamic.node

import br.com.zup.nimbus.core.Nimbus
import br.com.zup.nimbus.core.scope.CloneAfterInitializationError
import br.com.zup.nimbus.core.scope.DoubleInitializationError
import br.com.zup.nimbus.core.ServerDrivenState
import br.com.zup.nimbus.core.dependency.CommonDependency
import br.com.zup.nimbus.core.dependency.Dependency
import br.com.zup.nimbus.core.dependency.Dependent
import br.com.zup.nimbus.core.scope.CommonScope
import br.com.zup.nimbus.core.scope.LazilyScoped
import br.com.zup.nimbus.core.scope.Scope
import br.com.zup.nimbus.core.scope.closestScopeWithType
import br.com.zup.nimbus.core.tree.ServerDrivenNode
import br.com.zup.nimbus.core.tree.dynamic.container.NodeContainer
import br.com.zup.nimbus.core.tree.dynamic.container.PropertyContainer
Expand Down Expand Up @@ -67,9 +71,28 @@ open class DynamicNode(
hasChanged = true
}

/**
* Compute the values of states that have been provided expressions as their initial values.
*/
private fun initializeDependentStates() {
if (states?.isEmpty() != false) return
val expressionParser = closestScopeWithType<Nimbus>()?.expressionParser ?: return

states?.forEach { state ->
val value = state.get()
if (value is String && expressionParser.containsExpression(value)) {
val parsed = expressionParser.parseString(value, true)
if (parsed is LazilyScoped<*>) parsed.initialize(this)
if (parsed is Dependent) parsed.update()
state.setSilently(parsed.getValue())
}
}
}

override fun initialize(scope: Scope) {
if (parent != null) throw DoubleInitializationError()
parent = scope
initializeDependentStates()
propertyContainer?.initialize(this)
childrenContainer?.initialize(this)
propertyContainer?.addDependent(this)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import br.com.zup.nimbus.core.network.ServerDrivenHttpMethod
import br.com.zup.nimbus.core.network.ViewRequest
import br.com.zup.nimbus.core.ActionTriggeredEvent
import br.com.zup.nimbus.core.deserialization.AnyServerDrivenData
import br.com.zup.nimbus.core.deserialization.SerializationError
import br.com.zup.nimbus.core.ui.action.error.ActionExecutionError
import br.com.zup.nimbus.core.ui.action.error.ActionDeserializationError

Expand All @@ -35,10 +34,12 @@ private fun requestFromEvent(event: ActionEvent, isPushOrPresent: Boolean): View
val headers = properties.get("headers").asMapOrNull()?.mapValues { it.value.asString() }
val body = attemptJsonSerialization(properties.get("body"), event)
val fallback = properties.get("fallback").asMapOrNull()?.mapValues { it.value.asAnyOrNull() }
val params = if (isPushOrPresent) properties.get("params").asMapOrNull()?.mapValues { it.value.asAnyOrNull() }
val state = if (isPushOrPresent) properties.get("state").asMapOrNull()?.mapValues { it.value.asAnyOrNull() }
else null
val events = if (isPushOrPresent) properties.get("events").asMapOrNull()?.map { it.value.asEvent() }
else null
if (properties.hasError()) throw ActionDeserializationError(event, properties)
return ViewRequest(url, method, headers, body, fallback, params)
return ViewRequest(url, method, headers, body, fallback, state, events)
}

private fun pushOrPresent(event: ActionTriggeredEvent, isPush: Boolean) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2023 ZUP IT SERVICOS EM TECNOLOGIA E INOVACAO SA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package br.com.zup.nimbus.core.ui.action

import br.com.zup.nimbus.core.ActionTriggeredEvent
import br.com.zup.nimbus.core.deserialization.AnyServerDrivenData
import br.com.zup.nimbus.core.ui.action.error.ActionDeserializationError

internal fun triggerViewEvent(event: ActionTriggeredEvent) {
val data = AnyServerDrivenData(event.action.properties)
val nameOfEventToTrigger = data.get("event").asString()
val valueForEventToTrigger = data.get("value").asAnyOrNull()
if (data.hasError()) {
throw ActionDeserializationError(event, data)
}
val eventToTrigger = event.scope.view.events?.find { it.name == nameOfEventToTrigger }
if (eventToTrigger == null) {
event.scope.nimbus.logger.error("Can't trigger view event named \"$nameOfEventToTrigger\" because the current " +
"view has no such event.")
} else {
eventToTrigger.run(valueForEventToTrigger)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ import br.com.zup.nimbus.core.ui.action.present
import br.com.zup.nimbus.core.ui.action.push
import br.com.zup.nimbus.core.ui.action.sendRequest
import br.com.zup.nimbus.core.ui.action.setState
import br.com.zup.nimbus.core.ui.action.triggerViewEvent
import br.com.zup.nimbus.core.ui.operations.registerArrayOperations
import br.com.zup.nimbus.core.ui.operations.registerLogicOperations
import br.com.zup.nimbus.core.ui.operations.registerNumberOperations
import br.com.zup.nimbus.core.ui.operations.registerObjectOperations
import br.com.zup.nimbus.core.ui.operations.registerOtherOperations
import br.com.zup.nimbus.core.ui.operations.registerStringOperations

Expand All @@ -42,6 +44,7 @@ val coreUILibrary = UILibrary("")
.addAction("popTo") { popTo(it) }
.addAction("present") { present(it) }
.addAction("dismiss") { dismiss(it) }
.addAction("triggerViewEvent") { triggerViewEvent(it) }
.addAction("log") { log(it) }
.addAction("sendRequest") { sendRequest(it) }
.addAction("setState") { setState(it) }
Expand All @@ -56,5 +59,6 @@ val coreUILibrary = UILibrary("")
registerNumberOperations(this)
registerOtherOperations(this)
registerStringOperations(this)
registerObjectOperations(this)
this
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import br.com.zup.nimbus.core.ui.UILibrary

internal fun registerArrayOperations(library: UILibrary) {
library
.addOperation("array") { it }
.addOperation("insert") {
val arguments = AnyServerDrivenData(it)
val list = if (arguments.at(0).isList()) (arguments.at(0).value as List<*>).toMutableList()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2023 ZUP IT SERVICOS EM TECNOLOGIA E INOVACAO SA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package br.com.zup.nimbus.core.ui.operations

import br.com.zup.nimbus.core.ui.UILibrary

internal fun registerObjectOperations(library: UILibrary) {
library
.addOperation("object") {
val objectMap = mutableMapOf<String, Any?>()
for (i in it.indices step 2) {
objectMap[it.getOrNull(i).toString()] = it.getOrNull(i + 1)
}
objectMap
}
.addOperation("entries") {
val result = it.firstOrNull()?.let { map ->
if (map is Map<*, *>) map.entries.map { entry -> mapOf("key" to entry.key, "value" to entry.value) }
else null
}
result ?: emptyList<Any>()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

package br.com.zup.nimbus.core.ui.operations

import br.com.zup.nimbus.core.deserialization.AnyServerDrivenData
import br.com.zup.nimbus.core.ui.UILibrary
import br.com.zup.nimbus.core.utils.Null
import br.com.zup.nimbus.core.utils.compareTo
Expand All @@ -28,7 +27,7 @@ private fun areNumbersEqual(left: Any?, right: Any?): Boolean {
return leftNumber.compareTo(rightNumber) == 0
}

@Suppress("ComplexMethod", "LongMethod")
@Suppress("ComplexMethod")
internal fun registerOtherOperations(library: UILibrary) {
library
.addOperation("contains"){
Expand Down Expand Up @@ -85,11 +84,4 @@ internal fun registerOtherOperations(library: UILibrary) {
else -> Null.isNull(collection)
}
}
.addOperation("entries"){
val result = it.firstOrNull()?.let { map ->
if (map is Map<*, *>) map.entries.map { entry -> mapOf("key" to entry.key, "value" to entry.value) }
else null
}
result ?: emptyList<Any>()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ class ObservableNavigator(
}

override fun push(request: ViewRequest) {
val states = request.params?.map { ServerDrivenState(it.key, it.value) }
val view = ServerDrivenView(nimbus, states = states, description = request.url) { this }
val states = request.state?.map { ServerDrivenState(it.key, it.value) }
val view = ServerDrivenView(nimbus, states = states, events = request.events, description = request.url) { this }
testScope.launch {
try {
val tree = nimbus.viewClient.fetch(request)
Expand Down
Loading