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

Add wizard #30

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package de.cotto.bitbook.cli;

public interface PromptChangeListener {
void changePrompt(String newState);

void changePromptToDefault();
}
44 changes: 44 additions & 0 deletions cli/base/src/main/java/de/cotto/bitbook/cli/TerminalSupport.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package de.cotto.bitbook.cli;

import org.apache.commons.lang3.StringUtils;
import org.jline.reader.LineReader;
import org.jline.terminal.Terminal;
import org.springframework.stereotype.Component;

import java.io.PrintWriter;
import java.util.Optional;

@Component
public class TerminalSupport {
private final LineReader lineReader;
private final PromptChangeListener promptChangeListener;
private final PrintWriter writer;

public TerminalSupport(Terminal terminal, LineReader lineReader, PromptChangeListener promptChangeListener) {
this.lineReader = lineReader;
this.promptChangeListener = promptChangeListener;
writer = terminal.writer();
}

public void write(String text) {
writer.println(text);
writer.flush();
}

public Optional<String> request(String prompt) {
String answer = lineReader.readLine(prompt + " ");
if (StringUtils.isBlank(answer)) {
return Optional.empty();
} else {
return Optional.of(answer);
}
}

public void changePrompt(String newPrompt) {
promptChangeListener.changePrompt(newPrompt);
}

public void changePromptToDefault() {
promptChangeListener.changePromptToDefault();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package de.cotto.bitbook.cli;

import org.jline.reader.LineReader;
import org.jline.terminal.Terminal;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;

import java.io.PrintWriter;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class TerminalSupportTest {
private TerminalSupport terminalSupport;

@Mock
private Terminal terminal;

@Mock
private LineReader lineReader;

@Mock
private PromptChangeListener promptChangeListener;

@Mock
private PrintWriter writer;

@BeforeEach
void setUp() {
when(terminal.writer()).thenReturn(writer);
terminalSupport = new TerminalSupport(terminal, lineReader, promptChangeListener);
}

@Test
void write() {
terminalSupport.write("x");
InOrder inOrder = Mockito.inOrder(writer);
inOrder.verify(writer).println("x");
inOrder.verify(writer).flush();
}

@Test
void request_prompt() {
terminalSupport.request("?");
verify(lineReader).readLine("? ");
}

@Test
void request_empty() {
when(lineReader.readLine(anyString())).thenReturn("");
assertThat(terminalSupport.request("?")).isEmpty();
}

@Test
void request_blank() {
when(lineReader.readLine(anyString())).thenReturn("\t \r\n");
assertThat(terminalSupport.request("?")).isEmpty();
}

@Test
void request() {
when(lineReader.readLine(anyString())).thenReturn("hello");
assertThat(terminalSupport.request("?")).contains("hello");
}

@Test
void changePrompt() {
terminalSupport.changePrompt("x");
verify(promptChangeListener).changePrompt("x");
}

@Test
void changePromptToDefault() {
terminalSupport.changePromptToDefault();
verify(promptChangeListener).changePromptToDefault();
}
}
1 change: 1 addition & 0 deletions cli/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ dependencies {
implementation project(':cli:ownership')
implementation project(':cli:lnd')
implementation project(':cli:base')
implementation project(':cli:wizard')
implementation 'org.flywaydb:flyway-core'
testImplementation testFixtures(project(':backend:transaction'))
testImplementation testFixtures(project(':backend:transaction:models'))
Expand Down
16 changes: 14 additions & 2 deletions cli/src/main/java/de/cotto/bitbook/cli/CustomPromptProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,27 @@
import org.springframework.stereotype.Component;

@Component
public class CustomPromptProvider implements PromptProvider {
public class CustomPromptProvider implements PromptProvider, PromptChangeListener {
private static final String DEFAULT_PROMPT = "BitBook$ ";

private String prompt = DEFAULT_PROMPT;

public CustomPromptProvider() {
// default constructor
}

@Override
public AttributedString getPrompt() {
return new AttributedString("BitBook$ ", AttributedStyle.DEFAULT.foreground(AttributedStyle.YELLOW));
return new AttributedString(prompt, AttributedStyle.DEFAULT.foreground(AttributedStyle.YELLOW));
}

@Override
public void changePrompt(String newPrompt) {
prompt = newPrompt;
}

@Override
public void changePromptToDefault() {
changePrompt(DEFAULT_PROMPT);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,20 @@ class CustomPromptProviderTest {
private final CustomPromptProvider customPromptProvider = new CustomPromptProvider();

@Test
void customPrompt_text() {
void default_prompt() {
assertThat(customPromptProvider.getPrompt()).hasToString("BitBook$ ");
}

@Test
void changePrompt() {
customPromptProvider.changePrompt("foo");
assertThat(customPromptProvider.getPrompt()).hasToString("foo");
}

@Test
void changePromptToDefault() {
customPromptProvider.changePrompt("foo");
customPromptProvider.changePromptToDefault();
assertThat(customPromptProvider.getPrompt()).hasToString("BitBook$ ");
}
}
13 changes: 13 additions & 0 deletions cli/wizard/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
plugins {
id 'bitbook.java-library-conventions'
}

dependencies {
implementation project(':cli:base')
implementation project(':wizard')
testImplementation testFixtures(project(':backend:transaction:models'))
}

jar {
archivesBaseName = 'wizard-cli'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package de.cotto.bitbook.wizard.cli;

import de.cotto.bitbook.cli.PromptChangeListener;
import de.cotto.bitbook.wizard.WizardService;
import org.springframework.shell.Availability;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;

@ShellComponent
public class WizardCommands {
private final WizardService wizardService;
private final PromptChangeListener promptChangeListener;

public WizardCommands(WizardService wizardService, PromptChangeListener promptChangeListener) {
this.wizardService = wizardService;
this.promptChangeListener = promptChangeListener;
}

@ShellMethod("Start the wizard which helps you complete the ownership information")
public void wizard() {
wizardService.enableWizard();
promptChangeListener.changePrompt("wizard$ ");
}

@ShellMethod("Exit the wizard")
public void exitWizard() {
wizardService.disableWizard();
promptChangeListener.changePromptToDefault();
}

public Availability wizardAvailability() {
if (wizardService.isEnabled()) {
return Availability.unavailable("wizard is active");
}
return Availability.available();
}

public Availability exitWizardAvailability() {
if (wizardService.isEnabled()) {
return Availability.available();
}
return Availability.unavailable("wizard is not active");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package de.cotto.bitbook.wizard.cli;

import de.cotto.bitbook.cli.PromptChangeListener;
import de.cotto.bitbook.wizard.WizardService;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class WizardCommandsTest {
@InjectMocks
private WizardCommands wizardCommands;

@Mock
private WizardService wizardService;

@Mock
private PromptChangeListener promptChangeListener;

@Nested
class WizardDisabled {
@Test
void enables_wizard() {
wizardCommands.wizard();
verify(wizardService).enableWizard();
}

@Test
void exit_wizard_command_initially_not_available() {
assertThat(wizardCommands.exitWizardAvailability().isAvailable()).isEqualTo(false);
assertThat(wizardCommands.exitWizardAvailability().getReason()).isEqualTo("wizard is not active");
}

@Test
void wizard_command_initially_available() {
assertThat(wizardCommands.wizardAvailability().isAvailable()).isEqualTo(true);
}
}

@Nested
class WizardEnabled {
@Test
void changes_prompt() {
wizardCommands.wizard();
verify(promptChangeListener).changePrompt("wizard$ ");
}

@Test
void exit_wizard_command_available() {
when(wizardService.isEnabled()).thenReturn(true);
assertThat(wizardCommands.exitWizardAvailability().isAvailable()).isEqualTo(true);
}

@Test
void exit_wizard_changes_prompt_to_default() {
wizardCommands.exitWizard();
verify(promptChangeListener).changePromptToDefault();
}

@Test
void exit_wizard_notifies_service() {
wizardCommands.exitWizard();
verify(wizardService).disableWizard();
}

@Test
void start_wizard_command_not_available() {
when(wizardService.isEnabled()).thenReturn(true);
assertThat(wizardCommands.wizardAvailability().isAvailable()).isEqualTo(false);
assertThat(wizardCommands.wizardAvailability().getReason()).isEqualTo("wizard is active");
}
}
}
6 changes: 1 addition & 5 deletions documentation/ideas.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,4 @@
It may be helpful to be able to answer questions like "How many Bitcoin Cash do I have, and where?"
for forks like Bitcoin Cash, Bitcoin Gold, ... that share parts of their blockchain with Bitcoin.
This can be done by making use of the block height information (before/after a fork?), and making the
code fork-aware.

## Interactive CLI
Running `get-neighbour-transactions` and the other commands repeatedly is quite frustrating.
BitBook should offer an interactive mode that guides the user to completion.
code fork-aware.
2 changes: 2 additions & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ include 'cli'
include 'cli:base'
include 'cli:lnd'
include 'cli:ownership'
include 'cli:wizard'
include 'backend'
include 'backend:price'
include 'backend:blockheight'
Expand All @@ -24,3 +25,4 @@ include 'backend:request'
include 'backend:request:models'
include 'lnd'
include 'ownership'
include 'wizard'
8 changes: 8 additions & 0 deletions wizard/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
plugins {
id 'bitbook.java-library-conventions'
}

dependencies {
implementation project(':ownership')
testImplementation testFixtures(project(':backend:transaction:models'))
}
Loading