Quarkusに入っているQuteというテンプレートエンジン単体で使ってみる

QuarkusにはQuteというテンプレートエンジンが入っています。 今回は、これをQuarkusじゃないプロジェクトで使ってみます。 使ったバージョンは1.1.1.Final です。

quarkus.io

今回のコードは以下のリポジトリにおいてあります。 github.com

quarkus-sandboxと言いつつ、quarkusを動かしているフォルダは一つもない・・・

とりあえず動かしてみる

JUnitのテストでQuteを動かしてみます。(実はこのままでは動かないんですが)

public class QuteTest {

    @Test
    public void test() { // 後述するが、このままでは動かない
        final Engine engine = Engine.builder()
                .addDefaults()
                .build();
        final String content = engine.parse("{name} は {price} 円です")
                .data(new Item(BigDecimal.TEN, "スイカ"))
                .render();

        assertThat(content).isEqualTo("スイカ は 10 円です");
    }
}

特に難しいところはないですが、解説をしていきます。

まずテンプレートエンジンの初期化です。

       final Engine engine = Engine.builder()
                .addDefaults() // addDefaultSectionHelpers()とaddDefaultValueResolvers()を呼び出している
                .build();

addDefaultsはaddDefaultValueResolversとaddDefaultSectionHelpersを中で呼び出しています。 まず、ValueResovlerというのは、名前の通りなんですが 下のようなインターフェースです。(正確には下のインターフェースを継承したインターフェース)

public interface Resolver {

    /**
     * 
     * @param context
     * @return the result
     * @see Results#NOT_FOUND
     */
    CompletionStage<Object> resolve(EvalContext context);

}

contextから値をresolveするのがVallueResolverの役割になります。

次にSectionHelpersなんですが実装を見てみましょう。 下のようなコードが書かれています。

    public EngineBuilder addDefaultSectionHelpers() {
        return addSectionHelpers(new IfSectionHelper.Factory(), new LoopSectionHelper.Factory(),
                new WithSectionHelper.Factory(), new IncludeSectionHelper.Factory(), new InsertSectionHelper.Factory(),
                new SetSectionHelper.Factory());
    }

下のリファレンスガイドにある、section tagに対応するHelperのようです。 quarkus.io

エンジンの初期化の次はtemplateのレンダリングです。 これはそんな難しくないですね。

       final String content = engine.parse("{name} は {price} 円です") // テンプレートをparse
                .data(new Item(BigDecimal.TEN, "スイカ")) // データを提供して
                .render(); // レンダリング

dataというメソッドで、テンプレートに使うパラメータを渡しています。

ちなみに、最初に書いた通り、上にあげたテストは失敗します。

何がダメだったのか

リファレンスガイドにある、以下の記述を見てください。

A value resolver is automatically generated for a type annotated with @TemplateData. This allows Quarkus to avoid using reflection to access the data at runtime.

quarkus.io

value resolverは @TemplateData アノテーションをつけておくと自動で生成されるよと書いてあり これによりランタイムにリフレクションを避けることができる、と書かれています。

じゃあ、アノテーションを書いたらすぐうまくいくのかというと また別の話になってきます。

Quarkusがアノテーションを見てValueResolverを生成するのはコンパイルより後のようです。 gradleで普通にできるjarと中身を見て比べてみます。

$ ./gradlew clean quarkusBuild
$ jar tf build/qute-only-1.0-SNAPSHOT-runner.jar # quarkusが生成したjar
# 一部省略しています。
META-INF/
META-INF/MANIFEST.MF
org/
org/acme/
org/acme/qute/
org/acme/qute/Item_ValueResolver.class # なんか出来てる
META-INF/services/
org/acme/qute/Item.class
templates/
templates/hello.html
templates/items.html
META-INF/services/io.quarkus.arc.ComponentsProvider
$ jar tf build/libs/qute-only-1.0-SNAPSHOT.jar # gradleで普通にできるjar
# ValueResolverは見当らない
META-INF/
META-INF/MANIFEST.MF
org/
org/acme/
org/acme/qute/
org/acme/qute/Item.class
templates/
templates/hello.html
templates/items.html

ValueResolverは普通のjarにないのでアノテーションをつけても動かないのは当然です。

ValueResolverを実装して動かしてみる

Quarkus抜きで動かそうとしているので、アノテーションをつけても動かないのは仕方ないので ValueResolverを自分で実装して動かしてみます。

public class QuteTest {

    @Test
    public void test() {
        final ValueResolver valueResolver = context -> { // 増えた
            if (context.getName().equals("name")) {
                final Item item = (Item) context.getBase();
                return CompletableFuture.completedFuture(item.name);
            }
            if (context.getName().equals("price")) {
                final Item item = (Item) context.getBase();
                return CompletableFuture.completedFuture(item.price);
            }
            return Results.NOT_FOUND;
        };
        final Engine engine = Engine.builder()
                .addDefaults()
                .addValueResolver(valueResolver) // 増えた
                .build();
        final String content = engine.parse("{name} は {price} 円です")
                .data(new Item(BigDecimal.TEN, "スイカ"))
                .render();

        assertThat(content).isEqualTo("スイカ は 10 円です");
    }
}

という訳で、こんな感じで動きました。

まとめ

Quarkusの中にあるQuteというテンプレートエンジンをQuarkusの外で使ってみましたが、シンプルで簡単に使えそうです。 @TemplateData は、Quarkusでテストしたらちゃんと動くのかな?また今度試してみます。 Quarkusのビルドライフサイクル周りの知識がないのでハマった感じでした。

終わり。

少し疑問になったんだけど、Quarkusのテストってどんな感じで動くんだろう? quteを使ったサンプルだと、RestAssuredでレンダリングされたレスポンスの検証をしているので テンプレートエンジンが正しく動いているはずなんだけど どのタイミングでValueResolverが提供されるんだろう・・・

今回のコードは以下のリポジトリにおいてあります。 github.com

追記: QuarkusTestの中の話

QuarkusTestの中で使われているQuarkusTestExtensionのコードを読んでみたところ ValueResolverに限らず、テスト時にクラスファイルを生成しているようです。 github.com

なるほど・・・

追記: Quarkusのgradle projectで ./gradlew clean test をすると QuarkusTestが失敗する

Quteを使っている時だけ起きる問題なのかな?他にもありそうだけどここにメモしておく。

src/main/resources/templatesにあるtemplateを./gradlew quarkusBuildした際に build/classes/java/templatesにコピーしている挙動になっている。 そのため、単にclean testすると、QuarkusTestを使ったテストで以下のエラーが発生する

org.junit.jupiter.api.extension.TestInstantiationException: TestInstanceFactory [io.quarkus.test.junit.QuarkusTestExtension] failed to instantiate test class [org.acme.qute.ItemsResourceTest]: io.quarkus.builder.BuildException: Build failure: Build failed due to errors
    [error]: Build step io.quarkus.arc.deployment.ArcProcessor#generateResources threw an exception: javax.enterprise.inject.spi.DeploymentException: Found 2 deployment problems: 
[1] No template found for org.acme.qute.HelloResource#hello
[2] No template found for org.acme.qute.ItemResource#items
    at io.quarkus.arc.processor.BeanDeployment.processErrors(BeanDeployment.java:873)
    at io.quarkus.arc.processor.BeanProcessor.processValidationErrors(BeanProcessor.java:116)
    at io.quarkus.arc.deployment.ArcProcessor.generateResources(ArcProcessor.java:301)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

templateがコピーされてなくてtemplateが見つからない、というエラーが出ている。 ./gradlew quarkusBuild test とすれば動く。

参考