Skip to content

Commit

Permalink
#10 Only allow depositing after logging in
Browse files Browse the repository at this point in the history
Refactoring: Introducing a CommandProcessor
  • Loading branch information
mustofa-id committed Jan 9, 2020
1 parent 951d66a commit 4c250a3
Show file tree
Hide file tree
Showing 12 changed files with 164 additions and 45 deletions.
Expand Up @@ -4,8 +4,8 @@ import dagger.Component
import id.mustofa.atm.module.HelloWorldModule
import id.mustofa.atm.module.LoginCommandModule
import id.mustofa.atm.module.SystemOutModule
import id.mustofa.atm.module.UserCommandsModule
import id.mustofa.atm.router.CommandRouter
import id.mustofa.atm.router.CommandProcessor
import id.mustofa.atm.router.UserCommandsRouter
import javax.inject.Singleton

/**
Expand All @@ -14,18 +14,21 @@ import javax.inject.Singleton
* But instead of us writing the implementation of CommandRouterFactory,
* we can annotate it with @Component to have Dagger generate an implementation
* for us: DaggerCommandRouterFactory.
*
* refactor: CommandRouterFactory to CommandProcessorFactory
* CommandProcessorFactory is CommandProcessor factory that contains a stack of CommandRouters.
*/
@Singleton
@Component(
modules = [
HelloWorldModule::class,
LoginCommandModule::class,
UserCommandsModule::class,
UserCommandsRouter.InstallationModule::class,
SystemOutModule::class
]
)
interface CommandRouterFactory {
interface CommandProcessorFactory {

// CommandRouter constructor should annotated with @Inject
fun route(): CommandRouter
// CommandProcessor constructor should annotated with @Inject
fun processor(): CommandProcessor
}
8 changes: 4 additions & 4 deletions src/main/kotlin/id/mustofa/atm/main.kt
Expand Up @@ -2,21 +2,21 @@

package id.mustofa.atm

import id.mustofa.atm.factory.DaggerCommandRouterFactory
import id.mustofa.atm.factory.DaggerCommandProcessorFactory
import java.util.*

fun main() {

val scanner = Scanner(System.`in`)

val commandRouterFactory = DaggerCommandRouterFactory.create()
val commandRouter = commandRouterFactory.route()
val commandProcessorFactory = DaggerCommandProcessorFactory.create()
val commandProcessor = commandProcessorFactory.processor()

while (scanner.hasNextLine()) {
val input = scanner.nextLine()
if (input == "/exit") {
break
}
commandRouter.route(input)
commandProcessor.process(input)
}
}
32 changes: 32 additions & 0 deletions src/main/kotlin/id/mustofa/atm/model/BigDecimalCommand.kt
@@ -0,0 +1,32 @@
package id.mustofa.atm.model

import id.mustofa.atm.model.base.Command
import id.mustofa.atm.model.base.Outputter
import id.mustofa.atm.model.base.SingleArgCommand
import id.mustofa.atm.util.handled
import java.math.BigDecimal

abstract class BigDecimalCommand constructor(
private val outputter: Outputter
) : SingleArgCommand() {

override fun handleArg(arg: String): Command.Result {
val amount = tryParse(arg)
when {
amount == null -> outputter.output("$arg is not a valid number")
amount.signum() <= 0 -> outputter.output("amount must be positive")
else -> handleAmount(amount)
}
return Command.Result.handled
}

private fun tryParse(arg: String): BigDecimal? {
return try {
BigDecimal(arg)
} catch (e: NumberFormatException) {
null
}
}

protected abstract fun handleAmount(amount: BigDecimal)
}
16 changes: 4 additions & 12 deletions src/main/kotlin/id/mustofa/atm/model/DepositCommand.kt
@@ -1,27 +1,19 @@
package id.mustofa.atm.model

import id.mustofa.atm.model.base.Command
import id.mustofa.atm.model.base.Command.Status
import id.mustofa.atm.model.base.Outputter
import id.mustofa.atm.router.Database
import java.math.BigDecimal
import javax.inject.Inject

class DepositCommand @Inject constructor(
private val database: Database,
private val account: Database.Account,
private val outputter: Outputter
) : Command {
) : BigDecimalCommand(outputter) {

override fun key() = "deposit"

override fun handleInput(input: List<String>): Status {
if (input.size != 2) {
return Status.INVALID
}

val account = database.getAccount(input[0])
account.deposit(BigDecimal(input[1]))
override fun handleAmount(amount: BigDecimal) {
account.deposit(amount)
outputter.output("${account.username} now has: ${account.balance}")
return Status.HANDLED
}
}
10 changes: 6 additions & 4 deletions src/main/kotlin/id/mustofa/atm/model/HelloWorldCommand.kt
@@ -1,8 +1,10 @@
package id.mustofa.atm.model

import id.mustofa.atm.model.base.Command
import id.mustofa.atm.model.base.Command.Status
import id.mustofa.atm.model.base.Command.Result
import id.mustofa.atm.model.base.Outputter
import id.mustofa.atm.util.handled
import id.mustofa.atm.util.invalid
import javax.inject.Inject

class HelloWorldCommand @Inject constructor(
Expand All @@ -11,12 +13,12 @@ class HelloWorldCommand @Inject constructor(

override fun key() = "hello"

override fun handleInput(input: List<String>): Status {
override fun handleInput(input: List<String>): Result {
return if (input.isEmpty()) {
Status.INVALID
Result.invalid
} else {
outputter.output("world!")
Status.HANDLED
Result.handled
}
}
}
11 changes: 7 additions & 4 deletions src/main/kotlin/id/mustofa/atm/model/LoginCommand.kt
@@ -1,21 +1,24 @@
package id.mustofa.atm.model

import id.mustofa.atm.model.base.Command.Status
import id.mustofa.atm.model.base.Command.Result
import id.mustofa.atm.model.base.Outputter
import id.mustofa.atm.model.base.SingleArgCommand
import id.mustofa.atm.router.Database
import id.mustofa.atm.router.UserCommandsRouter
import id.mustofa.atm.util.enterNestedCommandSet
import javax.inject.Inject

class LoginCommand @Inject constructor(
private val database: Database,
private val outputter: Outputter
private val outputter: Outputter,
private val userCommandsRouterFactory: UserCommandsRouter.Factory
) : SingleArgCommand() {

override fun key() = "login"

override fun handleArg(arg: String): Status {
override fun handleArg(arg: String): Result {
val account = database.getAccount(arg)
outputter.output("${account.username} is logged in with balance: ${account.balance}.")
return Status.HANDLED
return Result.enterNestedCommandSet(userCommandsRouterFactory.create(account).router())
}
}
15 changes: 13 additions & 2 deletions src/main/kotlin/id/mustofa/atm/model/base/Command.kt
@@ -1,13 +1,24 @@
package id.mustofa.atm.model.base

import id.mustofa.atm.router.CommandRouter
import java.util.*

interface Command {

fun key(): String

fun handleInput(input: List<String>): Status
fun handleInput(input: List<String>): Result

class Result(
val status: Status,
val nestedCommandRouter: Optional<CommandRouter>
) {
companion object
}

enum class Status {
INVALID,
HANDLED
HANDLED,
INPUT_COMPLETED
}
}
9 changes: 5 additions & 4 deletions src/main/kotlin/id/mustofa/atm/model/base/SingleArgCommand.kt
@@ -1,12 +1,13 @@
package id.mustofa.atm.model.base

import id.mustofa.atm.model.base.Command.Status
import id.mustofa.atm.model.base.Command.Result
import id.mustofa.atm.util.invalid

abstract class SingleArgCommand : Command {

final override fun handleInput(input: List<String>): Status {
return if (input.size == 1) handleArg(input[0]) else Status.INVALID
final override fun handleInput(input: List<String>): Result {
return if (input.size == 1) handleArg(input[0]) else Result.invalid
}

abstract fun handleArg(arg: String): Status
abstract fun handleArg(arg: String): Result
}
30 changes: 30 additions & 0 deletions src/main/kotlin/id/mustofa/atm/router/CommandProcessor.kt
@@ -0,0 +1,30 @@
package id.mustofa.atm.router

import id.mustofa.atm.model.base.Command.Status
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class CommandProcessor @Inject constructor(
firstCommandRouter: CommandRouter
) {

private val commandRouterStack: Deque<CommandRouter> = ArrayDeque()

init {
commandRouterStack.push(firstCommandRouter)
}

fun process(input: String): Status {
val result = commandRouterStack.peek().route(input)
if (result.status == Status.INPUT_COMPLETED) {
commandRouterStack.pop()
return if (commandRouterStack.isEmpty())
Status.INPUT_COMPLETED else Status.HANDLED
}

result.nestedCommandRouter.ifPresent(commandRouterStack::push)
return result.status
}
}
22 changes: 13 additions & 9 deletions src/main/kotlin/id/mustofa/atm/router/CommandRouter.kt
@@ -1,7 +1,10 @@
package id.mustofa.atm.router

import id.mustofa.atm.model.base.Command
import id.mustofa.atm.model.base.Command.Result
import id.mustofa.atm.model.base.Command.Status
import id.mustofa.atm.model.base.Outputter
import id.mustofa.atm.util.invalid
import javax.inject.Inject

/**
Expand All @@ -12,10 +15,11 @@ import javax.inject.Inject
* CommandRouter, Dagger should call new CommandRouter().
*/
class CommandRouter @Inject constructor(
private val commands: Map<String, @JvmSuppressWildcards Command>
private val commands: Map<String, @JvmSuppressWildcards Command>,
private val outputter: Outputter
) {

fun route(input: String): Status {
fun route(input: String): Result {
val inputs = split(input)
if (inputs.isEmpty()) {
return invalidCommand(input)
Expand All @@ -24,16 +28,16 @@ class CommandRouter @Inject constructor(
val key = inputs.first()
val command = commands[key] ?: return invalidCommand(input)

val status = command.handleInput(inputs.subList(1, inputs.size))
if (status == Status.INVALID) {
println("$key: Invalid arguments")
val result = command.handleInput(inputs.subList(1, inputs.size))
if (result.status == Status.INVALID) {
outputter.output("$key: Invalid arguments")
}
return status
return result
}

private fun invalidCommand(input: String): Status {
println("couldn't understand $input. please try again.")
return Status.INVALID
private fun invalidCommand(input: String): Result {
outputter.output("Couldn't understand $input. Please try again.")
return Result.invalid
}

private fun split(input: String): List<String> {
Expand Down
22 changes: 22 additions & 0 deletions src/main/kotlin/id/mustofa/atm/router/UserCommandsRouter.kt
@@ -0,0 +1,22 @@
package id.mustofa.atm.router

import dagger.BindsInstance
import dagger.Module
import dagger.Subcomponent
import id.mustofa.atm.module.UserCommandsModule
import id.mustofa.atm.router.Database.Account

@Subcomponent(modules = [UserCommandsModule::class])
interface UserCommandsRouter {

fun router(): CommandRouter

@Subcomponent.Factory
interface Factory {

fun create(@BindsInstance account: Account): UserCommandsRouter
}

@Module(subcomponents = [UserCommandsRouter::class])
interface InstallationModule
}
19 changes: 19 additions & 0 deletions src/main/kotlin/id/mustofa/atm/util/ext.kt
@@ -0,0 +1,19 @@
package id.mustofa.atm.util

import id.mustofa.atm.model.base.Command
import id.mustofa.atm.model.base.Command.Status
import id.mustofa.atm.router.CommandRouter
import java.util.*

val Command.Result.Companion.invalid
get() = Command.Result(Status.INVALID, Optional.empty())

val Command.Result.Companion.handled
get() = Command.Result(Status.HANDLED, Optional.empty())

val Command.Result.Companion.inputCompleted
get() = Command.Result(Status.INPUT_COMPLETED, Optional.empty())

fun Command.Result.Companion.enterNestedCommandSet(nestedCommandRouter: CommandRouter): Command.Result {
return Command.Result(Status.HANDLED, Optional.of(nestedCommandRouter))
}

0 comments on commit 4c250a3

Please sign in to comment.