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
Refactor parser #39
Conversation
into build.gradle
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.
There was a problem hiding this 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; |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)
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); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 @@ | |||
|
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this 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
To facilitate testing. Package private access.
Instead of magic strings.
Mainly integration tests with the commands. Deleted AddCommandIntegrationTest which is a copy of AddCommandTest.
There was a problem hiding this 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"; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
Tags and Addr not implemented as they will be removed by the end of the iteration |
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
Command
s should be thought of as 'generalized functions': You only need to specify as private variableswhat parameters the
Command
takes in, and how theCommand
should use these parameters to produce changes in theModel
and output aCommandResult
.Example
In the code, first we declare some
Parameter
s. These parameters will be used in the parsing. Their parsed value can beretrieved later in the
execute
function using theParameter<T>#consume()
function. This function will return the valueas a type
T
object. Note how each declaration ofParameter
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
Parameter
s.The
execute
function is the same as before, and will contain the main logic to be executed. The only difference is that we nowaccess variables using
Parameter<T>#consume()
instead of accessing variables stored by the constructor.Finally, the command will be 'hooked' into the
McGymmyParser
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 newCommand
.There are more examples of
Commands
you can find in the source code. In particular the changed classes are theEditCommand
,DeleteCommand
andFindCommand
.Just a few things left to note:
Optional Parameters
Some parameters may be declared as optional, e.g.
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 usingParameter<T>#consume()
we have a new functionOptionalParameter<T>#getValue()
that returns aOptional<T>
value. If the user supplied this parameter, it will beOptional.of
the parsed value, otherwise it will beOptional.empty
.Unnamed Parameters
Some parameters do not have flags, e.g.
1
indelete 1
.To create a parameter that does this, simply leave the flag as an empty string, i.e.
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:
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 whatI say here to objective improvements from removing it.
for these classes also contribute significantly to the codebase (see point 1)
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 thePerson
object, and theAddCommand
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 allsorts 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 aT
typed variable, and a function to convert aString
to aT
.Command
'sthis.addParameter
is a protected function that creates theParameter
object and stores it in aParameterSet
.Parsing is a 2-step process.
Command
.Command
'sParameterSet
and gives passes the raw input parameters as strings to eachParameter
. Each parameter will then store theT
type variable produced from the converter function. These parameters are now ready to use in the Command'sexecute
function. The parser returns the ready-to-useCommand
.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 runAddCommand.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 anOptionalParameter
and vice versa