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

Refactor parser #39

Merged
merged 16 commits into from Oct 2, 2020
Merged

Conversation

JoeyChenSmart
Copy link

@JoeyChenSmart JoeyChenSmart commented Sep 27, 2020

Closes #33

Much cleaner/usable/pleasant API. Do let me know if anything here is unclear.

Removed many tests that were irrelevant (addressbook-specific), replaced them with a short comment summary of what the test was trying to achieve.

New API

Brief idea

Ideally, the new Commands should be thought of as 'generalized functions': You only need to specify as private variables
what parameters the Command takes in, and how the Command should use these parameters to produce changes in the Model and output a CommandResult.

Example

public class AddCommand extends Command {
    public static final String MESSAGE_SUCCESS = "New person added: %1$s";
    public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book";

    private final Parameter<Name> nameParameter = this.addParameter(
        "name",                           // The name for this parameter
        "n",                              // The flag for this parameter, i.e. n for '-n' in the command 'add -n John Doe ...'
        "Name of person to add",          // Short description of the value of this parameter. Used in auto-generating help
        "John Doe",                       // Example of the value of this parameter. Used in auto-generating help
        ParserUtil::parseName             // A function that converts a String to a Name object for use later
    );
    private final Parameter<Phone> phoneParameter = <REDACTED>
    private final Parameter<Email> emailParameter = <REDACTED>

    @Override
    public CommandResult execute(Model model) throws CommandException {
        requireNonNull(model);
        // rewriting this class as an example, address and tags not implemented.
        Name newName = nameParameter.consume(); // Parameter#consume() will return the Name parsed using ParserUtil::parseName
        Phone newPhone = phoneParameter.consume();
        Person toAdd = new Person(newName, newPhone, emailParameter.consume(),
            new Address("dummy value"), new HashSet<>());

        if (model.hasPerson(toAdd)) {
            throw new CommandException(MESSAGE_DUPLICATE_PERSON);
        }

        model.addPerson(toAdd);
        return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd));
    }
}

In the code, first we declare some Parameters. These parameters will be used in the parsing. Their parsed value can be
retrieved later in the execute function using the Parameter<T>#consume() function. This function will return the value
as a type T object. Note how each declaration of Parameter also serves as immediate documentation for the command.

Each parameter will take in every character as a string until the next parameter is encountered. E.g. in
add -n John Doe Donald -p 1234
the -n parameter will recieve "John Doe Donald"

Note that there is no constructor. All variables to be used should either be globals or supplied as Parameters.

The execute function is the same as before, and will contain the main logic to be executed. The only difference is that we now
access variables using Parameter<T>#consume() instead of accessing variables stored by the constructor.

Finally, the command will be 'hooked' into the McGymmyParser

private void addDefaultCommands() {
        this.addCommand("add", AddCommand::new);
        this.addCommand("edit", EditCommand::new);
        this.addCommand("delete", DeleteCommand::new);
        ... <REDACTED>
    }

The first argument "add" is the 'command word' to invoke the command, and the second argument is the command's constructor. Note that this does not have to be a constructor, just a function that takes in no arguments and returns a new Command.

There are more examples of Commands you can find in the source code. In particular the changed classes are the EditCommand, DeleteCommand and FindCommand.

Just a few things left to note:

Optional Parameters

Some parameters may be declared as optional, e.g.

// from EditCommand.java
private final OptionalParameter<Name> nameParameter = this.addOptionalParameter(
        "name",
        "n",
        "Name of person to add",
        "John Doe",
        ParserUtil::parseName
    );

The arguments are exactly the same as its Parameter counterpart. These parameters may be omitted by the user when entering the command. The difference is that instead of using Parameter<T>#consume() we have a new function OptionalParameter<T>#getValue() that returns a Optional<T> value. If the user supplied this parameter, it will be Optional.of the parsed value, otherwise it will be Optional.empty.

Unnamed Parameters

Some parameters do not have flags, e.g. 1 in delete 1.
To create a parameter that does this, simply leave the flag as an empty string, i.e.

private final Parameter<Index> indexParameter = this.addParameter(
        "index",
        "",
        "index number used in the displayed person list.",
        "2",
        ParserUtil::parseIndex
    );

The parameter will work exactly the same way as before, and you can retrieve its value normally using Parameter<T>#consume().

Can we declare multiple of the same parameters? (e.g. 2 unnamed parameters)

Nope, if you try to declare multiple of the same parameters there will be an assertion error somewhere.

Technically you can work around this, by declaring a new class that contains the two parameters, e.g.
the command:
takeTwoIntegers 1 2
may have a parameter
private Parameter<IntPair> param = this.addParameter(..., IntPair::parse)

and the IntPair class is as follows:

class IntPair {
  public int a;
  public int b;
  public parse(String str) {
    IntPair result = new IntPair();
    result.a = ...
  }

Don't think any of our commands will involve these kinds of parameters, but if they do we may have to either redesign the command or the parser. (We'll solve that problem when it happens)

Does the position of the unnamed parameter matter?

For example, is there a difference between
edit 1 -n John Doe
and
edit -n John Doe 1

Yes, recall that each parameter will take in all the subsequent characters until the next parameter as a string.

For the first example the 1 will be considered as input to the -n parameter, and the unnamed parameter will have no input (so there will be an error).

Motivation

Here I will try to detail some concrete benefits gained from this change.

Most of the changes here are inspired by Django. For example creating forms in Django is very declarative and I think that is a better way to create code.

Remove the need for classes like EditPersonDescriptor

There are many things imo that are wrong philosophically with the EditPersonDescriptor class, but I'll limit what
I say here to objective improvements from removing it.

  1. Having less code will make the codebase easier to understand and speed up development.
  2. Creating an extra class like this for no reason will give us more surface area for bugs to fester. Writing tests
    for these classes also contribute significantly to the codebase (see point 1)
  3. The class in is just very unintuitive (why do I have to create a new object just to edit an existing object that is very simple?).

The Parser seemed like it was doing most of the work in the old architecture.

E.g. the AddCommandParser was doing most of the work creating the Person object, and the AddCommand
simply put the new object in the model. Granted, this technically fits the definition of a 'parser' but now the
idea is that the parser should create objects at such a high level of abstraction, and whatever work that is left
for the Command. Also, now that the parser produces objects at a high level of abstraction, we have to do all
sorts of OOP gymnastics (like creating an EditPersonDescriptor) to get simple jobs done.

Using a library reduces the amount we need to implement/test. Also gives us features for free.

The library also helps us automatically generate USAGE strings for each commands. Isn't that just great?

Parsers are now 'automatically generated'

The logic of parsing is abstracted to creating a Parameter. Parsing each parameter is also very modular,
as we only need a String -> T function to parse each parameter.

Also now that we don't need to write individual parsers, we have less things to test.

But won't this make the Parser and Command tightly coupled?

In our use case they're inherently tightly coupled, so separating them will bring more harm than good.

Either way they were already quite tightly coupled in the first place.

Internals/Implementation

Here I will give a brief description of how this works internally. Will add more details if needed.

Each Parameter is simply a container, which can hold a T typed variable, and a function to convert a String to a T.

Command's this.addParameter is a protected function that creates the Parameter object and stores it in a ParameterSet.

Parsing is a 2-step process.

  1. The parser reads in the first word in the user's raw input string, and selects the creates a new instance of the corresponding Command.
  2. The parser pulls out the Command's ParameterSet and gives passes the raw input parameters as strings to each Parameter. Each parameter will then store the T type variable produced from the converter function. These parameters are now ready to use in the Command's execute function. The parser returns the ready-to-use Command.

Enhancements

A few possible enhancements that can be implemented in the future

1. Implement CommandToFunction helper method

Allow a Command to be executed in another command as a void function.
e.g. in EditCommand we may want to run AddCommand.asFunction(model, name, phone, email, ...) to add the edited person.

Sounds useful, not sure if it will be in practice

2. Create Parameter#toOptionalParameter methods

Create a method to easily convert a Parameter into an OptionalParameter and vice versa

Added skeleton for parameter classes, and some convenience function
in command.java. Also copied over functions from iP and added some
basic functionality.

TODO: JavaDoc
TODO: implement parser
TODO: replace old commands
TODO: add/change tests
Add exceptions and tests for parameter related classes.
mainly NPE. Code works now
Deleted most of the old parser code and the related tests. Did not
delete all the test files, but instead replaced them with a todo
containing what was being tested before.
also deleted many irrelevant tests.
@JoeyChenSmart JoeyChenSmart added the type.Enhancement New feature or request (!DELETE ME IF DASHBOARD IS NOT UPDATING) label Sep 27, 2020
@JoeyChenSmart JoeyChenSmart changed the title [WIP] Refactor parser Refactor parser Sep 28, 2020
Copy link

@aidoxe-123 aidoxe-123 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, this new parser idea and implementation looks good to me. However, I think it would be better to add more test cases to make sure that this parser works well with the address book first, since we know the old parser's behavior in address book. After making sure that the parser works fine, we can adopt it to McGymmy later.

@@ -1,196 +0,0 @@
package seedu.address.logic.parser;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since ParserUtil.java is still there, is it better to keep this test file instead of deleting it?

@@ -68,26 +60,6 @@ public void execute_validCommand_success() throws Exception {
assertCommandSuccess(listCommand, ListCommand.MESSAGE_SUCCESS, model);
}

@Test

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's wrong with this test

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm looks like I may have accidentally removed this during the safe delete or something, will update and restore it.

this.index = index;
this.editPersonDescriptor = new EditPersonDescriptor(editPersonDescriptor);
}
private final Parameter<Index> indexParameter = this.addParameter(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the position of the unnamed parameter matter?

How does the program behave towards these commands:
edit -n JohnDoe 1
edit1 -n JohnDoe

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, for the first one the 1 will be considered as input to the -n parameter, and the unnamed parameter will have no input (so there will be an error).

I'll add this and the comment below into the PR's docs.

this.index = index;
this.editPersonDescriptor = new EditPersonDescriptor(editPersonDescriptor);
}
private final Parameter<Index> indexParameter = this.addParameter(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to justify my curiosity, can a command have multiple unnamed parameters?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, if you try to declare multiple of the same parameters there will be an assertion error somewhere.

Technically you can work around this, by declaring a new class that contains the two parameters, e.g.
the command:
takeTwoIntegers 1 2
may have a parameter
private Parameter<IntPair> param = this.addParameter(..., IntPair::parse)

and the IntPair class is as follows:

class IntPair {
  public int a;
  public int b;
  public parse(String str) {
    IntPair result = new IntPair();
    result.a = ...
  }

Don't think any of our commands will involve these kinds of parameters, but if they do we may have to either redesign the command or the parser. (We'll solve that problem when it happens)

Comment on lines 53 to 60
this.addCommand("add", AddCommand::new);
this.addCommand("edit", EditCommand::new);
this.addCommand("delete", DeleteCommand::new);
this.addCommand("clear", ClearCommand::new);
this.addCommand("exit", ExitCommand::new);
this.addCommand("find", FindCommand::new);
this.addCommand("list", ListCommand::new);
this.addCommand("help", HelpCommand::new);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be AddCommand.COMMAND_WORD instead of "add", the same for other commands

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that seems to be better. Good catch.

@@ -2,11 +2,6 @@

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as AddCommandTest.java

@@ -1,109 +1,13 @@
package seedu.address.logic.commands;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as AddCommandTest.java

@@ -1,173 +1,17 @@
package seedu.address.logic.commands;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as AddCommandTest.java

@@ -1,83 +1,12 @@
package seedu.address.logic.commands;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as AddCommandTest.java

@@ -0,0 +1,98 @@
package seedu.address.logic.parser;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add some more tests for the old address book's logic, to make sure that it works exactly as expected as the old parser?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add a couple more tests for this, along with some unit/integration tests with the Commands.

Copy link

@Jh123x Jh123x left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM
Might need a little more test cases and need to add back tags later

@chewypiano chewypiano added this to the v1.2 milestone Sep 29, 2020
To facilitate testing. Package private access.
Instead of magic strings.
Mainly integration tests with the commands.

Deleted AddCommandIntegrationTest which is a copy of AddCommandTest.
Copy link

@Jh123x Jh123x left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be able to merge soon >.<

String addCommand = AddCommand.COMMAND_WORD + NAME_DESC_AMY + PHONE_DESC_AMY + EMAIL_DESC_AMY
+ ADDRESS_DESC_AMY;
Person expectedPerson = new PersonBuilder(AMY).withTags().build();
String addCommand = AddCommand.COMMAND_WORD + " -n amy -p 99999999 -e amy@amy.com";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might want to change this to a constant?
Like VALID_NAME_AMY, similar to the one u did in edit command test

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No point imo since I'm not reusing the string outside this function.
Also would add more noise which we're gonna change anyway (when we change people to food).

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ookie in that case LTGM

Copy link

@Jh123x Jh123x left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@Jh123x
Copy link

Jh123x commented Oct 2, 2020

Tags and Addr not implemented as they will be removed by the end of the iteration

@Jh123x Jh123x merged commit 881dea8 into AY2021S1-CS2103T-W17-3:master Oct 2, 2020
@JoeyChenSmart JoeyChenSmart mentioned this pull request Oct 20, 2020
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type.Enhancement New feature or request (!DELETE ME IF DASHBOARD IS NOT UPDATING)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add commons-cli library
4 participants