Giter Club home page Giter Club logo

jstachio's Introduction

Maven Central Github

jstachio

A type-safe Java Mustache templating engine.

Templates are compiled into readable Java source code and value bindings are statically checked.

Documentation

The doc is also on javadoc.io but is not aggregated like the above. The aggregated javadoc is the preferred documentation and the rest of this readme is mainly for propaganda marketing purposes.

For previous releases:

https://jstach.io/doc/jstachio/VERSION/apidocs

Where VERSION is the version you want.

Why choose JStachio

Covered in why_jstachio_is_better.md.

Features

  • Logicless Mustache (v 1.3) syntax.

    • Full support of non-optional Mustache spec v1.3.0 requirements (including whitespace)
    • Optional inheritance support with some caveats
    • Optional lambda support with some differences due to static nature
  • Get JEP 430 like support today but wth even more power.

  • Templates are compiled into Java code

  • Value bindings are statically checked.

  • Methods, fields and getter-methods can be referenced in templates.

  • Friendly error messages with context.

  • Zero configuration. No plugins or tweaks are required. Everything is done with standard javac with any IDE and/or build-system.

  • Non-HTML templates are supported. Set of supported escaping content types is extensible.

  • Layouts are supported via the Mustache inheritance spec.

  • Fallback render service extension point via ServiceLoader

    • Seamlessly Fallback to reflection based runtime rendering via JMustache and mustache.java (useful for development and changing templates in real time)
    • If you are not a fan of generated code you can still use JStachio to type check your mustache templates.
  • Customize allowed types that can be outputted otherwise compiler error (to avoid toString on classes that do not have a friendly toString).

  • Formatter for custom toString of variables at runtime

  • Add extra implements interfaces to generated code for trait like add ons (@JStacheInterfaces)

  • Powerful Lambda support

  • Map<String, ?> support

  • Optional<?> support

  • Compatible with JMustache and Handlebars list index extensions (like -first, -last, -index)

  • It is by far the fastest Java Mustache-like template engine as well one of the fastest in general.

  • Zero dependencies other than JStachio itself

  • An absolutely zero runtime dependency option is avaialable (as in all the code needed is generated and not even jstachio is needed during runtime). No need to use Maven shade for annotation processors and other zero dep projects. Also useful for Graal VM native projects for as minimal footprint as possible.

  • First class support for Spring Framework (as in the project itself will provide plugins as opposed to an aux project)

Performance

It is not a goal of this project to be the fastest java templating engine!

(however it is currently the fastest that I know when this readme was last updated)

Not that peformance matters much with templating languges (it is rarely the bottleneck) but JStachio is very fast:

https://github.com/agentgt/template-benchmark

String Output

Template Comparison

UTF-8 byte Output with extended characters

Template Comparison

Quick Example

@JStache(template = """
        {{#people}}
        {{message}} {{name}}! You are {{#ageInfo}}{{age}}{{/ageInfo}} years old!
        {{#-last}}
        That is all for now!
        {{/-last}}
        {{/people}}
        """)
public record HelloWorld(String message, List<Person> people) implements AgeLambdaSupport {
}

public record Person(String name, LocalDate birthday) {
}

public record AgeInfo(long age, String date) {
}

public interface AgeLambdaSupport {

    @JStacheLambda
    default AgeInfo ageInfo(Person person) {
        long age = ChronoUnit.YEARS.between(person.birthday(), LocalDate.now());
        String date = person.birthday().format(DateTimeFormatter.ISO_DATE);
        return new AgeInfo(age, date);
    }

}

@Test
public void testPerson() throws Exception {
    Person rick = new Person("Rick", LocalDate.now().minusYears(70));
    Person morty = new Person("Morty", LocalDate.now().minusYears(14));
    Person beth = new Person("Beth", LocalDate.now().minusYears(35));
    Person jerry = new Person("Jerry", LocalDate.now().minusYears(35));
    String actual = JStachio.render(new HelloWorld("Hello alien", List.of(rick, morty, beth, jerry)));
    String expected = """
            Hello alien Rick! You are 70 years old!
            Hello alien Morty! You are 14 years old!
            Hello alien Beth! You are 35 years old!
            Hello alien Jerry! You are 35 years old!
            That is all for now!
                            """;
    assertEquals(expected, actual);

}

Installation

Maven

<properties>
    <io.jstach.version>0.6.0-SNAPSHOT</io.jstach.version>
</properties>
...
<dependencies>
    <dependency>
        <groupId>io.jstach</groupId>
        <artifactId>jstachio</artifactId>
        <version>${io.jstach.version}</version>
    </dependency>
</dependencies>
...
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.1</version>
            <configuration>
                <source>17</source> <!-- 17 is the minimum -->
                <target>17</target> <!-- 17 is the minimum -->
                <annotationProcessorPaths>
                    <path>
                        <groupId>io.jstach</groupId>
                        <artifactId>jstachio-apt</artifactId>
                        <version>${io.jstach.version}</version>
                    </path>
                    <!-- other annotation processors -->
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

N.B. The annotations jar (jstachio-annotation) is pulled in transitively

Gradle

dependencies {
 
    implementation 'io.jstach:jstachio:VERSION'
 
    annotationProcessor 'io.jstach:jstachio-apt:VERSION'
}

Examples

user.mustache

{{#name}}
<p>Name: {{.}}, Name Length is {{length}}</p>
{{/name}}

<p>Age: {{  age  }}</p>

<p>Achievements:</p>

<ul>
{{#array}}
  <li>{{.}}</li>
{{/array}}
</ul>

{{^array}}
<p>No achievements</p>
{{/array}}

<p>Items:</p>

<ol>
{{#list1}}
  <li>{{value}}</li>
{{/list1}}
</ol>

User.java

Following class can be used to provide actual data to fill into above template.

@JStache(
    // points to src/main/resources/user.mustache file
    path = "user.mustache",
   
    // or alternatively you can inline the template
    template = "", 

    )
public record User(String name, int age, String[] array, List<Item<String>> list) {

   public static class Item<T> {
        private final T value;
        public Item(T value) {
            this.value = value;
        }
        T value() {
            return value;
        }
    }
}

Rendering

New class UserRenderer will be mechanically generated with the above code. This class can be used to render template filled with actual data. To render template following code can be used:

class Main {
    public static void main(String[] args) throws IOException {
        User user = new User("John Doe", 21, new String[] {"Knowns nothing"}, list);
        StringBuilder appendable = new StringBuilder();
        JStachio.render(user, appendable);
    }
}

The result of running this code will be

<p>Name: John Doe, Name Length is 8</p>

<p>Age: 21</p>

<p>Achievements:</p>

<ul>
  <li>Knowns nothing</li>
</ul>

<p>Items:</p>

<ol>
  <li>helmet</li>
  <li>shower</li>
</ol>

Referencing non existent fields, or fields with non renderable type, all result in compile-time errors. These errors are reported at your project's compile-time alone with other possible errors in java sources.

target/classes/user.mustache:5: error: Field not found in current context: 'age1'
  <p>Age: {{  age1  }} ({{birthdate}}) </p>
                  ^
  symbol: mustache directive
  location: mustache template
target/classes/user.mustache:5: error: Unable to render field: type error: Can't render data.birthdate expression of java.util.Date type
  <p>Age: {{  age  }} ({{birthdate}}) </p>
                                    ^
  symbol: mustache directive
  location: mustache template

See test/examples project for more examples.

Java specific extensions

Enum matching support

Basically enums have boolean keys that are the enums name (Enum.name()) that can be used as conditional sections.

Assume light is an enum like:

public enum Light {
  RED,
  GREEN,
  YELLOW
}

You can conditinally select on the enum like a pattern match:

{{#light.RED}}
STOP
{{/light.RED}}
{{#light.GREEN}}
GO
{{/light.GREEN}}
{{#light.YELLOW}}
Proceeed with caution
{{/light.YELLOW}}

Index support

JStachio is compatible with both handlebars and JMustache index keys for iterable sections.

  • -first is boolean that is true when you are on the first item
  • -last is a boolean that is true when you are on the last item in the iterable
  • -index is a one based index. The first item would be 1 and not 0

Lambda support

JStachio supports lambda section calls in a similar manner to JMustache. Just tag your methods with @JStacheLambda and the returned models will be used to render the contents of the lambda section. The top of the context stack can be passed to the lambda.

JStachio unlike the spec does not support returning dynamic templates that are then rendered against the context stack. However dynamic output can be achieved by the caller changing the contents of the lambda section as the contents of the section act as an inline template.

Design

The idea is to create templating engine combining mustache logicless philosophy with Java's single responsibility and static-typing. Full compile-time check of syntax and data-binding is the main requirement.

Currently Java-code is generated for templates. Generated Java-code should never fail to compile. If it is impossible to generate valid Java-code from some template, friendly compile-time error pointing to template file should be generated. Users should never be exposed to generated Java-code.

Original mustache uses Javascript-objects to define rendering context. Fields of selected Javascript-objects are binded with template fields.

Static mustache uses Java-objects to define rendering context. Binding of template fields is defined and checked at compile-time. Missing fields are compile-time error.

License

JStachio is under BSD 3-clause license.

jstachio's People

Contributors

actions-user avatar agentgt avatar dependabot[bot] avatar dsyer avatar sullis avatar sviperll avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

jstachio's Issues

JStacheType = STACHE for 0.10 causes nullpointer for no formatter types

invoke "javax.lang.model.element.TypeElement.asType()" because "type" is null
[ERROR]   	at io.jstach.apt.internal.context.JavaLanguageModel.supers(JavaLanguageModel.java:161)
[ERROR]   	at io.jstach.apt.GenerateRendererProcessor.resolveFormatterTypes(GenerateRendererProcessor.java:418)
[ERROR]   	at io.jstach.apt.GenerateRendererProcessor.model(GenerateRendererProcessor.java:525)
[ERROR]   	at io.jstach.apt.GenerateRendererProcessor.writeRenderableAdapterClass(GenerateRendererProcessor.java:689)
[ERROR]   	at io.jstach.apt.GenerateRendererProcessor._process(GenerateRendererProcessor.java:182)

Suggestion: something like TemplateLoader from JMustache

Hi

I have converted one of the templates in my jaxrs generator. It has found a number of fields I did not provide data for, so it has been most excellent to get "static" typing templates. Thanks!

The only feedback I have for now is that maybe you should consider/explore adding something like a TemplateLoader argument to @JStache.

I had to change most of my template files to include the path of partials inclusion, and fix their names.
Not a big issue for me, as I only have one set of templates.

But something like https://github.com/OpenAPITools/openapi-generator would be hard pressed to use jstachio (without big changes), because they rely on folder-namespacing to control load location for partials.

It might be worth considering to help some projects adopt jstacio (should they want to).

I will convert my remaining templates and let you know if I find any problems (or other suggestions).

Cheers!

Renderer -> Template documentation

Original the Template interface did not exist so everywhere "Renderer" was used.

Also need to change default suffix of "Renderer" to "Template".

[FR] Custom annotations instead of JStache

Because we no longer allow multiple JStache on a single class (see #15) there is now the issue if you want to have multiple templates generated for a single model class there is no option.

The issue of why JStaches (see #15) was removed boils down to two problems:

  • lookup disambiguation (a template is currently only looked up by the model class and no other parameters)
  • class name collision (when the template classes are generated they need different names).

The two major use cases for multiple JStaches are different content types (e.g. html and text) and possibly locale or i18n.

One solution to the problem is allowing custom annotations.

@Text
@Html
record SomeModel() {}

Then @Text might be defined like:

@JStacheConfig(naming=@JStacheName(suffix="Text"), contentType=PlainText.class)
public @interface Text {
}

Then in all the template finding mechanisms you just say which annotation you want to use.

This adds quite a bit of complexity to both the annotation processor and reflection lookup.

JStachioModelView should override getContentType()

JStachioModelView helpfully provides a contentType() method that sort of hides the getContentType() from the super type - at least from idiots like me. Wouldn't it be better to actually rename the existing method to be the same as the supertype, or at least provide a default implementation that is the same?

JStache.template() -> JStache.inline() refactor

The JStache annotation attribute template is confusing as it is unclear if it is the path or a string literal.

Thus:

JStache(template="{{message}}")

Will become:

JStache(inline="{{message}}")

Quote in inline template throws IllegalArgumentException

Hello,

it looks like there is a bug when a quote is present in a inlined template.

For example:

import io.jstach.jstache.JStache;

@JStache(template = """
        bugged "here"
        """)
public record BugReport() {
}

generates:

...
public class BugReportRenderer implements io.jstach.jstachio.Template<com.decathlon.onecatalog.meta.referential.BugReport>,
...
    /**
     * Inline template string copied.
     * @hidden
     */

    public static final String TEMPLATE_STRING = 
    "bugged \\"here\\"\n";
 ...

the TEMPLATE_STRING var contains \\" instead of just \", it should look like this:

    public static final String TEMPLATE_STRING = 
    "bugged \"here\"\n";

I can reproduce this in 0.7.0 and 0.8.0 (I didn't try previous version to check if it's a regression)

Make newline the same for reproducible builds

In a couple of places we use System.lineSeparator() for code generation.

Its best to just use LF everywhere to make the builds byte reproducible for the source jars and possibly javadoc jars (I'm fairly sure line endings do not impact class jars).

[FR] Nullable checking to avoid null checking code generation

To determine if DeclaredType aka class instances are falsey we generate code to check if they are null.

While that seems like a good idea in the context of Javascript in most Java codebases today things are not null and if they are it is often a serious bug. JStachio exacerbates this because it will hide it instead of failing fast like you would if you were to write rendering code manually or using more programmatic templating like JTE or Rocker.

In my opinion this violates JStachio core philosophy of being as typesafe as possible and failing as fast as possible preferable during compile time.

Thus I propose an option where we support TYPE_USE @Nullable annotations like JSpecify (not released yet), Eclipses and kind of Intellijs. Yes there are other nullable annotations that are not TYPE_USE but those are far more difficult to support (frankly that ones that do not... they are fucking wrong and is disappointing because TYPE_USE has been around since JDK 8 but I will leave that rant for another day... findbugs aka spotbugs)

What we will do is check if there is annotation is present on the DeclaredType that has a simple name of Nullable (potentially configurable) and if it is then we will do null checking otherwise it is assumed it is nonnull like a native type (and currently Optional as Optional already has this behavior).

As a side note this will increase rendering performance slightly but that is not the reason we want to do it.

This is a lot of work but worth it. I'm not sure if I can get it in before end of year which I plan on 1.0.0.

cc @sviperll

EDIT

I should go over the various level of support of nullable we want to achieve

EXHAUST_NULLABLE

Not implemented yet

Force sectioning on a nullable variable

Example:

@JStache
public record Model(@Nullable String message){}

Template code like below will fail during compilation:

{{message}}

As message could be null.

To correct it would be to do the following:

{{#message}}
{{.}} non null case
{{/message}}
{{^message}}
null case
{{/message}}

NULL_CHECK_DISABLE

โœ… implemented see:
NO_NULL_CHECKING

#134 and #164

Remove conditional checking if a variable is null if it is not @Nullable

Currently anytime a section is accessed we check if its not null.

Example:

{{model.action.message}}

The code generated is roughly like:

if (model != null) { 
   if (model.action() != null) {
      format(model.action().message());
   }
}

Notice that action got called twice. It maybe a separate feature that we store the action return results as variable but it is relevant to nullable support as there is no guarantee calling the method twice will yield the same results. Furthermore the above hides potential bugs if we are expecting action to never be null.

Anyway NULL_CHECK_DISABLE turned on would yield code like:

format(model.action().message())

N.B. That NULL_CHECK_DISABLE does not necessarily need to read nullable annotations to work unlike EXHAUST_NULLABLE.

NULL_CHECK_DOTTED_PATH

Not implemented

There also could be various levels of NULL_CHECK_DISABLE in the case of dotted paths. That is maybe only the first name in path (model in this case) is checked or not checked. Or maybe non dotted paths are treated differently

For example:

{{#model}}
{{/model}}

Should generate:

if(model != null) {
}

But maybe not. If nullable annotations are not available we need to assume they are testing for null to prevent NPE and thus we would always generate the above code for an undotted name (model).

The question is what to do for:

{{#model.action}}
{{/model.action}}

Should model only be checked? Should model and action be checked? Should only action (last in dotted name) be checked?


Need to get feedback. EXHAUST_NULLABLE is not going to work at the moment or anything that needs to absolutely confirm something is nullable as TYPE_USE annotations are not available across compile time boundaries unless we do some --add-modules hacks to load up Symbol.

Furthermore a separate feature that overlaps is storing property accessors on the stack as variables to avoid repeatedly calling the accessor methods.

Refactoring to be consistent with existing java mustache implementations and non service loader choices

We need to do some refactoring before going 1.0.0 ( @sviperll any comments would be helpful )

Current API is here for reference: https://jstach.io/jstachio/

Rename Methods

For non static methods

render -> execute

For static methods

execute -> render

We currently have it inverted where render is used instead of execute.

This is to follow inline with JMustache and mustache.java

JStachio static class

Secondly the JStachio static methods class should possibly be renamed to JStachios to be more consistent with Java practice of plural static utility classes (e.g. Files, Objects). Still on the fence about that one.

The actual real problem is JStachio is very static singleton like which for frameworks like Spring does not work well. It was done this way because JStachio is very much a static like library because the code is generated.

Regardless we basically need a non static version of JStachio aka an interface or abstract classes with the method:

execute(Object model, Appendable a) throws IOException;

If we keep JStachio as the utility class maybe we call the interface JStacheEngine or JStacheExecutor or JStacher.

Or if we move JStachio to JStachios then we can make JStachio the interface.

Common interfaces

I really am not a fan of the word Renderer. It seems awkward. Still I prefer it over RenderableAdapter (no offence @sviperll)

Renderer -> JStacher (this seems worse though) | or maybe it should be RenderFunction. I'm tempted to just keep this one as Renderer for now.

RenderFunction -> Renderable | JStacheRenderable | JStachable

Yes I am fan with prefixing public API classes with JStach(e|io) because it makes API discovery easier in the IDE as well as less conflicts. I can't stand libraries with Manager or Executor or Engine generic names. Even Template is overloaded. Just load up an IDE and type Template, press ctrl+space and watch the sheer number of them popup. I am fine with inner classes having names like that though.

Support java.util.stream.Stream for list sections

Support java.util.stream.Stream for sections.

Probably should only support it for method calls as the method call essentially acts like a factory (e.g. like Iterable is to Iterator) since Streams or more like iterators then iterables.

Reflection free JStachioTemplateFinder option

We currently generate a listing of all templates as ServiceLoader services registration file (META-INF/services/io.jstach.jstachio.TemplateProvider) but this does not work well for modularized applications (module-info.java) as registration of ServiceLoader services needs to go into module-info.java.

Furthermore the serviceloader is still reflection based albeit Graal VM native is pretty smart about that.

I propose generating a class whose name and location is configurable that has a listing of all the Templates.

Something like:

@JStacheConfig(generateTemplateFinder="MyFQClassName")
module my.module {
}

The generated class could be an enum and implement TemplateProvider.

EDIT this should probably be its own annotation like JStachePath (see #14) where it is annotation as well as can sit JStacheConfig:

@JStacheConfig(templateFinder={@JStacheTemplateProvider(className="fqn")})
module my.module {
}

and

@JStacheTemplateProvider(className="fqn")
module my.module {
}

OR alternatively we could make it slightly similar to how JStacheFormatter and JStacheContentType work where you create an interface.

@JStacheTemplateProvider
public interface MyTemplateProvider {
}

Gradle build problems with multiple annotation processors

I have been working in Eclipse getting some of my templates working. And it has been fine.

But every other build (it seems) from gradle fails.
I think it may be dependent on which order the annotation processors are run in.

I uses Immutables generator to create the classes used for template input.

(This bug description is crap - feel free to ignore/close. But I hope it may tell you something.)

Here is an example output (the ls just shows that my attempted copy does work - but I think the folder is deleted before the proper compile operation is started).

$ ./gradlew --console plain compileJava                                                                                                                                                                                                                      [152/1847]
                                                                                                                                                               
> Task :compileJava FAILED                                                                                                                                     
total 0                                                                                                                                                        
drwxr-xr-x. 1 jskov jskov  520 Oct 23 10:06 jstache                                                                                                            
drwxr-xr-x. 1 jskov jskov   16 Oct 23 10:06 META-INF                                                                                                           
drwxr-xr-x. 1 jskov jskov 1166 Oct 23 10:06 templates                                                                                                          

/home/jskov/git/openapi-jaxrs-client/src/main/java/dk/mada/jaxrs/generator/api/tmpl/CtxApi.java:18: error: java.nio.file.NoSuchFileException: /home/jskov/git/openapi-jaxrs-client/build/classes/java/main/jstache/api.mustache
public interface CtxApi {                                                                                                                                      
       ^                                                                                                                                                       
        at java.base/sun.nio.fs.UnixException.translateToIOException(UnixException.java:92)                                                                    
        at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:106)                                       
        at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:111)                      
        at java.base/sun.nio.fs.UnixFileSystemProvider.newByteChannel(UnixFileSystemProvider.java:218)                             
        at java.base/java.nio.file.Files.newByteChannel(Files.java:380)                                                                                        
        at java.base/java.nio.file.Files.newByteChannel(Files.java:432)                                                                                        
        at java.base/java.nio.file.spi.FileSystemProvider.newInputStream(FileSystemProvider.java:422)               
        at java.base/java.nio.file.Files.newInputStream(Files.java:160)                                                                                        
        at jdk.compiler/com.sun.tools.javac.file.PathFileObject.openInputStream(PathFileObject.java:461)            
        at [email protected]/javax.tools.ForwardingFileObject.openInputStream(ForwardingFileObject.java:77)    
        at io.jstach.apt.TextFileObject.openInputStream(TextFileObject.java:76)                                                                                
        at io.jstach.apt.CodeWriter.lambda$compileTemplate$0(CodeWriter.java:111)                                                
        at io.jstach.apt.TemplateCompiler$RootTemplateCompiler.<init>(TemplateCompiler.java:696)                          
        at io.jstach.apt.TemplateCompiler$SimpleTemplateCompiler.<init>(TemplateCompiler.java:728)                         
        at io.jstach.apt.TemplateCompiler.createCompiler(TemplateCompiler.java:62)                                               
        at io.jstach.apt.CodeWriter.compileTemplate(CodeWriter.java:125)                                                                                       
        at io.jstach.apt.ClassWriter.writeRendererDefinitionMethod(GenerateRendererProcessor.java:618)                         
        at io.jstach.apt.ClassWriter.writeRenderableAdapterClass(GenerateRendererProcessor.java:595)             
        at io.jstach.apt.GenerateRendererProcessor.writeRenderableAdapterClass(GenerateRendererProcessor.java:427)       
        at io.jstach.apt.GenerateRendererProcessor._process(GenerateRendererProcessor.java:171)                          
        at io.jstach.apt.GenerateRendererProcessor.process(GenerateRendererProcessor.java:133)

So @Immutable is probably run first. And then your annotation processor runs on the output, using another context than if it is run first.
And that causes confusion about where to load the template files from.

If you want to try it, you can clone https://github.com/jskov/openapi-jaxrs-client/tree/jstachio

[doc] Too many related packages in Javadoc

Every package page has a big giant "Related Packages" section that has every package even if they are not related.

The javadoc Related Package algorithm appears to not like having base packages like the current io.jstach. The current algorithm causes every package to be listed regardless of package.

I tried to look around to see how its managed and how the JDK does its javadoc and found bizarre behavior that finally made sense once I looked at the code:

jdk.javadoc.internal.doclets.formats.html.PackageWriterImpl

    private List<PackageElement> findRelatedPackages() {
        String pkgName = packageElement.getQualifiedName().toString();

        // always add super package
        int lastdot = pkgName.lastIndexOf('.');
        String pkgPrefix = lastdot > 0 ? pkgName.substring(0, lastdot) : null;
        List<PackageElement> packages = new ArrayList<>(
                filterPackages(p -> p.getQualifiedName().toString().equals(pkgPrefix)));
        boolean hasSuperPackage = !packages.isEmpty();

        // add subpackages unless there are very many of them
        Pattern subPattern = Pattern.compile(pkgName.replace(".", "\\.") + "\\.\\w+");
        List<PackageElement> subpackages = filterPackages(
                p -> subPattern.matcher(p.getQualifiedName().toString()).matches());
        if (subpackages.size() <= MAX_SUBPACKAGES) {
            packages.addAll(subpackages);
        }

        // only add sibling packages if there is a non-empty super package, we are beneath threshold,
        // and number of siblings is beneath threshold as well
        if (hasSuperPackage && pkgPrefix != null && packages.size() <= MAX_SIBLING_PACKAGES) {
            Pattern siblingPattern = Pattern.compile(pkgPrefix.replace(".", "\\.") + "\\.\\w+");

            List<PackageElement> siblings = filterPackages(
                    p -> siblingPattern.matcher(p.getQualifiedName().toString()).matches());
            if (siblings.size() <= MAX_SIBLING_PACKAGES) {
                packages.addAll(siblings);
            }
        }
        return packages;
    }

Yeah those MAX_SUBPACKAGES and friends ... hard coded. Probably should file some issues with the JDK.

Possible solutions:

  1. The easy fix is CSS hide display the Related Packages but that is a crappy fix.
  2. The medium level fix is to not have base package of io.jstach and instead make it like io.jstach.runtime or something.
  3. The much harder fix is to write our own doclet.

Number 2 seems like the best solution but is a big namespace change.

JStacheInterfaces should handle parameterization correctly

Given an interface like:

public interface ViewFactory<T> {
}

When we do

@JStacheInterfaces(templateImplements={ViewFactory.class})

With some model:

public record Some(){}

It should make generated code like:

public class SomeRenderer implements ViewFactory<Some> {
}

But currently screws it up and does

public class SomeRenderer implements ViewFactory {
}

This is desirable for #10

Add templateExtends to JStacheInterfaces (and possibly remove constructorAnnotated)

JStacheInterfaces currently allows you to add interfaces to the generated templates as well as annotations but does not allow you to make the templates extend a class. This was because the original code generator used an Abstract class for the base of all generated templates (renderers).

This feature is desirable for Spring injection and may remove the need for templateConstructorAnnotations as that configuration begs the question of which constructor annotation (currently the one that takes TemplateConfig).

Template paths starting with "/" slash fail

java.lang.IllegalArgumentException: Invalid relative name: /config/jooq-config.xml
  	at jdk.compiler/com.sun.tools.javac.file.JavacFileManager.getFileForOutput(JavacFileManager.java:894)
  	at [email protected]/javax.tools.JavaFileManager.getFileForOutputForOriginatingFiles(JavaFileManager.java:535)
  	at jdk.compiler/com.sun.tools.javac.processing.JavacFiler.getResource(JavacFiler.java:596)
  	at io.jstach.apt.TextFileObject.openInputStream(TextFileObject.java:69)
  	at io.jstach.apt.CodeWriter.lambda$compileTemplate$0(CodeWriter.java:116)
  	at io.jstach.apt.TemplateCompiler$RootTemplateCompiler.<init>(TemplateCompiler.java:710)
  	at io.jstach.apt.TemplateCompiler$SimpleTemplateCompiler.<init>(TemplateCompiler.java:742)
  	at io.jstach.apt.TemplateCompiler.createCompiler(TemplateCompiler.java:66)
  	at io.jstach.apt.CodeWriter.compileTemplate(CodeWriter.java:130)

Iterable and boolean virtual key to pass to lambdas

There is no way to pass a list or a boolean to a lambda.

This feature will add a virtual key that will make the iterable or boolean treated like normal objects (DeclaredType).

The virtual name is still being decided but @this or -this is the current thinking.

{{#mylist.@this}}
{{#myLambda}}...{{/myLambda}}
{{/mylist.@this}}

jstachio logo

I went ahead and drew what I imagine may be a logo for jstachio. I thought that current logo is more like a place holder. What do you think?

jstachio-icon

Handling I18N

There are a lot of techniques to handle internationalization (I18N).
JStachio currently does not ship with an opinionated way to handle it.

Perhaps ship with a lambda that does it.

Improve Lambda support

Power lambdas, context stack access and compile time extensions to check lambda body for domain specific languages

This is a work in progress aka brainstorm.

Currently Lambdas sectionals ({{#lambda}}body{{/lambda}}) can pass the raw body to the lambda as well as the current context (top of the stack). They can then return either a raw string or new object that gets put onto the stack. If they return an object the contents of the lambda body are used as template and rendered with returned object as the context.

Thus you can really only pass two parameters to lambdas. Similarly the lambda has to generate the entire returned string if it wants to alter or wrap the body template. The classic case wanting to wrap html tags around the rendered body is sadly not supported. Basically JStachio does not support dynamic templates nor does it support full access to the context stack (albeit the latter being much easier to fix).

EDIT

There are several problems with Lambdas currently:

  1. No way to wrap rendered body section (e.g. to wrap HTML tags around section body template)
  2. Can currently only access top of stack
  3. Cannot access entire section bindings like lists and booleans
  4. Using the body as a way to pass parameters is not very type safe and they can only be literal.
  5. If you do use the body to pass string literal parameters then you can no longer use the section body as a template.

Parameters

Lets first talk about parameters

Here is an example:

{{#contextParameter}}
{{#lambda}}
body
{{/lambda}}
{{/contextParameter}}

The lambda can get the raw "body" string and whatever contextParameter is.

Some ways to get more parameters are to parse the "body" string for raw parameters. I say raw because you cannot pass other variable from the context. e.g:

{{#contextParameter}}
{{#lambda}}
{{a}},{{b}},{{c}}
{{/lambda}}
{{/contextParameter}}

To the lambda method call the body will not be interpolated and still be {{a}},{{b}},{{c}}. Notice I said to the lambda because it can be eventually interpolated if the lambda returns an object it just that the lambda method cannot currently see the results. So one of the first questions is whether to allow eager interpolation as an option. Currently this is easily supported in JMustache because its lambdas have access to the JMustache compiler.
Even if we have eager evaluation the parameters would become strings in some special format which is not ideal.

Wrapping section rendering

Eager evaluation: JStachio could support this by essentially generating code that renders to a StringBuilder and then passes that StringBuilder or String to the lambda. To do this we would add another annotation. @JStacheLambda.Rendered analogous to the current @JStacheLambda.Raw. Perhaps JStacheLambda.Raw should really be JStacheLambda.Parameter with an num for Raw or Rendered <-- that idea is nasty.

A better solution for wrapping assuming context parameter is of type Person assuming following:

interface Person {
  String firstName();
  String lastName();
}

interface DecoratedPerson {
  Person person();
  String fullName();
}
{{#person}}
{{#lambda}}
{{fullName}} <-- our lambda has converted Person to DecoratedPerson
{{/lambda}}
{{/person}}

The above is currently supported (shown below) but we cannot do anything with the results of the rendered section:

// Below will use the contents of the section body as a template with
// decorated person as the model.
@JStacheLambda
DecoratedPerson lambda(Person person) {
  return decorate(person);
}

What I propose adding is JStachio handling rendering functions like:

@JStacheLambda
@JStacheLambda.Raw
String lambda(Person person, Function<DecoratedPerson, String> renderFunction) {
  DecoratedPerson dp = decorate(person);
  return "<em>" + renderFunction.apply(dp) + "</em>";
}

Or going a step further and avoiding the lambda returning a raw string:

public record PersonView(String inner) {
}

@JStacheLambda
@JStache(template = "<em>{{{inner}}}</em>")
PersonView lambda(Person person, Function<DecoratedPerson, String> renderFunction) {
  DecoratedPerson dp = decorate(person);
  return new PersonView(renderFunction.apply(dp));
}

The above is very interesting because this would allow multiple templates for a model class (currently a top level class requires the JStache annotation on it)! See #15 and #42

EDIT the problem with the above is creating the Function<DecoratedPerson, String> for iteration as Java does not support mutable lexical closure-like bindings. So supporting -last, @index etc would require a reference like AtomicInteger as well as the item in the list itself. This is because the section body now has full stack access thanks to #109.

So instead of the Function we could make a virtual partial called *section to allow wrapping of section body.

So again assume root template like:

{{#person}}
{{#lambda}}
{{fullName}} <-- our lambda has converted Person to DecoratedPerson
{{/lambda}}
{{/person}}
@JStacheLambda(template = "My name is {{person.lastName}}, <em>{{>*section}}</em>.")
DecoratedPerson lambda(Person person) {
  return decorate(person);
}

This achieves the wrapping as well as escape issues... I really like it.

See mustache/spec#135

Reference config by annotated proto type class

          Using Petclinic as an example:

We have a class called org.springframework.samples.system.MyJStacheConfig

@JStacheConfig(...) // all standard config here
public class MyJStacheConfig {
}

Then in the org.springframework.samples.vet.package-info.java

import org.springframework.samples.system.MyJStacheConfig;

@JStacheConfig(ref=MyJStacheConfig.class)
package org.springframework.samples.vet;

and org.springframework.samples.owner.package-info.java

import org.springframework.samples.system.MyJStacheConfig;

@JStacheConfig(ref=MyJStacheConfig.class)
package org.springframework.samples.owner;

I guess if the problem is making package-info.java I will have a hard time fixing that but even then you could in each class just put @JStacheConfig(ref=MyJStacheConfig.class).

package org.springframework.samples.owner;

@JStache(path=...)
@JStacheConfig(ref=MyJStacheConfig.class)
public record CreateOrUpdateOwnerForm() {
}

And if the extra annotation is a problem we could do:

package org.springframework.samples.owner;

@JStache(path=..., config=MyJStacheConfig.class)
public record CreateOrUpdateOwnerForm() {
}

How is that?

BTW although it is painful for package-info for every package you are technically supposed to do it for JSpecify and other TYPE_USE like nullable annotations. So I feel the pain but you kind of have to do it anyway and is encouraged by the javadoc system.

EDIT the above I could add very easily in very little time if interested.

Originally posted by @agentgt in #69 (comment)

FYI 0.8.1 causes infinite loop in Gradle compilation

And 0.7.0 and 0.8.0 before it.

Regrettably, I have not had time to investigate, if this is because something I have failed to fix - a requirement for updating from 0.8

But I thought I would notify you, before you suddenly release 1.0 with (maybe?) problems in the Gradle plugin.

I hope I will have time to dig into it this weekend.

You can see the dependabot result on my project here:
https://github.com/jskov/openapi-jaxrs-client/actions/runs/3639636836/jobs/6143271240

Illegal start of type with wildcard generics

With 0.10.1, template like this will not compile:

@JStache(template = """
        {{#options}}
        {{#transform}}
        {{name}}
        {{/transform}}
        {{/options}}""")
public record FooGen(List<GenericRecord<?>> options) {
    record GenericRecord<T>(T value) {
    }

    @JStacheLambda
    public CRecord transform(GenericRecord<?> option) {
        return new CRecord("repro");
    }

    record CRecord(String name) {
    }
}

The generated code in render(...):

...
        for (java.util.Iterator<? extends FooGen.GenericRecord<>> elementIt = data.options().iterator(); elementIt.hasNext(); i++) {
            FooGen.GenericRecord<> element = elementIt.next();
        if (element != null) { 
...

the wildcard is missing so the code doesn't compile


If we remove the wildcard like this

-public record FooGen(List<GenericRecord<?>> options) {
+public record FooGen(List<GenericRecord> options) {

or specify the type, the code will compile

-public record FooGen(List<GenericRecord<?>> options) {
+public record FooGen(List<GenericRecord<String>> options) {

Allow registering formatter types on a formatter

          I quick thought to remove the pain and errors of registering both for formatterTypes and formatter is to have formatterTypes annotation on the formatter class itself. Then if you have the formatter picked the types will be pulled in as well.

Thus formatterTypes would not live on JStacheConfig but the formatter (provider) itself.

@JStacheFormatter
@JStacheFormatterTypes(...)
public class MyFormatter implements Formatter {
}

It is an interesting feature to potentially allow multiple formatters but I think a DI framework is far more capable of solving that problem (to your point of Spring Conversion service). At least with the above the types and how they are converted are located in the same place.

Originally posted by @agentgt in #69 (comment)

Lambdas not using entire context stack

When lambdas return models to be rendered with sections only the returned model was used as the context.

What should happen is the model should be pushed on to the context stack so that the section template can access the entire context.

JStacheLambda section partials

Section partials are a new feature to allow lambdas to render the body section while wrapping.
The classic use case is to allow lambdas to wrap say html tags around an inputted section body.

interface Person {
  String firstName();
  String lastName();
}

interface DecoratedPerson {
  Person person();
  String fullName();
}
{{#person}}
{{#lambda}}
{{fullName}} <-- our lambda has converted Person to DecoratedPerson
{{/lambda}}
{{/person}}
// Below will use the contents of the section body as a template with
// decorated person as the model.
@JStacheLambda
DecoratedPerson lambda(Person person) {
  return decorate(person);
}

The above is currently supported (shown above) but we cannot do anything with the results of the rendered section such as wrapping it with tags or perhaps repeating the output.

What I propose adding is lambda templates that can reference the section body as a partial. The section partial will be named *section.

@JStacheLambda(template = "My name is {{person.lastName}}, <em>{{>*section}}</em>.")
DecoratedPerson lambda(Person person) {
  return decorate(person);
}

This achieves the wrapping as well as escape issues.

See #107

[FR] Zero dependency code generation

JStachio itself has zero dependencies (other than the Eclipse nullable annotations which are retention class so in theory folks can omit that dependency and jstachio has them as optional anyway).

However if you generate code with JStachio you will need JStachio's runtime module io.jstach as the code generated depends on some interfaces.

I plan on making an option where all generated code has zero references to jstachio runtime module and in theory once the code is generated nothing of jstachio has to be on the classpath.

The library has always had this planned and indeed many features like @JStacheInterfaces, the formatters and escapers being function-like as well as the decoupling of annotation was done early for this but the complete implementation was lesser priority over ease of use.

We won't generate any escapers or formatters as this features use case is mainly for code generation and not web development and thus no escaping is fine (and they can always provide their own like regular jstachio).

One question remains whether to have a new set of annotations (and module) that have a RetentionPolicy of CLASS, or SOURCE as currently the annotations while separated are RUNTIME.

The other question is whether to just generate static methods and have the method names be the template name but that would require a lot more work.

Otherwise it is not a lot of work once the above choices are made. I was hoping for 1.0.0 with this but maybe I wait for 2.0.0.

cc @sviperll

Remove @JStaches

Having multiple @JStache on a single model is problematic for the JStachio runtime as well as complicates the annotation processor.

Option to handle escaping of raw JStacheLambda output

Currently if lambdas do their own output via Raw they need to handle escaping on their own which requires knowing which escaper is set.

I propose adding a simple escape attribute so that the burden is removed from the lambda output.

JStacheLambda.Raw(escape=true)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.