Demo ready

This commit is contained in:
Johan Maasing 2025-03-16 14:16:16 +01:00
parent 743432e071
commit 620999a992
Signed by: johan
GPG key ID: FFD31BABEE2DEED2
7 changed files with 190 additions and 74 deletions

View file

@ -0,0 +1,9 @@
projekt/create/ -> createProjekt(
id: ProjektId,
properties: ProjektProperties
)
projekt/update/ -> updateProjekt(
id: ProjektId,
properties: ProjektProperties
)

View file

@ -53,5 +53,9 @@
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
</dependency>
</dependencies>
</project>

View file

@ -1,70 +1,99 @@
package nu.zoom.tapir;
import nu.zoom.tapir.parser.*;
import java.io.IOException;
import java.io.StringReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.Callable;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
@Command(name = "tapirgen", mixinStandardHelpOptions = true, description = "Generate source code from a tapir endpoint specification file.")
public class Generator implements Callable<Integer> {
@Parameters(index = "0", description = "The source tapir file.")
private Path file;
@Option(names = {"-t", "--template"}, description = "The template directory. Default is ~/tapir-templates")
private Path templateDir = Paths.get(System.getProperty("user.dir"), "tapir-templates");
@Option(names = {"-o", "--output"}, description = "The directory to write the gerenated code to. Default is ~/tapir-output")
private Path outputDir = Paths.get(System.getProperty("user.dir"), "tapir-output");
public static void main(String[] args) throws ParseException {
int exitCode = new CommandLine(new Generator()).execute(args);
System.exit(exitCode);
}
@Override
public Integer call() {
try {
validateTemplateDirectory();
validateInputFile();
StringReader reader = new StringReader("foo/bar/baz/ -> fooHandler(id:String, name:NonEmptyString)foo/baz/->bazHandler(nothing:Any)");
var rootNode = new TapirParser(reader).endpoints();
var endpoints = NodeTransformer.transform(rootNode);
rootNode.dump("");
endpoints.forEach(endpoint -> {
System.out.println(endpoint);
});
return 0;
} catch (Exception e) {
System.err.println(e.getMessage());
return 1;
}
}
private void validateTemplateDirectory() throws IOException {
if (!Files.isDirectory(this.templateDir)) {
throw new IllegalArgumentException("Template directory '" + this.templateDir + "' does not exist.");
}
Path endpointTemplate = this.templateDir.resolve("endpoints.ftl") ;
if (Files.notExists(endpointTemplate)) {
throw new IllegalArgumentException("Can not find: '" + endpointTemplate + "'.");
}
}
private void validateInputFile() throws IOException {
if (Files.notExists(this.file)) {
throw new IllegalArgumentException("Input file '" + this.file + "' does not exist.");
}
if (!Files.isReadable(this.file) || !Files.isRegularFile(this.file)) {
throw new IllegalArgumentException("Input file '" + this.file + "' is not a readable file.");
}
}
}
package nu.zoom.tapir;
import nu.zoom.tapir.parser.*;
import java.io.IOException;
import java.io.StringReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.Callable;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
@Command(name = "tapirgen", mixinStandardHelpOptions = true, description = "Generate source code from a tapir endpoint specification file.")
public class Generator implements Callable<Integer> {
@Parameters(index = "0", description = "The source tapir file.")
private Path file;
@Option(names = {"-t", "--template"}, description = "The template directory. Default is ~/tapir-templates")
private Path templateDir = Paths.get(System.getProperty("user.dir"), "tapir-templates");
@Option(names = {"-o", "--output"}, description = "The directory to write the gerenated code to. Default is ~/tapir-output")
private Path outputDir = Paths.get(System.getProperty("user.dir"), "tapir-output");
@Option(names = {"-v", "--verbose"}, description = "Print verbose debug messages.")
private Boolean verbose = false;
public static void main(String[] args) throws ParseException {
int exitCode = new CommandLine(new Generator()).execute(args);
System.exit(exitCode);
}
@Override
public Integer call() {
try {
validateTemplateDirectory();
validateInputFile();
validateOutputDirectory();
var rootNode = new TapirParser(Files.newBufferedReader(this.file)).endpoints();
if (this.verbose) {
System.out.println("====== Parse Tree ======");
rootNode.dump("");
}
var endpoints = NodeTransformer.transform(rootNode);
if (endpoints.isEmpty()) {
System.err.println("No tapir endpoints found.");
return 2;
}
if (this.verbose) {
System.out.println("\n====== AST ======");
endpoints.forEach(endpoint -> {
System.out.println(endpoint);
});
}
TargetGenerator targetGenerator = new TargetGenerator(
this.verbose,
this.outputDir,
this.templateDir,
endpoints
);
targetGenerator.generate();
return 0;
} catch (Exception e) {
System.err.println(e.getMessage());
return 1;
}
}
private void validateOutputDirectory() throws IOException {
if (Files.notExists(this.outputDir)) {
Files.createDirectories(this.outputDir);
}
if (!Files.isDirectory(this.outputDir)) {
throw new IllegalArgumentException("Output directory: '" + this.outputDir + " 'is not a directory.");
}
}
private void validateTemplateDirectory() throws IOException {
if (!Files.isDirectory(this.templateDir)) {
throw new IllegalArgumentException("Template directory '" + this.templateDir + "' does not exist.");
}
Path endpointTemplate = this.templateDir.resolve(TargetGenerator.ENDPOINTS_TEMPLATE_NAME);
if (Files.notExists(endpointTemplate)) {
throw new IllegalArgumentException("Can not find: '" + endpointTemplate + "'.");
}
}
private void validateInputFile() throws IOException {
if (Files.notExists(this.file)) {
throw new IllegalArgumentException("Input file '" + this.file + "' does not exist.");
}
if (!Files.isReadable(this.file) || !Files.isRegularFile(this.file)) {
throw new IllegalArgumentException("Input file '" + this.file + "' is not a readable file.");
}
}
}

View file

@ -1,12 +1,24 @@
package nu.zoom.tapir;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import freemarker.template.TemplateExceptionHandler;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
public class TargetGenerator {
private final Path outputPath;
private final Path templatePath;
private final boolean verbose;
public static String ENDPOINTS_TEMPLATE_NAME = "endpoints.ftl";
private final List<EndpointNode> endpoints;
public static class TargetGeneratorException extends Exception {
public TargetGeneratorException(String message) {
@ -18,10 +30,12 @@ public class TargetGenerator {
}
public TargetGenerator(
final boolean verbose,
Path outputPath,
Path templatePath,
List<EndpointNode> endpoints
) {
this.verbose = verbose;
this.outputPath = Objects.requireNonNull(
outputPath,
"Output path is required"
@ -30,9 +44,26 @@ public class TargetGenerator {
templatePath,
"Template path is required"
);
this.endpoints = Objects.requireNonNull(endpoints) ;
}
public void generate() throws TargetGeneratorException {
public void generate() throws TargetGeneratorException, IOException, TemplateException {
Configuration cfg = new Configuration(Configuration.VERSION_2_3_34);
cfg.setDirectoryForTemplateLoading(this.templatePath.toFile());
cfg.setDefaultEncoding("UTF-8");
cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
cfg.setLogTemplateExceptions(false);
cfg.setWrapUncheckedExceptions(true);
cfg.setFallbackOnNullLoopVariable(false);
Template temp = cfg.getTemplate(ENDPOINTS_TEMPLATE_NAME);
try (var outputFile = Files.newBufferedWriter(
outputPath.resolve("endpoints.scala"),
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING
)) {
HashMap<String, List<EndpointNode>> templateData = new HashMap<>();
templateData.put("endpoints", endpoints);
temp.process(templateData, outputFile);
}
}
}

View file

@ -27,8 +27,9 @@ TOKEN : {
| <SLASH: "/">
| <COLON: ":">
| <COMMA: ",">
| <LETTER: [ "A"-"Z", "a"-"z" ]>
| <IDENTIFIER: ( <LETTER>)+ >
| <FIRST_LETTER: [ "A"-"Z", "a"-"z" ]>
| <LETTER: [ "A"-"Z", "a"-"z", "0"-"9", "[", "]"] >
| <IDENTIFIER: <FIRST_LETTER> (<LETTER>)* >
}
void path() :

View file

@ -30,6 +30,11 @@
<artifactId>picocli</artifactId>
<version>4.7.6</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.34</version>
</dependency>
</dependencies>
</dependencyManagement>

View file

@ -0,0 +1,37 @@
package se.senashdev.projekt.api
import se.rutdev.projekt.api.HttpProtocol.VersionedResponse
import se.rutdev.framework.json.circe.RutUtilsCodec
import se.rutdev.framework
import se.rutdev.framework.service.api.{OAuthUtils, RequestMeta, RutTapir}
import se.rutdev.pd.ProblemDetailProtocol.ProblemDetail
import sttp.tapir.Schema
class Endpoints(override val config: OAuthUtils.OAuthConfig) extends framework.service.api.Endpoints with RutTapir with RutUtilsCodec:
type ApiEndpoint[I, O] = OAuthEndpoint[RequestMeta.OAuthRequestMeta, I, ProblemDetail, O]
<#list endpoints as endpoint>
case class ${endpoint.handler.name?cap_first}(
<#list endpoint.handler.fields as field>
${field.name} : ${field.type},
</#list>
)
</#list>
<#list endpoints as endpoint>
given Codec[${endpoint.handler.name?cap_first}] = deriveCodec
</#list>
<#list endpoints as endpoint>
val ${endpoint.handler.name}Endpoint = ApiEndpoint[${endpoint.handler.name?cap_first}, VersionedResponse] =
<#list endpoint.paths.paths>
apiV1Endpoint
.post
<#items as segment>
.in("${segment}")
</#items>
.post
.in(jsonBody[${endpoint.handler.name?cap_first}])
.out(jsonBody[VersionedResponse])
</#list>
</#list>