Nasdanika Rules is a framework for building solutions which inspect data for compliance with a set of rules/standards/principles/guidelines. For example:
The below diagram shows the key concepts of the framework and their relationships:
Green shapes represent Ecore model elements, blue shapes represent Java-only concepts.
An important thing to note is that rules can be used without inspectors and inspectors can be used without rules. One usage scenario is to define and publish a set of rules and then gradually introduce inspectors enforcing the rules.
Rules and rule sets can be created programmatically, loaded from XMI or YAML, or defined in inspector and inspector set annotations. This section explains how to define rule sets and rules in YAML and register rule set capability factories.
Below is an example of rule set YAML definition:
rules-rule-set:
uris: nasdanika://rules/demo-rule-set
id: demo-rule-set
name: Demo Rule Set
documentation:
content-markdown:
source:
content-resource:
location: demo-rule-set.md
interpolate: true
severities:
error:
name: Error
documentation:
content-markdown:
source:
content-text: |+2
Inline markdown ``example``.
* One
* Two
rules:
my-rule:
rules-rule:
name: My rule
severity: nasdanika://rules/demo-rule-set/severities/error
Consult rule set load specification for supported configuration keys. See Markdown for details regarding how to write markdown documentation.
Rule sets can be registered as capabilities to make them available to reflective inspectors. To register a rule set create a class extending RuleSetCapabilityFactory:
public class DemoRuleSetCapabilityFactory extends RuleSetCapabilityFactory {
@Override
protected URI getResourceSetURI() {
return URI.createURI("demo-rule-set.yml").resolve(Util.createClassURI(getClass()));
}
}
The above factory loads rule set from a class loader resource. Then open the package which contains rule set definition and add provides
to module-info.java
:
opens <rule set package>;
provides CapabilityFactory with DemoRuleSetCapabilityFactory;
After that rule set and its rules can be referenced from reflective inspectors, used by CLI commands to list available rule sets and generate documentation, and loaded programmatically as shown below:
CapabilityLoader capabilityLoader = new CapabilityLoader();
ProgressMonitor progressMonitor = ...
Iterable<CapabilityProvider<Object>> ruleSetProviders = capabilityLoader.load(ServiceCapabilityFactory.createRequirement(RuleSet.class), progressMonitor);
for (CapabilityProvider<Object> provider: ruleSetProviders) {
provider.getPublisher().subscribe(ruleSetConsumer, errorConsumer);
}
Inspectors implement Inspector interface. They can be created by “traditional” means of implementing Inspector
interfaces and by using Inspector annotation. The “traditional” way is kinda obvious, so this section focuses on how to create inspectors using annotations and register then as capabilities.
// Name is derived from the class name
@RuleSet("""
severities:
error:
name: Error
description: Artifacts with this severity are not allowed to be further processed (e.g. deployed, published to a repository)
documentation:
exec.content.Markdown:
source:
exec.content.Text: |
TODO:
* specRef attribute to RuleSet and Rule - support of loading from classloader resources
* Generation of HTML documentation
""")
public class DemoInspectors {
@Inspector(value = """
name: Invalid YAML
documentation:
exec.content.Markdown:
source:
exec.content.Text: |
YAML with syntax errors, e.g. duplicate keys.
""",
severity = "error",
condition = "!errors.isEmpty()")
public Collection<String> invalidYaml(YamlResource yamlResource) {
return yamlResource.getErrors().stream().map(Diagnostic::getMessage).toList();
}
@Inspector(rule = "nasdanika://rules/demo-rule-set/rules/my-rule")
public String myRuleInspector(YamlResource yamlResource) {
return "My finding";
}
}
In the above snippet the class is a collection of inspector methods. It is annotated with RuleSet annotation which defines an in-line rule set. invalidYaml
is annotated with Inspector annotation which contains in-line rule definition. myRuleInspector
is also annotated with Inspector annotation, but references an externally defined rule by its logical URI - nasdanika://rules/demo-rule-set/rules/my-rule
.
Type of the first parameter of inspector methods defines the inspection target type, YamlResource
in the above example. Inspector set dispatches targets it inspects to compatible methods. Inspector methods may have parameters compatible with the below types in any order:
BiPredicate
- takes inspected object and InspectionResult. Can be used if the inspector reports more than one finding or findings shall be associated an object other than the target.Return values of non-void inspector methods are processed as follows:
Iterator
, Iterable
, Stream
, or array then it is iterated and each element is processed as explained hereInspectionResult
(Violation
or Failure
) then it is used AS-ISString
then it is wrapped into a Violation
. The string value is used as the violation name. The violation is associated with the inspector rule.If an inspector method throws an exception, it is wrapped into a Failure
.
To register an inspector, create a subclass of InspectorCapabilityFactory:
public class ReflectiveInspectorFactory extends InspectorCapabilityFactory<Object> {
@Override
protected CompletionStage<Iterable<CapabilityProvider<Inspector<Object>>>> createService(
Class<Inspector<Object>> serviceType,
Predicate<Inspector<Object>> serviceRequirement,
BiFunction<Object, ProgressMonitor, CompletionStage<Iterable<CapabilityProvider<Object>>>> resolver,
ProgressMonitor progressMonitor) {
Inspector<Object> inspector = new InspectorSet(
RuleManager.LOADING_RULE_MANAGER,
serviceRequirement,
false,
progressMonitor,
new DemoInspectors());
return serviceRequirement == null || serviceRequirement.test(inspector) ? wrap(inspector) : empty();
}
}
and add opens
and provides
to module-info.java
:
opens <inspectors package> to org.nasdanika.common; // For inspector reflection
provides CapabilityFactory with <factory class>;
Registered inspectors can be loaded by calling Inspector.load()
. This method can load all registered inspectors or inspectors matching a predicate, e.g. enforcing rules from a specific rule set.
Inspectors are responsible for traversing (visiting) their targets and shall be aware of the targets’ internal structure. NotifierInspector is aware of internal structures of Notifiers - ResourceSet, Resource, EObject. To inspect notifiers:
NotifierInspector.adapt()
asContentInspector()
inspect
Inspector<Object> inspector = loadInspector(progressMonitor.split("Loading Inspector", 1));
NotifierInspector notifierInspector = NotifierInspector.adapt(inspector);
Resource inputResource = resourceSet.getResource(input, true); // Notifier to inspect
notifierInspector
.asContentsInspector(parallel, createPredicate(input))
.inspect(
inputResource,
inspectionResultConsumer,
context,
inputProgressMonitor);
org.nasdanika.models.rules.cli
module provides several concrete and abstract command classes and mix-ins for building commands which deal with rule sets, rules, and inspectors.
For concrete classes see Nasdanika CLI rules documentation. Some abstract classes are outlined below, consult JavaDoc for more details.
This command provides options to include and exclude rules and rule sets. All options can be specified more than once.
--exclude-rule
- URI of a rule to exclude from, say, inspection.--include-rule
- URI of a rule to include to, say, inspection.--exclude-rule-set
- URI of a rule set to exclude from, say, inspection.--include-rule-set
- URI of a rule set to include to, say, inspection.If neither of --include-rule
or --include-rule-set
options are specified, then all rules and rule sets are included by default unless they are excluded by one of exclude options. Include and exclude options can be used together. For example, you may include a rule set and then exclude some rules from it.
This command extends AbstractRuleCommand. It loads registered inspectors using Inspector.load()
and includes only inspectors for matching rules and rule sets.
This command can be used as a base for commands which generate documentation about available rules and rule sets.
Below is a fragment of the list-rules command which extends AbstractInspectorCommand
:
@Command(
description = "Lists available rules",
name = "list-inspectable-rules",
versionProvider = ModuleVersionProvider.class,
mixinStandardHelpOptions = true)
@ParentCommands(RootCommand.class)
public class ListInspectableRulesCommand extends AbstractInspectorCommand {
...
@Override
public Integer call() throws Exception {
ProgressMonitor progressMonitor = progressMonitorMixIn.createProgressMonitor(1);
Inspector<Object> inspector = loadInspector(progressMonitor);
Map<EObject, List<Rule>> grouped = Util.groupBy(inspector.getRules(), EObject::eContainer);
if (output == null) {
generateReport(grouped, System.out, progressMonitor);
} else {
try (PrintStream out = new PrintStream(output)) {
generateReport(grouped, out, progressMonitor);
} catch (FileNotFoundException e) {
throw new NasdanikaException(e);
}
}
return 0;
}
...
}
This command extends AbstractInspectorCommand
and provides base functionality for inspecting resources loaded from URI’s and their contents.
In addition to rule and rule set inclusion/exclusion provided by AbstractRuleCommand
this class provides the following options:
-e
, --exclude-resource
- An Ant pattern of resources to exclude from inspection. The pattern is matched against the resource path computed relative to the root resource. For example, dev/*.yml
- exclude YAML files in the dev
folder. This option can be specified multiple times.-i
, --include-resource
- An Ant pattern for resources to include. This option can be specified multiple times.--exclude-type
- model type to exclude from inspection. For example, exclude constructors or field declarations from inspection of Java code. This option can be specified multiple times. Matching includes super types, i.e. excluding Type would exclude its sub-types as well - Class, Interface, … Type can be specified in the following ways:
Type
java.Type
<EPackage NS URI>#//<Class name
, e.g. ecore://nasdanika.org/models/java#//Type
--include-type
- model type to include. This option can be specified multiple times. Can be combined with type exclusion. For example, you may exclude a super-type, but include one of its sub-types. Say, exclude types, but include interfaces. Or, the opposite - include types, but exclude interfaces.-f
, --fail-on
- Names of severities to fail on (return non-zero exit code). E.g. Error
. This option can be specified multiple times.--parallel
- Perform inspection in multiple threads.--stop-on-first-fail
- stop inspection on first failure - a Violation
with severity specified in --fail-on
or a Failure
. This option can be used in builds/pipelines to fail fast.--limit
- Maximum number of results to report. Stop inspection once the limit is reached. This option can be used, for example, to gradually address technical debt - collect a specified number of things to work on in the next iteration (e.g. sprint).Subclasses must implement getInputs()
method and may override createResourceSet()
method to register resource factories, adapter factories, or URI handlers as shown below in a fragment of inspect-yaml command:
@Command(
description = "Demo of YAML inspection",
name = "inspect-yaml",
versionProvider = ModuleVersionProvider.class,
mixinStandardHelpOptions = true)
@ParentCommands(RootCommand.class)
public class InspectYamlCommand extends AbstractInspectionCommand {
@Parameters(description = {
"Files and directories",
"to inspect"
},
arity = "1..*")
File[] inputs;
@Override
protected List<URI> getInputs() {
List<URI> ret = new ArrayList<>();
for (File input: inputs) {
URI uri = URI.createFileURI(input.getAbsolutePath());
if (input.isDirectory()) {
uri = uri.appendSegment("");
}
ret.add(uri);
}
return ret;
}
...
@Override
protected ResourceSet createResourceSet(ProgressMonitor progressMonitor) {
ResourceSet resourceSet = super.createResourceSet(progressMonitor);
// Basic YAML. Add semantic handlers for your problem domain as needed (you'd need to create them).
YamlResourceFactory yamlResourceFactory = new YamlResourceFactory(new NcoreYamlHandler());
Map<String, Object> extensionToFactoryMap = resourceSet.getResourceFactoryRegistry().getExtensionToFactoryMap();
extensionToFactoryMap.put("yml", yamlResourceFactory);
extensionToFactoryMap.put("yaml", yamlResourceFactory);
// To load directories as resources in order to traverse them
resourceSet.getURIConverter().getURIHandlers().add(0, new DirectoryContentFileURIHandler());
return resourceSet;
}
@Override
protected boolean isIncluded(String path) {
String[] includes = getResourceIncludes();
if (includes == null) {
return path.endsWith(".yml") || path.endsWith(".yaml");
}
return super.isIncluded(path);
}
...
}