Skip to content

Commit

Permalink
Add final validation to positional arguments (#91)
Browse files Browse the repository at this point in the history
  • Loading branch information
rvesse committed Apr 16, 2019
1 parent d711152 commit 96cd226
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,22 @@
/**
* Annotation to mark a field as the default option
* <p>
* This means that supplying the name of the associated option is optional since
* any non-option input seen will be treating as supplying a value for the
* associated option.
* </p>
* <p>
* This annotation can only be used <strong>once</strong> on a command field
* provided the following requirements are met:
* </p>
* <ul>
* <li>The field is also annotated with {@link Option}</li>
* <li>The {@linkplain Option} annotation has an arity of 1</li>
* <li>The {@linkplain Option} annotation has a type of {@link OptionType#COMMAND}</li>
* <li>The command does not have any field annotated with {@link Arguments}</li>
* <li>The {@linkplain Option} annotation has a type of
* {@link OptionType#COMMAND}</li>
* <li>The command does not have any field annotated with {@link Arguments} or
* {@link PositionalArgument} as otherwise it would be ambiguous as to whether a
* non-option value should be parsed as an argument or as an option value</li>
* </ul>
*
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,26 +76,30 @@ public CommandMetadata(String name,
// Check that we don't have required positional/non-positional arguments
// after optional positional arguments
boolean posArgsRequired = false;
for (int i = 0; i < this.positionalArgs.size(); i++) {
if (i == 0) {
posArgsRequired = this.positionalArgs.get(i).isRequired();
} else if (!posArgsRequired && this.positionalArgs.get(i).isRequired()) {
throw new IllegalArgumentException(String.format(
"Positional argument %d (%s) is declared as @Required but one/more preceeding positional arguments are optional",
i, this.positionalArgs.get(i).getTitle()));
} else {
posArgsRequired = this.positionalArgs.get(i).isRequired();
if (this.positionalArgs != null) {
for (int i = 0; i < this.positionalArgs.size(); i++) {
if (i == 0) {
posArgsRequired = this.positionalArgs.get(i).isRequired();
} else if (!posArgsRequired && this.positionalArgs.get(i).isRequired()) {
throw new IllegalArgumentException(String.format(
"Positional argument %d (%s) is declared as @Required but one/more preceeding positional arguments are optional",
i, this.positionalArgs.get(i).getTitle()));
} else {
posArgsRequired = this.positionalArgs.get(i).isRequired();
}
}
}
if (this.positionalArgs.size() > 0 && !posArgsRequired) {
if (this.arguments.isRequired()) {
throw new IllegalArgumentException(
"Non-positional arguments are declared as required but one/more preceding positional arguments are optional");
if (this.positionalArgs.size() > 0 && !posArgsRequired) {
if (this.arguments.isRequired()) {
throw new IllegalArgumentException(
"Non-positional arguments are declared as required but one/more preceding positional arguments are optional");
}
}
}

if (this.defaultOption != null && this.arguments != null) {
throw new IllegalArgumentException("Command cannot declare both @Arguments and @DefaultOption");
if (this.defaultOption != null
&& (this.arguments != null || (this.positionalArgs != null && this.positionalArgs.size() > 0))) {
throw new IllegalArgumentException(
"Command cannot declare both @Arguments/@PositionalArgument and use @DefaultOption");
}

this.metadataInjections = AirlineUtils.unmodifiableListCopy(metadataInjections);
Expand Down Expand Up @@ -185,7 +189,7 @@ public String toString() {
sb.append(" , globalOptions=").append(globalOptions).append('\n');
sb.append(" , groupOptions=").append(groupOptions).append('\n');
sb.append(" , commandOptions=").append(commandOptions).append('\n');
sb.append(" , positionalArguments=").append(positionalArgs).append('\n');
sb.append(" , positionalArguments=").append(positionalArgs).append('\n');
sb.append(" , arguments=").append(arguments).append('\n');
sb.append(" , metadataInjections=").append(metadataInjections).append('\n');
sb.append(" , type=").append(type).append('\n');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@
import com.github.rvesse.airline.model.OptionMetadata;
import com.github.rvesse.airline.model.ParserMetadata;
import com.github.rvesse.airline.parser.aliases.AliasResolver;
import com.github.rvesse.airline.parser.errors.ParseException;
import com.github.rvesse.airline.parser.options.OptionParser;
import com.github.rvesse.airline.restrictions.ArgumentsRestriction;
import com.github.rvesse.airline.restrictions.OptionRestriction;
import com.github.rvesse.airline.utils.AirlineUtils;
import com.github.rvesse.airline.utils.predicates.parser.AbbreviatedCommandFinder;
import com.github.rvesse.airline.utils.predicates.parser.AbbreviatedGroupFinder;
Expand Down Expand Up @@ -275,7 +278,8 @@ private ParseState<T> parseArg(ParseState<T> state, PeekingIterator<String> toke
OptionMetadata defaultOption) {
if (arguments != null || positionalArgs.size() > 0) {
// Argument
state = state.withArgument(positionalArgs, arguments, tokens.next());
state = state.pushContext(Context.ARGS);
state = state.withArgument(positionalArgs, arguments, tokens.next()).popContext();
} else if (defaultOption != null) {
// Default Option
state = state.pushContext(Context.OPTION).withOption(defaultOption);
Expand All @@ -286,4 +290,63 @@ private ParseState<T> parseArg(ParseState<T> state, PeekingIterator<String> toke
}
return state;
}

/**
* Validates that a command meets all the option, positional argument and
* argument level restrictions
*
* @param state
* Parser state
* @param command
* Command meta-data
*/
protected void validateCommand(ParseState<T> state, CommandMetadata command) {
if (command == null)
return;

// Positional arguments restrictions
List<PositionalArgumentMetadata> posArgs = command.getPositionalArguments();
if (posArgs != null) {
for (PositionalArgumentMetadata posArg : posArgs) {
for (ArgumentsRestriction restriction : posArg.getRestrictions()) {
if (restriction == null)
continue;
try {
restriction.finalValidate(state, posArg);
} catch (ParseException e) {
state.getParserConfiguration().getErrorHandler().handleError(e);
}
}
}
}

// Arguments restrictions
ArgumentsMetadata arguments = command.getArguments();
if (arguments != null) {
for (ArgumentsRestriction restriction : arguments.getRestrictions()) {
if (restriction == null)
continue;
try {
restriction.finalValidate(state, arguments);
} catch (ParseException e) {
state.getParserConfiguration().getErrorHandler().handleError(e);
}
}
}

// Option restrictions
for (OptionMetadata option : command.getAllOptions()) {
if (option == null)
continue;
for (OptionRestriction restriction : option.getRestrictions()) {
if (restriction == null)
continue;
try {
restriction.finalValidate(state, option);
} catch (ParseException e) {
state.getParserConfiguration().getErrorHandler().handleError(e);
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@

/**
* Represents parsing results
*
* @author rvesse
*
* @param <T>
* Command type
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,31 @@
*/
package com.github.rvesse.airline.parser.command;

import com.github.rvesse.airline.model.ArgumentsMetadata;
import com.github.rvesse.airline.model.CommandMetadata;
import com.github.rvesse.airline.model.GlobalMetadata;
import com.github.rvesse.airline.model.OptionMetadata;
import com.github.rvesse.airline.parser.AbstractCommandParser;
import com.github.rvesse.airline.parser.ParseResult;
import com.github.rvesse.airline.parser.ParseState;
import com.github.rvesse.airline.parser.errors.ParseException;
import com.github.rvesse.airline.restrictions.ArgumentsRestriction;
import com.github.rvesse.airline.restrictions.GlobalRestriction;
import com.github.rvesse.airline.restrictions.OptionRestriction;

/**
* A parser that parses full CLIs
*
* @param <T>
* Command type
*/
public class CliParser<T> extends AbstractCommandParser<T> {

/**
* Parses the input arguments returning the parse result
*
* @param metadata
* CLI meta-data
* @param args
* CLI arguments to parse
* @return Parse result
*/
public ParseResult<T> parseWithResult(GlobalMetadata<T> metadata, Iterable<String> args) {
if (args == null)
throw new NullPointerException("args cannot be null");
Expand All @@ -49,6 +60,16 @@ public ParseResult<T> parseWithResult(GlobalMetadata<T> metadata, Iterable<Strin
return metadata.getParserConfiguration().getErrorHandler().finished(state);
}

/**
* Parses the input arguments returning the users specified command
*
* @param metadata
* CLI meta-data
* @param args
* CLI arguments to parse
* @return Command which may be {@code null} if a command could not be
* parsed
*/
public T parse(GlobalMetadata<T> metadata, Iterable<String> args) {
ParseResult<T> result = parseWithResult(metadata, args);
return result.getCommand();
Expand Down Expand Up @@ -76,36 +97,6 @@ protected void validate(ParseState<T> state) {
}
}
CommandMetadata command = state.getCommand();
if (command != null) {

// Argument restrictions
ArgumentsMetadata arguments = command.getArguments();
if (arguments != null) {
for (ArgumentsRestriction restriction : arguments.getRestrictions()) {
if (restriction == null)
continue;
try {
restriction.finalValidate(state, arguments);
} catch (ParseException e) {
state.getParserConfiguration().getErrorHandler().handleError(e);
}
}
}

// Option restrictions
for (OptionMetadata option : command.getAllOptions()) {
if (option == null)
continue;
for (OptionRestriction restriction : option.getRestrictions()) {
if (restriction == null)
continue;
try {
restriction.finalValidate(state, option);
} catch (ParseException e) {
state.getParserConfiguration().getErrorHandler().handleError(e);
}
}
}
}
validateCommand(state, command);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,35 @@

import org.apache.commons.collections4.IteratorUtils;

import com.github.rvesse.airline.model.ArgumentsMetadata;
import com.github.rvesse.airline.model.CommandMetadata;
import com.github.rvesse.airline.model.OptionMetadata;
import com.github.rvesse.airline.model.ParserMetadata;
import com.github.rvesse.airline.parser.AbstractCommandParser;
import com.github.rvesse.airline.parser.ParseResult;
import com.github.rvesse.airline.parser.ParseState;
import com.github.rvesse.airline.parser.errors.ParseException;
import com.github.rvesse.airline.restrictions.ArgumentsRestriction;
import com.github.rvesse.airline.restrictions.GlobalRestriction;
import com.github.rvesse.airline.restrictions.OptionRestriction;

/**
* A parser that parses a single command
*
* @param <T>
* Command type
*/
public class SingleCommandParser<T> extends AbstractCommandParser<T> {

/**
* Parses the command
*
* @param parserConfig
* Parser configuration
* @param commandMetadata
* Command meta-data
* @param restrictions
* Global restrictions to apply
* @param args
* Command arguments to parse
* @return Parse result
*/
public ParseResult<T> parseWithResult(ParserMetadata<T> parserConfig, CommandMetadata commandMetadata,
Iterable<GlobalRestriction> restrictions, Iterable<String> args) {
if (args == null)
Expand All @@ -45,6 +60,19 @@ public ParseResult<T> parseWithResult(ParserMetadata<T> parserConfig, CommandMet

}

/**
* Parses the command
*
* @param parserConfig
* Parser configuration
* @param commandMetadata
* Command meta-data
* @param restrictions
* Global restrictions to apply
* @param args
* Command arguments to parse
* @return Command which may be {@code null} if parsing failed
*/
public T parse(ParserMetadata<T> parserConfig, CommandMetadata commandMetadata,
Iterable<GlobalRestriction> restrictions, Iterable<String> args) {
ParseResult<T> result = parseWithResult(parserConfig, commandMetadata, restrictions, args);
Expand Down Expand Up @@ -73,35 +101,6 @@ protected void validate(ParseState<T> state, List<GlobalRestriction> restriction
}
}
CommandMetadata command = state.getCommand();
if (command != null) {
// Arguments restrictions
ArgumentsMetadata arguments = command.getArguments();
if (arguments != null) {
for (ArgumentsRestriction restriction : arguments.getRestrictions()) {
if (restriction == null)
continue;
try {
restriction.finalValidate(state, arguments);
} catch (ParseException e) {
state.getParserConfiguration().getErrorHandler().handleError(e);
}
}
}

// Option restrictions
for (OptionMetadata option : command.getAllOptions()) {
if (option == null)
continue;
for (OptionRestriction restriction : option.getRestrictions()) {
if (restriction == null)
continue;
try {
restriction.finalValidate(state, option);
} catch (ParseException e) {
state.getParserConfiguration().getErrorHandler().handleError(e);
}
}
}
}
validateCommand(state, command);
}
}

0 comments on commit 96cd226

Please sign in to comment.