Compare commits

...

3 commits

Author SHA1 Message Date
a546d257f3 Grammar for states and transitions 2025-04-19 07:08:06 +02:00
8050d35811 WIP state visitor 2025-04-18 10:22:18 +02:00
10a4e115d9 Split grammar 2025-04-18 08:19:38 +02:00
14 changed files with 274 additions and 41 deletions

4
.gitignore vendored
View file

@ -32,4 +32,6 @@ build/
.vscode/
### Mac OS ###
.DS_Store
.DS_Store
/endpoints-output/**

View file

@ -57,6 +57,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.4.2</version>
<configuration>
<archive>
<manifest>

View file

@ -0,0 +1,41 @@
// Copyright 2025 "Johan Maasing" <johan@zoom.nu>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
grammar Common;
generatorconfig : '{' (configitem)? (',' configitem)* '}';
configitem : configkey ':' configvalue ;
configkey : IDENTIFIER ;
configvalue : (IDENTIFIER|VALUE) ;
namedTypeDeclaration : typeName typeDeclaration ;
typeName : IDENTIFIER ;
typeDeclaration : '(' typeField (',' typeField)* ')' ;
typeField : fieldName COLON fieldType ;
fieldName : IDENTIFIER ;
fieldType : IDENTIFIER ;
fragment LOWERCASE : [a-z] ;
fragment UPPERCASE : [A-Z] ;
fragment GENERICS : '['|']'|'<'|'>' ;
fragment DOT : '.' ;
fragment COMMENT_BEGIN : '/*' ;
fragment COMMENT_END : '*/' ;
fragment DIGIT : [0-9] ;
WS : [ \t\n\r]+ -> skip;
COMMENT : COMMENT_BEGIN .*? COMMENT_END -> skip;
LEFT_ARROW : '<-' ;
RIGHT_ARROW : '->' ;
IDENTIFIER : (LOWERCASE | UPPERCASE) (LOWERCASE | UPPERCASE | DIGIT | GENERICS | DOT)* ;
VALUE : ~[ ,{}:()/="#';*\n\r\t]+ ;
SLASH : '/' ;
COLON : ':' ;

View file

@ -12,35 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
grammar Endpoints;
import Common;
document : generatorconfig? (namedTypeDeclaration|endpoint)* ;
generatorconfig : '{' (configitem)? (',' configitem)* '}';
configitem : configkey ':' configvalue ;
configkey : IDENTIFIER ;
configvalue : (IDENTIFIER|VALUE) ;
namedTypeDeclaration : typeName typeDeclaration ;
typeName : IDENTIFIER ;
typeDeclaration : '(' typeField (',' typeField)* ')' ;
typeField : fieldName ':' fieldType ;
fieldName : IDENTIFIER ;
fieldType : IDENTIFIER ;
requestBody : REQUEST_PREFIX (namedTypeDeclaration | typeDeclaration | IDENTIFIER) ;
responseBody : RESPONSE_PREFIX (namedTypeDeclaration | typeDeclaration | IDENTIFIER) ;
requestBody : LEFT_ARROW (namedTypeDeclaration | typeDeclaration | IDENTIFIER) ;
responseBody : RIGHT_ARROW (namedTypeDeclaration | typeDeclaration | IDENTIFIER) ;
endpoint : path requestBody responseBody?;
path : (pathSegment)+ ;
pathSegment : SLASH (IDENTIFIER|VALUE) ;
fragment DIGIT : [0-9] ;
fragment LOWERCASE : [a-z] ;
fragment UPPERCASE : [A-Z] ;
fragment GENERICS : '['|']'|'<'|'>' ;
fragment DOT : '.' ;
fragment COMMENT_BEGIN : '/*' ;
fragment COMMENT_END : '*/' ;
WS : [ \t\n\r]+ -> skip;
COMMENT : COMMENT_BEGIN .*? COMMENT_END -> skip;
REQUEST_PREFIX : '<-' ;
RESPONSE_PREFIX : '->' ;
SLASH : '/' ;
IDENTIFIER : (LOWERCASE | UPPERCASE) (LOWERCASE | UPPERCASE | DIGIT | GENERICS | DOT)* ;
VALUE : ~[ ,{}:()/="#';*\n\r\t]+ ;

View file

@ -0,0 +1,22 @@
// Copyright 2025 "Johan Maasing" <johan@zoom.nu>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
grammar States;
import Common;
document : generatorconfig? transition (',' transition)* ;
transition : from RIGHT_ARROW to COLON message ;
from : state ;
to : state ;
message : typeName typeDeclaration? ;
state : typeName typeDeclaration? ;

View file

@ -15,9 +15,11 @@ package nu.zoom.dsl.ast;
import java.util.List;
import java.util.Map;
import java.util.Set;
public record DocumentNode(
Map<String, String> config,
List<TypeNode> typeDefinitions,
List<EndpointNode> endpoints) {
Set<TypeNode> typeDefinitions,
List<EndpointNode> endpoints,
Set<StateNode> states) {
}

View file

@ -19,7 +19,8 @@ import org.antlr.v4.runtime.tree.TerminalNode;
import java.util.*;
public class EndpointsVisitorTransformer extends EndpointsBaseVisitor<EndpointsParser.DocumentContext> {
public class EndpointsVisitorTransformer
extends EndpointsBaseVisitor<EndpointsParser.DocumentContext> {
private final ArrayList<EndpointNode> endpoints = new ArrayList<>();
private final HashMap<String,String> config = new HashMap<>();
private final HashSet<TypeNode> dataTypes = new HashSet<>();
@ -35,8 +36,8 @@ public class EndpointsVisitorTransformer extends EndpointsBaseVisitor<EndpointsP
return Map.copyOf(config);
}
public List<TypeNode> getDataTypes() {
return List.copyOf(dataTypes);
public Set<TypeNode> getDataTypes() {
return Set.copyOf(dataTypes);
}
@Override
@ -130,7 +131,7 @@ public class EndpointsVisitorTransformer extends EndpointsBaseVisitor<EndpointsP
).toList();
}
// Concatenate the text from to terminal nodes. Useful for contexts that are either an identifier or a value,
// Concatenate the text from two terminal nodes. Useful for contexts that are either an identifier or a value,
// and you just want the text from whichever is not null.
private String getText(TerminalNode identifier, TerminalNode value) {
return

View file

@ -13,23 +13,43 @@
// limitations under the License.
package nu.zoom.dsl.ast;
import nu.zoom.dsl.parser.EndpointsLexer;
import nu.zoom.dsl.parser.EndpointsParser;
import nu.zoom.dsl.parser.*;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.List;
import java.util.Set;
public class ParserWrapper {
public static DocumentNode parse(Path sourcePath) throws IOException {
public static DocumentNode parseEndpoints(Path sourcePath) throws IOException {
var ins = CharStreams.fromPath(sourcePath, StandardCharsets.UTF_8);
EndpointsLexer lexer = new EndpointsLexer(ins);
EndpointsParser parser = new EndpointsParser(new CommonTokenStream(lexer));
var document = parser.document();
var astTransformer = new EndpointsVisitorTransformer();
astTransformer.visit(document);
return new DocumentNode(astTransformer.getConfig(), astTransformer.getDataTypes(), astTransformer.getEndpoints());
return new DocumentNode(
astTransformer.getConfig(),
astTransformer.getDataTypes(),
astTransformer.getEndpoints(),
Set.of()
);
}
public static DocumentNode parseStates(Path sourcePath) throws IOException {
var ins = CharStreams.fromPath(sourcePath, StandardCharsets.UTF_8);
StatesLexer lexer = new StatesLexer(ins);
StatesParser parser = new StatesParser(new CommonTokenStream(lexer));
var document = parser.document();
var astTransformer = new StatesVisitorTransformer();
astTransformer.visit(document);
return new DocumentNode(
astTransformer.getConfig(),
astTransformer.getTypes(),
List.of(),
astTransformer.getStates()
);
}
}

View file

@ -0,0 +1,6 @@
package nu.zoom.dsl.ast;
import java.util.Set;
public record StateNode(String name, String data, Set<TransitionNode> transitions) {
}

View file

@ -0,0 +1,106 @@
package nu.zoom.dsl.ast;
import nu.zoom.dsl.parser.StatesBaseVisitor;
import nu.zoom.dsl.parser.StatesParser;
import org.antlr.v4.runtime.tree.TerminalNode;
import java.util.*;
import java.util.stream.Stream;
public class StatesVisitorTransformer extends StatesBaseVisitor<StatesParser.DocumentContext> {
private final HashMap<String,String> config = new HashMap<>();
private final HashSet<TypeNode> nodeTypes = new HashSet<>();
private final HashSet<TypeNode> messageTypes = new HashSet<>();
// from -> <to, message>
private final HashMap<String,HashMap<String, String>> transitions = new HashMap<>();
@Override
public StatesParser.DocumentContext visitTransition(StatesParser.TransitionContext ctx) {
String from = ctx.from().state().typeName().IDENTIFIER().getText() ;
String to = ctx.to().state().typeName().IDENTIFIER().getText() ;
String message = ctx.message().typeName().IDENTIFIER().getText() ;
this.transitions.computeIfAbsent(from, k -> new HashMap<>()).put(to, message);
return super.visitTransition(ctx);
}
@Override
public StatesParser.DocumentContext visitState(StatesParser.StateContext ctx) {
String stateName = ctx.typeName().IDENTIFIER().getText() ;
List<FieldNode> fields = extractFields(ctx.typeDeclaration()) ;
this.nodeTypes.add(new TypeNode(stateName, fields));
return super.visitState(ctx);
}
@Override
public StatesParser.DocumentContext visitMessage(StatesParser.MessageContext ctx) {
String messageName = ctx.typeName().IDENTIFIER().getText() ;
List<FieldNode> fields = extractFields(ctx.typeDeclaration()) ;
this.messageTypes.add(new TypeNode(messageName, fields));
return super.visitMessage(ctx);
}
@Override
public StatesParser.DocumentContext visitConfigitem(StatesParser.ConfigitemContext ctx) {
String configKey = ctx.configkey().IDENTIFIER().getText();
String configValue = getText(ctx.configvalue().IDENTIFIER(), ctx.configvalue().VALUE());
this.config.put(configKey, configValue);
return super.visitConfigitem(ctx);
}
public Set<StateNode> getStates() {
HashSet<StateNode> states = new HashSet<>();
this.transitions.forEach((state,v)->{
HashSet<TransitionNode> transitionNodes = new HashSet<>();
v.forEach((to, message) -> transitionNodes.add(new TransitionNode(message, to)));
states.add(new StateNode(state, "", transitionNodes)) ;
}) ;
return states ;
}
public Map<String,String> getConfig() {
return Map.copyOf(config);
}
public Set<TypeNode> getTypes() {
// TODO calculate data types from NodeTypes and MessageTypes with duplicate check.
HashMap<String, TypeNode> typeNodes = new HashMap<>();
this.nodeTypes.forEach(typeNode -> {
if (typeNodes.containsKey(typeNode.name())) {
throw new RuntimeException("Duplicate type name: " + typeNode.name());
} else {
typeNodes.put(typeNode.name(), typeNode);
}
}) ;
this.messageTypes.forEach(typeNode -> {
if (typeNodes.containsKey(typeNode.name())) {
throw new RuntimeException("Duplicate type name: " + typeNode.name());
} else {
typeNodes.put(typeNode.name(), typeNode);
}
}) ;
return Set.of(typeNodes.values().toArray(new TypeNode[0])) ;
}
private List<FieldNode> extractFields(StatesParser.TypeDeclarationContext declaration) {
if (declaration == null) {
return Collections.emptyList();
}
return declaration
.typeField()
.stream()
.map(
ctx ->
new FieldNode(ctx.fieldName().getText(), ctx.fieldType().getText())
)
.toList();
}
// Concatenate the text from two terminal nodes. Useful for contexts that are either an identifier or a value,
// and you just want the text from whichever is not null.
private String getText(TerminalNode identifier, TerminalNode value) {
return
((identifier != null) ? identifier.getText() : "") +
((value != null) ? value.getText() : "");
}
}

View file

@ -0,0 +1,4 @@
package nu.zoom.dsl.ast;
public record TransitionNode(String message, String toState) {
}

View file

@ -34,6 +34,10 @@ import java.util.concurrent.Callable;
description = "Generate source code from an endpoints specification file."
)
public class EndpointsCLI implements Callable<Integer> {
public enum ParserType {
Endpoints,
States
}
@SuppressWarnings("unused")
@Parameters(index = "0", description = "The source endpoints DSL file.")
private Path file;
@ -50,6 +54,9 @@ public class EndpointsCLI implements Callable<Integer> {
@Option(names = {"-v", "--verbose"}, description = "Print verbose debug messages.")
private Boolean verbose = false;
@Option(names = {"-p", "--parser"}, description = "Force use of a specific parser instead of determining from filename. Valid values: ${COMPLETION-CANDIDATES}.")
private ParserType parser = null;
public static void main(String[] args) {
int exitCode = new CommandLine(new EndpointsCLI()).execute(args);
System.exit(exitCode);
@ -61,8 +68,20 @@ public class EndpointsCLI implements Callable<Integer> {
validateTemplateDirectory();
validateInputFile();
validateOutputDirectory();
verbose("Parsing " + file.toAbsolutePath());
DocumentNode rootNode = ParserWrapper.parse(file);
verbose("Parsing: " + file.toAbsolutePath());
if (parser == null) {
if (file.getFileName().toString().endsWith(".states")) {
parser = ParserType.States;
}
}
final DocumentNode rootNode ;
if (parser == ParserType.States) {
verbose("using state grammar.") ;
rootNode = ParserWrapper.parseStates(file);
} else {
verbose("using endpoints grammar.") ;
rootNode = ParserWrapper.parseEndpoints(file);
}
verbose("AST: " + rootNode);
verbose("Generating from templates in: " + templateDir.toAbsolutePath());
Generator generator = new Generator(templateDir, rootNode, outputDir);

View file

@ -0,0 +1,29 @@
```
Copyright 2025 "Johan Maasing" <johan@zoom.nu>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
```
# The nodes
```mermaid
---
title: ${config.title}
---
stateDiagram-v2
<#list states as state>
<#list state.transitions as transition>
${state.name} --> ${transition.toState} : ${transition.message}
</#list>
</#list>
```

4
test01.states Normal file
View file

@ -0,0 +1,4 @@
{ title: SomeNodes }
start(s:S) -> middle: message(foo:bar),
middle -> middle: selfmessage,
middle -> end: endmessage(bar:baz)