Skip to content

Commit

Permalink
Merge branch 'dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
serjihsklovski committed Aug 5, 2018
2 parents 6b5557f + b4edf6b commit fc99843
Show file tree
Hide file tree
Showing 25 changed files with 842 additions and 45 deletions.
36 changes: 31 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ allprojects {
}
dependencies {
compile 'com.github.serjihsklovski:unicli:0.1.1'
compile 'com.github.serjihsklovski:unicli:0.2.0'
}
// ...
Expand All @@ -38,7 +38,7 @@ argument:
$ java -jar your-app.jar your-task
```

If the task is not specified, the nameless (or *root*) task will be performed.
If the task is not specified, Unicli will try to invoke the *root task*.

### Creating a Unicli Application

Expand All @@ -51,7 +51,9 @@ demoapp
| ├── gradle-wrapper.jar
| └── gradle-wrapper.properties
├── src/main/java/com/someone/demoapp
| ├── cli/RootTask.java
| ├── cli
| | ├── RootTask.java
| | └── DisplayVersionTask.java
| └── Application.java
├── build.gradle
├── gradlew
Expand Down Expand Up @@ -97,7 +99,7 @@ allprojects {
}
dependencies {
compile 'com.github.serjihsklovski:unicli:0.1.1'
compile 'com.github.serjihsklovski:unicli:0.2.0'
}
```

Expand Down Expand Up @@ -128,7 +130,7 @@ package com.someone.demoapp.cli;
import com.serjihsklovski.unicli.annotation.Task;
import com.serjihsklovski.unicli.annotation.Usage;

@Task
@Task(root = true)
public class RootTask {

@Usage
Expand All @@ -139,10 +141,34 @@ public class RootTask {
}
```

#### `src/main/java/com/someone/demoapp/cli/DisplayVersionTask.java`
```java
package com.someone.demoapp.cli;

import com.serjihsklovski.unicli.annotation.Task;
import com.serjihsklovski.unicli.annotation.Usage;

@Task("version")
public class DisplayVersionTask {

@Usage
public void displayVersion() {
System.out.println("0.1.0");
}

}
```

Now let's build the project (it will be a fat JAR) and run it. By default the root
task's `printWelcome` usage method will be performed.
```
$ ./gradlew build
$ java -jar build/libs/demoapp-0.1.0.jar
Welcome to Unicli!
```

But you can also print your application's version:
```
$ java -jar build/libs/demoapp-0.1.0.jar version
0.1.0
```
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ repositories {

dependencies {
testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-core:2.21.0'
}

group = 'com.serjihsklovski.unicli'
version = '0.1.1'
version = '0.2.0'

description = """
Unicli is a frontend framework for building CLI applications in Java.
Expand Down
Binary file modified gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.7-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
46 changes: 10 additions & 36 deletions src/main/java/com/serjihsklovski/unicli/Unicli.java
Original file line number Diff line number Diff line change
@@ -1,59 +1,33 @@
package com.serjihsklovski.unicli;

import com.serjihsklovski.unicli.annotation.Task;
import com.serjihsklovski.unicli.annotation.Usage;
import com.serjihsklovski.unicli.exception.AmbiguityException;
import com.serjihsklovski.unicli.engine.lexer.ArgumentLexer;
import com.serjihsklovski.unicli.engine.lexer.ArgumentLexerImpl;
import com.serjihsklovski.unicli.engine.parser.ArgumentParser;
import com.serjihsklovski.unicli.engine.parser.ArgumentParserImpl;
import com.serjihsklovski.unicli.service.TaskService;
import com.serjihsklovski.unicli.service.TaskServiceImpl;
import com.serjihsklovski.unicli.service.UsageService;
import com.serjihsklovski.unicli.service.UsageServiceImpl;
import com.serjihsklovski.unicli.util.classprovider.ClassProvider;
import com.serjihsklovski.unicli.util.classprovider.ClassProviderImpl;
import com.serjihsklovski.unicli.util.runner.LazyActionsRunner;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;

public class Unicli {

// TODO: temporary implementation to enable single Task with its single Usage (v0.1.0)
public static void run(String root, String... args) {
if (args.length > 0) {
throw new RuntimeException("Cannot parse arguments now :(");
}
ClassProvider classProvider = new ClassProviderImpl();
Set<Class> classPool = classProvider.fetchAllClassesByRoots(Collections.singleton(root));

TaskService taskService = new TaskServiceImpl(classPool);
Set<Class> taskClasses = taskService.getAllTaskClasses().collect(Collectors.toSet());
if (taskClasses.size() == 0) {
return;
}
if (taskClasses.size() > 1) {
throw new AmbiguityException(String.format("Ambiguity: there are multiple classes annotated with `%s`.",
Task.class.getCanonicalName()));
}
Class<?> taskClass = taskClasses.stream().findFirst().orElseThrow(RuntimeException::new);

UsageService usageService = new UsageServiceImpl();
Set<Method> usageMethods = usageService.getAllUsagesByTaskClass(taskClass).collect(Collectors.toSet());
if (usageMethods.size() == 0) {
return;
}
if (usageMethods.size() > 1) {
throw new AmbiguityException(String.format("Ambiguity: there are multiple methods annotated with `%s`.",
Usage.class.getCanonicalName()));
}
Method usageMethod = usageMethods.stream().findFirst().orElseThrow(RuntimeException::new);

try {
Object object = taskClass.newInstance();
usageMethod.invoke(object);
} catch (InstantiationException | InvocationTargetException | IllegalAccessException e) {
e.printStackTrace();
}

ArgumentLexer lexer = new ArgumentLexerImpl();
ArgumentParser parser = new ArgumentParserImpl(taskService, usageService);

LazyActionsRunner.run(parser.parseLexemes(lexer.getLexemes(args)));
}

}
45 changes: 45 additions & 0 deletions src/main/java/com/serjihsklovski/unicli/annotation/Task.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,53 @@

/**
* A class annotated with `@Task` defines a Unicli task.
*
* Unicli tasks are the actions that your CLI application
* performs. For example, it can be disk operations, or
* the business processes automating, or some another
* concrete actions that cover your problems domain.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Task {

/**
* Alias to `name`.
* @see Task#name()
*/
String value() default "";

/**
* The name of the Unicli task with which you can call
* this task through your CLI. For example, if you
* specified the name as "install", then you could
* call it like this:
*
* `$ java -jar your-cli.jar install`
*
* The name must be specified if the task is not the
* root task.
* @see Task#root()
*
* You can use the name's alias (`value`)
* @see Task#value()
*/
String name() default "";

/**
* Should this Unicli task be the root task?
*
* A root task is the Unicli task that can be invoked
* without specifying its name. Example:
*
* `$ java -jar my-cli.jar root-task --param=value`
* `$ java -jar my-cli.jar --param=value`
*
* You do need to specify a name for a root task.
* @see Task#name()
*
* NOTE: only one root task should be defined!
*/
boolean root() default false;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.serjihsklovski.unicli.engine.lexer;

import java.util.stream.Stream;

/**
* Argument lexer makes assumptions about what type
* of the JAR arguments are given into a program.
* To do that, we use the following wrapper class:
* @see com.serjihsklovski.unicli.engine.lexer.Lexeme
*
* In most cases, the argument lexer cannot accept
* the exact type of the lexeme - this is a responsibility
* of another processors (e.g. parsers, interpreters).
* It also does not provide a stateful argument processing.
*
* You can find all the lexeme types in the following enumeration:
* @see com.serjihsklovski.unicli.engine.lexer.Lexeme.LexemeType
*/
public interface ArgumentLexer {

/**
* Makes an assumption for each argument in the
* given array of arguments.
*
* @param args "raw" arguments coming from a program
* @return stream of lexemes
* @see com.serjihsklovski.unicli.engine.lexer.Lexeme
*/
Stream<Lexeme> getLexemes(String... args);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.serjihsklovski.unicli.engine.lexer;

import java.util.regex.Pattern;
import java.util.stream.Stream;

public class ArgumentLexerImpl implements ArgumentLexer {

@Override
public Stream<Lexeme> getLexemes(String... args) {
return Stream.of(args)
.map(arg -> {
if (isValidTaskIdentifier(arg)) {
return new Lexeme(Lexeme.LexemeType.TASK_NAME_OR_VALUE, arg);
} else if (isOption(arg)) {
return new Lexeme(Lexeme.LexemeType.OPTION_OR_VALUE, arg);
} else if (isParameter(arg)) {
return new Lexeme(Lexeme.LexemeType.PARAMETER_OR_VALUE, arg);
} else if (isShortOptionParamList(arg)) {
return new Lexeme(Lexeme.LexemeType.SHORTCUTS_OR_VALUE, arg);
} else {
return new Lexeme(Lexeme.LexemeType.VALUE, arg);
}
});
}

private boolean isValidTaskIdentifier(String arg) {
return Pattern.compile("^[a-zA-Z_](?:-?[a-zA-Z_\\d]+)*$").matcher(arg).matches();
}

private boolean isOption(String arg) {
return Pattern.compile("^--[a-zA-Z_](?:-?[a-zA-Z_\\d]+)*$").matcher(arg).matches();
}

private boolean isParameter(String arg) {
return Pattern.compile("^--[a-zA-Z_](?:-?[a-zA-Z_\\d]+)*=.*$").matcher(arg).matches();
}

private boolean isShortOptionParamList(String arg) {
return Pattern.compile("^-[a-zA-Z]+$").matcher(arg).matches();
}

}
83 changes: 83 additions & 0 deletions src/main/java/com/serjihsklovski/unicli/engine/lexer/Lexeme.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.serjihsklovski.unicli.engine.lexer;

/**
* Represents an argument as a lexeme: `type` + `value`.
*/
public class Lexeme {

/**
* If the argument matches some pattern,
* the lexer can assign the type to it.
*
* @see com.serjihsklovski.unicli.engine.lexer.ArgumentLexer
*/
public enum LexemeType {

/**
* Represents strings, numbers, paths, etc.
* The lexer can assign this type to an argument
* if it does not match any pattern.
*/
VALUE,

/**
* Represents task names. Examples:
* `install`
* `httpGet`
* `my-task`
*/
TASK_NAME_OR_VALUE,

/**
* Represents options. Followed by a task, the options
* are related to this exact task, configuring its
* behaviour. Options start with 2 dashes. Examples:
* `--verbose`
* `--sync`
*/
OPTION_OR_VALUE,

/**
* Represents parameters. Followed by a task, the parameters
* are related to this exact task, setting its required and
* optional parameters. Parameter lexeme consists of the parameter
* name, assignment character, and argument value. Parameter names
* start with 2 dashes. Examples:
* `--file=/tmp/test`
* `--level=4`
* `--empty-value=`
*/
PARAMETER_OR_VALUE,

/**
* Represents shortcuts. Usually, a shortcut is a one-character
* synonym for a task option/parameter. Less often, a shortcut
* can be self-sufficient, i.e. there may not be a full option or
* parameter name at all. Many shortcuts can be joined into one
* so-called shortcut list. The shortcut/shortcut list start
* with a dash. Examples:
* `-o`
* `-xcf`
*/
SHORTCUTS_OR_VALUE,

}

private final LexemeType type;

private final String value;

public Lexeme(LexemeType type, String value) {
this.type = type;
this.value = value;
}

public LexemeType getType() {
return type;
}

public String getValue() {
return value;
}

}
Loading

0 comments on commit fc99843

Please sign in to comment.