msawady’s engineering-note

なにも分からないエンジニアです。

openapi-generator をカスタマイズして、OpenAPI から自由自在にコード生成する

これは FOLIO Advent Calendar 2021 の7日目の記事です。6日目は @nameless_gyoza さんでした。

zenn.dev

OpenAPI からのコード生成を頑張る

Web APIを開発する際に「OpenAPI Specificationをかいて、そこからopenapi-generatorを使ってControllerやDTOのコードを自動生成しようぜ」というのは良くある流れです。 ただ、自分のチームでは「openapi-generatorにFinatraに対応しているジェネレータとテンプレートがない」という問題があり、自前でジェネレータとテンプレートを実装することになりました。

この記事では自前ジェネレータ実装の中で得た「OpenAPIコード生成の開発ノウハウ」や「具体的なカスタマイズ例」を書いていきます。

そもそも何をどうカスタマイズするのか?

openapi-generatorによるコード生成実行時のコマンドは以下のような感じです。

openapi-generator generate \
      -g com.mypackage.MyCodeGen \
      -t path/to/templates/ \
      -i spec.yaml \
      -o outdir \
      -c config.yaml

カスタマイズする対象は -g オプションで指定するジェネレータと、 -t オプションで指定するテンプレートです。

ジェネレータ、テンプレートとは?

大ざっぱに言うと、openapi-generatorは以下のような流れでコードの自動生成をしています。

  1. OpenAPI Specの読み込み
  2. Specをもとに"出力モデル"を生成する
  3. テンプレートに"出力モデル"を読み込ませて生成コードを作る

ここで、2の「Specをもとに"出力モデル"を生成する」処理を担当するのがジェネレータです。 ジェネレータを言語やフレームワークに応じてカスタマイズすることで、出力モデルを修正したり、追加のデータを付与することができます。

実装する時は既存のopenapi-generatorに実装されているXxxCodeGen( implements CodegenConfig)をoverrideして使うのがラクです。

import org.openapitools.codegen.languages.AbstractScalaCodegen;

public class MyCodeGen extends AbstractScalaCodegen {
    /* ここで処理をoverriideしてカスタマイズする */
}

テンプレートはmustache 形式でかかれています。 Logic-less というだけありシンプルで少機能ですが、コレクションのループ・Bool値や値の有無による分岐は出来るため、 テンプレートをカスタマイズするだけでも対応できることは少なくありません。

しかし、mustacheテンプレートをいくらこねくり回しても出力モデルに付与されていないデータを生成コードに乗せることはできませんし、 mustacheテンプレートでは「この値がXxxだったらYyyする」といった分岐はできません。そのため、少し凝ったことをしようとすると、ジェネレータのカスタマイズをする必要が出てきます。

開発tips

ここからは、ジェネレータ開発やテンプレートでよく使う小技やカスタマイズ例を挙げていきます。

出力モデルを確認する

公式ドキュメントの Debugging を参考にデバッグオプションを指定します。 特に以下の2つのオプションをつけて実行し、結果をファイル出力してGrepしながら確認することが多いです。

  • --global-property debugModels : model(DTOなどのデータ)の出力モデルが標準出力される
  • --global-property debugOperations: operation(API定義)の出力モデルが標準出力される

また、IDEからDebug実行することで任意のタイミングでのモデルの値を確認することも出来るので、 本格的にデバッグしたい場合にはIDEから動かすのも一案です。

mustacheテンプレートで最後の要素かどうかで分岐する

公式ドキュメントのMustache Tipsに記載されていますが、 {{#-last}} あるいは {{^-last}} を利用することで「最後だったらXxx, 最後じゃなかったらYyy」をできます。

例えば、

{{^-last}},{{/-last}}

とすれば「最後の要素じゃなければ , をつける」とできます。

ファイルを返すAPIで、返り値を java.io.File ではなく Array[Byte] にする

コンストラクタの中で、 typeMapping を修正します。

public MyCodeGen() {
  super();
  typeMapping.put(
      "File", "Array[Byte]"
  );
}

サービスで利用する認証/認可に関するデータを出力できるようにする

ベースとするジェネレータの実装によってはOAuthに関する情報を読み込まない設定になっているので、 必要に応じてコンストラクタの中で指定します。

public MyCodeGen() {
    super();
    // OAuth2.0の認可コードフロー、もしくはクライアント・クレデンシャルフローのみを扱う
    modifyFeatureSet(features -> features.securityFeatures(EnumSet.of(
        SecurityFeature.OAuth2_AuthorizationCode,
        SecurityFeature.OAuth2_ClientCredentials
    )));
}

特定の条件を満たすクラスにimport文を付与し、生成クラスから使えるようにする

postProcessModels をoverrideして、特定の条件を満たす出力モデルを操作することができます。

@Override
@SuppressWarnings("unchecked")
public Map<String, Object> postProcessModels(Map<String, Object> objs) {
  super.postProcessModels(objs);

  List<Map<String, String>> imports = (List<Map<String, String>>) objs.get("imports");
  List<Map<String, Object>> models = (List<Map<String, Object>>) objs.get("models");

  boolean isXxx = /* models に関係する何がしかの条件*/
   if (isXxx) {
        imports.add(Map.of("import", SOME_IMPORT_PASS));
   }

   return objs;
}

mustacheテンプレートから呼び出せる関数(lambda)を登録する

mustache上で渡された文字列をJavaコードで整形して書き込む関数を追加できます。

まずaddMustacheLambdas をoverrideして関数を登録します。

@Override
protected ImmutableMap.Builder<String, Mustache.Lambda> addMustacheLambdas() {
  var builder = super.addMustacheLambdas();
  builder.put("my_lambda", new MyLambda());
  return builder;
}

private static class MyLambda implements Mustache.Lambda {

  @Override
  public void execute(Template.Fragment fragment, Writer writer) throws IOException {
    String baseValue = fragment.execute();
    if(StringUtils.equals(baseValue, "foo")) {
      writer.write("FooFoo");
    } else {
      writer.write(baseValue);
    }
  }
}

そして、mustacheテンプレート上で以下のように利用することで、 「valueが"foo"だったら"FooFoo"に置換、そうじゃなかったらvalueのまま」といった処理が可能になります。

{{#lambda.my_lambda}}{{value}}{{/lambda.my_lambda}}

おわりに

あちこちデバッガで型を確認しながらキャストしたり、Javaならではのnullチェックが必要になったりと 日頃Scalaを使っている自分からするとしんどいところも有りましたが、わかってくると生成コードをかなり自由に動かせるようになります。

意外とデフォルトだとお茶目な実装になっていることも有りますし、 業務だとAPI共通でのバリデーション処理や変換処理が必要になることも多いので、 ジェネレータのカスタマイズが出来ると便利なシーンも多いと思います。

この記事がなにかの参考になったら幸いです。

参考文献

明日は編集者の@shitara_sachioさんによる記事です!お楽しみに!