From e7f35a89490f3a7191378984c0287d4dd8aaf71c Mon Sep 17 00:00:00 2001 From: Wern Date: Sun, 12 Nov 2023 19:41:59 +0800 Subject: [PATCH 01/10] Remove legacy enum value, unify enum lookups --- .../seedu/address/commons/util/EnumUtil.java | 46 +++++++++++++++++++ .../seedu/address/model/contact/Contact.java | 1 - .../seedu/address/model/contact/Type.java | 35 ++++++++------ .../InvalidContactTypeStringException.java | 17 ------- .../jobapplication/ApplicationStage.java | 24 +++++----- .../model/jobapplication/JobStatus.java | 26 ++++++----- .../address/storage/JsonAdaptedContact.java | 9 +++- .../jobapplication/ApplicationStageTest.java | 7 +-- .../model/jobapplication/JobStatusTest.java | 9 ++-- .../storage/JsonAdaptedContactTest.java | 2 +- 10 files changed, 112 insertions(+), 64 deletions(-) create mode 100644 src/main/java/seedu/address/commons/util/EnumUtil.java delete mode 100644 src/main/java/seedu/address/model/contact/exceptions/InvalidContactTypeStringException.java diff --git a/src/main/java/seedu/address/commons/util/EnumUtil.java b/src/main/java/seedu/address/commons/util/EnumUtil.java new file mode 100644 index 00000000000..4c0b6166592 --- /dev/null +++ b/src/main/java/seedu/address/commons/util/EnumUtil.java @@ -0,0 +1,46 @@ +package seedu.address.commons.util; + +import java.util.Arrays; + +/** + * Helper class to for enums to perform common operations. + */ +public class EnumUtil { + + private static final String ENUM_LOOKUP_FAILURE_STRING_FORMAT = + "'%s' is not a valid string representation for any enum constants of type '%s'"; + + /** + * Obtains the enum constant by looking up the enum's {@link Enum#toString()} value that matches the given input. + * + * @param input The input string to match against the enum's string with. + * @param cls The enum class. + * @param The enum type. + * @return The enum constant. + * @throws IllegalArgumentException if the string does not match any of the enum's values. + */ + public static > E lookupByToString(Class cls, String input) + throws IllegalArgumentException { + + return Arrays.stream(cls.getEnumConstants()) + .filter(e -> e.toString().equals(input)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(String.format( + ENUM_LOOKUP_FAILURE_STRING_FORMAT, + input, cls.getSimpleName() + ))); + } + + /** + * Checks whether there exists an enum constant whose {@link Enum#toString()} value matches the given input. + * + * @param input The input string to match against the enum's string with. + * @param cls The enum class to enumerate through. + * @param The enum type. + * @return true if an enum constant with the given input string as text representation exists, false otherwise. + */ + public static > boolean hasMatchingToString(Class cls, String input) { + return Arrays.stream(cls.getEnumConstants()).anyMatch(e -> e.toString().equals(input)); + } + +} diff --git a/src/main/java/seedu/address/model/contact/Contact.java b/src/main/java/seedu/address/model/contact/Contact.java index 5f307dcf484..f681202099d 100644 --- a/src/main/java/seedu/address/model/contact/Contact.java +++ b/src/main/java/seedu/address/model/contact/Contact.java @@ -19,7 +19,6 @@ * Guarantees: name and id are present and not null, field values are immutable and if present, are validated. */ public abstract class Contact { - public static final String MESSAGE_MISSING_TYPE = "There is no type specified for this Contact"; // Identity fields private final Name name; diff --git a/src/main/java/seedu/address/model/contact/Type.java b/src/main/java/seedu/address/model/contact/Type.java index c3b70c62081..08222552f0c 100644 --- a/src/main/java/seedu/address/model/contact/Type.java +++ b/src/main/java/seedu/address/model/contact/Type.java @@ -1,19 +1,23 @@ package seedu.address.model.contact; -import seedu.address.model.contact.exceptions.InvalidContactTypeStringException; +import static java.util.Objects.requireNonNull; + +import seedu.address.commons.util.EnumUtil; /** * Represents the type of {@code Contact} instances. */ public enum Type { + ORGANIZATION("organization"), - RECRUITER("recruiter"), - UNKNOWN("unknown"); + RECRUITER("recruiter"); + public static final String MESSAGE_CONSTRAINTS = "Contact type must be 'organization' or 'recruiter'."; private final String textRepresentation; Type(String textRepresentation) { + requireNonNull(textRepresentation); this.textRepresentation = textRepresentation; } @@ -29,21 +33,24 @@ public String toString() { } /** - * Returns a corresponding {@code ContactType} enum value matching the given string representation of it. + * Returns a corresponding {@code ContactType} enum value matching the given text representation of it. * * @param textRepresentation The text representation of the {@code ContactType}. * @return The corresponding {@code ContactType}. - * @throws InvalidContactTypeStringException if the given input does not represent any known {@code ContactType}. + * @throws IllegalArgumentException if the given input does not represent any known {@code ContactType}. + */ + public static Type fromString(String textRepresentation) throws IllegalArgumentException { + return EnumUtil.lookupByToString(Type.class, textRepresentation); + } + + /** + * Verifies if the given input is a valid contact type. + * + * @param textRepresentation The text representation of the {@code Type}. + * @return Whether the contact type matches a known value. */ - public static Type fromString(String textRepresentation) { - for (Type type : Type.values()) { - if (type.textRepresentation.equalsIgnoreCase(textRepresentation)) { - return type; - } - } - return UNKNOWN; - // TODO: We should throw an exception instead like the below. We are using UNKNOWN for now for compatibility. - // throw new InvalidContactTypeStringException(textRepresentation); + public static boolean isValidType(String textRepresentation) { + return EnumUtil.hasMatchingToString(Type.class, textRepresentation); } } diff --git a/src/main/java/seedu/address/model/contact/exceptions/InvalidContactTypeStringException.java b/src/main/java/seedu/address/model/contact/exceptions/InvalidContactTypeStringException.java deleted file mode 100644 index cbfa012692a..00000000000 --- a/src/main/java/seedu/address/model/contact/exceptions/InvalidContactTypeStringException.java +++ /dev/null @@ -1,17 +0,0 @@ -package seedu.address.model.contact.exceptions; - -/** - * Signals that the given string representation of a {@code ContactType} enum is invalid, i.e., there are no - * enum values with the given string representation. - */ -public class InvalidContactTypeStringException extends RuntimeException { - - /** - * Constructs an exception for invalid contact type string representations. - * - * @param incorrectRepresentation The incorrect string representation used. - */ - public InvalidContactTypeStringException(String incorrectRepresentation) { - super(String.format("'%s' is not a valid contact type", incorrectRepresentation)); - } -} diff --git a/src/main/java/seedu/address/model/jobapplication/ApplicationStage.java b/src/main/java/seedu/address/model/jobapplication/ApplicationStage.java index 3b08588a5a3..f868eedda8e 100644 --- a/src/main/java/seedu/address/model/jobapplication/ApplicationStage.java +++ b/src/main/java/seedu/address/model/jobapplication/ApplicationStage.java @@ -2,14 +2,15 @@ import static java.util.Objects.requireNonNull; +import seedu.address.commons.util.EnumUtil; + /** * The different stages of internship application. */ public enum ApplicationStage { RESUME("resume"), ONLINE_ASSESSMENT("online assessment"), - INTERVIEW("interview"), - UNKNOWN("unknown"); + INTERVIEW("interview"); public static final ApplicationStage DEFAULT_STAGE = ApplicationStage.RESUME; public static final String MESSAGE_CONSTRAINTS = "Applications accept one of these values: resume | online " @@ -31,22 +32,23 @@ public String toString() { } /** - * Returns a corresponding {@code ApplicationStage} enum value matching the given string representation of it. + * Returns a corresponding {@code ApplicationStage} enum value matching the given text representation of it. * * @param textRepresentation The text representation of the {@code ApplicationStage}. * @return The corresponding {@code JobStatus}. + * @throws IllegalArgumentException if the text representation does not match any known values. */ public static ApplicationStage fromString(String textRepresentation) { - for (ApplicationStage applicationStage : ApplicationStage.values()) { - if (applicationStage.textRepresentation.equalsIgnoreCase(textRepresentation)) { - return applicationStage; - } - } - return UNKNOWN; - // TODO: We should throw an exception instead. We are using UNKNOWN for now for compatibility. + return EnumUtil.lookupByToString(ApplicationStage.class, textRepresentation); } + /** + * Verifies if the given input is a valid job application stage. + * + * @param textRepresentation The text representation of the {@code ApplicationStage}. + * @return Whether the application stage matches a known value. + */ public static boolean isValidApplicationStage(String textRepresentation) { - return !ApplicationStage.fromString(textRepresentation).equals(UNKNOWN); + return EnumUtil.hasMatchingToString(ApplicationStage.class, textRepresentation); } } diff --git a/src/main/java/seedu/address/model/jobapplication/JobStatus.java b/src/main/java/seedu/address/model/jobapplication/JobStatus.java index 7b7b801fee8..95f24a2e466 100644 --- a/src/main/java/seedu/address/model/jobapplication/JobStatus.java +++ b/src/main/java/seedu/address/model/jobapplication/JobStatus.java @@ -2,6 +2,8 @@ import static java.util.Objects.requireNonNull; +import seedu.address.commons.util.EnumUtil; + /** * Information on the status of the job application: pending, rejected, offered, accepted, turned-down */ @@ -10,8 +12,7 @@ public enum JobStatus { REJECTED("rejected"), OFFERED("offered"), ACCEPTED("accepted"), - TURNED_DOWN("turned down"), - UNKNOWN("unknown"); + TURNED_DOWN("turned down"); public static final JobStatus DEFAULT_STATUS = JobStatus.PENDING; @@ -35,22 +36,23 @@ public String toString() { } /** - * Returns a corresponding {@code JobStatus} enum value matching the given string representation of it. + * Returns a corresponding {@code JobStatus} enum value matching the given text representation of it. * * @param textRepresentation The text representation of the {@code JobStatus}. * @return The corresponding {@code JobStatus}. + * @throws IllegalArgumentException if the text representation does not match any known values. */ public static JobStatus fromString(String textRepresentation) { - for (JobStatus jobStatus : JobStatus.values()) { - if (jobStatus.textRepresentation.equalsIgnoreCase(textRepresentation)) { - return jobStatus; - } - } - return UNKNOWN; - // TODO: We should throw an exception instead. We are using UNKNOWN for now for compatibility. + return EnumUtil.lookupByToString(JobStatus.class, textRepresentation); } - public static boolean isValidJobStatus(String status) { - return !JobStatus.fromString(status).equals(UNKNOWN); + /** + * Verifies if the given input is a valid job status. + * + * @param textRepresentation The text representation of the {@code JobStatus}. + * @return Whether the job status matches a known value. + */ + public static boolean isValidJobStatus(String textRepresentation) { + return EnumUtil.hasMatchingToString(JobStatus.class, textRepresentation); } } diff --git a/src/main/java/seedu/address/storage/JsonAdaptedContact.java b/src/main/java/seedu/address/storage/JsonAdaptedContact.java index 5be6ecb7e50..cd6c8307b78 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedContact.java +++ b/src/main/java/seedu/address/storage/JsonAdaptedContact.java @@ -160,6 +160,10 @@ public Contact toModelType(ReadOnlyAddressBook reference) throws IllegalValueExc if (type == null) { throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Type.class.getSimpleName())); } + if (!Type.isValidType(type)) { + throw new IllegalValueException(Type.MESSAGE_CONSTRAINTS); + } + final Type modelType = Type.fromString(type); switch (modelType) { @@ -192,7 +196,10 @@ public Contact toModelType(ReadOnlyAddressBook reference) throws IllegalValueExc ); } default: - throw new IllegalValueException(Contact.MESSAGE_MISSING_TYPE); + assert false : "We should not reach this stage - there is a developer error and the contact type " + + modelType + "is not handled!"; + + throw new IllegalStateException(); } } diff --git a/src/test/java/seedu/address/model/jobapplication/ApplicationStageTest.java b/src/test/java/seedu/address/model/jobapplication/ApplicationStageTest.java index 25a14f07852..72caa0585d3 100644 --- a/src/test/java/seedu/address/model/jobapplication/ApplicationStageTest.java +++ b/src/test/java/seedu/address/model/jobapplication/ApplicationStageTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; @@ -24,9 +25,9 @@ public void fromString_validStrings_givesCorrectStages() { @Test public void fromString_invalidStrings_givesUnknown() { - assertEquals(ApplicationStage.UNKNOWN, ApplicationStage.fromString("")); - assertEquals(ApplicationStage.UNKNOWN, ApplicationStage.fromString(" ")); - assertEquals(ApplicationStage.UNKNOWN, ApplicationStage.fromString("resumee")); + assertThrows(IllegalArgumentException.class, () -> ApplicationStage.fromString("")); + assertThrows(IllegalArgumentException.class, () -> ApplicationStage.fromString(" ")); + assertThrows(IllegalArgumentException.class, () -> ApplicationStage.fromString("resumee")); } @Test diff --git a/src/test/java/seedu/address/model/jobapplication/JobStatusTest.java b/src/test/java/seedu/address/model/jobapplication/JobStatusTest.java index 414be6841c0..60768fa5cd5 100644 --- a/src/test/java/seedu/address/model/jobapplication/JobStatusTest.java +++ b/src/test/java/seedu/address/model/jobapplication/JobStatusTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; @@ -21,10 +22,10 @@ public void fromString_validStrings_givesCorrectStatuses() { } @Test - public void fromString_invalidStrings_givesUnknown() { - assertEquals(JobStatus.fromString(""), JobStatus.UNKNOWN); - assertEquals(JobStatus.fromString("test"), JobStatus.UNKNOWN); - assertEquals(JobStatus.fromString("pening"), JobStatus.UNKNOWN); + public void fromString_invalidStrings_throwsException() { + assertThrows(IllegalArgumentException.class, () -> JobStatus.fromString("")); + assertThrows(IllegalArgumentException.class, () -> JobStatus.fromString("test")); + assertThrows(IllegalArgumentException.class, () -> JobStatus.fromString("pening")); } @Test diff --git a/src/test/java/seedu/address/storage/JsonAdaptedContactTest.java b/src/test/java/seedu/address/storage/JsonAdaptedContactTest.java index a54f2559fcd..783c3fe4155 100644 --- a/src/test/java/seedu/address/storage/JsonAdaptedContactTest.java +++ b/src/test/java/seedu/address/storage/JsonAdaptedContactTest.java @@ -74,7 +74,7 @@ public void toModelType_invalidType_throwsIllegalValueException() { VALID_PHONE, VALID_EMAIL, VALID_URL, VALID_ADDRESS, VALID_OID, VALID_TAGS, null ); - String expectedMessage = Contact.MESSAGE_MISSING_TYPE; + String expectedMessage = Type.MESSAGE_CONSTRAINTS; assertThrows(IllegalValueException.class, expectedMessage, () -> contact.toModelType(addressBook)); } From 06e3205f42afe51f52f0c7593bea665954e40123 Mon Sep 17 00:00:00 2001 From: Wern Date: Sun, 12 Nov 2023 21:09:06 +0800 Subject: [PATCH 02/10] Improve code quality --- .../logic/parser/ArgumentMultimap.java | 29 ++-- .../logic/parser/ArgumentTokenizer.java | 21 ++- .../java/seedu/address/logic/parser/Flag.java | 140 +++++++++++++----- .../seedu/address/logic/parser/FlagTest.java | 27 ++++ .../storage/JsonAdaptedContactTest.java | 1 - 5 files changed, 155 insertions(+), 63 deletions(-) diff --git a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java b/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java index 80fa541d61f..2eb4d2f96b2 100644 --- a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java +++ b/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java @@ -19,7 +19,7 @@ */ public class ArgumentMultimap { - /** Flags mapped to their respective arguments. **/ + /** Flags mapped to their respective values. **/ private final Map> argMultimap = new HashMap<>(); /** The preamble value (the text before the first valid flag). **/ @@ -112,10 +112,19 @@ public Optional getValue(Flag flag) { * Modifying the returned list will not affect the underlying data structure of the ArgumentMultimap. */ public List getAllValues(Flag flag) { - if (!argMultimap.containsKey(flag)) { - return new ArrayList<>(); + // Attempt to look up the flag as is + if (argMultimap.containsKey(flag)) { + return new ArrayList<>(argMultimap.get(flag)); } - return new ArrayList<>(argMultimap.get(flag)); + + // Attempt to search for the alias-only version as fallback + Flag aliasOnlyDefinition = flag.getAliasOnlyDefinition(); + if (argMultimap.containsKey(aliasOnlyDefinition)) { + return new ArrayList<>(argMultimap.get(aliasOnlyDefinition)); + } + + // No results found + return new ArrayList<>(); } /** @@ -127,11 +136,11 @@ public String getPreamble() { /** * Throws a {@code ParseException} if any of the flags given in {@code flags} appeared more than - * once among the arguments. + * once among the arguments, i.e., the given flags may only be used once each. */ public void verifyNoDuplicateFlagsFor(Flag... flags) throws ParseException { Flag[] duplicatedFlags = Stream.of(flags).distinct() - .filter(flag -> argMultimap.containsKey(flag) && argMultimap.get(flag).size() > 1) + .filter(flag -> getAllValues(flag).size() > 1) .toArray(Flag[]::new); if (duplicatedFlags.length > 0) { @@ -141,7 +150,7 @@ public void verifyNoDuplicateFlagsFor(Flag... flags) throws ParseException { /** * Throws a {@code ParseException} if there exists any more flags than the ones given in {@code flags} - * among the ones put in this map. + * among the ones put in this map, i.e., the given flags are the maximally allowed set of flags. */ public void verifyNoExtraneousFlagsOnTopOf(Flag... flags) throws ParseException { List referenceFlagsList = List.of(flags); @@ -157,7 +166,7 @@ public void verifyNoExtraneousFlagsOnTopOf(Flag... flags) throws ParseException /** * Throws a {@code ParseException} if any of the flags given in {@code flags} have a non-empty value - * assigned to it. + * assigned to it, i.e., the given flags must be empty and not have values set. */ public void verifyAllEmptyValuesAssignedFor(Flag... flags) throws ParseException { Flag[] flagsWithUsefulValues = Stream.of(flags).distinct() @@ -172,11 +181,11 @@ public void verifyAllEmptyValuesAssignedFor(Flag... flags) throws ParseException /** * Throws a {@code ParseException} if there are more than one of the given {@code flags} simultanouesly - * put in this map, i.e., they cannot be used together. + * put in this map, i.e., the given flags cannot be used together. */ public void verifyAtMostOneOfFlagsUsedOutOf(Flag... flags) throws ParseException { Flag[] existingFlags = Stream.of(flags).distinct() - .filter(flag -> argMultimap.containsKey(flag) && argMultimap.get(flag).size() > 0) + .filter(flag -> getAllValues(flag).size() > 0) .toArray(Flag[]::new); if (existingFlags.length > 1) { diff --git a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java b/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java index 2b191410e6a..e0277a1ff81 100644 --- a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java +++ b/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java @@ -36,7 +36,7 @@ public class ArgumentTokenizer { * *

* Unlike {@link #autoTokenize(String, Flag...)}, this will throw an error when unspecified flags - * are found. This means you must provide all the necessary flags you intend to us via the {@code flags} + * are found. This means you must provide all the necessary flags you intend to use via the {@code flags} * parameter. *

* @@ -46,12 +46,12 @@ public class ArgumentTokenizer { * {@link ArgumentMultimap#verifyNoExtraneousFlagsOnTopOf(Flag...)}. *

* - * @param argsString Arguments string of the form: {@code preamble value value ...} - * @param flags Flags to tokenize the arguments string with - * @return ArgumentMultimap object that maps flag to their arguments + * @param argsString Arguments string of the form: {@code preamble value value ...}. + * @param flags Flags to tokenize the arguments string with. + * @return ArgumentMultimap object that maps flag to their arguments. * @throws ParseException if there are extraneous flags detected on top of the provided flags. * - * @see #autoTokenize tokenize(String, Flag...) + * @see #autoTokenize(String, Flag...) */ public static ArgumentMultimap tokenize(String argsString, Flag... flags) throws ParseException { ArgumentMultimap multimap = autoTokenize(argsString, flags); @@ -66,13 +66,12 @@ public static ArgumentMultimap tokenize(String argsString, Flag... flags) throws * *

* Unlike {@link #tokenize(String, Flag...)}, this will not throw an error when unspecified flags are - * found. In other words, an unknown flag not present in {@code mainFlags} is always accepted, albeit with - * no information about name-alias mapping and so on, so it may be hard to match against flags you expect. + * found. In other words, an unknown flag not present in {@code mainFlags} is always accepted. *

* - * @param argsString Arguments string of the form: {@code preamble value value ...} - * @param mainFlags Optional set of primary flags to prioritize tokenizing the arguments string with - * @return ArgumentMultimap object that maps flag to their arguments + * @param argsString Arguments string of the form: {@code preamble value value ...}. + * @param mainFlags Optional set of primary flags to prioritize tokenizing the arguments string with. + * @return ArgumentMultimap object that maps flag to their arguments. * * @see #tokenize(String, Flag...) */ @@ -92,7 +91,7 @@ private static String[] splitByWords(String argsString) { } /** - * Locate all the locations in the words list that represent flags. + * Locates all the locations in the words list that represent flags. * * @param wordsArray An array of words. * @param targetedFlags An array of flags should be checked explicitly. diff --git a/src/main/java/seedu/address/logic/parser/Flag.java b/src/main/java/seedu/address/logic/parser/Flag.java index 3d0216e025d..4ecc1bd4541 100644 --- a/src/main/java/seedu/address/logic/parser/Flag.java +++ b/src/main/java/seedu/address/logic/parser/Flag.java @@ -7,9 +7,12 @@ import seedu.address.logic.parser.exceptions.ParseException; /** - * A flag is an argument in and of itself. It functions as a option specifier, or as a marker for the beginning of a + * A flag is an argument in and of itself. It functions as an option specifier, or as a marker for the beginning of a * command argument. - * E.g. '--t' in 'add James --t friend'. + * + *

+ * For example, '--t' in 'add James --t friend' is a flag with name 't' and prefix '--'. + *

*/ public class Flag { @@ -104,8 +107,23 @@ public static Flag ofCustomFormat(String name, String prefix, String postfix) { /** * Parses the given string using the default prefix and postfix format into a {@link Flag}. - * This will work for both full flag strings and flag aliases. However, this may not return the same result - * as an existing flag that has both a full value and alias pair - for those, try {@link #findMatch} instead. + * + *

+ * This will work for both full flag strings and flag aliases. However, this will not return the same instance + * as an existing flag, especially if it has both a full value and alias pair. For those, try {@link #findMatch} + * instead. + *

+ * + *

+ * Full flag strings passed into this method and flags constructed in {@link #Flag(String)} are equal + * if they have the same name, prefix, and postfix. + *

+ * + *

+ * Aliased flag strings passed into this method and flags constructed in {@link #Flag(String)}, and then + * obtaining an alias-only definition via {@link #getAliasOnlyDefinition()}, will have an exact equivalent + * representation. + *

* * @param string The string to parse as a flag. * @return The corresponding {@link Flag} instance. @@ -144,6 +162,7 @@ public static Flag parse(String string) throws ParseException { * * @param string The string to check for flag-like formats. * @return An optional containing the flag if it is a valid flag format. + * @see #parse(String) */ public static Optional parseOptional(String string) { try { @@ -153,7 +172,6 @@ public static Optional parseOptional(String string) { } } - /** * Finds a {@link Flag} from the given {@code flags} that matches the given string representation. * @@ -175,6 +193,41 @@ public static Optional findMatch(String string, Flag[] flags) { return Optional.empty(); } + /** + * Checks whether the given string representation resembles a flag. + * If this is true, then it resembles the default prefix-name-postfix format specified in {@link Flag}, + * or resembles the equivalent format for the alias counterpart, and thus a plausible output from + * {@link Flag#getFlagString()} or {@link Flag#getFlagAliasString()}. + * + * @param string The string to check for flag-like formats. + * @return true if the string resembles a flag, false otherwise. + */ + public static boolean isFlagSyntax(String string) { + if (string == null) { + return false; + } + + if (string.startsWith(DEFAULT_PREFIX) && string.endsWith(DEFAULT_POSTFIX)) { + String part = string.substring( + DEFAULT_PREFIX.length(), + string.length() - DEFAULT_POSTFIX.length() + ); + return part.matches(FULL_OR_ALIAS_NAME_VALIDATION_REGEX); + } + + if (string.startsWith(DEFAULT_ALIAS_PREFIX) && string.endsWith(DEFAULT_ALIAS_POSTFIX)) { + String part = string.substring( + DEFAULT_ALIAS_PREFIX.length(), + string.length() - DEFAULT_ALIAS_POSTFIX.length() + ); + return part.matches(FULL_OR_ALIAS_NAME_VALIDATION_REGEX); + } + + return false; + } + + + public String getName() { return name; } @@ -199,6 +252,13 @@ public String getAliasPostfix() { return aliasPostfix; } + /** + * Returns whether the flag has a distinct alias from its full string form. + */ + public boolean hasAlias() { + return !this.getFlagString().equals(this.getFlagAliasString()); + } + /** * Returns the full string that would be used by the user to input a flag. * @@ -225,43 +285,27 @@ public String getFlagAliasString() { } /** - * Returns whether the flag has a distinct alias from its full string form. + * Returns a flag variant derived from the current one which uses the alias as the name. + * + *

+ * If this flag does not have an alias (as per {@link #hasAlias()}), the newly created flag would be equivalent to + * the current flag in all properties. + *

*/ - public boolean hasAlias() { - return !this.getFlagString().equals(this.getFlagAliasString()); + public Flag getAliasOnlyDefinition() { + return new Flag(alias, prefix, postfix, alias, aliasPrefix, aliasPostfix); } /** - * Checks whether the given string representation resembles a flag. - * If this is true, then it resembles the default prefix-name-postfix format specified in {@link Flag}, - * or resembles the equivalent format for the alias counterpart, and thus a plausible output from - * {@link Flag#getFlagString()} or {@link Flag#getFlagAliasString()}. + * Returns a flag variant derived from the current one which has no alias. * - * @param string The string to check for flag-like formats. - * @return true if the string resembles a flag, false otherwise. + *

+ * If this flag already does not have an alias (as per {@link #hasAlias()}), the newly created flag would be + * equivalent to the current flag in all properties. + *

*/ - public static boolean isFlagSyntax(String string) { - if (string == null) { - return false; - } - - if (string.startsWith(DEFAULT_PREFIX) && string.endsWith(DEFAULT_POSTFIX)) { - String part = string.substring( - DEFAULT_PREFIX.length(), - string.length() - DEFAULT_POSTFIX.length() - ); - return part.matches(FULL_OR_ALIAS_NAME_VALIDATION_REGEX); - } - - if (string.startsWith(DEFAULT_ALIAS_PREFIX) && string.endsWith(DEFAULT_ALIAS_POSTFIX)) { - String part = string.substring( - DEFAULT_ALIAS_PREFIX.length(), - string.length() - DEFAULT_ALIAS_POSTFIX.length() - ); - return part.matches(FULL_OR_ALIAS_NAME_VALIDATION_REGEX); - } - - return false; + public Flag getNameOnlyDefinition() { + return new Flag(name, prefix, postfix, null, null, null); } /** @@ -277,7 +321,7 @@ public String toString() { @Override public int hashCode() { - return Objects.hash(name, prefix, postfix, alias, aliasPrefix, aliasPostfix); + return Objects.hash(name, prefix, postfix); } /** @@ -291,7 +335,6 @@ public boolean equalsFlagString(Flag other) { return this.getFlagString().equals(other.getFlagString()); } - /** * Returns whether the two flags have the same flag alias formats. */ @@ -303,6 +346,14 @@ public boolean equalsFlagAliasString(Flag other) { return this.getFlagAliasString().equals(other.getFlagAliasString()); } + /** + * Returns whether the two objects are equal. + * + *

+ * Two flags are equal as long as the full form is equal for all properties, or if not, + * if at least one of them doesn't have a name, they're equal if their alias forms are equal for all properties. + *

+ */ @Override public boolean equals(Object other) { if (other == this) { @@ -315,11 +366,18 @@ public boolean equals(Object other) { } Flag otherFlag = (Flag) other; - return Objects.equals(name, otherFlag.name) + + boolean isFullFormEqual = Objects.equals(name, otherFlag.name) && Objects.equals(prefix, otherFlag.prefix) - && Objects.equals(postfix, otherFlag.postfix) - && Objects.equals(alias, otherFlag.alias) + && Objects.equals(postfix, otherFlag.postfix); + + boolean isAliasFormEqual = Objects.equals(alias, otherFlag.alias) && Objects.equals(aliasPrefix, otherFlag.aliasPrefix) && Objects.equals(aliasPostfix, otherFlag.aliasPostfix); + + boolean requiresAliasOnlyMatch = + alias.equals(name) || otherFlag.alias.equals(otherFlag.name); + + return isFullFormEqual || (requiresAliasOnlyMatch && isAliasFormEqual); } } diff --git a/src/test/java/seedu/address/logic/parser/FlagTest.java b/src/test/java/seedu/address/logic/parser/FlagTest.java index 5573c16fa8c..a427f00170b 100644 --- a/src/test/java/seedu/address/logic/parser/FlagTest.java +++ b/src/test/java/seedu/address/logic/parser/FlagTest.java @@ -170,6 +170,7 @@ public void findMatch() { @Test public void equals() { + // Case 1: Basic equality checks Flag aaa = Flag.ofCustomFormat("aaa", "-", ""); assertEquals(aaa, aaa); @@ -179,6 +180,32 @@ public void equals() { assertNotEquals(aaa, "-aaa"); assertNotEquals(aaa, Flag.ofCustomFormat("aab", "-", null)); assertNotEquals(aaa, Flag.ofCustomFormat("aaa", null, "/")); + + // Case 2: Advanced equality checks + Flag a = new Flag("aaa", "a"); + Flag aCopy = new Flag("aaa", "a"); + Flag aAlt = a.getNameOnlyDefinition(); + + Flag b = new Flag("bbb", "b"); + Flag bAlt = b.getAliasOnlyDefinition(); + + Flag aAliasB = new Flag("aaa", "b"); + + // - Trivial comparisons + assertEquals(a, a); + assertEquals(a, aCopy); + + assertNotEquals(a, b); + assertNotEquals(a, bAlt); + assertNotEquals(b, aAlt); + assertNotEquals(aAlt, bAlt); + + // - Non-trivial comparisons + assertEquals(a, aAlt); // Same name, excluding alias: equal + assertEquals(a, aAliasB); // Same name, different alias: equal + + assertEquals(b, bAlt); // Excluding name, same alias: equal + assertNotEquals(b, aAliasB); // Diff name; same alias: not equal } @Test diff --git a/src/test/java/seedu/address/storage/JsonAdaptedContactTest.java b/src/test/java/seedu/address/storage/JsonAdaptedContactTest.java index 783c3fe4155..62afdf658cd 100644 --- a/src/test/java/seedu/address/storage/JsonAdaptedContactTest.java +++ b/src/test/java/seedu/address/storage/JsonAdaptedContactTest.java @@ -16,7 +16,6 @@ import seedu.address.commons.exceptions.IllegalValueException; import seedu.address.model.AddressBook; import seedu.address.model.contact.Address; -import seedu.address.model.contact.Contact; import seedu.address.model.contact.Email; import seedu.address.model.contact.Name; import seedu.address.model.contact.Organization; From 43c238b6035200a2ffd55b0adec0965f907d70f1 Mon Sep 17 00:00:00 2001 From: Wern Date: Sun, 12 Nov 2023 21:30:01 +0800 Subject: [PATCH 03/10] Rename AutocompleteDataSet to AutocompleteItemSet --- docs/DeveloperGuide.md | 10 +-- .../autocomplete/AutocompleteGenerator.java | 1 + .../autocomplete/AutocompleteSupplier.java | 22 +++--- .../AutocompleteConstraint.java | 6 +- .../AutocompleteItemSet.java} | 69 ++++++++++--------- .../{ => components}/FlagValueSupplier.java | 2 +- .../{ => components}/PartitionedCommand.java | 8 +-- .../address/logic/commands/AddCommand.java | 10 +-- .../address/logic/commands/ApplyCommand.java | 4 +- .../address/logic/commands/DeleteCommand.java | 4 +- .../address/logic/commands/EditCommand.java | 14 ++-- .../logic/commands/ReminderCommand.java | 4 +- .../address/logic/commands/SortCommand.java | 10 +-- .../AutocompleteGeneratorTest.java | 10 +-- .../AutocompleteSupplierTest.java | 25 +++---- .../AutocompleteConstraintTest.java | 2 +- 16 files changed, 105 insertions(+), 96 deletions(-) rename src/main/java/seedu/address/logic/autocomplete/{data => components}/AutocompleteConstraint.java (97%) rename src/main/java/seedu/address/logic/autocomplete/{data/AutocompleteDataSet.java => components/AutocompleteItemSet.java} (77%) rename src/main/java/seedu/address/logic/autocomplete/{ => components}/FlagValueSupplier.java (97%) rename src/main/java/seedu/address/logic/autocomplete/{ => components}/PartitionedCommand.java (96%) rename src/test/java/seedu/address/logic/autocomplete/{data => components}/AutocompleteConstraintTest.java (98%) diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 6b7d5718360..e987596c011 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -194,7 +194,7 @@ It consists of several key components: - **`AutocompleteSupplier`**: - This class is responsible for generating possible flags and values to be used for suggestions. - - It takes an `AutocompleteDataSet` of flags, an optional `FlagValueSupplier` mapped to each flag, and can have corresponding `AutocompleteConstraint` applied to flags. + - It takes an `AutocompleteItemSet` of flags, an optional `FlagValueSupplier` mapped to each flag, and can have corresponding `AutocompleteConstraint` applied to flags. - It helps determine what flags can be added to an existing command phrase based on constraints and existing flags. - **`AutocompleteGenerator`**: @@ -213,9 +213,9 @@ It offers static factory methods for quickly defining common rulesets. Examples - `#where(item)#isPrerequisiteFor(dependents...)`: Defines dependencies between items, indicating that certain flags are prerequisites for others. - `#where(item)#cannotExistAlongsideAnyOf(items...)`: Defines that an item cannot be present when any of the others are present. -#### `AutocompleteDataSet` +#### `AutocompleteItemSet` -The `AutocompleteDataSet` is a set of flags that retains knowledge of which flags have what rules and constraints. It helps determine which flags can be added to an existing set of flags given the known constraints. +The `AutocompleteItemSet` is a set of flags that retains knowledge of which flags have what rules and constraints. It helps determine which flags can be added to an existing set of flags given the known constraints. This dataset can be constructed manually with flags and constraints, but it also offers static factory methods for quick creation of flag sets with common constraints. For example: - `#oneAmongAllOf(items...)`: Creates a set where at most one out of all the provided items may appear. @@ -238,9 +238,9 @@ By taking in both the command and the app model, it is possible to specify arbit #### `AutocompleteSupplier` -The `AutocompleteSupplier` leverages the capabilities of `AutocompleteDataSet` and `FlagValueSupplier`. +The `AutocompleteSupplier` leverages the capabilities of `AutocompleteItemSet` and `FlagValueSupplier`. -Internally, it uses `AutocompleteDataSet` to determine what flags can be added after a given set of flags has been used in a command. +Internally, it uses `AutocompleteItemSet` to determine what flags can be added after a given set of flags has been used in a command. This allows it to make suggestions based on constraints like "`--org` cannot exist together with `--rec`." diff --git a/src/main/java/seedu/address/logic/autocomplete/AutocompleteGenerator.java b/src/main/java/seedu/address/logic/autocomplete/AutocompleteGenerator.java index 10162e37bd6..d7f32a6b431 100644 --- a/src/main/java/seedu/address/logic/autocomplete/AutocompleteGenerator.java +++ b/src/main/java/seedu/address/logic/autocomplete/AutocompleteGenerator.java @@ -10,6 +10,7 @@ import java.util.stream.Stream; import seedu.address.commons.util.StringUtil; +import seedu.address.logic.autocomplete.components.PartitionedCommand; import seedu.address.logic.parser.Flag; import seedu.address.model.Model; diff --git a/src/main/java/seedu/address/logic/autocomplete/AutocompleteSupplier.java b/src/main/java/seedu/address/logic/autocomplete/AutocompleteSupplier.java index a68586e80ce..45ab8ebc1c9 100644 --- a/src/main/java/seedu/address/logic/autocomplete/AutocompleteSupplier.java +++ b/src/main/java/seedu/address/logic/autocomplete/AutocompleteSupplier.java @@ -7,7 +7,9 @@ import java.util.function.Consumer; import java.util.stream.Stream; -import seedu.address.logic.autocomplete.data.AutocompleteDataSet; +import seedu.address.logic.autocomplete.components.AutocompleteItemSet; +import seedu.address.logic.autocomplete.components.FlagValueSupplier; +import seedu.address.logic.autocomplete.components.PartitionedCommand; import seedu.address.logic.parser.Flag; import seedu.address.model.Model; @@ -16,7 +18,7 @@ */ public class AutocompleteSupplier { - private final AutocompleteDataSet flags; + private final AutocompleteItemSet flags; private final Map values; /** @@ -27,7 +29,7 @@ public class AutocompleteSupplier { * @param values A map of auto-completable values for each flag that may be obtained via a model. */ public AutocompleteSupplier( - AutocompleteDataSet flags, + AutocompleteItemSet flags, Map values ) { // Create new copies to prevent external modification. @@ -41,9 +43,9 @@ public AutocompleteSupplier( * * @param flags The set of flags that should be used as part of the autocomplete results. * - * @see #AutocompleteSupplier(AutocompleteDataSet, Map) + * @see #AutocompleteSupplier(AutocompleteItemSet, Map) */ - public static AutocompleteSupplier from(AutocompleteDataSet flags) { + public static AutocompleteSupplier from(AutocompleteItemSet flags) { return new AutocompleteSupplier(flags, Map.of()); } @@ -54,8 +56,8 @@ public static AutocompleteSupplier from(AutocompleteDataSet flags) { * @param flagSets The sets of flags that should be used together as part of the autocomplete results. */ @SafeVarargs - public static AutocompleteSupplier from(AutocompleteDataSet... flagSets) { - return from(AutocompleteDataSet.concat(flagSets)); + public static AutocompleteSupplier from(AutocompleteItemSet... flagSets) { + return from(AutocompleteItemSet.concat(flagSets)); } /** @@ -67,7 +69,7 @@ public static AutocompleteSupplier from(AutocompleteDataSet... flagSets) { */ public static AutocompleteSupplier fromUniqueFlags(Flag... uniqueFlags) { return AutocompleteSupplier.from( - AutocompleteDataSet.onceForEachOf(uniqueFlags) + AutocompleteItemSet.onceForEachOf(uniqueFlags) ); } @@ -80,7 +82,7 @@ public static AutocompleteSupplier fromUniqueFlags(Flag... uniqueFlags) { */ public static AutocompleteSupplier fromRepeatableFlags(Flag... repeatableFlags) { return AutocompleteSupplier.from( - AutocompleteDataSet.anyNumberOf(repeatableFlags) + AutocompleteItemSet.anyNumberOf(repeatableFlags) ); } @@ -129,7 +131,7 @@ public Optional> getValidValues(Flag flag, PartitionedCommand cur * Configures the set of flags within this autocomplete supplier using the given {@code operator}. * This also returns {@code this} instance, which is useful for chaining. */ - public AutocompleteSupplier configureFlagSet(Consumer> operator) { + public AutocompleteSupplier configureFlagSet(Consumer> operator) { operator.accept(this.flags); return this; } diff --git a/src/main/java/seedu/address/logic/autocomplete/data/AutocompleteConstraint.java b/src/main/java/seedu/address/logic/autocomplete/components/AutocompleteConstraint.java similarity index 97% rename from src/main/java/seedu/address/logic/autocomplete/data/AutocompleteConstraint.java rename to src/main/java/seedu/address/logic/autocomplete/components/AutocompleteConstraint.java index 1e0ecab3db5..aadeac2d128 100644 --- a/src/main/java/seedu/address/logic/autocomplete/data/AutocompleteConstraint.java +++ b/src/main/java/seedu/address/logic/autocomplete/components/AutocompleteConstraint.java @@ -1,4 +1,4 @@ -package seedu.address.logic.autocomplete.data; +package seedu.address.logic.autocomplete.components; import java.util.Collection; import java.util.Set; @@ -11,8 +11,12 @@ @FunctionalInterface public interface AutocompleteConstraint { + /** + * Returns whether {@code input} can be added to the set of {@code existingElements}. + */ boolean isAllowed(T input, Set existingElements); + // Constraint operators /** diff --git a/src/main/java/seedu/address/logic/autocomplete/data/AutocompleteDataSet.java b/src/main/java/seedu/address/logic/autocomplete/components/AutocompleteItemSet.java similarity index 77% rename from src/main/java/seedu/address/logic/autocomplete/data/AutocompleteDataSet.java rename to src/main/java/seedu/address/logic/autocomplete/components/AutocompleteItemSet.java index a70ddabf118..2e732ff9836 100644 --- a/src/main/java/seedu/address/logic/autocomplete/data/AutocompleteDataSet.java +++ b/src/main/java/seedu/address/logic/autocomplete/components/AutocompleteItemSet.java @@ -1,4 +1,4 @@ -package seedu.address.logic.autocomplete.data; +package seedu.address.logic.autocomplete.components; import java.util.Arrays; import java.util.Collection; @@ -9,7 +9,7 @@ import java.util.stream.Collectors; /** - * A set of items that can be used for autocompletion with any corresponding constraints on them. + * A set of items that can be used for autocompletion. It stores any corresponding constraints on them in itself. * *

* Suppose a command already has specific items, and some items have restrictions such as being only able to @@ -29,19 +29,19 @@ * preservation of insertion order. *

*/ -public final class AutocompleteDataSet extends LinkedHashSet { +public final class AutocompleteItemSet extends LinkedHashSet { private final Set> constraints = new LinkedHashSet<>(); /** - * Creates an empty {@link AutocompleteDataSet}. + * Creates an empty {@link AutocompleteItemSet}. */ - public AutocompleteDataSet() { + public AutocompleteItemSet() { super(); } /** - * Creates a {@link AutocompleteDataSet} with the given elements and constraints. + * Creates a {@link AutocompleteItemSet} with the given elements and constraints. * *

* This is mainly useful if your rules are complex. Otherwise, the convenience factory @@ -56,7 +56,7 @@ public AutocompleteDataSet() { * @see #oneAmongAllOf * @see #anyNumberOf */ - private AutocompleteDataSet( + private AutocompleteItemSet( Collection collection, Collection> constraints ) { @@ -65,47 +65,48 @@ private AutocompleteDataSet( } /** - * Creates an {@link AutocompleteDataSet} with the given elements, + * Creates an {@link AutocompleteItemSet} with the given elements, * and the constraint that each given element may exist at most once in a command. */ @SafeVarargs - public static AutocompleteDataSet onceForEachOf(T... items) { - return new AutocompleteDataSet( + public static AutocompleteItemSet onceForEachOf(T... items) { + return new AutocompleteItemSet( List.of(items), List.of(AutocompleteConstraint.onceForEachOf(items)) ); } /** - * Creates an {@link AutocompleteDataSet} with the given elements, + * Creates an {@link AutocompleteItemSet} with the given elements, * and the constraint that only one of the given elements may exist in a command. */ @SafeVarargs - public static AutocompleteDataSet oneAmongAllOf(T... items) { - return new AutocompleteDataSet( + public static AutocompleteItemSet oneAmongAllOf(T... items) { + return new AutocompleteItemSet( List.of(items), List.of(AutocompleteConstraint.oneAmongAllOf(items)) ); } /** - * Creates an {@link AutocompleteDataSet} with the given elements, + * Creates an {@link AutocompleteItemSet} with the given elements, * and the constraint that all given elements may exist any number of times in a command. */ @SafeVarargs - public static AutocompleteDataSet anyNumberOf(T... items) { - return new AutocompleteDataSet( + public static AutocompleteItemSet anyNumberOf(T... items) { + return new AutocompleteItemSet( List.of(items), List.of() ); } /** - * Concatenates all provided {@link AutocompleteDataSet}s. + * Concatenates all provided {@link AutocompleteItemSet}s. This includes merging all information between them, + * i.e., all items and constraints. */ @SafeVarargs - public static AutocompleteDataSet concat(AutocompleteDataSet... sets) { - return new AutocompleteDataSet( + public static AutocompleteItemSet concat(AutocompleteItemSet... sets) { + return new AutocompleteItemSet( Arrays.stream(sets).flatMap(Collection::stream).collect(Collectors.toList()), Arrays.stream(sets).flatMap(s -> s.constraints.stream()).collect(Collectors.toList()) ); @@ -114,8 +115,8 @@ public static AutocompleteDataSet concat(AutocompleteDataSet... sets) /** * Returns a copy of the current instance. */ - public AutocompleteDataSet copy() { - return new AutocompleteDataSet(this, constraints); + public AutocompleteItemSet copy() { + return new AutocompleteItemSet(this, constraints); } @@ -126,9 +127,9 @@ public AutocompleteDataSet copy() { * elements in {@code dependencies} to exist in a command. */ @SafeVarargs - public final AutocompleteDataSet addDependents(AutocompleteDataSet... dependencies) { + public final AutocompleteItemSet addDependents(AutocompleteItemSet... dependencies) { - AutocompleteDataSet mergedDependencies = AutocompleteDataSet.concat(dependencies); + AutocompleteItemSet mergedDependencies = AutocompleteItemSet.concat(dependencies); // Create a dependency array // - The unchecked cast is required for generics since generic arrays cannot be made. @@ -155,7 +156,7 @@ public final AutocompleteDataSet addDependents(AutocompleteDataSet... depe * @param constraint The constraint to add. * @return A reference to {@code this} instance, useful for chaining. */ - public AutocompleteDataSet addConstraint(AutocompleteConstraint constraint) { + public AutocompleteItemSet addConstraint(AutocompleteConstraint constraint) { this.constraints.add(constraint); return this; } @@ -166,7 +167,7 @@ public AutocompleteDataSet addConstraint(AutocompleteConstraint co * @param constraint The constraint to remove. * @return A reference to {@code this} instance, useful for chaining. */ - public AutocompleteDataSet removeConstraint(AutocompleteConstraint constraint) { + public AutocompleteItemSet removeConstraint(AutocompleteConstraint constraint) { this.constraints.remove(constraint); return this; } @@ -177,7 +178,7 @@ public AutocompleteDataSet removeConstraint(AutocompleteConstraint * @param constraints The constraints to add. * @return A reference to {@code this} instance, useful for chaining. */ - public AutocompleteDataSet addConstraints(Collection> constraints) { + public AutocompleteItemSet addConstraints(Collection> constraints) { this.constraints.addAll(constraints); return this; } @@ -188,7 +189,7 @@ public AutocompleteDataSet addConstraints(Collection removeConstraints(Collection> constraints) { + public AutocompleteItemSet removeConstraints(Collection> constraints) { this.constraints.removeAll(constraints); return this; } @@ -206,7 +207,7 @@ public Set> getConstraints() { * Adds the item to the set. * Equivalent to {@link #add}, but returns {@code this}, so is useful for chaining. */ - public AutocompleteDataSet addElement(T e) { + public AutocompleteItemSet addElement(T e) { this.add(e); return this; } @@ -215,7 +216,7 @@ public AutocompleteDataSet addElement(T e) { * Removes the item from the set. * Equivalent to {@link #remove}, but returns {@code this}, so is useful for chaining. */ - public AutocompleteDataSet removeElement(T e) { + public AutocompleteItemSet removeElement(T e) { this.remove(e); return this; } @@ -224,7 +225,7 @@ public AutocompleteDataSet removeElement(T e) { * Adds the items to the set. * Equivalent to {@link #addAll}, but returns {@code this}, so is useful for chaining. */ - public AutocompleteDataSet addElements(Collection e) { + public AutocompleteItemSet addElements(Collection e) { this.addAll(e); return this; } @@ -233,14 +234,14 @@ public AutocompleteDataSet addElements(Collection e) { * Removes the items from the set. * Equivalent to {@link #removeAll}, but returns {@code this}, so is useful for chaining. */ - public AutocompleteDataSet removeElements(Collection e) { + public AutocompleteItemSet removeElements(Collection e) { this.removeAll(e); return this; } /** * Returns the elements in this instance in a new {@link Set} instance. - * Properties like iteration order are preserved. + * Iteration order is preserved. */ public Set getElements() { return new LinkedHashSet<>(this); @@ -266,11 +267,11 @@ public Set getElementsAfterConsuming(Set existingElements) { public boolean equals(Object o) { // instanceof checks null implicitly. - if (!(o instanceof AutocompleteDataSet)) { + if (!(o instanceof AutocompleteItemSet)) { return false; } - AutocompleteDataSet otherSet = (AutocompleteDataSet) o; + AutocompleteItemSet otherSet = (AutocompleteItemSet) o; return super.equals(o) && this.constraints.equals(otherSet.constraints); } diff --git a/src/main/java/seedu/address/logic/autocomplete/FlagValueSupplier.java b/src/main/java/seedu/address/logic/autocomplete/components/FlagValueSupplier.java similarity index 97% rename from src/main/java/seedu/address/logic/autocomplete/FlagValueSupplier.java rename to src/main/java/seedu/address/logic/autocomplete/components/FlagValueSupplier.java index 6307c613b69..31a8bcf4f2d 100644 --- a/src/main/java/seedu/address/logic/autocomplete/FlagValueSupplier.java +++ b/src/main/java/seedu/address/logic/autocomplete/components/FlagValueSupplier.java @@ -1,4 +1,4 @@ -package seedu.address.logic.autocomplete; +package seedu.address.logic.autocomplete.components; import java.util.function.BiFunction; import java.util.stream.Stream; diff --git a/src/main/java/seedu/address/logic/autocomplete/PartitionedCommand.java b/src/main/java/seedu/address/logic/autocomplete/components/PartitionedCommand.java similarity index 96% rename from src/main/java/seedu/address/logic/autocomplete/PartitionedCommand.java rename to src/main/java/seedu/address/logic/autocomplete/components/PartitionedCommand.java index c8293b5b762..76bda07dde4 100644 --- a/src/main/java/seedu/address/logic/autocomplete/PartitionedCommand.java +++ b/src/main/java/seedu/address/logic/autocomplete/components/PartitionedCommand.java @@ -1,4 +1,4 @@ -package seedu.address.logic.autocomplete; +package seedu.address.logic.autocomplete.components; import java.util.List; import java.util.Objects; @@ -22,7 +22,7 @@ public class PartitionedCommand { /** * Initializes and prepares the given command as distinct partitions. */ - PartitionedCommand(String partialCommand) { + public PartitionedCommand(String partialCommand) { List words = List.of(partialCommand.split(" ", -1)); // -1 stops stripping adjacent spaces. if (words.size() <= 1) { @@ -170,8 +170,8 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) { return false; } - seedu.address.logic.autocomplete.PartitionedCommand - other = (seedu.address.logic.autocomplete.PartitionedCommand) o; + PartitionedCommand + other = (PartitionedCommand) o; return Objects.equals(name, other.name) && Objects.equals(middleText, other.middleText) && Objects.equals(trailingText, other.trailingText); diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java index 9455de4f6d1..641d17054fe 100644 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ b/src/main/java/seedu/address/logic/commands/AddCommand.java @@ -22,8 +22,8 @@ import seedu.address.commons.util.ToStringBuilder; import seedu.address.logic.Messages; import seedu.address.logic.autocomplete.AutocompleteSupplier; -import seedu.address.logic.autocomplete.data.AutocompleteConstraint; -import seedu.address.logic.autocomplete.data.AutocompleteDataSet; +import seedu.address.logic.autocomplete.components.AutocompleteConstraint; +import seedu.address.logic.autocomplete.components.AutocompleteItemSet; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; import seedu.address.model.contact.Address; @@ -44,15 +44,15 @@ public abstract class AddCommand extends Command { public static final String COMMAND_WORD = "add"; public static final AutocompleteSupplier AUTOCOMPLETE_SUPPLIER = AutocompleteSupplier.from( - AutocompleteDataSet.oneAmongAllOf( + AutocompleteItemSet.oneAmongAllOf( FLAG_ORGANIZATION, FLAG_RECRUITER ).addDependents( - AutocompleteDataSet.onceForEachOf( + AutocompleteItemSet.onceForEachOf( FLAG_NAME, FLAG_ID, FLAG_PHONE, FLAG_EMAIL, FLAG_ADDRESS, FLAG_URL, FLAG_ORGANIZATION_ID ), - AutocompleteDataSet.anyNumberOf(FLAG_TAG) + AutocompleteItemSet.anyNumberOf(FLAG_TAG) ).addConstraints(List.of( AutocompleteConstraint.where(FLAG_RECRUITER) .isPrerequisiteFor(FLAG_ORGANIZATION_ID) diff --git a/src/main/java/seedu/address/logic/commands/ApplyCommand.java b/src/main/java/seedu/address/logic/commands/ApplyCommand.java index afd57a98829..cc5ec68aa90 100644 --- a/src/main/java/seedu/address/logic/commands/ApplyCommand.java +++ b/src/main/java/seedu/address/logic/commands/ApplyCommand.java @@ -15,7 +15,7 @@ import seedu.address.commons.exceptions.IllegalValueException; import seedu.address.commons.util.StringUtil; import seedu.address.logic.autocomplete.AutocompleteSupplier; -import seedu.address.logic.autocomplete.data.AutocompleteDataSet; +import seedu.address.logic.autocomplete.components.AutocompleteItemSet; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; import seedu.address.model.contact.Contact; @@ -37,7 +37,7 @@ public class ApplyCommand extends Command { public static final String COMMAND_WORD = "apply"; public static final AutocompleteSupplier AUTOCOMPLETE_SUPPLIER = AutocompleteSupplier.from( - AutocompleteDataSet.onceForEachOf( + AutocompleteItemSet.onceForEachOf( FLAG_TITLE, FLAG_DESCRIPTION, FLAG_DEADLINE, FLAG_STAGE, FLAG_STATUS ) diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java index 06db50c9c62..ef615852c40 100644 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ b/src/main/java/seedu/address/logic/commands/DeleteCommand.java @@ -13,7 +13,7 @@ import seedu.address.commons.util.ToStringBuilder; import seedu.address.logic.Messages; import seedu.address.logic.autocomplete.AutocompleteSupplier; -import seedu.address.logic.autocomplete.data.AutocompleteDataSet; +import seedu.address.logic.autocomplete.components.AutocompleteItemSet; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; import seedu.address.model.contact.Contact; @@ -29,7 +29,7 @@ public class DeleteCommand extends Command { public static final String COMMAND_WORD = "delete"; public static final AutocompleteSupplier AUTOCOMPLETE_SUPPLIER = AutocompleteSupplier.from( - AutocompleteDataSet.oneAmongAllOf( + AutocompleteItemSet.oneAmongAllOf( FLAG_RECURSIVE, FLAG_APPLICATION ) ).configureValueMap(map -> { diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java index 998d01b1322..56c2efc9772 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/seedu/address/logic/commands/EditCommand.java @@ -32,8 +32,8 @@ import seedu.address.commons.util.ToStringBuilder; import seedu.address.logic.Messages; import seedu.address.logic.autocomplete.AutocompleteSupplier; -import seedu.address.logic.autocomplete.data.AutocompleteConstraint; -import seedu.address.logic.autocomplete.data.AutocompleteDataSet; +import seedu.address.logic.autocomplete.components.AutocompleteConstraint; +import seedu.address.logic.autocomplete.components.AutocompleteItemSet; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.Flag; import seedu.address.model.Model; @@ -59,19 +59,19 @@ public class EditCommand extends Command { public static final String COMMAND_WORD = "edit"; - public static final AutocompleteDataSet AUTOCOMPLETE_SET_STANDARD = AutocompleteDataSet.concat( - AutocompleteDataSet.onceForEachOf( + public static final AutocompleteItemSet AUTOCOMPLETE_SET_STANDARD = AutocompleteItemSet.concat( + AutocompleteItemSet.onceForEachOf( FLAG_NAME, FLAG_ID, FLAG_PHONE, FLAG_EMAIL, FLAG_ADDRESS, FLAG_URL, FLAG_ORGANIZATION_ID ), - AutocompleteDataSet.anyNumberOf(FLAG_TAG) + AutocompleteItemSet.anyNumberOf(FLAG_TAG) ); - public static final AutocompleteDataSet AUTOCOMPLETE_SET_APPLICATION = AutocompleteDataSet + public static final AutocompleteItemSet AUTOCOMPLETE_SET_APPLICATION = AutocompleteItemSet .onceForEachOf(FLAG_APPLICATION) .addDependents( - AutocompleteDataSet.onceForEachOf( + AutocompleteItemSet.onceForEachOf( FLAG_TITLE, FLAG_DESCRIPTION, FLAG_DEADLINE, FLAG_STAGE, FLAG_STATUS )) .addConstraint( diff --git a/src/main/java/seedu/address/logic/commands/ReminderCommand.java b/src/main/java/seedu/address/logic/commands/ReminderCommand.java index ba49fe11e6b..d3cd072ee5e 100644 --- a/src/main/java/seedu/address/logic/commands/ReminderCommand.java +++ b/src/main/java/seedu/address/logic/commands/ReminderCommand.java @@ -5,7 +5,7 @@ import static seedu.address.logic.parser.CliSyntax.FLAG_LATEST; import seedu.address.logic.autocomplete.AutocompleteSupplier; -import seedu.address.logic.autocomplete.data.AutocompleteDataSet; +import seedu.address.logic.autocomplete.components.AutocompleteItemSet; import seedu.address.model.Model; import seedu.address.model.jobapplication.JobApplication; @@ -16,7 +16,7 @@ public class ReminderCommand extends Command { public static final String COMMAND_WORD = "remind"; public static final AutocompleteSupplier AUTOCOMPLETE_SUPPLIER = AutocompleteSupplier.from( - AutocompleteDataSet.oneAmongAllOf( + AutocompleteItemSet.oneAmongAllOf( FLAG_EARLIEST, FLAG_LATEST ) ).configureValueMap(map -> { diff --git a/src/main/java/seedu/address/logic/commands/SortCommand.java b/src/main/java/seedu/address/logic/commands/SortCommand.java index b6f1c959acb..de6de129df8 100644 --- a/src/main/java/seedu/address/logic/commands/SortCommand.java +++ b/src/main/java/seedu/address/logic/commands/SortCommand.java @@ -20,8 +20,8 @@ import java.util.Objects; import seedu.address.logic.autocomplete.AutocompleteSupplier; -import seedu.address.logic.autocomplete.data.AutocompleteConstraint; -import seedu.address.logic.autocomplete.data.AutocompleteDataSet; +import seedu.address.logic.autocomplete.components.AutocompleteConstraint; +import seedu.address.logic.autocomplete.components.AutocompleteItemSet; import seedu.address.model.Model; import seedu.address.model.contact.Contact; import seedu.address.model.jobapplication.JobApplication; @@ -33,12 +33,12 @@ public class SortCommand extends Command { public static final String COMMAND_WORD = "sort"; public static final AutocompleteSupplier AUTOCOMPLETE_SUPPLIER = AutocompleteSupplier.from( - AutocompleteDataSet.concat( - AutocompleteDataSet.oneAmongAllOf( + AutocompleteItemSet.concat( + AutocompleteItemSet.oneAmongAllOf( FLAG_NAME, FLAG_ID, FLAG_PHONE, FLAG_EMAIL, FLAG_ADDRESS, FLAG_URL, FLAG_STALE, FLAG_STAGE, FLAG_STATUS, FLAG_DEADLINE, FLAG_TITLE, FLAG_NONE - ), AutocompleteDataSet.oneAmongAllOf( + ), AutocompleteItemSet.oneAmongAllOf( FLAG_ASCENDING, FLAG_DESCENDING ) ).addConstraint( diff --git a/src/test/java/seedu/address/logic/autocomplete/AutocompleteGeneratorTest.java b/src/test/java/seedu/address/logic/autocomplete/AutocompleteGeneratorTest.java index 27565b5239c..391148d6cb8 100644 --- a/src/test/java/seedu/address/logic/autocomplete/AutocompleteGeneratorTest.java +++ b/src/test/java/seedu/address/logic/autocomplete/AutocompleteGeneratorTest.java @@ -9,8 +9,8 @@ import org.junit.jupiter.api.Test; -import seedu.address.logic.autocomplete.data.AutocompleteConstraint; -import seedu.address.logic.autocomplete.data.AutocompleteDataSet; +import seedu.address.logic.autocomplete.components.AutocompleteConstraint; +import seedu.address.logic.autocomplete.components.AutocompleteItemSet; import seedu.address.logic.parser.Flag; public class AutocompleteGeneratorTest { @@ -58,9 +58,9 @@ public void generateCompletions_usingAutocompleteSupplier_correctResult() { Flag flagC2 = new Flag("code"); AutocompleteSupplier supplier = new AutocompleteSupplier( - AutocompleteDataSet.concat( - AutocompleteDataSet.onceForEachOf(flagA1, flagA2, flagA3), - AutocompleteDataSet.anyNumberOf(flagB, flagC1, flagC2) + AutocompleteItemSet.concat( + AutocompleteItemSet.onceForEachOf(flagA1, flagA2, flagA3), + AutocompleteItemSet.anyNumberOf(flagB, flagC1, flagC2) ).addConstraint( AutocompleteConstraint.oneAmongAllOf(flagA1, flagA2) ), diff --git a/src/test/java/seedu/address/logic/autocomplete/AutocompleteSupplierTest.java b/src/test/java/seedu/address/logic/autocomplete/AutocompleteSupplierTest.java index 43a4366b9c1..c4a41af0b34 100644 --- a/src/test/java/seedu/address/logic/autocomplete/AutocompleteSupplierTest.java +++ b/src/test/java/seedu/address/logic/autocomplete/AutocompleteSupplierTest.java @@ -10,8 +10,9 @@ import org.junit.jupiter.api.Test; -import seedu.address.logic.autocomplete.data.AutocompleteConstraint; -import seedu.address.logic.autocomplete.data.AutocompleteDataSet; +import seedu.address.logic.autocomplete.components.AutocompleteConstraint; +import seedu.address.logic.autocomplete.components.AutocompleteItemSet; +import seedu.address.logic.autocomplete.components.PartitionedCommand; import seedu.address.logic.parser.Flag; public class AutocompleteSupplierTest { @@ -37,8 +38,8 @@ public void getAllPossibleFlags() { assertEquals(Set.of(FLAG_A, FLAG_B), supplier.getAllPossibleFlags()); supplier = AutocompleteSupplier.from( - AutocompleteDataSet.onceForEachOf(FLAG_A, FLAG_B), - AutocompleteDataSet.anyNumberOf(FLAG_C, FLAG_D) + AutocompleteItemSet.onceForEachOf(FLAG_A, FLAG_B), + AutocompleteItemSet.anyNumberOf(FLAG_C, FLAG_D) ); assertEquals(Set.of(FLAG_A, FLAG_B, FLAG_C, FLAG_D), supplier.getAllPossibleFlags()); } @@ -63,8 +64,8 @@ public void getOtherPossibleFlagsAsideFromFlagsPresent() { // Mixed flags supplier = AutocompleteSupplier.from( - AutocompleteDataSet.onceForEachOf(FLAG_A, FLAG_B), - AutocompleteDataSet.anyNumberOf(FLAG_C, FLAG_D) + AutocompleteItemSet.onceForEachOf(FLAG_A, FLAG_B), + AutocompleteItemSet.anyNumberOf(FLAG_C, FLAG_D) ); assertEquals( Set.of(FLAG_A, FLAG_B, FLAG_C, FLAG_D), @@ -77,9 +78,9 @@ public void getOtherPossibleFlagsAsideFromFlagsPresent() { // Mixed advanced combination. supplier = AutocompleteSupplier.from( - AutocompleteDataSet.concat( - AutocompleteDataSet.onceForEachOf(FLAG_A, FLAG_B), - AutocompleteDataSet.anyNumberOf(FLAG_C, FLAG_D) + AutocompleteItemSet.concat( + AutocompleteItemSet.onceForEachOf(FLAG_A, FLAG_B), + AutocompleteItemSet.anyNumberOf(FLAG_C, FLAG_D) ).addConstraints(List.of( AutocompleteConstraint.oneAmongAllOf(FLAG_A, FLAG_B), // A & B cannot coexist AutocompleteConstraint.oneAmongAllOf(FLAG_B, FLAG_C) // B & C cannot coexist @@ -111,9 +112,9 @@ public void getOtherPossibleFlagsAsideFromFlagsPresent() { @Test public void getValidValues() { var supplier = new AutocompleteSupplier( - AutocompleteDataSet.concat( - AutocompleteDataSet.onceForEachOf(FLAG_A, FLAG_B, FLAG_C), - AutocompleteDataSet.anyNumberOf(FLAG_D) + AutocompleteItemSet.concat( + AutocompleteItemSet.onceForEachOf(FLAG_A, FLAG_B, FLAG_C), + AutocompleteItemSet.anyNumberOf(FLAG_D) ).addConstraint( AutocompleteConstraint.oneAmongAllOf(FLAG_A, FLAG_B) // A & B cannot coexist ), diff --git a/src/test/java/seedu/address/logic/autocomplete/data/AutocompleteConstraintTest.java b/src/test/java/seedu/address/logic/autocomplete/components/AutocompleteConstraintTest.java similarity index 98% rename from src/test/java/seedu/address/logic/autocomplete/data/AutocompleteConstraintTest.java rename to src/test/java/seedu/address/logic/autocomplete/components/AutocompleteConstraintTest.java index ba3cfef3e5a..8b765be07e5 100644 --- a/src/test/java/seedu/address/logic/autocomplete/data/AutocompleteConstraintTest.java +++ b/src/test/java/seedu/address/logic/autocomplete/components/AutocompleteConstraintTest.java @@ -1,4 +1,4 @@ -package seedu.address.logic.autocomplete.data; +package seedu.address.logic.autocomplete.components; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; From 796320ecd6d2be60ca132caa189a4296e8503a39 Mon Sep 17 00:00:00 2001 From: Wern Date: Sun, 12 Nov 2023 21:48:37 +0800 Subject: [PATCH 04/10] Improve command retrieval documentation --- .../seedu/address/logic/commands/Command.java | 6 +--- .../address/logic/parser/ClassMappings.java | 34 ++++++++++++++++--- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/main/java/seedu/address/logic/commands/Command.java b/src/main/java/seedu/address/logic/commands/Command.java index dbc8a71413e..b9bcae1b1e1 100644 --- a/src/main/java/seedu/address/logic/commands/Command.java +++ b/src/main/java/seedu/address/logic/commands/Command.java @@ -64,11 +64,7 @@ public static Optional getCommandWord(Class cls) { } } catch (NoSuchFieldException | IllegalAccessException e) { - // Should not reach here... - - assert cls.equals(Command.class) - : "a public COMMAND_WORD static field should be present for Command subclasses, but missing in " - + cls.getName(); + // Leave value as null... } return Optional.ofNullable(value); diff --git a/src/main/java/seedu/address/logic/parser/ClassMappings.java b/src/main/java/seedu/address/logic/parser/ClassMappings.java index 859f13352ae..0cb2ae2916d 100644 --- a/src/main/java/seedu/address/logic/parser/ClassMappings.java +++ b/src/main/java/seedu/address/logic/parser/ClassMappings.java @@ -4,7 +4,9 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; +import java.util.logging.Logger; +import seedu.address.commons.core.LogsCenter; import seedu.address.logic.commands.AddCommand; import seedu.address.logic.commands.ApplyCommand; import seedu.address.logic.commands.ClearCommand; @@ -27,8 +29,14 @@ public class ClassMappings { public static final Map, Optional>>> COMMAND_TO_PARSER_MAP = getCommandToParserMap(); - private ClassMappings() { } // Should not be initialized. - + /** + * Creates and returns an ordered map of command classes as keys, and optionally their corresponding parsers + * if they accept arguments. + * + *

+ * Developer's note: This method may be modified to include support for new commands. + *

+ */ private static Map, Optional>>> getCommandToParserMap() { @@ -58,16 +66,32 @@ private ClassMappings() { } // Should not be initialized. return orderedMap; } + /** + * This is a helper method to validate whether the given map of commands and parsers meet the expected + * specifications. + * + *
    + *
  • All commands must have a command word.
  • + *
  • All commands must either have a parser that can initialize with no arguments, or itself be initalized + * with no-arguments
  • + *
+ * + *

+ * This method's purpose for validating the specifications is because we would be retrieving values + * directly via Java's Reflection API and initializing the instances that way, which does not have compile time + * checks. Adding an assertion to ensure this works helps validate that no programmer error has slipped by. + *

+ */ private static boolean isCommandToParserMapOperational( Map, Optional>>> map ) { try { - // Assert that no programmer error slips by, - // since we're utilizing reflections to obtain values and initialize classes. for (var entry: map.entrySet()) { // We must have a command word for every command. - assert Command.getCommandWord(entry.getKey()).isPresent(); + assert Command.getCommandWord(entry.getKey()).isPresent() + : "All commands must have COMMAND_WORD set, but " + + entry.getKey().getSimpleName() + " does not!"; if (entry.getValue().isPresent()) { // If there's a parser class, we must be able to initialize them with no args without errors. From bea3d220875c55a73b382efd1cdcde67210352c4 Mon Sep 17 00:00:00 2001 From: Wern Date: Sun, 12 Nov 2023 21:59:53 +0800 Subject: [PATCH 05/10] Fix tests --- .../java/seedu/address/storage/JsonAdaptedContact.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/seedu/address/storage/JsonAdaptedContact.java b/src/main/java/seedu/address/storage/JsonAdaptedContact.java index 919a7826e09..63ee0219481 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedContact.java +++ b/src/main/java/seedu/address/storage/JsonAdaptedContact.java @@ -211,6 +211,16 @@ public Contact toModelType(ReadOnlyAddressBook reference) throws IllegalValueExc @Override public int compareTo(JsonAdaptedContact o) { + boolean isThisTypeValid = Type.isValidType(this.type); + boolean isOtherTypeValid = Type.isValidType(o.type); + + if (!isThisTypeValid) { + return isOtherTypeValid ? 1 : 0; + } + if (!isOtherTypeValid) { + return -1; + } + return Type.fromString(this.type).compareTo(Type.fromString(o.type)); } } From 85d46521b7ee4bb27f17f957aa5e27a495dcb705 Mon Sep 17 00:00:00 2001 From: Wern Date: Sun, 12 Nov 2023 22:18:58 +0800 Subject: [PATCH 06/10] Fix checkstyle --- src/main/java/seedu/address/logic/parser/ClassMappings.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/seedu/address/logic/parser/ClassMappings.java b/src/main/java/seedu/address/logic/parser/ClassMappings.java index 0cb2ae2916d..dfd1e76f6a5 100644 --- a/src/main/java/seedu/address/logic/parser/ClassMappings.java +++ b/src/main/java/seedu/address/logic/parser/ClassMappings.java @@ -4,9 +4,7 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; -import java.util.logging.Logger; -import seedu.address.commons.core.LogsCenter; import seedu.address.logic.commands.AddCommand; import seedu.address.logic.commands.ApplyCommand; import seedu.address.logic.commands.ClearCommand; From b630ad8342515c112918c60965feebd215d64a30 Mon Sep 17 00:00:00 2001 From: Wern Date: Sun, 12 Nov 2023 23:06:10 +0800 Subject: [PATCH 07/10] Fix autocomplete UP, DOWN, ENTER keystroke handling --- .../address/ui/AutocompleteTextField.java | 90 +++++++++++++++---- .../java/seedu/address/ui/CommandBox.java | 8 +- 2 files changed, 77 insertions(+), 21 deletions(-) diff --git a/src/main/java/seedu/address/ui/AutocompleteTextField.java b/src/main/java/seedu/address/ui/AutocompleteTextField.java index 318d9e8c1a1..f208e5446bd 100644 --- a/src/main/java/seedu/address/ui/AutocompleteTextField.java +++ b/src/main/java/seedu/address/ui/AutocompleteTextField.java @@ -4,6 +4,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.Stack; import java.util.function.Function; import java.util.stream.Collectors; @@ -13,6 +14,7 @@ import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.geometry.Side; +import javafx.scene.Node; import javafx.scene.control.ContextMenu; import javafx.scene.control.CustomMenuItem; import javafx.scene.control.Label; @@ -59,6 +61,13 @@ public String toString() { } } + /** + * An enum of execution status. + */ + public enum ActionResult { + EXECUTED, NOT_EXECUTED; + } + /** * A functional interface that generates a stream of auto-completion results * based on the given partial input. @@ -66,14 +75,19 @@ public String toString() { @FunctionalInterface public interface CompletionGenerator extends Function> { } + // Internal JavaFX ID values + private static final String AUTOCOMPLETE_MENU_ITEM_ID_PREFIX = "autocomplete-completion-item-"; + // GUI elements private final ContextMenu autocompletePopup; + // Configuration variables private CompletionGenerator completionGenerator = s -> Stream.empty(); private String autocompleteHintString = "[Select to autocomplete]"; private int popupLimit = 10; + // History tracking for autocomplete undo operations private final Stack autocompleteHistory = new Stack<>(); @@ -94,6 +108,37 @@ public AutocompleteTextField() { // We disallow this by intercepting it before it does. e.consume(); } + + if (e.getCode() == KeyCode.ENTER) { + // The default behaviour of ENTER seems buggy. + // We will intercept it and do our own processing. + // This uses JavaFX private APIs to locate the focused element, as found here: + // https://stackoverflow.com/questions/27332981/contextmenu-and-programmatically-selecting-an-item + e.consume(); + + int highlightedIndex = 0; + + Set items = autocompletePopup.getSkin().getNode().lookupAll(".menu-item"); + for (Node item : items) { + if (!item.isFocused() && !item.isHover()) { + continue; + } + + if (!item.getId().startsWith(AUTOCOMPLETE_MENU_ITEM_ID_PREFIX)) { + continue; + } + + try { + String indexPart = item.getId().substring(AUTOCOMPLETE_MENU_ITEM_ID_PREFIX.length()); + highlightedIndex = Integer.parseInt(indexPart, 10); + break; + } catch (NumberFormatException exception) { + // Ignore... + } + } + + this.triggerImmediateAutocompletion(highlightedIndex); + } }); // Setup autocompletion popup menu UI updates @@ -129,18 +174,23 @@ public int getPopupLimit() { /** * Triggers autocompletion immediately using the first suggested value, if any. - * - * @return true if an autocompleted result has been filled in, false otherwise. */ - public boolean triggerImmediateAutocompletion() { + public ActionResult triggerImmediateAutocompletion() { + return triggerImmediateAutocompletion(0); + } + + /** + * Triggers autocompletion immediately using the given suggested value index, if any. + */ + public ActionResult triggerImmediateAutocompletion(int index) { ObservableList menuItems = autocompletePopup.getItems(); - if (!isPopupVisible() || menuItems.isEmpty()) { - return false; + if (!isPopupVisible() || menuItems.size() <= index || index < 0) { + return ActionResult.NOT_EXECUTED; } - menuItems.get(0).fire(); - return true; + menuItems.get(index).fire(); + return ActionResult.EXECUTED; } /** @@ -168,16 +218,16 @@ private void triggerImmediateAutocompletionUsingResult(String result) { * This only does something when invoked at a stage where the current text matches * a previously autocompleted result. */ - public boolean undoLastImmediateAutocompletion() { + public ActionResult undoLastImmediateAutocompletion() { if (autocompleteHistory.isEmpty()) { - return false; + return ActionResult.NOT_EXECUTED; } AutocompleteSnapshot snapshot = autocompleteHistory.peek(); // Verify that the current text correctly matches the latest snapshot if (!this.getText().trim().equals(snapshot.completedValue)) { - return false; + return ActionResult.NOT_EXECUTED; } // Pop the result @@ -190,7 +240,8 @@ public boolean undoLastImmediateAutocompletion() { this.requestFocus(); this.end(); this.refreshPopupState(); - return true; + + return ActionResult.EXECUTED; } /** @@ -275,6 +326,7 @@ public void refreshPopupState() { // Create the context menu item CustomMenuItem item = new CustomMenuItem(completionBox, false); item.setOnAction(e -> triggerImmediateAutocompletionUsingResult(completion)); + item.setId(AUTOCOMPLETE_MENU_ITEM_ID_PREFIX + i); menuItems.add(item); // Handle special case styling @@ -292,6 +344,7 @@ public void refreshPopupState() { completionLabelFront.setText("... (more options hidden)"); completionLabelBack.setText(null); item.setDisable(true); + item.setOnAction(e -> {}); break; // Stop further processing immediately - only one of this should be displayed. } @@ -301,13 +354,16 @@ public void refreshPopupState() { autocompletePopup.getItems().clear(); autocompletePopup.getItems().addAll(menuItems); - // Update popup display state accordingly + // Hide the popup regardless if it should be shown or not. + // + // This is done to work around a weird quirk/bug in JavaFX where the text field would steal focus after an + // autocompletion result has been invoked, leading to the inability to use arrow keys to navigate the + // autocomplete list. + autocompletePopup.hide(); + + // Show the popup given that we have items in the list to show. if (menuItems.size() > 0) { - if (!autocompletePopup.isShowing()) { - autocompletePopup.show(AutocompleteTextField.this, Side.BOTTOM, 0, 0); - } - } else { - autocompletePopup.hide(); + autocompletePopup.show(AutocompleteTextField.this, Side.BOTTOM, 0, 0); } } diff --git a/src/main/java/seedu/address/ui/CommandBox.java b/src/main/java/seedu/address/ui/CommandBox.java index 057859a9834..54f26612b81 100644 --- a/src/main/java/seedu/address/ui/CommandBox.java +++ b/src/main/java/seedu/address/ui/CommandBox.java @@ -132,8 +132,8 @@ private void handleCommandEntered() { private void handleCommandAutocompleted(KeyEvent keyEvent) { logger.fine("User invoked auto-completion!"); - boolean hasAutocompletedResult = commandTextField.triggerImmediateAutocompletion(); - if (hasAutocompletedResult) { + var actionResult = commandTextField.triggerImmediateAutocompletion(); + if (actionResult == AutocompleteTextField.ActionResult.EXECUTED) { keyEvent.consume(); commandTextField.requestFocus(); @@ -147,8 +147,8 @@ private void handleCommandAutocompleted(KeyEvent keyEvent) { private void handleCommandUndoAutocomplete(KeyEvent keyEvent) { logger.fine("User invoked undo auto-completion!"); - boolean hasUndoneCompletion = commandTextField.undoLastImmediateAutocompletion(); - if (hasUndoneCompletion) { + var actionResult = commandTextField.undoLastImmediateAutocompletion(); + if (actionResult == AutocompleteTextField.ActionResult.EXECUTED) { keyEvent.consume(); commandTextField.requestFocus(); From fc374bc33707209b200f2713c997672747dac13c Mon Sep 17 00:00:00 2001 From: Wern Date: Sun, 12 Nov 2023 23:47:39 +0800 Subject: [PATCH 08/10] Fix docs --- .../java/seedu/address/ui/AutocompleteTextField.java | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/main/java/seedu/address/ui/AutocompleteTextField.java b/src/main/java/seedu/address/ui/AutocompleteTextField.java index f208e5446bd..81ec8ecc853 100644 --- a/src/main/java/seedu/address/ui/AutocompleteTextField.java +++ b/src/main/java/seedu/address/ui/AutocompleteTextField.java @@ -43,9 +43,7 @@ */ public class AutocompleteTextField extends TextField { - /** - * A snapshot of a text-change due to autocompletion. - */ + /** A snapshot of a text-change due to autocompletion. */ protected static class AutocompleteSnapshot { public final String partialValue; public final String completedValue; @@ -61,9 +59,7 @@ public String toString() { } } - /** - * An enum of execution status. - */ + /** An enum of execution status. */ public enum ActionResult { EXECUTED, NOT_EXECUTED; } @@ -75,7 +71,7 @@ public enum ActionResult { @FunctionalInterface public interface CompletionGenerator extends Function> { } - // Internal JavaFX ID values + /** Internal ID prefix to note the exact menu item per index. */ private static final String AUTOCOMPLETE_MENU_ITEM_ID_PREFIX = "autocomplete-completion-item-"; // GUI elements @@ -321,7 +317,7 @@ public void refreshPopupState() { HBox completionBox = new HBox(completionLabelFront, completionLabelBack); completionBox.getStyleClass().add("autocomplete-box"); completionBox.setPadding(new Insets(0, 8, 0, 8)); - completionBox.setAlignment(Pos.BASELINE_CENTER); + completionBox.setAlignment(Pos.BASELINE_LEFT); // Create the context menu item CustomMenuItem item = new CustomMenuItem(completionBox, false); From 178451488b7dabe401d6c0ff6735a707bddb8bfd Mon Sep 17 00:00:00 2001 From: Wern Date: Mon, 13 Nov 2023 01:23:54 +0800 Subject: [PATCH 09/10] Improve code quality --- .../java/seedu/address/commons/core/Version.java | 2 +- src/main/java/seedu/address/logic/Logic.java | 8 +++++--- .../logic/autocomplete/AutocompleteSupplier.java | 3 +-- .../java/seedu/address/logic/parser/AppParser.java | 10 +++++----- .../address/logic/parser/ArgumentTokenizer.java | 2 +- .../java/seedu/address/logic/parser/Parser.java | 1 - src/main/java/seedu/address/model/Model.java | 14 ++++++++------ .../seedu/address/ui/AutocompleteTextField.java | 2 +- src/main/java/seedu/address/ui/ContactCard.java | 9 ++++----- 9 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/main/java/seedu/address/commons/core/Version.java b/src/main/java/seedu/address/commons/core/Version.java index 491d24559b4..35323cf6808 100644 --- a/src/main/java/seedu/address/commons/core/Version.java +++ b/src/main/java/seedu/address/commons/core/Version.java @@ -7,7 +7,7 @@ import com.fasterxml.jackson.annotation.JsonValue; /** - * Represents a version with major, minor and patch number + * Represents a version with major, minor and patch number. */ public class Version implements Comparable { diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java index bd09086c06f..65f0c9444c3 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/address/logic/Logic.java @@ -18,6 +18,7 @@ public interface Logic { /** * Executes the command and returns the result. + * * @param commandText The command as entered by the user. * @return the result of the command execution. * @throws CommandException If an error occurs during command execution. @@ -27,6 +28,7 @@ public interface Logic { /** * Parses the command and returns any autocompletion results. + * * @param commandText The command as entered by the user. * @return the result of the command execution. */ @@ -39,10 +41,10 @@ public interface Logic { */ ReadOnlyAddressBook getAddressBook(); - /** Returns a view of the list of contacts */ + /** Returns an unmodifiable view of the filtered and sorted list of contacts. */ ObservableList getDisplayedContactList(); - /** Returns an unmodifiable view of the filtered list of applications. */ + /** Returns an unmodifiable view of the filtered and sorted list of applications. */ ObservableList getDisplayedApplicationList(); /** @@ -56,7 +58,7 @@ public interface Logic { GuiSettings getGuiSettings(); /** - * Set the user prefs' GUI settings. + * Sets the user prefs' GUI settings. */ void setGuiSettings(GuiSettings guiSettings); } diff --git a/src/main/java/seedu/address/logic/autocomplete/AutocompleteSupplier.java b/src/main/java/seedu/address/logic/autocomplete/AutocompleteSupplier.java index 45ab8ebc1c9..b35f68974e8 100644 --- a/src/main/java/seedu/address/logic/autocomplete/AutocompleteSupplier.java +++ b/src/main/java/seedu/address/logic/autocomplete/AutocompleteSupplier.java @@ -110,8 +110,7 @@ public Set getOtherPossibleFlagsAsideFromFlagsPresent(Set flagsPrese * * @param flag The flag to check against. This may be null to represent the preamble. * @param currentCommand The current command structure. This should not be null. - * @param model The model to be supplied for generation. This may be null if model-data is not essential - * for any purpose. + * @param model The model to be supplied for generation. This may be null if the model is unavailable. */ public Optional> getValidValues(Flag flag, PartitionedCommand currentCommand, Model model) { try { diff --git a/src/main/java/seedu/address/logic/parser/AppParser.java b/src/main/java/seedu/address/logic/parser/AppParser.java index 722c70e6006..fd64851f330 100644 --- a/src/main/java/seedu/address/logic/parser/AppParser.java +++ b/src/main/java/seedu/address/logic/parser/AppParser.java @@ -35,9 +35,9 @@ public class AppParser { /** * Parses user input into command for execution. * - * @param userInput full user input string - * @return the command based on the user input - * @throws ParseException if the user input does not conform the expected format + * @param userInput full user input string. + * @return the command based on the user input. + * @throws ParseException if the user input does not conform the expected format. */ public Command parseCommand(String userInput) throws ParseException { final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); @@ -107,8 +107,8 @@ public Command parseCommand(String userInput) throws ParseException { /** * Parses user input into an evaluator that can be executed to obtain autocompletion results. * - * @param userInput full user input string - * @return the command based on the user input + * @param userInput full user input string. + * @return the command based on the user input. */ public AutocompleteGenerator parseCompletionGenerator(String userInput) { final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); diff --git a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java b/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java index e0277a1ff81..b77fb3c3e1b 100644 --- a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java +++ b/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java @@ -83,7 +83,7 @@ public static ArgumentMultimap autoTokenize(String argsString, Flag... mainFlags /** * Splits an arguments string into individual words, separated by space. * - * @param argsString Arguments string of the form: {@code preamble value value ...} + * @param argsString Arguments string of the form: {@code preamble value value ...}. * @return The terms formed after splitting the arguments string by the space character. */ private static String[] splitByWords(String argsString) { diff --git a/src/main/java/seedu/address/logic/parser/Parser.java b/src/main/java/seedu/address/logic/parser/Parser.java index 73673a7ae3f..d6551ad8e3f 100644 --- a/src/main/java/seedu/address/logic/parser/Parser.java +++ b/src/main/java/seedu/address/logic/parser/Parser.java @@ -13,5 +13,4 @@ public interface Parser { * @throws ParseException if {@code userInput} does not conform the expected format */ T parse(String userInput) throws ParseException; - } diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index 3df9a6d2509..cc88c0388c6 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -18,7 +18,6 @@ * The API of the Model component. */ public interface Model { - /** {@code Predicate} that always evaluate to true */ Predicate PREDICATE_SHOW_ALL_CONTACTS = contact -> true; Predicate PREDICATE_SHOW_ONLY_ORGANIZATIONS = contact -> contact.getType() == Type.ORGANIZATION; Predicate PREDICATE_SHOW_ONLY_RECRUITERS = contact -> contact.getType() == Type.RECRUITER; @@ -141,7 +140,7 @@ public interface Model { Contact getContactByIdXorIndex(Id id, Index index) throws IllegalValueException; /** - * Replaces the old {@code JobApplication} with the new {@code JobApplication} + * Replaces the old {@code JobApplication} with the new {@code JobApplication}. */ void replaceApplication(JobApplication oldApplication, JobApplication newApplication) throws IllegalValueException; @@ -151,18 +150,21 @@ public interface Model { void deleteApplication(JobApplication application) throws IllegalValueException; - /** Returns an unmodifiable view of the displayed contact list */ + /** Returns an unmodifiable view of the displayed contact list. */ ObservableList getDisplayedContactList(); /** - * Updates the filter of the filtered contact list to filter by the given {@code predicate}. - * @throws NullPointerException if {@code predicate} is null. + * Updates the filtered contact list to filter by the given {@code predicate}. May not be null. */ void updateFilteredContactList(Predicate predicate); + /** + * Updates the sorted contact list to sort by the given {@code comparator}. May be null to disable sorting. + */ void updateSortedContactList(Comparator comparator); - /** Returns an unmodifiable view of the filtered application list */ + + /** Returns an unmodifiable view of the displayed application list. */ ObservableList getDisplayedApplicationList(); void updateSortedApplicationList(Comparator comparator); diff --git a/src/main/java/seedu/address/ui/AutocompleteTextField.java b/src/main/java/seedu/address/ui/AutocompleteTextField.java index 81ec8ecc853..3afa991b164 100644 --- a/src/main/java/seedu/address/ui/AutocompleteTextField.java +++ b/src/main/java/seedu/address/ui/AutocompleteTextField.java @@ -364,7 +364,7 @@ public void refreshPopupState() { } /** - * Update undo history tracked state based on the change for text field old values to new values. + * Updates undo history tracked state based on the change for text field old values to new values. */ protected void updateUndoHistoryState(String previousValue, String currentValue) { diff --git a/src/main/java/seedu/address/ui/ContactCard.java b/src/main/java/seedu/address/ui/ContactCard.java index 46bc7d44563..7ef863a41e7 100644 --- a/src/main/java/seedu/address/ui/ContactCard.java +++ b/src/main/java/seedu/address/ui/ContactCard.java @@ -15,7 +15,7 @@ import seedu.address.model.contact.Recruiter; /** - * An UI component that displays information of a {@code Contact}. + * A UI component that displays information of a {@code Contact}. */ public class ContactCard extends UiPart { @@ -83,10 +83,9 @@ public ContactCard(Contact contact, int displayedIndex) { final Optional linkedOrgId = recruiter.getOrganizationId(); - setVboxInnerLabelText( - linkedParentOrganization, () -> - linkedOrgId.map(oid -> String.format( - "from %s (%s)", "organization" /* TODO: Use org name instead */, oid.value)) + // TODO: This should display organization name instead of ID in the future + setVboxInnerLabelText(linkedParentOrganization, () -> + linkedOrgId.map(oid -> String.format("from %s (%s)", "organization", oid.value)) .orElse(null) ); break; From 9b8c8f873a35f4f7cf19a973d6feb9c6212fd613 Mon Sep 17 00:00:00 2001 From: Wern Date: Mon, 13 Nov 2023 01:58:56 +0800 Subject: [PATCH 10/10] Add some comments and minor tweaks --- .../AutocompleteGeneratorTest.java | 46 +++++++++++++++---- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/src/test/java/seedu/address/logic/autocomplete/AutocompleteGeneratorTest.java b/src/test/java/seedu/address/logic/autocomplete/AutocompleteGeneratorTest.java index 391148d6cb8..435c4e7e562 100644 --- a/src/test/java/seedu/address/logic/autocomplete/AutocompleteGeneratorTest.java +++ b/src/test/java/seedu/address/logic/autocomplete/AutocompleteGeneratorTest.java @@ -3,7 +3,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import java.util.List; -import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -38,6 +37,8 @@ public void generateCompletions_usingGivenExpectedCommands_correctResult() { "almond" ); + // Generates, in the correct order, the correct set of completions from the source list and the given input, + // by subsequence matching. assertEquals( resultList, new AutocompleteGenerator(sourceList::stream) @@ -57,20 +58,21 @@ public void generateCompletions_usingAutocompleteSupplier_correctResult() { Flag flagC1 = new Flag("cde", "c"); Flag flagC2 = new Flag("code"); - AutocompleteSupplier supplier = new AutocompleteSupplier( + AutocompleteSupplier supplier = AutocompleteSupplier.from( AutocompleteItemSet.concat( AutocompleteItemSet.onceForEachOf(flagA1, flagA2, flagA3), AutocompleteItemSet.anyNumberOf(flagB, flagC1, flagC2) ).addConstraint( AutocompleteConstraint.oneAmongAllOf(flagA1, flagA2) - ), - Map.of( - flagA3, (c, m) -> Stream.of("apple", "banana", "car"), - flagC1, (c, m) -> null ) - ); + ).configureValueMap(map -> { + map.put(flagA3, (c, m) -> Stream.of("apple", "banana", "car")); + map.put(flagC1, (c, m) -> null); + map.put(flagC2, null); + }); // autocomplete: -a + // - flag subsequence matching (same priority results returned in original order) assertEquals( List.of( "cmd --aaa", @@ -83,6 +85,7 @@ public void generateCompletions_usingAutocompleteSupplier_correctResult() { ); // autocomplete: -b + // - flag subsequence matching (prefers results closer to prefix) assertEquals( List.of( "cmd --book", @@ -92,6 +95,7 @@ public void generateCompletions_usingAutocompleteSupplier_correctResult() { .generateCompletions("cmd -b") .collect(Collectors.toList()) ); + // - flag subsequence matching (performs constraint validation) assertEquals( List.of( "cmd --aaa --book" @@ -101,6 +105,9 @@ public void generateCompletions_usingAutocompleteSupplier_correctResult() { .generateCompletions("cmd --aaa -b") .collect(Collectors.toList()) ); + + // autocomplete: -b + // - flag value result instant generation (none provided by supplier) assertEquals( List.of(), // leading space yields no results since it's suggesting the part new AutocompleteGenerator(supplier) @@ -109,6 +116,7 @@ public void generateCompletions_usingAutocompleteSupplier_correctResult() { ); // autocomplete: --adg + // - flag value result instant generation (some provided by supplier, immediately given after finishing flag) assertEquals( List.of( "cmd -b --adg apple", @@ -119,6 +127,7 @@ public void generateCompletions_usingAutocompleteSupplier_correctResult() { .generateCompletions("cmd -b --adg ") .collect(Collectors.toList()) ); + // - flag value result matching (performs subsequence match, works with values before it) assertEquals( List.of("cmd -b --adg banana"), new AutocompleteGenerator(supplier) @@ -127,6 +136,7 @@ public void generateCompletions_usingAutocompleteSupplier_correctResult() { ); // autocomplete: --cd + // - flag subsequence generation (performs subsequence match, works with values before it) assertEquals( List.of( "cmd -a x y --cde", @@ -138,6 +148,7 @@ public void generateCompletions_usingAutocompleteSupplier_correctResult() { ); // autocomplete: -o + // - flag subsequence generation (performs subsequence match, constraint validation) assertEquals( List.of( "cmd -a x y --code z --book", @@ -150,9 +161,11 @@ public void generateCompletions_usingAutocompleteSupplier_correctResult() { ); // autocomplete: --cde + // - flag suggestion instant generation (immediately supply flags after a part with no value) (null type 1) assertEquals( List.of( - // Rationale: --cde does not accept values, so a next flag is immediately suggested + // Rationale: --cde does not accept values (FlagValueSupplier *returns* null), + // so a next flag is immediately suggested "cmd --cde --aaa", "cmd --cde --abc", "cmd --cde --adg", @@ -165,5 +178,22 @@ public void generateCompletions_usingAutocompleteSupplier_correctResult() { .collect(Collectors.toList()) ); + // autocomplete: --code + // - flag suggestion instant generation (immediately supply flags after a part with no value) (null type 2) + assertEquals( + List.of( + // Rationale: --code does not accept values (FlagValueSupplier *is* null), + // so a next flag is immediately suggested + "cmd --code --aaa", + "cmd --code --abc", + "cmd --code --adg", + "cmd --code --book", + "cmd --code --cde", + "cmd --code --code" + ), + new AutocompleteGenerator(supplier) + .generateCompletions("cmd --code ") + .collect(Collectors.toList()) + ); } }