Skip to content

Commit

Permalink
jte-models: Add support to generate Kotlin code (#282)
Browse files Browse the repository at this point in the history
* jte-models: Add support to generate Kotlin code

* Remove unused constructor

* Report configuration error when language is not supported

* Rename Language enum values to follow project camel case convention

* Language configuration is now case sensitive
  • Loading branch information
marcospereira authored Oct 6, 2023
1 parent 9230876 commit 64786c0
Show file tree
Hide file tree
Showing 14 changed files with 303 additions and 29 deletions.
31 changes: 29 additions & 2 deletions jte-models/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ To use jte-models, set up your build script to include one of these:
<extensions>
<extension>
<className>gg.jte.models.generator.ModelExtension</className>
<!-- optional settings to configure the target language (Java and Kotlin are supported):
<settings>
<language>java</language>
</settings>
-->
<!-- optional settings to include annotations on generated classes:
<settings>
<interfaceAnnotation>@foo.bar.MyAnnotation</interfaceAnnotation>
Expand Down Expand Up @@ -72,11 +77,17 @@ jte {
generate()
binaryStaticContent = true
jteExtension 'gg.jte.models.generator.ModelExtension'
// or to add annotations to generated classes:
// or to configure the generator
/*
jteExtension('gg.jte.models.generator.ModelExtension') {
// Target language (Java and Kotlin are supported). "Java" is the default.
language = 'java'
// Annotations to add to generated interfaces and classes
interfaceAnnotation = '@foo.bar.MyAnnotation'
implementationAnnotation = '@foo.bar.MyAnnotation'
// Patterns to include (or exclude) certain templates
includePattern = '\.pages\..*'
excludePattern = '\.components\..*'
}
Expand All @@ -103,11 +114,17 @@ jte {
generate()
binaryStaticContent.set(true)
jteExtension("gg.jte.models.generator.ModelExtension")
// or to add annotations to generated classes:
// or to configure the generator:
/*
jteExtension("gg.jte.models.generator.ModelExtension") {
// Target language (Java and Kotlin are supported). "Java" is the default.
property("language", "java")
// Annotations to add to generated interfaces and classes
property("interfaceAnnotation", "@foo.bar.MyAnnotation")
property("implementationAnnotation", "@foo.bar.MyAnnotation")
// Patterns to include (or exclude) certain templates
property("includePattern", "\.pages\..*")
property("excludePattern", '\.components\..*")
}
Expand All @@ -124,12 +141,22 @@ Additional generated classes will include a facade interface named `gg.jte.gener

`Templates` has a method for each of your templates, for example:

### Java

```java
public interface Templates {
JteModel helloWorld(String greeting);
}
```

### Kotlin

```kotlin
interface Templates {
fun helloWord(greeting: String): JteModel
}
```

(The package name can be changed by setting the packageName option in the build).

## Usage
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package gg.jte.models.generator;

public enum Language {
Java, Kotlin
}
14 changes: 14 additions & 0 deletions jte-models/src/main/java/gg/jte/models/generator/ModelConfig.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package gg.jte.models.generator;

import java.util.Arrays;
import java.util.Map;
import java.util.regex.Pattern;

Expand All @@ -18,6 +19,19 @@ public String implementationAnnotation() {
return map.getOrDefault("implementationAnnotation", "");
}

public Language language() {
String configuredLanguage = map.getOrDefault("language", "Java");
try {
return Language.valueOf(configuredLanguage);
} catch (IllegalArgumentException ex) {
String supportedValues = Arrays.toString(Language.values());
throw new IllegalArgumentException(
String.format("JTE ModelExtension 'language' property is not configured correctly (current value is '%s'). Supported values: %s", configuredLanguage, supportedValues),
ex
);
}
}

public Pattern includePattern() {
String includePattern = map.get("includePattern");
if (includePattern == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
Expand All @@ -21,7 +20,7 @@ public class ModelExtension implements JteExtension {

@Override
public String name() {
return "Generate type-safe model facade for templates";
return "Generate type-safe model facade for templates in Java";
}

@Override
Expand All @@ -37,15 +36,17 @@ public Collection<Path> generate(JteConfig config, Set<TemplateDescription> temp
Pattern includePattern = modelConfig.includePattern();
Pattern excludePattern = modelConfig.excludePattern();

Language language = modelConfig.language();

var templateDescriptionsFiltered = templateDescriptions.stream() //
.filter(x -> includePattern == null || includePattern.matcher(x.fullyQualifiedClassName()).matches()) //
.filter(x -> excludePattern == null || !excludePattern.matcher(x.fullyQualifiedClassName()).matches()) //
.collect(Collectors.toSet());

return Stream.of(
new ModelGenerator(engine, "interfacetemplates", "Templates", "Templates"),
new ModelGenerator(engine, "statictemplates", "StaticTemplates", "Templates"),
new ModelGenerator(engine, "dynamictemplates", "DynamicTemplates", "Templates")
new ModelGenerator(engine, "interfacetemplates", "Templates", "Templates", language),
new ModelGenerator(engine, "statictemplates", "StaticTemplates", "Templates", language),
new ModelGenerator(engine, "dynamictemplates", "DynamicTemplates", "Templates", language)
).map(g -> g.generate(config, templateDescriptionsFiltered, modelConfig))
.collect(Collectors.toList());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,23 @@ public class ModelGenerator {
private final String templateSubDirectory;
private final String targetClassName;
private final String interfaceName;
private final Language language;

public ModelGenerator(TemplateEngine engine, String templateSubDirectory, String targetClassName, String interfaceName) {
public ModelGenerator(TemplateEngine engine, String templateSubDirectory, String targetClassName, String interfaceName, Language language) {
this.engine = engine;
this.templateSubDirectory = templateSubDirectory;
this.targetClassName = targetClassName;
this.interfaceName = interfaceName;
this.language = language;
}

public Path generate(JteConfig config, Set<TemplateDescription> templateDescriptions, ModelConfig modelConfig) {
String fileExtension = language == Language.Java ? ".java" : ".kt";
String templateName = language == Language.Java ? "/main.jte" : "/kmain.jte";

Path sourceFilePath = config.generatedSourcesRoot()
.resolve(config.packageName().replace('.', '/'))
.resolve(targetClassName + ".java");
.resolve(targetClassName + fileExtension);
Iterable<String> imports = templateDescriptions.stream()
.flatMap(t -> t.imports().stream())
.collect(Collectors.toCollection(TreeSet::new));
Expand All @@ -47,7 +52,7 @@ public Path generate(JteConfig config, Set<TemplateDescription> templateDescript
paramMap.put("templates", templateDescriptions);
paramMap.put("imports", imports);
paramMap.put("modelConfig", modelConfig);
engine.render(templateSubDirectory + "/main.jte", paramMap, new SquashBlanksOutput(new WriterOutput(w)));
engine.render(templateSubDirectory + templateName, paramMap, new SquashBlanksOutput(new WriterOutput(w)));
}
} catch (IOException e) {
throw new UncheckedIOException(e);
Expand Down
5 changes: 5 additions & 0 deletions jte-models/src/main/java/gg/jte/models/generator/Util.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package gg.jte.models.generator;

import gg.jte.TemplateEngine;
import gg.jte.extension.api.ParamDescription;
import gg.jte.extension.api.TemplateDescription;

Expand All @@ -13,6 +14,10 @@ public static String typedParams(TemplateDescription template) {
return template.params().stream().map(param -> String.format("%s %s", param.type(), param.name())).collect(Collectors.joining(", "));
}

public static String kotlinTypedParams(TemplateDescription template) {
return template.params().stream().map(param -> String.format("%s: %s", param.name(), param.type())).collect(Collectors.joining(", "));
}

public static String paramNames(TemplateDescription template) {
if (template.params().isEmpty()) {
return "";
Expand Down
26 changes: 26 additions & 0 deletions jte-models/src/main/jte/dynamictemplates/kmain.jte
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@import gg.jte.extension.api.*
@import gg.jte.models.generator.ModelConfig
@import java.util.Set

@param String targetClassName
@param String interfaceName
@param JteConfig config
@param Set<TemplateDescription> templates
@param Iterable<String> imports
@param ModelConfig modelConfig

@file:Suppress("ktlint")
package ${config.packageName()}

import gg.jte.TemplateEngine
import gg.jte.models.runtime.*
@for(String imp: imports)
import ${imp}
@endfor

${modelConfig.implementationAnnotation()}
class ${targetClassName}(private val engine: TemplateEngine) : ${interfaceName} {
@for(TemplateDescription template: templates)
@template.dynamictemplates.kmethod(template = template)
@endfor
}
12 changes: 12 additions & 0 deletions jte-models/src/main/jte/dynamictemplates/kmethod.jte
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@import gg.jte.extension.api.*
@import gg.jte.models.generator.Util

@param TemplateDescription template

override fun ${Util.methodName(template)}(${Util.kotlinTypedParams(template)}): JteModel {
val paramMap = mapOf(
@for(ParamDescription param: template.params())
"${param.name()}" to ${param.name()},@endfor
)
return DynamicJteModel(engine, "${template.name()}", paramMap);
}
24 changes: 24 additions & 0 deletions jte-models/src/main/jte/interfacetemplates/kmain.jte
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
@import gg.jte.extension.api.*
@import gg.jte.models.generator.ModelConfig
@import java.util.Set

@param String targetClassName
@param JteConfig config
@param Set<TemplateDescription> templates
@param Iterable<String> imports
@param ModelConfig modelConfig

@file:Suppress("ktlint")
package ${config.packageName()}

import gg.jte.models.runtime.*
@for(String imp: imports)
import ${imp}
@endfor

${modelConfig.interfaceAnnotation()}
interface ${targetClassName} {
@for(TemplateDescription template: templates)
@template.interfacetemplates.kmethod(template = template)
@endfor
}
7 changes: 7 additions & 0 deletions jte-models/src/main/jte/interfacetemplates/kmethod.jte
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
@import gg.jte.extension.api.*
@import gg.jte.models.generator.Util

@param TemplateDescription template

@JteView("${template.name()}")
fun ${Util.methodName(template)}(${Util.kotlinTypedParams(template)}): JteModel
29 changes: 29 additions & 0 deletions jte-models/src/main/jte/statictemplates/kmain.jte
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
@import gg.jte.extension.api.*
@import gg.jte.models.generator.ModelConfig
@import java.util.Set

@param String targetClassName
@param String interfaceName
@param JteConfig config
@param Set<TemplateDescription> templates
@param Iterable<String> imports
@param ModelConfig modelConfig

@file:Suppress("ktlint")
package ${config.packageName()}

import gg.jte.models.runtime.*
import gg.jte.ContentType
import gg.jte.TemplateOutput
import gg.jte.html.HtmlInterceptor
import gg.jte.html.HtmlTemplateOutput
@for(String imp: imports)
import ${imp}
@endfor

${modelConfig.implementationAnnotation()}
class ${targetClassName} : ${interfaceName} {
@for(TemplateDescription template: templates)
@template.statictemplates.kmethod(config = config, template = template)
@endfor
}
17 changes: 17 additions & 0 deletions jte-models/src/main/jte/statictemplates/kmethod.jte
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
@import gg.jte.ContentType
@import gg.jte.extension.api.*
@import gg.jte.models.generator.Util

@param JteConfig config
@param TemplateDescription template

!{String outputClass = config.contentType() == ContentType.Html ? "HtmlTemplateOutput" : "TemplateOutput";}
override fun ${Util.methodName(template)}(${Util.kotlinTypedParams(template)}): JteModel {
return StaticJteModel<${outputClass}>(
ContentType.${config.contentType()},
{ output, interceptor -> ${template.fullyQualifiedClassName()}.render(output, interceptor${Util.paramNames(template)}) },
"${template.name()}",
"${template.packageName()}",
${template.fullyQualifiedClassName()}.JTE_LINE_INFO
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package gg.jte.models.generator;

import org.junit.jupiter.api.Test;

import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class TestModelConfig {

@Test
public void configureInterfaceAnnotation() {
var modelConfig = new ModelConfig(Map.of("interfaceAnnotation", "@Deprecated"));
assertEquals(modelConfig.interfaceAnnotation(), "@Deprecated");
}

@Test
public void interfaceAnnotationBlankWhenConfigurationNotPresent() {
var modelConfig = new ModelConfig(Map.of());
assertEquals("", modelConfig.interfaceAnnotation());
}

@Test
public void configureImplementationAnnotation() {
var modelConfig = new ModelConfig(Map.of("implementationAnnotation", "@Singleton"));
assertEquals("@Singleton", modelConfig.implementationAnnotation());
}

@Test
public void implementationAnnotationNullWhenConfigurationNotPresent() {
var modelConfig = new ModelConfig(Map.of());
assertEquals("", modelConfig.implementationAnnotation());
}

@Test
public void languageConfigurationSupportsJava() {
var modelConfig = new ModelConfig(Map.of("language", "Java"));
assertEquals(modelConfig.language(), Language.Java);
}

@Test
public void languageConfigurationDefaultsToJava() {
var modelConfig = new ModelConfig(Map.of());
assertEquals(modelConfig.language(), Language.Java);
}

@Test
public void languageConfigurationSupportsKotlin() {
var modelConfig = new ModelConfig(Map.of("language", "Kotlin"));
assertEquals(modelConfig.language(), Language.Kotlin);
}

@Test
public void languageConfigurationIsCaseSensitive() {
assertThrows(IllegalArgumentException.class, () -> {
new ModelConfig(Map.of("language", "jAvA")).language();
});
}

@Test
public void languageConfigurationFailsWhenLanguageIsNotSupported() {
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
new ModelConfig(Map.of("language", "Ooops")).language();
});

assertEquals(
"JTE ModelExtension 'language' property is not configured correctly (current value is 'Ooops'). Supported values: [Java, Kotlin]",
exception.getMessage()
);
}
}
Loading

0 comments on commit 64786c0

Please sign in to comment.