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

UserViewState, UserModel, AccountModel #3 #19

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions account/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ buildscript {

dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"

api 'io.reactivex.rxjava2:rxjava:2.1.9'

testCompile 'junit:junit:4.12'
testImplementation 'org.amshove.kluent:kluent:1.34'
}

sourceCompatibility = '1.7'
Expand Down
45 changes: 45 additions & 0 deletions account/src/main/java/io/github/novacrypto/account/AccountModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* NovaWallet, Cryptocurrency Wallet for Android
* Copyright (C) 2018 Alan Evans, NovaCrypto
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* Original source: https://github.com/NovaCrypto/NovaWallet
* You can contact the authors via github issues.
*/

package io.github.novacrypto.account

import io.reactivex.Observable
import java.util.*

data class AccountId(val uuid: UUID = UUID.randomUUID())

data class AccountModel(
val id: AccountId = AccountId(),
val name: String = ""
)

fun <T : AccountModelIntent> Observable<T>.toAccountModelStream(): Observable<AccountModel> {
return this.scan(AccountModel(),
{ model: AccountModel, intent: AccountModelIntent ->
accountModelReducer(intent, model)
})
}

fun accountModelReducer(intent: AccountModelIntent, model: AccountModel): AccountModel {
return when (intent) {
is AccountModelIntent.Rename -> model.copy(name = intent.name)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* NovaWallet, Cryptocurrency Wallet for Android
* Copyright (C) 2018 Alan Evans, NovaCrypto
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* Original source: https://github.com/NovaCrypto/NovaWallet
* You can contact the authors via github issues.
*/

package io.github.novacrypto.account

sealed class AccountModelIntent {
class Rename(val name: String) : AccountModelIntent()
}

fun AccountModel.toAddIntent() =
UserModelIntent.AddAccountIntent(this).forward()

fun AccountModel.toInsertIntent(index: Int) =
UserModelIntent.AddAccountIntent(this, index).forward()

fun AccountModel.toRemoveIntent() =
UserModelIntent.RemoveAccountIntent(this.id).forward()

fun AccountModel.toRenameIntent(newName: String) =
renameAccount(newName).toUserModelIntent(this).forward()

fun renameAccount(newName: String) =
AccountModelIntent.Rename(newName)

fun AccountModelIntent.toUserModelIntent(account: AccountModel) =
UserModelIntent.ForwardAccountModelIntent(account.id, this)
28 changes: 28 additions & 0 deletions account/src/main/java/io/github/novacrypto/account/ErrorModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* NovaWallet, Cryptocurrency Wallet for Android
* Copyright (C) 2018 Alan Evans, NovaCrypto
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* Original source: https://github.com/NovaCrypto/NovaWallet
* You can contact the authors via github issues.
*/

package io.github.novacrypto.account

data class ErrorModel(
val errorMessage: String
)

class UserReportableException(message: String) : Exception(message)
37 changes: 37 additions & 0 deletions account/src/main/java/io/github/novacrypto/account/UserModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* NovaWallet, Cryptocurrency Wallet for Android
* Copyright (C) 2018 Alan Evans, NovaCrypto
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* Original source: https://github.com/NovaCrypto/NovaWallet
* You can contact the authors via github issues.
*/

package io.github.novacrypto.account

data class UserModel(
val accounts: List<AccountModel> = emptyList()
)

sealed class UserModelIntent {
class AddAccountIntent(val account: AccountModel, val index: Int? = null) : UserModelIntent()
class RemoveAccountIntent(val accountId: AccountId) : UserModelIntent()
class ForwardAccountModelIntent(
val accountId: AccountId,
val intent: AccountModelIntent
) : UserModelIntent()
}

fun UserModelIntent.forward() = UserModelViewStateIntent.ForwardUserModelIntent(this)
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* NovaWallet, Cryptocurrency Wallet for Android
* Copyright (C) 2018 Alan Evans, NovaCrypto
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* Original source: https://github.com/NovaCrypto/NovaWallet
* You can contact the authors via github issues.
*/

package io.github.novacrypto.account

import io.reactivex.Observable

data class UserModelViewState(
val userModel: UserModel = UserModel(),
val error: ErrorModel? = null,
val undoModel: UserModelViewStateUndoModel? = null
)

data class UserModelViewStateUndoModel(
val message: String,
val undoIntent: UserModelViewStateIntent
)

fun <T : UserModelViewStateIntent> Observable<T>.toUserModelStream(): Observable<UserModelViewState> {
return this.scan(UserModelViewState(),
{ model: UserModelViewState, intent: UserModelViewStateIntent ->
model.transformWithIntent(intent)
})
}

private fun UserModelViewState.transformWithIntent(intent: UserModelViewStateIntent): UserModelViewState {
return when (intent) {
is UserModelViewStateIntent.DismissError -> copy(error = null)
is UserModelViewStateIntent.ForwardUserModelIntent ->
try {
val (newModel, undoModel) = userModel.transformWithIntent(intent.userModelIntent)
copy(userModel = newModel, undoModel = undoModel)
} catch (e: UserReportableException) {
copy(error = ErrorModel(e.message ?: ""))
}
is UserModelViewStateIntent.Undo ->
undoModel?.undoIntent?.let {
transformWithIntent(it
)
} ?: this
}
}

private fun UserModel.transformWithIntent(intent: UserModelIntent) =
when (intent) {
is UserModelIntent.AddAccountIntent -> {
copy(accounts = if (intent.index != null) {
accounts.insert(intent.account, intent.index)
} else {
accounts + intent.account
}).withNoUndo()
}
is UserModelIntent.RemoveAccountIntent -> {
withAccountAndIndexForId(intent.accountId) { account, index ->
Pair(copy(accounts = accounts - account),
UserModelViewStateUndoModel("Removed account '${account.name}'",
account.toInsertIntent(index)))
}
}
is UserModelIntent.ForwardAccountModelIntent -> {
withAccountAndIndexForId(intent.accountId) { account, index ->
val newAccounts = accounts.replace(index, accountModelReducer(intent.intent, account))
copy(accounts = newAccounts).withNoUndo()
}
}
}

private fun <T> UserModel.withAccountAndIndexForId(accountId: AccountId, transform: (account: AccountModel, index: Int) -> T): T {
val accountIdx = accounts.indexOfFirst { it.id == accountId }.also {
if (it == -1) throw cannotFind(accountId)
}
val existingAccount = accounts[accountIdx]
return transform(existingAccount, accountIdx)
}

private fun UserModel.withNoUndo() = Pair(this, null)

private inline fun <E> List<E>.copyAndModify(modify: MutableList<E>.() -> Unit): List<E> =
toMutableList().apply(modify)

private fun <E> List<E>.insert(newItem: E, index: Int) =
copyAndModify {
this.add(index, newItem)
}

private fun <E> List<E>.replace(index: Int, replacement: E) =
copyAndModify {
this[index] = replacement
}

private fun cannotFind(accountId: AccountId) =
UserReportableException("Cannot find account $accountId")
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* NovaWallet, Cryptocurrency Wallet for Android
* Copyright (C) 2018 Alan Evans, NovaCrypto
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* Original source: https://github.com/NovaCrypto/NovaWallet
* You can contact the authors via github issues.
*/

package io.github.novacrypto.account

sealed class UserModelViewStateIntent {
class DismissError : UserModelViewStateIntent()
class Undo : UserModelViewStateIntent()
class ForwardUserModelIntent(
val userModelIntent: UserModelIntent
) : UserModelViewStateIntent()
}

fun dismissError() = UserModelViewStateIntent.DismissError()
67 changes: 67 additions & 0 deletions account/src/test/java/io/github/novacrypto/AccountModelTests.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* NovaWallet, Cryptocurrency Wallet for Android
* Copyright (C) 2018 Alan Evans, NovaCrypto
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* Original source: https://github.com/NovaCrypto/NovaWallet
* You can contact the authors via github issues.
*/

package io.github.novacrypto

import io.github.novacrypto.account.AccountModel
import io.github.novacrypto.account.renameAccount
import io.github.novacrypto.account.toAccountModelStream
import io.reactivex.Observable
import org.amshove.kluent.`should be`
import org.amshove.kluent.`should equal`
import org.amshove.kluent.`should not equal`
import org.junit.Test

class AccountModelTests {

@Test
fun `new account gets new id`() {
AccountModel().id `should not equal` AccountModel().id
}

}

class AccountModelRenameIntentTests {

@Test
fun `user can rename account`() {
Observable.just(
renameAccount("My Account")
).toAccountModelStream()
.assertWithLastElement {
it.name `should equal` "My Account"
}
}

@Test
fun `user can rename account twice`() {
Observable.just(
renameAccount("My Account"),
renameAccount("My Account 2")
).toAccountModelStream()
.assertWithLastElement {
it.name `should equal` "My Account 2"
}
.assertWithLastTwoElements { penultimate, ultimate ->
penultimate.id `should be` ultimate.id
}
}
}
Loading