Skip to content

Commit

Permalink
UserViewState, UserModel, AccountModel #3 With Undo for remove
Browse files Browse the repository at this point in the history
  • Loading branch information
westonal committed Feb 12, 2018
1 parent d0a97f6 commit 5b21569
Show file tree
Hide file tree
Showing 10 changed files with 775 additions and 0 deletions.
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

0 comments on commit 5b21569

Please sign in to comment.