Ch. 10 Creating a Custom Command
Jellyfish itself can be extended by adding new commands. Commands are objects that perform some type of processing on an SD project. Examples of functionality that can be implemented in commands include:
- Generating documentation directly from a modeling project.
- Generating code from a model.
- Analyzing a model for anti-patterns.
Creating a New Command
Commands are implemented like validators and scenario verbs. A new command typically includes the following
dependencies in its build.gradle
file:
build.gradle
dependencies {
implementation "com.ngc.seaside:jellyfish.api:$jellyfishVersion"
implementation "com.ngc.seaside:jellyfish.service.api:$jellyfishVersion"
implementation "com.ngc.seaside:jellyfish.utilities:$jellyfishVersion"
implementation "com.google.inject:guice:$guiceVersion"
implementation "com.google.inject.extensions:guice-multibindings:$jellyfishVersion"
}
Jellyfish commands implement either the ICommand
or IJellyfishCommand
interface. Commands that implement the
ICommand
interface are commands that do not require a System Descriptor in order to be run; whereas, commands
that implement IJellyfishCommand
do require a System Descriptor. Most of the commands included with Jellyfish
implement the IJellyfishCommand
interface.
Types of Commands
Various abstract base classes are provided to make implementing commands easier. Generic commands can extend the
class AbstractJellyfishCommand
. This includes commands that generate code, documentation, etc.
Commands may also perform various types of analyses and may report any findings. These findings are consolidated into
a final report when jellyfish analyze
is executed. These types of commands typically extend
AbstractJellyfishAnalysisCommand
.
An Example Command
Below is an example of a simple command that is ready to be implemented:
A skeleton of an AbstractJellyfishCommand
public class ExampleCommand extends AbstractJellyfishCommand {
private static final String NAME = "example-command";
public CreateJellyFishGradleProjectCommand() {
super(NAME);
}
@Override
protected IUsage createUsage() {
// This is used when showing help to the user.
return new DefaultUsage(
"This is the description of what the command does and how to use it.",
// These are required and optional parameters for the command.
new DefaultParameter<>("foo").setDescription("the foo parameter is used to ...").setRequired(true),
new DefaultParameter<>("bar").setDescription("the bar parameter is used to ...").setRequired(false)
);
}
@Override
protected void doRun() {
// The command options provide the runtime arguments given to Jellyfish as well as the System Descriptor project
// that Jellyfish is being run against.
IJellyFishCommandOptions commandOptions = getOptions();
// Do any logic here.
}
}
The NAME
of the command is what users will use to run the command. For example, a user might run this command as follows:
jellyfish example-command foo=...
.
createUsage
is where the command describes itself. The usage object includes a description of the command and the
parameters that the command supports. Each parameter has a name, a description, and may be required or optional. This
information is used when showing help to the user.
The doRun
method is where the actual logic of the command resides. The getOptions
method
provided by the base class allows commands to obtain a reference to the System Descriptor project that Jellyfish
is being run against. Users can explore the SD project like this:
Exploring an SD project programmatically
IJellyFishCommandOptions commandOptions = getOptions();
ISystemDescriptor project = commandOptions.getSystemDescriptor();
// Get a particular model:
Optional<IModel> model = project.findModel("com.foo.MyModel");
// Explore all models in the project:
for (IPackage sdPackage : project.getPackages()) {
for (IModel model : sdPackage.getModels()) {
// Do something with the model.
System.out.println("Found the model " + model.getFullyQualifiedName());
}
}
Users can also use the IJellyFishCommandOptions
to retrieve arguments provided by the user. For example,
Getting runtime arguments
IJellyFishCommandOptions commandOptions = getOptions();
String foo = commandOptions.getParameters().getParameter("foo").getStringValue();
Commonly used parameters can be found in the CommonParameters
enumeration. These can be referenced both when
creating usages and getting parameter values:
Using CommonParameters
@Override
protected IUsage createUsage() {
return new DefaultUsage(
"This is the description of what the command does and how to use it.",
// These are required parameters for the command.
CommonParameters.MODEL.required(),
new DefaultParameter<>("foo").setDescription("the foo parameter is used to ...").setRequired(true),
new DefaultParameter<>("bar").setDescription("the bar parameter is used to ...").setRequired(false)
);
}
@Override
protected void doRun() {
IJellyFishCommandOptions commandOptions = getOptions();
String modelName = commandOptions.getParameters().getParameter(CommonParameters.MODEL.getName()).getStringValue();
}
Registering the Command
Like validators and scenario verbs, we need to configure a Guice module to register the command. Below is an example module:
Creating a Guice module for the command
public class ExampleCommandModule extends AbstractModule {
@Override
protected void configure() {
Multibinder.newSetBinder(binder(), IJellyFishCommand.class)
.addBinding()
.to(ExampleCommand.class);
}
}
As before, be sure to create a new file in src/main/resources/META-INF/services/
named com.google.inject.Module
. Its
contents should contain the fully qualified class name of the module:
com.google.inject.Module
com.mystuff.modeling.validation.module.ExampleCommandModule
An Example Analysis Command
An analysis command is implemented by extending AbstractJellyfishAnalysisCommand
:
Implementing an analysis command
public class ExampleAnalysisCommand extends AbstractJellyfishAnalysisCommand {
public static final String NAME = "example-analysis";
public AnalyzeInputsOutputsCommand() {
super(NAME);
}
@Override
protected IUsage createUsage() {
return new DefaultUsage("Checks the model has at least one output.", CommonParameters.MODEL);
}
@Override
protected void analyzeModel(IModel model) {
if (model.getOutputs().isEmpty()) {
String message = "The model has no outputs. Consider adding some outputs.";
ISourceLocation location = sourceLocatorService.getLocation(model, false);
SystemDescriptorFinding<?> finding = ExampleFindingTypes.NO_OUTPUTS.createFinding(message, location, 1);
reportFinding(finding);
}
}
}
This analysis checks models to ensure they have at least one output field declared. It does this by overriding the
method analyzeModel
. Analysis commands can also override analyzeData
and analyzeEnumeration
to review data types
and enumerations.
If an analysis finds a problem, it reports the finding using the method reportFinding
. This requires an instance of
SystemDescriptorFinding
. Command writers create findings and finding types like this:
Declaring a new type of finding
public enum ExampleFindingTypes implements ISystemDescriptorFindingType {
NO_OUTPUTS("no-outputs", "docs/no-inputs.md", Severity.WARNING);
private final String id;
private final String description;
private final Severity severity;
ExampleFindingTypes(String id,
String resource,
Severity severity) {
this.id = id;
this.description = AbstractJellyfishAnalysisCommand.getResource(resource, InputsOutputsFindingTypes.class);
this.severity = severity;
}
@Override
public String getId() {
return id;
}
@Override
public String getDescription() {
return description;
}
@Override
public Severity getSeverity() {
return severity;
}
}
This declares the different types of findings. Findings have a unique ID, a description, and a severity level. Note
that this example actually loads the description from a Markdown file. Markdown will be converted to HTML when the
HTML report is generated. In order for this to work, we’ll need a file named no-inputs.md
in the directory
src/main/resources/docs/
. This file might look like this:
no-inputs.md
## Avoid components that have inputs but no outputs
Models of components that receive inputs but do not produce outputs can result in components that cannot be tested or
verified. Minimally, a component should produce some type of output that indicates it has received some input. This
allows the component to be tested as well as inspected. This applies to both components that use pub/sub messaging an
well as components that use request/response to exchange messages.
See [Avoid components that have inputs but no outputs](http://www.some.more.info/stuff) for more information.
Developers create specific instances of a type of finding like this:
Creating an instance of a finding
ISourceLocation location = sourceLocatorService.getLocation(model, false);
SystemDescriptorFinding<?> finding = ExampleFindingTypes.NO_OUTPUTS.createFinding(message, location, 1);
The finding can then be reported.
Registering the Analysis Command
Like any other type of command, analysis commands must be registered in a Guice module:
ExampleAnalysisCommandModule.java
public class ExampleAnalysisCommandModule extends AbstractModule {
@Override
protected void configure() {
Multibinder.newSetBinder(binder(), IJellyFishCommand.class)
.addBinding()
.to(ExampleAnalysisCommand.class);
}
}
Deploying and Using the New Command
Jellyfish commands are deployed like scenario verbs and validators. Copy the JAR that contains the command
to the plugins/
directory of JELLYFISH_USER_HOME
. The example command can now be run like this:
Running the example command
$> jellyfish example-command gav=my.group:my.sd.project:1.0.0 model=my.Model foo=whatever
The example-command
will be invoked on the SD project with the group of my.group
, the artifact ID of
my.sd.project
, and a version of 1.0.0. The command will be invoked with the arguments my.Model
and whatever
.
Likewise, the analysis command can be invoked like this:
Running the analysis command
jellyfish analyze \
analyses=example-analysis \
reports=html-report \
reportName=analysis-results \
outputDirectory=output/results/ \
gav=my.group:my.sd.project:1.0.0
Any findings reported by the command will be in the HTML report located at output/results/analysis-results.html
.
Conclusion
Teams can build a single JAR that contains all their custom validators, scenario verbs, and commands. This JAR can contain a single module that registers all the components. This is useful because it allows teams to distribute a single JAR that contains all of their customizations.