Demo ready
This commit is contained in:
parent
743432e071
commit
620999a992
7 changed files with 190 additions and 74 deletions
|
@ -0,0 +1,9 @@
|
||||||
|
projekt/create/ -> createProjekt(
|
||||||
|
id: ProjektId,
|
||||||
|
properties: ProjektProperties
|
||||||
|
)
|
||||||
|
|
||||||
|
projekt/update/ -> updateProjekt(
|
||||||
|
id: ProjektId,
|
||||||
|
properties: ProjektProperties
|
||||||
|
)
|
|
@ -53,5 +53,9 @@
|
||||||
<groupId>info.picocli</groupId>
|
<groupId>info.picocli</groupId>
|
||||||
<artifactId>picocli</artifactId>
|
<artifactId>picocli</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.freemarker</groupId>
|
||||||
|
<artifactId>freemarker</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|
|
@ -1,70 +1,99 @@
|
||||||
package nu.zoom.tapir;
|
package nu.zoom.tapir;
|
||||||
|
|
||||||
import nu.zoom.tapir.parser.*;
|
import nu.zoom.tapir.parser.*;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
import java.nio.file.Files;
|
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.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
|
|
||||||
import picocli.CommandLine;
|
import picocli.CommandLine;
|
||||||
import picocli.CommandLine.Command;
|
import picocli.CommandLine.Command;
|
||||||
import picocli.CommandLine.Option;
|
import picocli.CommandLine.Option;
|
||||||
import picocli.CommandLine.Parameters;
|
import picocli.CommandLine.Parameters;
|
||||||
|
|
||||||
@Command(name = "tapirgen", mixinStandardHelpOptions = true, description = "Generate source code from a tapir endpoint specification file.")
|
@Command(name = "tapirgen", mixinStandardHelpOptions = true, description = "Generate source code from a tapir endpoint specification file.")
|
||||||
public class Generator implements Callable<Integer> {
|
public class Generator implements Callable<Integer> {
|
||||||
@Parameters(index = "0", description = "The source tapir file.")
|
@Parameters(index = "0", description = "The source tapir file.")
|
||||||
private Path file;
|
private Path file;
|
||||||
|
|
||||||
@Option(names = {"-t", "--template"}, description = "The template directory. Default is ~/tapir-templates")
|
@Option(names = {"-t", "--template"}, description = "The template directory. Default is ~/tapir-templates")
|
||||||
private Path templateDir = Paths.get(System.getProperty("user.dir"), "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")
|
@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");
|
private Path outputDir = Paths.get(System.getProperty("user.dir"), "tapir-output");
|
||||||
|
|
||||||
public static void main(String[] args) throws ParseException {
|
@Option(names = {"-v", "--verbose"}, description = "Print verbose debug messages.")
|
||||||
int exitCode = new CommandLine(new Generator()).execute(args);
|
private Boolean verbose = false;
|
||||||
System.exit(exitCode);
|
|
||||||
}
|
public static void main(String[] args) throws ParseException {
|
||||||
|
int exitCode = new CommandLine(new Generator()).execute(args);
|
||||||
@Override
|
System.exit(exitCode);
|
||||||
public Integer call() {
|
}
|
||||||
try {
|
|
||||||
validateTemplateDirectory();
|
@Override
|
||||||
validateInputFile();
|
public Integer call() {
|
||||||
StringReader reader = new StringReader("foo/bar/baz/ -> fooHandler(id:String, name:NonEmptyString)foo/baz/->bazHandler(nothing:Any)");
|
try {
|
||||||
var rootNode = new TapirParser(reader).endpoints();
|
validateTemplateDirectory();
|
||||||
var endpoints = NodeTransformer.transform(rootNode);
|
validateInputFile();
|
||||||
rootNode.dump("");
|
validateOutputDirectory();
|
||||||
endpoints.forEach(endpoint -> {
|
var rootNode = new TapirParser(Files.newBufferedReader(this.file)).endpoints();
|
||||||
System.out.println(endpoint);
|
if (this.verbose) {
|
||||||
});
|
System.out.println("====== Parse Tree ======");
|
||||||
return 0;
|
rootNode.dump("");
|
||||||
} catch (Exception e) {
|
}
|
||||||
System.err.println(e.getMessage());
|
var endpoints = NodeTransformer.transform(rootNode);
|
||||||
return 1;
|
if (endpoints.isEmpty()) {
|
||||||
}
|
System.err.println("No tapir endpoints found.");
|
||||||
}
|
return 2;
|
||||||
|
}
|
||||||
private void validateTemplateDirectory() throws IOException {
|
if (this.verbose) {
|
||||||
if (!Files.isDirectory(this.templateDir)) {
|
System.out.println("\n====== AST ======");
|
||||||
throw new IllegalArgumentException("Template directory '" + this.templateDir + "' does not exist.");
|
endpoints.forEach(endpoint -> {
|
||||||
}
|
System.out.println(endpoint);
|
||||||
Path endpointTemplate = this.templateDir.resolve("endpoints.ftl") ;
|
});
|
||||||
if (Files.notExists(endpointTemplate)) {
|
}
|
||||||
throw new IllegalArgumentException("Can not find: '" + endpointTemplate + "'.");
|
TargetGenerator targetGenerator = new TargetGenerator(
|
||||||
}
|
this.verbose,
|
||||||
}
|
this.outputDir,
|
||||||
|
this.templateDir,
|
||||||
private void validateInputFile() throws IOException {
|
endpoints
|
||||||
if (Files.notExists(this.file)) {
|
);
|
||||||
throw new IllegalArgumentException("Input file '" + this.file + "' does not exist.");
|
targetGenerator.generate();
|
||||||
}
|
return 0;
|
||||||
if (!Files.isReadable(this.file) || !Files.isRegularFile(this.file)) {
|
} catch (Exception e) {
|
||||||
throw new IllegalArgumentException("Input file '" + this.file + "' is not a readable file.");
|
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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,12 +1,24 @@
|
||||||
package nu.zoom.tapir;
|
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.Path;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
public class TargetGenerator {
|
public class TargetGenerator {
|
||||||
private final Path outputPath;
|
private final Path outputPath;
|
||||||
private final Path templatePath;
|
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 static class TargetGeneratorException extends Exception {
|
||||||
public TargetGeneratorException(String message) {
|
public TargetGeneratorException(String message) {
|
||||||
|
@ -18,10 +30,12 @@ public class TargetGenerator {
|
||||||
}
|
}
|
||||||
|
|
||||||
public TargetGenerator(
|
public TargetGenerator(
|
||||||
|
final boolean verbose,
|
||||||
Path outputPath,
|
Path outputPath,
|
||||||
Path templatePath,
|
Path templatePath,
|
||||||
List<EndpointNode> endpoints
|
List<EndpointNode> endpoints
|
||||||
) {
|
) {
|
||||||
|
this.verbose = verbose;
|
||||||
this.outputPath = Objects.requireNonNull(
|
this.outputPath = Objects.requireNonNull(
|
||||||
outputPath,
|
outputPath,
|
||||||
"Output path is required"
|
"Output path is required"
|
||||||
|
@ -30,9 +44,26 @@ public class TargetGenerator {
|
||||||
templatePath,
|
templatePath,
|
||||||
"Template path is required"
|
"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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,8 +27,9 @@ TOKEN : {
|
||||||
| <SLASH: "/">
|
| <SLASH: "/">
|
||||||
| <COLON: ":">
|
| <COLON: ":">
|
||||||
| <COMMA: ",">
|
| <COMMA: ",">
|
||||||
| <LETTER: [ "A"-"Z", "a"-"z" ]>
|
| <FIRST_LETTER: [ "A"-"Z", "a"-"z" ]>
|
||||||
| <IDENTIFIER: ( <LETTER>)+ >
|
| <LETTER: [ "A"-"Z", "a"-"z", "0"-"9", "[", "]"] >
|
||||||
|
| <IDENTIFIER: <FIRST_LETTER> (<LETTER>)* >
|
||||||
}
|
}
|
||||||
|
|
||||||
void path() :
|
void path() :
|
||||||
|
|
5
pom.xml
5
pom.xml
|
@ -30,6 +30,11 @@
|
||||||
<artifactId>picocli</artifactId>
|
<artifactId>picocli</artifactId>
|
||||||
<version>4.7.6</version>
|
<version>4.7.6</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.freemarker</groupId>
|
||||||
|
<artifactId>freemarker</artifactId>
|
||||||
|
<version>2.3.34</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
Loading…
Add table
Reference in a new issue