WIP state visitor

This commit is contained in:
Johan Maasing 2025-04-18 10:22:18 +02:00
parent 10a4e115d9
commit 8050d35811
11 changed files with 162 additions and 17 deletions

View file

@ -19,7 +19,7 @@ configvalue : (IDENTIFIER|VALUE) ;
namedTypeDeclaration : typeName typeDeclaration ; namedTypeDeclaration : typeName typeDeclaration ;
typeName : IDENTIFIER ; typeName : IDENTIFIER ;
typeDeclaration : '(' typeField (',' typeField)* ')' ; typeDeclaration : '(' typeField (',' typeField)* ')' ;
typeField : fieldName ':' fieldType ; typeField : fieldName COLON fieldType ;
fieldName : IDENTIFIER ; fieldName : IDENTIFIER ;
fieldType : IDENTIFIER ; fieldType : IDENTIFIER ;
@ -33,8 +33,9 @@ fragment DIGIT : [0-9] ;
WS : [ \t\n\r]+ -> skip; WS : [ \t\n\r]+ -> skip;
COMMENT : COMMENT_BEGIN .*? COMMENT_END -> skip; COMMENT : COMMENT_BEGIN .*? COMMENT_END -> skip;
IDENTIFIER : (LOWERCASE | UPPERCASE) (LOWERCASE | UPPERCASE | DIGIT | GENERICS | DOT)* ;
VALUE : ~[ ,{}:()/="#';*\n\r\t]+ ;
LEFT_ARROW : '<-' ; LEFT_ARROW : '<-' ;
RIGHT_ARROW : '->' ; RIGHT_ARROW : '->' ;
SLASH : '/' ; IDENTIFIER : (LOWERCASE | UPPERCASE) (LOWERCASE | UPPERCASE | DIGIT | GENERICS | DOT)* ;
VALUE : ~[ ,{}:()/="#';*\n\r\t]+ ;
SLASH : '/' ;
COLON : ':' ;

View file

@ -20,4 +20,3 @@ responseBody : RIGHT_ARROW (namedTypeDeclaration | typeDeclaration |
endpoint : path requestBody responseBody?; endpoint : path requestBody responseBody?;
path : (pathSegment)+ ; path : (pathSegment)+ ;
pathSegment : SLASH (IDENTIFIER|VALUE) ; pathSegment : SLASH (IDENTIFIER|VALUE) ;

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.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
public record DocumentNode( public record DocumentNode(
Map<String, String> config, Map<String, String> config,
List<TypeNode> typeDefinitions, Set<TypeNode> typeDefinitions,
List<EndpointNode> endpoints) { List<EndpointNode> endpoints,
Set<StateNode> states) {
} }

View file

@ -19,7 +19,8 @@ import org.antlr.v4.runtime.tree.TerminalNode;
import java.util.*; 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 ArrayList<EndpointNode> endpoints = new ArrayList<>();
private final HashMap<String,String> config = new HashMap<>(); private final HashMap<String,String> config = new HashMap<>();
private final HashSet<TypeNode> dataTypes = new HashSet<>(); private final HashSet<TypeNode> dataTypes = new HashSet<>();
@ -35,8 +36,8 @@ public class EndpointsVisitorTransformer extends EndpointsBaseVisitor<EndpointsP
return Map.copyOf(config); return Map.copyOf(config);
} }
public List<TypeNode> getDataTypes() { public Set<TypeNode> getDataTypes() {
return List.copyOf(dataTypes); return Set.copyOf(dataTypes);
} }
@Override @Override
@ -130,7 +131,7 @@ public class EndpointsVisitorTransformer extends EndpointsBaseVisitor<EndpointsP
).toList(); ).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. // and you just want the text from whichever is not null.
private String getText(TerminalNode identifier, TerminalNode value) { private String getText(TerminalNode identifier, TerminalNode value) {
return return

View file

@ -0,0 +1,14 @@
package nu.zoom.dsl.ast;
import java.util.List;
import java.util.Map;
public interface ParseTreeTransformer {
List<EndpointNode> getEndpoints();
Map<String,String> getConfig();
List<TypeNode> getDataTypes();
}

View file

@ -13,23 +13,43 @@
// limitations under the License. // limitations under the License.
package nu.zoom.dsl.ast; package nu.zoom.dsl.ast;
import nu.zoom.dsl.parser.EndpointsLexer; import nu.zoom.dsl.parser.*;
import nu.zoom.dsl.parser.EndpointsParser;
import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream; import org.antlr.v4.runtime.CommonTokenStream;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List;
import java.util.Set;
public class ParserWrapper { 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); var ins = CharStreams.fromPath(sourcePath, StandardCharsets.UTF_8);
EndpointsLexer lexer = new EndpointsLexer(ins); EndpointsLexer lexer = new EndpointsLexer(ins);
EndpointsParser parser = new EndpointsParser(new CommonTokenStream(lexer)); EndpointsParser parser = new EndpointsParser(new CommonTokenStream(lexer));
var document = parser.document(); var document = parser.document();
var astTransformer = new EndpointsVisitorTransformer(); var astTransformer = new EndpointsVisitorTransformer();
astTransformer.visit(document); 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,56 @@
package nu.zoom.dsl.ast;
import nu.zoom.dsl.parser.StatesBaseVisitor;
import nu.zoom.dsl.parser.StatesParser;
import java.util.*;
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);
}
public Set<StateNode> getStates() {
// TODO: Calculate state nodes from this.transitions
return Set.of();
}
public Map<String,String> getConfig() {
return Map.copyOf(config);
}
public Set<TypeNode> getTypes() {
// TODO calculate data types from NodeTypes and MessageTypes with duplicate check.
return Set.of() ;
}
private List<FieldNode> extractFields(StatesParser.TypeDeclarationContext declaration) {
return declaration
.typeField()
.stream()
.map(
ctx ->
new FieldNode(ctx.fieldName().getText(), ctx.fieldType().getText())
)
.toList();
}
}

View file

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

View file

@ -26,6 +26,7 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
@Command( @Command(
@ -34,6 +35,10 @@ import java.util.concurrent.Callable;
description = "Generate source code from an endpoints specification file." description = "Generate source code from an endpoints specification file."
) )
public class EndpointsCLI implements Callable<Integer> { public class EndpointsCLI implements Callable<Integer> {
public static enum ParserType {
Endpoints,
States
}
@SuppressWarnings("unused") @SuppressWarnings("unused")
@Parameters(index = "0", description = "The source endpoints DSL file.") @Parameters(index = "0", description = "The source endpoints DSL file.")
private Path file; private Path file;
@ -50,6 +55,9 @@ public class EndpointsCLI implements Callable<Integer> {
@Option(names = {"-v", "--verbose"}, description = "Print verbose debug messages.") @Option(names = {"-v", "--verbose"}, description = "Print verbose debug messages.")
private Boolean verbose = false; 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) { public static void main(String[] args) {
int exitCode = new CommandLine(new EndpointsCLI()).execute(args); int exitCode = new CommandLine(new EndpointsCLI()).execute(args);
System.exit(exitCode); System.exit(exitCode);
@ -61,8 +69,20 @@ public class EndpointsCLI implements Callable<Integer> {
validateTemplateDirectory(); validateTemplateDirectory();
validateInputFile(); validateInputFile();
validateOutputDirectory(); validateOutputDirectory();
verbose("Parsing " + file.toAbsolutePath()); verbose("Parsing: " + file.toAbsolutePath());
DocumentNode rootNode = ParserWrapper.parse(file); if (parser == null) {
if (file.getFileName().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("AST: " + rootNode);
verbose("Generating from templates in: " + templateDir.toAbsolutePath()); verbose("Generating from templates in: " + templateDir.toAbsolutePath());
Generator generator = new Generator(templateDir, rootNode, outputDir); Generator generator = new Generator(templateDir, rootNode, outputDir);