Intuitive DSL is a zero-dependency Java library for building and executing domain-specific languages from annotated command classes.
It turns human-readable command strings into fully bound, type-safe command objects, validates command definitions at registration time, and executes each command on a fresh instance created by the engine.
The framework is designed for deterministic behavior, strong validation, rich syntax diagnostics, and straightforward integration into business applications.
- Single-annotation command definition: declare a command with
@DslCommandand define its syntax in iBNF. - Direct binding to fields or methods: bind parameters with
@Bindand boolean-style clauses with@OnClause. - Fresh instance per execution: every
execute(...)call creates a new command instance, eliminating state leakage between executions. - Runtime macro resolution: dynamic
${macro_name}segments are resolved at parse time from application state. - Custom type conversion: built-in converters cover common scalar types and the engine accepts additional converters.
- Strict upfront validation: malformed grammar, invalid metadata, unknown
@Bind(after = ...)contexts, and invalid@OnClause(...)phrases fail during registration. - Rich diagnostics: syntax errors include the unexpected token and the list of expected possibilities.
- Zero runtime dependencies: the library itself has no third-party runtime dependency footprint.
- Java 17+: the project targets Java 17 and is suitable for modern JVM deployments.
<dependency>
<groupId>ch.dbalabs</groupId>
<artifactId>intuitive-dsl</artifactId>
<version>2.0.0</version>
</dependency>The published coordinates in the current project metadata are ch.dbalabs:intuitive-dsl:2.0.0.
A command is a plain Java class annotated with @DslCommand.
The annotation contains:
- a human-readable name
- a syntax string written in the Intuitive DSL grammar notation
A command may:
- implement
Runnable, or - expose a public
run(ExecutionContext)method
If neither is true, registration fails. The engine validates this when the command is registered.
Parameters captured by the parser are injected with @Bind.
You can bind:
- to a field
- or to a single-argument method
When the same logical parameter name appears in multiple places, use @Bind(after = "...") to bind only values that occur after a specific keyword or exact multi-word clause present in the grammar. This context is validated during registration.
Use @OnClause("...") to react to exact clauses found in the parsed input.
@OnClause may target:
- a boolean /
Booleanfield, or - a no-argument method
The clause phrase must correspond to an exact keyword phrase present in the AST. Tail fragments and approximate matches are rejected during registration. Field-backed clause flags are deterministic: absent clauses leave fields at false, including wrapper Boolean.
ExecutionContext is a lightweight key/value store passed to command execution.
Use it to expose runtime state such as:
- authenticated user information
- request metadata
- tenant context
- transaction identifiers
The default implementation is map-backed and created with ExecutionContext.createDefault().
Macros are registered by name and resolved at parse time.
Grammar example:
syntax = "ASSIGN ROLE ${db_roles} TO username ;"At runtime, the engine calls the registered resolver and uses the returned choices as valid alternatives for that macro slot.
Important behavior:
- macro names are case-sensitive
- unknown macros resolve to an empty choice list
- macro values are bound using their semantic matched value
${...}is the canonical macro syntax
The engine stores and resolves macros through the registry and passes resolution into the parser during execute(...).
The binder converts captured strings to the target member type before injection.
The engine supports custom converters via:
engine.registerConverter(TargetType.class, value -> ...);Custom converters are propagated to already registered commands and also apply to commands registered later. The TypeConverter<T> contract is a single-method functional interface:
T convert(String value) throws Exception;The binder maintains a converter registry internally and uses it during injection.
import ch.dbalabs.intuitivedsl.annotation.Bind;
import ch.dbalabs.intuitivedsl.annotation.DslCommand;
import ch.dbalabs.intuitivedsl.annotation.OnClause;
@DslCommand(
name = "PROVISION USER",
syntax = "PROVISION USER username [ WITH AGE user_age ] { AS ACTIVE | AS SUSPENDED } [ FORCE ] ;"
)
public class ProvisionUserCommand implements Runnable {
@Bind("username")
private String username;
@Bind("user_age")
private int age;
@OnClause("AS ACTIVE")
private boolean active;
@OnClause("FORCE")
private boolean forced;
@Override
public void run() {
System.out.printf(
"Provisioning %s (age=%d, active=%b, forced=%b)%n",
username,
age,
active,
forced
);
}
}import ch.dbalabs.intuitivedsl.core.ExecutionContext;
import ch.dbalabs.intuitivedsl.core.IntuitiveDslEngine;
IntuitiveDslEngine engine = new IntuitiveDslEngine();
engine.register(ProvisionUserCommand.class);
ExecutionContext context = ExecutionContext.createDefault();
ProvisionUserCommand cmd = engine.execute(
"PROVISION USER 'john.doe' WITH AGE 35 AS ACTIVE FORCE ;",
context
);Important points:
- registration is done with the class, not with a prebuilt instance
execute(...)returns the fresh command instance used for that execution- the command is bound first, then executed, then returned to the caller
engine.register(MyCommand.class);The engine will instantiate the command through its zero-argument constructor. If instantiation fails, the engine raises a runtime error.
engine.register(MyCommand.class, MyCommand::new);Use a custom factory when command creation depends on your runtime or dependency injection strategy.
The engine validates that the factory:
- does not return
null - returns an instance assignable to the registered command class
@DslCommand(
name = "COPY",
syntax = "COPY { FILE path | DIRECTORY path } TO destination ;"
)
public class CopyCommand implements Runnable {
@Bind(value = "path", after = "FILE")
private String filePath;
@Bind(value = "path", after = "DIRECTORY")
private String directoryPath;
@Bind("destination")
private String destination;
@Override
public void run() {}
}after must reference an exact keyword phrase present in the grammar. For example, the framework accepts an exact phrase such as AS OF, but rejects fragments such as OF when the full clause is AS OF.
@DslCommand(
name = "ARCHIVE",
syntax = "ARCHIVE FILE [ DRY RUN ] ;"
)
public class ArchiveCommand implements Runnable {
@OnClause("DRY RUN")
private boolean dryRun;
@Override
public void run() {}
}Multi-word clauses are supported, but they must match the grammar exactly.
@Bind and @OnClause can also target methods.
This is useful when:
- you want to accumulate repeated values
- you want to transform or validate immediately
- you prefer immutable fields plus setter-like methods
Repeated values are injected in match order. A field typically ends up holding the last injected value, while a method can accumulate every occurrence. This behavior follows directly from the binder iterating over all matching parameters and invoking the target handle for each match.
engine.registerMacro("db_roles", registry -> roleRepository.findAllNames());@DslCommand(
name = "ASSIGN ROLE",
syntax = "ASSIGN ROLE ${db_roles} TO username ;"
)
public class AssignRoleCommand implements Runnable {
@Bind("db_roles")
private String role;
@Bind("username")
private String username;
@Override
public void run() {}
}Notes:
- macro names are resolved by exact name
- the resolver returns a list of allowed choices
- if the resolver returns no choices, the macro cannot match
- multiple different macros can appear in the same command and are resolved independently
A custom converter is required whenever a bound target type is not covered by the binder’s converter registry.
Example:
engine.registerConverter(java.time.LocalDate.class, java.time.LocalDate::parse);Then:
@DslCommand(
name = "SCHEDULE",
syntax = "SCHEDULE job_name FOR run_date ;"
)
public class ScheduleCommand implements Runnable {
@Bind("job_name")
private String jobName;
@Bind("run_date")
private java.time.LocalDate runDate;
@Override
public void run() {}
}If no converter exists for a target type, binding fails with a definition/runtime error depending on where the issue is detected. The binder explicitly throws when no converter is available for the target type.
execute(input, context) follows this high-level flow:
- tokenize the raw input
- try each registered command definition in registration order
- parse against the command AST
- keep the furthest syntax error for diagnostics
- instantiate a fresh command instance through the registered factory
- bind all parameters and clauses
- execute the command
- return the fully bound command instance
If multiple commands fail at the same furthest position, the engine merges expected possibilities to improve the final syntax error.
The framework distinguishes between:
- definition errors: invalid grammar, invalid metadata, missing annotation, invalid
after, invalid@OnClause, unsupported execution contract - syntax errors: user input does not match any registered command
- binding errors: conversion or injection failed
- execution errors: the command logic itself threw
Notable behavior:
- the engine keeps the furthest syntax failure across registered commands
- if two commands fail equally far, it merges expected possibilities
Errorsubclasses are rethrown directly during executionRuntimeExceptionis propagated directly- checked failures are wrapped as command execution failures
This README intentionally keeps the syntax overview concise.
For the authoritative grammar contract, including:
- optional groups
- required groups
- alternatives
- repetition with
... - quoted strings
- macro syntax
- delimiters and exact semantics
refer to the dedicated grammar reference document for Intuitive DSL.
- Documentation for Intuitive DSL for Java: https://www.dbalabs.ch/library/intuitive-dsl-for-java
- Language-agnostic grammar reference: https://www.dbalabs.ch/library/intuitive-dsl-grammar-reference
The current codebase guarantees the following:
...is the only repetition operator in grammar definitions.remains a literal delimiter, not a repetition alias- macro names are case-sensitive
@Bind(after = ...)is validated against exact grammar keywords / phrases@OnClause(...)is validated against exact grammar keywords / phrases- each
execute(...)call uses a fresh command instance - converter registration applies both to future commands and already registered commands
- command selection is attempted in registration order
- the engine remains intentionally lightweight and annotation-driven
- values containing spaces or punctuation that conflict with grammar delimiters should be quoted
- macro contracts and grammar exactness are strict by design
These constraints are deliberate and help keep the parser deterministic and the diagnostics predictable.
Intuitive DSL is dual-licensed:
For open-source usage, the project is available under the GNU Affero General Public License v3.0 (AGPL-3.0). The current project metadata and source headers explicitly describe the AGPL/commercial dual-licensing model.
A commercial license is available for proprietary or closed-source use.
For commercial licensing terms, pricing, and license requests, please visit: https://www.dbalabs.ch/engines/intuitive-dsl-for-java
You may also contact DBA Labs at contact@dbalabs.ch.
Copyright © 2026 DBA Labs - Switzerland.